2026-08-08 React 새로 알게 된 것들 팔로우업

작성일

useRef 관련 내용 #

Ref로 값 참조하기 https://ko.react.dev/learn/referencing-values-with-refs

기억해두고 싶지만 해당 정보의 업데이트가 일어났을 때 리렌더링이 유발되는 걸 막으려고 할 때 useRef를 쓴다. ref는 mutable로 사용할 수 있다. 실제 구현은 따로 되어 있지만 이런 식의 state라고 생각할 수 있다.

const [ref, _] = useState({ current: initialValue });

useRef는 항상 동일한 객체를 반환하므로 state setter는 필요하지 않다. current만 변경하면 된다.

언제 쓰는가 #

보통 UI 컴포넌트 형태에 영향을 미치지 않으면서 리렌더링시 초기화되지 않도록 저장해야 하는 값에 쓴다.

공식 문서의 예시는 이렇다.

  1. timeout ID 저장
  2. DOM 요소 저장/조작(다음 섹션에 정리)
  3. JSX 계산에는 필요없는 다른 객체 저장

단 렌더링 중엔 ref를 읽거나 쓰지 않아야 한다.

ref로 DOM 조작 #

useRef로 선언한 ref를 DOM 노드를 가져와야 하는 JSX 태그에 ref 속성으로 전달한다. 그러면 ref를 통해 DOMㅡㄹ 조작할 수 있다.

<div ref={myRef}>

초기에는 myRef.current가 null이다가 React가 DOM 노드를 생성할 때 myRef.current에 해당 노드 참조를 넣어준다. 그럼 그걸 통해 이벤트 핸들러에서 접근하거나 브라우저 API 사용 가능


ref callback #

https://ko.react.dev/reference/react-dom/components/common#ref-callback

ref가 붙은 요소가 여러 개 필요할 수 있다. 예를 들어 N개 항목이 있는 리스트의 각 항목에 scrollIntoView가 붙은 이벤트 핸들러를 달아줘야 할 수도 있고.

그렇다고 이렇게 할 수는 없다.

// 잘못된 예시
<ul>
  {items.map((item) => {
    // 안된다. 훅은 컴포넌트 최상단에서만 호출되어야 하기 때문
    const ref = useRef(null);
    return <li ref={ref} />;
  })}
</ul>

다음과 같이 DOM API를 쓸 수도 있다.

  1. 부모에 단일 ref를 만든다
  2. 거기에 querySelectorAll 등의 DOM API 결과물을 담는다
  3. 이벤트 핸들러 함수에서 해당 DOM에서 특정 자식 노드를 찾도록 한다.

하지만 이건 다루기 힘든 편이다. 따라서 ref 속성에 함수를 전달하는 ref 콜백 테크닉을 고려할 수 있다. ref에는 해당 노드의 DOM 참조를 첫번째 인수로 받는 함수를 전달할 수 있기 때문이다.

아까 리액트는 해당 노드가 렌더링될 때 ref.current에 노드 참조를 넣어준다고 했다. 만약 ref가 함수라면 리액트는 DOM 노드가 화면에 추가되는 시점에, ref 콜백을 호출하고 그 인자로 DOM 노드를 전달한다. 해당 DOM 노드가 제거되면 리액트가 콜백에서 반환하는 클린업 함수를 호출한다.

이때 전달되는 인수명은 el이나 node 같은 걸로 하는 듯 하다. 공식문서에서는 node를 썼다.

따라서 배열이나 Map을 관리하며 인덱스, ID 등으로 ref에 접근할 수 있게 한다. 예를 들어 긴 리스트에서 특정 노드에 스크롤을 하려 한다면 이런 식으로 할 수 있다.

function App() {
  const itemsRef = useRef(new Map<string, HTMLLIElement>());

  const scrollTo = (itemId: string) => {
    const item = itemsRef.current.get(itemId);
    if (!item) return;
    item.scrollIntoView({
      behavior: "smooth",
      block: "nearest",
      inline: "center",
    });
  };

  return (
    <div>
      <div>
        {ITEMS.map((item) => (
          <button key={item} onClick={() => scrollTo(item)}>
            {item}
          </button>
        ))}
      </div>

      <ul>
        {ITEMS.map((item) => (
          <li
            key={item}
            ref={(el) => {
              const mp = itemsRef.current;
              mp.set(item, el as HTMLLIElement);

              return () => {
                mp.delete(item);
              };
            }}
          >
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
}

itemsRef는 식별자-DOM 노드 쌍이 연결될 수 있는 map을 갖고 있다. 그리고 각 li 태그에 붙은 ref 콜백은 해당 DOM 노드가 생길 때 식별자-DOM 노드를 map을 통해 연관시킨다.

그리고 이벤트 핸들러(scrollTo)에서는 map을 통해 개별 DOM 노드를 읽게 된다. 이때 클린업 함수에서 map 정리를 잊지 말자.

strict mode는 잘 알려져 있다시피, 순수해야 하는 함수(state setter 등등)을 2번씩 실행한다. 순수 함수의 경우 당연히 2번 실행되어도 동작이 변하지 않기 때문에, 만약 strict mode에서 뭔가 의도하지 않은 동작이 생기면 버그라는 걸 알 수 있다.

ref 콜백에도 이건 적용되는데, 만약 위와 같은 ref 콜백에서 클린업이 없을경우 문제가 될 수 있다.

위와 같은 경우에는 ITEMS 배열 하나만 쓰기 때문에 괜찮을 수도 있겠지만, 만약 저 배열이 바뀔 수 있는 거였다면? 그럼 클린업이 되지 않고 계속 Map에 요소만 추가되게 된다. 따라서 해당 요소가 없어질 때 호출될 클린업을 꼭 해야 하고, 이게 안 되어 있을 경우 strict mode가 클린업 함수 누락 검출에 도움을 줄 수 있다.

주의사항 #

flushSync(() => {
  setTodos([...todos, newTodo]);
});
// todo 업데이트가 끝난 다음 실행된다.
listRef.current.lastChild.scrollIntoView();

https://ko.react.dev/learn/manipulating-the-dom-with-refs#flushing-state-updates-synchronously-with-flush-sync

<li ref={index === i ? selectedRef : null}>