[번역] useCallback Might Be What You Meant By useRef & useEffect

김형태·2023년 11월 21일
0

출처: https://medium.com/welldone-software/usecallback-might-be-what-you-meant-by-useref-useeffect-773bc0278ae

DOM에 React element가 mount 또는 unmount되는 것에 반응하기 위해, useRef를 사용하여 해당 요소에 대한 참조를 얻고, useEffect를 사용하는 것을 생각해 볼 수 있습니다. 하지만 실제로 useRef와 useEffect를 사용하는 방법은 작동하지 않죠.

이는 컴포넌트가 (un)mount되고 useRef를 통해 ref.current에 연결될 때 콜백이나 리렌더가 없기 때문입니다.

eslint의 react-hooks 룰은 이에 대해 warning을 줍니다. useEffect의 deps로서 refref.current를 사용할 때 어떤 warning이 나오는지를 확인해보죠.

아래 코드를 보면 useRef를 useEffect의 deps로 사용할 때 제대로 trigger되지 않음을 알 수 있습니다.

  • 원문에는 코드 샌드박스가 있습니다.
  • 아래 코드의 결과는 이곳에 배포되어 있습니다.
import React, { useEffect, useRef, useState } from "react";
import "./styles.css";
import catImageUrl from "./cat.png";

/* 
   for more info see:
   https://medium.com/welldone-software/usecallback-might-be-what-you-meant-by-useref-useeffect-773bc0278ae
*/
export default function App() {
  const [count, setCount] = useState(1);
  const shouldShowImageOfCat = count % 3 === 0;

  const [catInfo, setCatInfo] = useState(false);

  // notice how none of the deps of useEffect
  // manages to trigger the hook in time
  const catImageRef = useRef();
  useEffect(() => {
    console.log(catImageRef.current);
    setCatInfo(catImageRef.current?.getBoundingClientRect());
    // notice the warning below
  }, [catImageRef, catImageRef.current]);

  return (
    <div className="App">
      <h1>useEffect & useRef vs useCallback</h1>
      <p>
        An image of a cat would appear on every 3rd render.
        <br />
        <br />
        Would our hook be able to make the emoji see it?
        <br />
        <br />
        {catInfo ? "😂" : "😩"} - I {catInfo ? "" : "don't"} see the cat 🐈
        {catInfo ? `, it's height is ${catInfo.height}` : ""}!
      </p>
      <input disabled value={`render #${count}`} />
      <button onClick={() => setCount((c) => c + 1)}>next render</button>
      <br />
      {shouldShowImageOfCat ? (
        <img
          ref={catImageRef}
          src={catImageUrl}
          alt="cat"
          width="50%"
          style={{ padding: 10 }}
        />
      ) : (
        ""
      )}
    </div>
  );
}

그럼 우리가 무엇을 할 수 있을까요?

useCallback

(이와 관련된 공식문서)

예전 버전이라 지금도 이게 맞는지는 추가적인 확인이 필요해보입니다..

useCallback으로 래핑된 함수를 ref로 전달하여 반환되는 최신 DOM 노드를 참조할 수 있습니다.

  • 역시 원문에서는 코드 샌드박스로 바로 확인할 수 있습니다.
  • 실행 결과
import React, { useCallback, useState } from "react";
import "./styles.css";
import catImageUrl from "./cat.png";

/* 
   for more info see:
   https://medium.com/welldone-software/usecallback-might-be-what-you-meant-by-useref-useeffect-773bc0278ae
*/
export default function App() {
  const [count, setCount] = useState(1);
  const shouldShowImageOfCat = count % 3 === 0;

  const [catInfo, setCatInfo] = useState(false);

  // notice how this is a useCallback
  // that's used as the "ref" of the image below
  const catImageRef = useCallback((catImageNode) => {
    console.log(catImageNode);
    setCatInfo(catImageNode?.getBoundingClientRect());
  }, []);

  return (
    <div className="App">
      <h1>useEffect & useRef vs useCallback</h1>
      <p>
        An image of a cat would appear on every 3rd render.
        <br />
        <br />
        Would our hook be able to make the emoji see it?
        <br />
        <br />
        {catInfo ? "😂" : "😩"} - I {catInfo ? "" : "don't"} see the cat 🐈
        {catInfo ? `, it's height is ${catInfo.height}` : ""}!
      </p>
      <input disabled value={`render #${count}`} />
      <button onClick={() => setCount((c) => c + 1)}>next render</button>
      <br />
      {shouldShowImageOfCat ? (
        <img
          ref={catImageRef}
          src={catImageUrl}
          alt="cat"
          width="50%"
          style={{ padding: 10 }}
        />
      ) : (
        ""
      )}
    </div>
  );
}

주의 사항1: 엘리먼트 mount 및 unmount 시, 심지어 첫 번째 mount, 그리고 unmount가 상위 엘리먼트 unmount의 결과인 경우ref 함수가 호출되는 것이 보장됩니다.

주의 사항2: ref 콜백을 useCallback으로 래핑해야 합니다.

useCallback이 없으면 ref 콜백에서 렌더링을 발생시키면 ref 콜백이 null로 다시 트리거되어 React 내부로 인해 잠재적으로 무한 루프가 발생할 수 있습니다.

실험해보려면 위의 코드에서 useCallback을 제거해보거나 이 샌드박스를 보십시오.

이 패턴은 여러 가지 방법으로 사용될 수 있습니다.

useState

useState는 렌더링 사이에도 일관성을 유지하는 함수이므로 ref로도 사용할 수 있습니다.

이 경우 전체 노드state로 저장됩니다.

state로서, 변경되면 리렌더링을 일으키고, state는 렌더링 결과 및 useEffect의 deps로 안전하게 사용될 수 있습니다.

const [node, setRef] = useState(null);

useEffect(() => {
  if (!node) {
    console.log('unmounted!');
    return null;
  }
  
  console.log('mounted');
  
  const fn = e => console.log(e);
  
  node.addEventListener('mousedown', fn);
  return () => node.removeEventListener('mousedown', fn);
}, [node])

// <div ref={setRef}....

useStateRef

DOM에 접근하는 것은 비용이 많이 들기 때문에 우리는 이 작업을 가능한 한 적게 하고 싶습니다.

이전 훅에서와 같이 전체 node가 필요하지 않은 경우 상태에 일부만 저장하는 것이 좋습니다.

// the hook
function useStateRef(processNode) {
  const [node, setNode] = useState(null);
  const setRef = useCallback(newNode => {
    setNode(processNode(newNode));
  }, [processNode]);
  return [node, setRef];
}

// how it's used
const [clientHeight, setRef] = useStateRef(node => (node?.clientHeight || 0));

useEffect(() => {
  console.log(`the new clientHeight is: ${clientHeight}`);
}, [clientHeight])

// <div ref={setRef}....

// <div>the current height is: {clientHeight}</div>

보시다시피 우리는 ref를 전달하는 요소가 mount될 때만 DOM에 액세스하고 이 단계에서는 clientHeight만 저장합니다.

useRefWithCallback

그러나 때로는, 성능을 위해 ref로 사용하는 요소의 mount 및 unmount 시 다시 렌더링을 트리거하지 않고 수행할 수 있습니다.

다음 훅은 상태에 대한 노드를 저장하지 않습니다. 상태를 사용하는 대신, mount 및 unmount에 직접 응답하므로 다시 렌더링을 트리거하지 않습니다.

// the hook
function useRefWithCallback(onMount, onUnmount) {
  const nodeRef = useRef(null);

  const setRef = useCallback(node => {
    if (nodeRef.current) {
      onUnmount(nodeRef.current);
    }

    nodeRef.current = node;

    if (nodeRef.current) {
      onMount(nodeRef.current);
    }
  }, [onMount, onUnmount]);

  return setRef;
}

const onMouseDown = useCallback(e => console.log('hi!', e.target.clientHeight), []);

const setDivRef = useRefWithCallback(
  node => node.addEventListener("mousedown", onMouseDown),
  node => node.removeEventListener("mousedown", onMouseDown)
);

// <div ref={setDivRef}

결국, useCallbackref로 사용하는 원리를 이해하면, 특정 요구 사항에 맞는 아이디어를 얻을 수 있습니다.


Next.js에서 첫 마운트 시 엘리먼트의 크기를 계산해서 스크롤 해야하는 일이 있었는데, useRef, useEffect로 사부작대도 잘 안 되는 일이 있었다. 이걸로 시도를 해봐야겠다.

근데 새로운 버전의 공식문서에는 없는 걸 보니 다른 좋은 방법이 있는 걸까..?

profile
steady

0개의 댓글