간단한 Todo 서비스를 만들던 중, 수정을 눌렀을 때, 해당 버튼과 연관된 input element에 focus이벤트를 주고싶었다. useRef배열을 사용해야 했는데, input element가 동적으로 생기는 구조여서 어떻게 관리해야할지 고민하게 되었다.

useRef에 ref배열을 넣자


기존에는 아래와 같이 useRef배열에 바로 ref(HTMLElement)를 넣어서 index를 활용해서 접근을 하는 방식으로 구현했었다.

const myRef = useRef([]);

useEffect(() => {
	if (index > 0 && myRef && myRef.current) myRef.current[index].focus();
}, [index]);

하지만 이번에 고민하게 된 이유는 input element가 동적으로 관리되면서이다. 아래 코드를 보면, input element가 항상 랜더링 되는게 아니라 수정버튼을 누르는 순간 화면에 그려지게 된다.

<div className="contents">
  {isUpdateButtonTabbedId === todo.id ? (
    <>
      <input .../>
      <input .../>
    </>
  ) : (
    <>
      <div id="title">{todo.title}</div>
      <div id="content">{todo.content}</div>
    </>
  )}
</div>

useRef에 Object배열을 넣자


고민을 하다가 useRef에 Object배열을 넣어보기로 했다. 결과적으로 성공적이였다. 하지만 코드가 조금 가독성이 좋지 않았다.

const targets = useRef<{ id: string; ref: HTMLInputElement }[]>([]);

useEffect(() => {
  if (targets && targets.current.length > 0) {
    const currentTarget = targets.current.find(
      (target) => target.id === isUpdateButtonTabbedId
    );
    if (currentTarget) currentTarget.ref.focus();
  }
}, [isUpdateButtonTabbedId]);

return (
	...
		<input
       ref={(ref) => targets.current.push({ id: todo.id, ref })}
	...
);

코드 리팩토링 - 관심사 분리

마침 Wanted 프리온보딩 프론트엔드 챌린지 강의를 들은 내용을 바탕으로 관심사 분리를 진행해보았다. 한눈에 함수의 역활이 명확하게 보이고 비즈니스 로직이 상당히 깔끔해졌다. 특히 id이외의 값을 활용해서 비교할 때에도 함수만 추가하면 되기때문에 재사용성이 강력해졌다.

const currentTarget = (predicate: (element: TargetRef) => boolean) => targets.current.find(predicate);
const predicateTodo = (element: TargetRef) => element.id === isUpdateButtonTabbed;

useEffect(() => {
  const currentTargetToFocus = currentTarget(predicateTodo);

  if (targets && targets.current.length > 0 && currentTargetToFocus) {
    currentTargetToFocus.ref.focus();
  }
}, [isUpdateButtonTabbed]);