React useCallback

pds·2022년 12월 8일
0

TIL

목록 보기
22/60

useCallback

함수 메모이제이션

첫번째 인자로 넘어온 함수를 두번째 인자로 넘어온 의존성 배열 내의 값이 변경될 때까지

저장해놓고 재사용할 수 있게 해준다.

useCallback(fn, deps)

  • 컴포넌트가 리렌더링되어도 함수를 캐싱하여 남아있도록 해줌

  • 특정 함수를 리렌더링 시 새로 만들지 않고 재사용하고 싶을 때 사용한다.

  • 한 번 함수가 만들어지고 props 변경이 거의 없을 경우 virtual dom에 새로 렌더링 하지 않도록 최적화할 수 있다.



메모이제이션 목적으로의 사용

컴포넌트를 렌더링할 때 마다 함수를 새로 선언하는 것은 사실 성능상 문제가 크게 되진 않음

단순히 재생성을 방지하고자 하는 목적으로 사용하는 것은

오히려 코드 가독성을 떨어뜨리고 의미가 없을 수 있으며 오히려 성능 저하를 유발할 수 있다.

의존성 비교라는 비용을 상쇄할 수 있을 정도로 성능을 향상시킬 수 있을 때 사용할 것!


참조 동일성을 만족시키기 위해 사용

렌더링 할 때 선언된 함수를 초기화한다.

함수는 객체이고 참조 주소값을 가지며 리렌더링 시에 함수를 다시 그린다.

따라서 useEffect 같은 hook의 의존성에 사용하게 될 경우 선언된 값이 계속 변경되는 것으로

동작하기 때문에 문제가 발생할 수 있다.


영어로 이름을 입력하면 어떤 국적일 확률인지 보여주는 api를 사용하는 간단한 예제로 확인해보자

const UserProfile = ({ name }) => {
  const [countries, setCountries] = useState([]);
  const getCountries = () => {
    return fetch(`https://api.nationalize.io/?name=${name}`)
      .then((response) => response.json())
      .then((result) => {
        return result.country;
      });
  };

  useEffect(() => {
    getCountries().then((res) => setCountries(res));
  }, [getCountries]);

  return (
    <div>
      {name}의 국적은:
      {countries?.map((c, i) => (
        <div key={i}>
          <p>{c.country_id}일 확률이</p>
          <p>{(c.probability * 100).toFixed(2)}%</p>
        </div>
      ))}
    </div>
  );
};

getCountries 함수가 변경될 때만 api를 호출하도록 의도되었다.

하지만 위에서 언급했듯 렌더링 마다 다른 참조 주소 값을 가져

이전 렌더링 주기와 현재의 동등함을 만족하지 못하여 useEffect가 동작하게 되고

그로인해 무한 렌더링이 발생하게 된다.

무한 렌더링 때문에 계속 api 요청을 하다가 요청횟수 제한에 걸려버린 모습이다.


const UserProfile = ({ name }) => {
  const [countries, setCountries] = useState([]);
  console.log("RENDER!!");
  const getCountries = useCallback(() => {
    console.log("나는 getCountries");
    return fetch(`https://api.nationalize.io/?name=${name}`)
      .then((response) => response.json())
      .then((result) => {
        console.log(result);
        return result.country;
      });
  }, [name]);

  useEffect(() => {
    console.log("getCountries 호출");
    getCountries().then((res) => setCountries(res));
  }, [getCountries]);

  return (
    <div>
      {name}의 국적은:
      {countries?.map((c, i) => (
        <div key={i}>
          <p>{c.country_id}일 확률이</p>
          <p>{(c.probability * 100).toFixed(2)}%</p>
        </div>
      ))}
    </div>
  );
};

이와 같은 상황에서 useCallback hook을 사용하면

컴포넌트가 다시 랜더링되더라도 함수의 참조 주소값을 동일하게 유지시킬 수 있다.

의도했던 대로 useEffect에 넘어온 함수는 prop으로 넘어온 name 값이 변경되지 않는다면 재호출하지 않게 된다!





React.memo 와 사용하기


React.memo??

react.memo로 감싼 컴포넌트는 props 가 변경되지 않으면 다시 호출되지 않음

React.memo를 언제쓸까?

react는 컴포넌트를 렌더링하고 이전렌더와 현재를 비교해 다르면 dom을 업데이트 하는데 이 속도를 높일 수 있음 React.memo 로 래핑된 컴포넌트는 컴포넌트 렌더링 후 결과를 메모이징 해두고 다음 렌더링 시 props 변경이 없다면 그대로 재사용한다.


같은 props로 렌더링이 자주 일어나는 컴포넌트라고 예상될 때 사용하면 좋다

예를 들어 서버에서 일정 주기로 조회수를 패치해와 렌더링 해주는 컴포넌트가 있고 내부에 변경되지 않는 부분(타이틀, 내용)을 다루는 컴포넌트가 있을 때 조회수 업데이트로도 변경이 필요없는 컴포넌트가 같이 계속 렌더링 될텐데 이런 부분이 메모이제이션 적용에 적절한 케이스다.


언제쓰면안될까

props 가 자주 변하는 컴포넌트에 사용한다면 동등 비교를 계속 시도하고 결과는 false 이기 때문에 비효율적일 수 있다.

성능적인 이점이 있을거라 확신하지 못한다면 사용하지 않는 것이 좋으며

반드시 사용하지 않음을 먼저 고려하고 성능 테스트를 하며 적용하고 이득이 생기는지 확인할 것


주의점

react.memoprop 동등 비교 시 얕은비교(주소)를 수행하기 때문에

객체의 값(동일성) 비교를 원한다면 equal 함수를 정의해 사용해줘야 한다.



useCallback with memo 예시



메모이제이션이 없다면 App컴포넌트의 count또는 text의 변경에 대해

두 컴포넌트 모두를 리렌더링 해버리는데 메모이제이션을 활용해 이를 방지함.


App 컴포넌트의 함수 a() 또한 리렌더링 시 다시 선언되고

이는 count의 props 로 넘겨지기에 text를 변경해도 count가 같이 업데이트 되는데

useCallback을 통해 a함수를 캐싱해 이를 방지했다.


느낀점

최적화 해준다. 캐싱해준다. 리렌더링 방지해서 속도를 높여준다.

달콤하지만 세상이 그렇듯 일방적으로 우리에게 이득을 주기 위해 노력해주는 것 따위는 없다.

나중에 프로젝트를 할 때 성능적으로 최적화 할 수 있다고 판단되는 컴포넌트에 대해 꼭 적용해보고

성능 테스트도 공부해서 직접 비교해보아야 겠다.


References

profile
강해지고 싶은 주니어 프론트엔드 개발자

0개의 댓글