useEffect 의존성에 ref를 담을 때마다 찜찜하신 분들을 위해
🥱 TLDR;
ref가 변경되어도 컴포넌트가 새로 렌더링되지는 않으므로, useEffect는 ref의 변경에 대해 즉각 감지를 할 수 없습니다. 다른 값 변경 등의 이유로 컴포넌트가 렌더링이 새로 됐을 때 비로소 변경된 ref 값을 볼 수 있게 됩니다.
따라서 다음 조건에 따라 각 ref의 갱신을 관리하는 것이 좋습니다.
- 모든 렌더링 시마다 트리거되는 useEffect가 아니라면, 사용할 ref는useCallback+callback ref로 작성하기
- 모든 렌더링 시마다 트리거되는 useEffect라면,useRef의 값을 의존성으로 담아도 됨
다들 이런 경험 있죠?
리액트 프레임워크로 개발을 하다보면, DOM 노드의 사이즈(width, height)나 포지션(top, bottom, ...)값을 구해야하는 작업이 필요할 때가 있죠.
저도 종종 이런 상황을 겪는데요, 그 중 하나로 DOM 노드에 ref를 등록해서 height 값을 업데이트 시켜주는 커스텀 훅을 작성했었습니다. 코드는 다음과 같습니다.
// ref object에 DOM 노드가 설정되면 height 값을 설정해주는 커스텀 훅
const useGettingHeight = () => {
  const [height, setHeight] = useState(null);
  const ref = useRef<HTMLElement>();
  // ref에 DOM 노드가 설정되는 것을 감지하여 height 설정(?)
  useEffect(() => {
    if (!!ref.current) setHeight(ref.current.getBoundingClientRect().height);
  }, [ref.current]);
  return [height, ref];
};
function App() {
  const [height, ref] = useGettingHeight();
  return (
    <div ref={ref} style={{ height: `${height}px` }}>
      이렇게 작성해도 될까요?
    </div>
  );
}위 코드는 정상적으로 <div>에 인라인 스타일 height: 18px 값이 설정되기는 합니다. 언뜻 보기에는 문제가 없는 코드같아요. 그런데 과연 진짜 문제가 없는 코드일까요?🤔
useEffect에 ref object를 의존성으로 주입하는 것의 진짜 문제
위 예시에서 우리는 useEffect의 두 번째 인자로 ref.current를 추가함에 따라, useEffect가 ref 변경을 감지하여 setHeight이 실행된다고 이해하기 쉬운데요.
아쉽게도 이는 잘못된 이해입니다.
왜냐하면 ref는 컴포넌트가 다시 렌더링될 때까지 값이 업데이트 되었는지 확인할 수 없거든요. 또한 ref의 값이 갱신되어도 컴포넌트는 새로 렌더링이 되지 않습니다. 다른 이유로 인해 컴포넌트가 다시 렌더링되어서야 갱신된 값을 확인할 수 있어요. 이 말은 곧, 렌더링을 건너뛰는 useEffect는 다음 렌더링 전에는 ref에 대한 어떤 변경도 볼 수 없다는 것을 뜻합니다.
무슨 말인지 좀 어려우신가요? 아래 예시를 보면 좀 더 이해가 수월하실 거에요.
const useGettingHeight = () => {
  const [height, setHeight] = useState(null);
  const ref = useRef<HTMLElement>();
  useEffect(() => {
    if (!!ref.current) setHeight(ref.current.getBoundingClientRect().height);
  }, [ref.current]);
  return [height, ref];
};
const useLoading = () => {
  const [loading, setLoading] = useState(true);
  // 컴포넌트 마운트 시 loading false로 업데이트
  useEffect(() => setLoading(false), []);
  return loading;
};
function App() {
  // 첫 렌더링 시 ref가 attached됨
  const [height, ref] = useGettingHeight();
  // loading 값이 true가 된 이후 ref가 attached됨
  const [lazyHeight, lazyRef] = useGettingHeight();
  const loading = useLoading();
  return (
    <>
      <div ref={ref}>ref height: {height.toString()}</div>
      {!loading && <div ref={lazyRef}>Lazy ref height: {lazyHeight.toString()}</div>}
    </>
  );
}위에서 작성했던 DOM 노드의 height을 가져오는 훅을 사용했습니다. 눈 여겨볼 점은 useLoading가 추가된 것인데요, 컴포넌트 마운트 이후에 loading 값이 true가 되면서 추가로 두 번째 <div>가 렌더링되고 ref에 DOM node 참조값이 담깁니다. 첫 번째 ref와의 구분을 위해 lazyRef라고 명명했습니다.
이 코드를 실행하면 어떻게 될까요? 둘 다 height을 정상적으로 출력할까요?
링크를 클릭해서 직접 확인해보세요 Link
실행 결과

컴포넌트 마운트 이후에 설정되었던 lazyRef의 height은 값이 설정되지 않았습니다. 결과가 이해가 되지 않는다면 다음의 실행 순서를 천천히 따라가봅시다.
- 컴포넌트의 첫 렌더링- 첫 번째 <div>가 렌더링되고,ref에 DOM node 참조값이 담깁니다.
- lazyRef는 아직 두 번째- <div>가 렌더링되지 않았으므로 값이 없습니다.
 
- 첫 번째 
- 첫 렌더링 이후 실행되는 useEffect 콜백- useGettingHeight의- ref.current를 의존성으로 가지는- useEffect콜백이 실행됩니다.- Note. ref.current의존성 때문에 호출되는 것이 아닙니다! 모든 useEffect는 의존성과 관계없이 컴포넌트 첫 렌더링 이후 한 번 실행됩니다.
 
- Note. 
- useLoading의- useEffect콜백도 실행됩니다.
 
- useEffect에 의해 변경되는 state- useEffect 콜백이 처음으로 실행될 때, 첫 번째 ref.current는 DOM node 참조값을 가지고 있으므로setHeight을 호출하여heightstate를 업데이트해줍니다.
- 두 번째 lazyRef.current는 값이 없으므로setHeight이 호출되지 않습니다.
- loading값이- true가 됩니다.
 
- useEffect 콜백이 처음으로 실행될 때, 첫 번째 
- state 변경으로 인한 컴포넌트 리렌더링- 리렌더링 시, 첫 번째 height값이 존재하므로 첫 번째<div>의 innerText에 18이라는 값이 찍힙니다.
- loading값이 true이므로 두 번째- <div>가 렌더링되고,- lazyRef에 DOM node 참조값이 담깁니다.
 
- 리렌더링 시, 첫 번째 
- 동작 끝- 동작은 여기서 끝이 납니다. lazyRef.current값이 변경되었는데도 두 번째 훅의 useEffect는 호출되지 않습니다.
 
- 동작은 여기서 끝이 납니다. 
이제 위에서 했던 "렌더링을 건너뛰는 useEffect는 다음 렌더링 전에는 ref에 대한 어떤 변경도 볼 수 없다"는 말이 이해되시나요?
우리는 컴포넌트가 다른 state들을 다루는 마법의 방식처럼 ref의 변경에 대응할 수 있도록 할 수는 없습니다. useEffect는 ref의 변경을 즉각 감지할 수 없어요. 다만 다음 렌더링이 발생했을 때는 ref의 변경을 확인할 수 있어서 useEffect 콜백을 실행시켜 줄 수 있겠죠.
말이 길었는데 결국 한 문장으로 정리하자면 다음과 같습니다.
useEffect가 매 렌더링마다 호출되는 게 아니라면(ref의 변경을 계속해서 확인할 수 있는 상태가 아니라면), useEffect에 ref object를 의존성으로 담지 마세요🙅♂️
해결 방법
DOM Node를 참조하기 위한 방법으로 useRef를 사용하는 대신에, useCallback을 사용해서 Callback ref를 만드세요! 위의 useGettingHeight hook을 이를 사용하여 수정해보겠습니다.
const useGettingHeight = () => {
  const [height, setHeight] = useState(null);
  // ✅  useRef와 useEffect를 지우고 callback ref를 새로 작성
  const ref = useCallback((node: HTMLElement) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);
  return [height, ref];
};
function App() {
  const [height, ref] = useGettingHeight();
  const [lazyHeight, lazyRef] = useGettingHeight();
  const loading = useLoading();
  return (
    <>
      <div ref={ref}>ref height: {height.toString()}</div>
      {!loading && <div ref={lazyRef}>Lazy ref height: {lazyHeight.toString()}</div>}
    </>
  );
}커스텀 훅에서 리턴하는 ref를 callback ref의 형태로 변경한 것을 제외하고는 위의 코드와 동일합니다! 한 번 실행해볼까요?
위 링크에서 직접 코드를 수정해서 결과를 확인해보세요🙌
실행 결과

잘 작동하는군요! 리액트는 이제 ref(callback ref)가 node에 붙여질 때 이 콜백을 실행할 겁니다. 붙여지는 때도 상관없고, 다른 노드에 다시 붙게 되더라도 상관없어요. 그 때마다 해당 콜백을 실행시켜 줄겁니다. 이렇게 node를 인자로 받는 콜백이 호출되면 setHeight이 실행되어 업데이트된 height 값을 확인할 수 있게 됩니다.
추가로 useCallback에 빈 의존성([])을 전달한 것을 확인할 수 있는데요. 이렇게 하면 우리가 만든 ref 콜백이 리렌더링 시에도 참조값이 변경되지 않게끔 할 수 있습니다. 따라서 리액트가 매 렌더링마다 불필요하게 콜백을 호출하지 않을 거에요.
정리
ref가 변경되어도 컴포넌트가 새로 렌더링되지는 않으므로, useEffect는 ref의 변경에 대해 즉각 감지를 할 수 없습니다. 다른 값 변경 등의 이유로 컴포넌트가 렌더링이 새로 됐을 때 비로소 변경된 ref 값을 볼 수 있게 됩니다.
아마 지금까지 ref를 useEffect에 의존성으로 담으며 찜찜함을 느끼셨을 분들이 있을 겁니다. state를 의존성으로 담았을 때와는 미묘하게 다른 현상(값의 변경이 확인이 안되는)을 겪으신 분들, 이제 자신있게 작성하실 수 있겠죠?
- 모든 렌더링 시마다 트리거되는 useEffect가 아니라면, 사용할 ref는useCallback+callback ref로 작성하기
- 모든 렌더링 시마다 트리거되는 useEffect라면, 그대로useRef의 값을 의존성으로 담아도 됨
아래 리액트 공식 문서의 Hooks FAQ에 관련된 내용이 좀 더 상세하게 나와있습니다. 더 명확한 이해를 위해 추가로 읽어보시는 것을 권장합니다🙆♂️
참고
Author And Source
이 문제에 관하여(useEffect 의존성에 ref를 담을 때마다 찜찜하신 분들을 위해), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@shmoon2917/useEffect-의존성에-ref를-담을-때마다-찜찜하신-분들을-위해저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
                                
                                
                                
                                
                                
                                우수한 개발자 콘텐츠 발견에 전념
                                (Collection and Share based on the CC Protocol.)