프론트엔드/React

useMemo & useCallback

Hyeon_E 2023. 12. 7. 16:46

메모이제이션(Memoization)

값비싼 함수 호출의 결과를 캐싱하고 동일한 입력이 다시 발생할 때 캐싱된 결과를 반환하는 프로그래밍 기술

즉 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법

적절하게 활용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화 할 수 있음

 

[ useMemo ]

메모이제이션된 값을 반환하는 함수

리렌더링이 발생할 경우 의존성 배열에 지정된 deps가 변할때에만 useMemo에 등록한 함수가 실행되도록 처리하면 불필요한 연산을 하지 않게됨

 

useMemo(() => fn, [deps])

 

의존성 배열에 지정된 deps로 지정된 값이 변하게 되면 () => fn함수를 실행하고 그 함수의 반환 값을 반환해줌

deps는 dependency의 약어로 의존성을 뜻하며 useMemo가 deps에 의존하고 있다는 것을 말함

 

[ useCallback ]

메모이제이션된 함수를 반환

useCallback이 함수를 반환하기 때문에 그 함수를 가지는 const 변수에 초기화하는 것이 일반적인 모양

useCallback(fn, [deps])

 

useCallback 또한 deps 즉 의존성이 있는 값이 변하면 fn에 등록한 함수를 반환

 

▶ useCallback 사용처

자식 컴포넌트에 props로 함수를 전달하는 경우

함수는 값이 아닌 참조로 비교된다는 점을 알고 있어야 함

 

const functionOne = function() {
  return 5;
};
const functionTwo = function() {
  return 5;
};
// 서로의 참조가 다르기 때문에 false
console.log(functionOne === functionTwo);

 

동일한 값을 반환하지만 참조가 다르기 때문에 false가 나옴

컴포넌트에서 특정 함수를 정의할 경우 각각의 함수들은 모두 고유한 함수가 됨

이런 고유한 함수가 생성될 경우 부모를 통해 props에 함수를 전달받는 자식 컴포넌트에서는 props가 변경되었다고 판단해 리렌더링이 발생하게 됨

 

//useCallback X
function App() {
  const [name, setName] = useState('');
  const onSave = () => {};

  return (
    <div className="App">
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <Profile onSave={onSave} />
    </div>
  );
}

 

//useCallback O
function App() {
  const [name, setName] = useState('');
  const onSave = useCallback(() => {
    console.log(name);
  }, [name]);

  return (
    <div className="App">
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <Profile onSave={onSave} />
    </div>
  );
}

useCallback을 사용하지 않을 경우 name이 변경되어 리렌더링이 발생하면 onSave함수가 새로 만들어지고 Profile 컴포넌트의 props로 onSave함수가 새로 전달되게 됨

이때 Profile 컴포넌트에서 useMemo를 사용해도 이전 onSave와 이후 onSave가 같은 값을 반환하지만 참조가 다른 함수가 되어버리기 때문에 리렌더링이 일어나게 됨

하지만 useCallback을 사용해서 onSave라는 함수를 재사용하는 것으로 자식 컴포넌트의 리렌더링을 방지할 수 있음

 

외부에서 값을 가져오는 api를 호출하는 경우

//useCallback X
function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = () =>
    fetch(`https://your-api.com/users/${userId}`)
      .then((response) => response.json())
      .then(({ user }) => user);

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

 

//useCallback O
function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = useCallback(
    () =>
      fetch(`https://your-api.com/users/${userId}`)
        .then((response) => response.json())
        .then(({ user }) => user),
    [userId]
  );

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

fetchUser 함수가 변경될 때만 외부에서 api를 가져와 useEffect가 실행됨

하지만 이렇게 되면 무한루프에 빠지게 됨

Profile 컴포넌트가 리렌더링이 발생할 경우 fetchUser함수에는 새로운 함수가 할당되게 됨

그러면 useEffect가 호출되어 user값이 바뀌에 되고 state값이 바뀌었기 때문에 다시 리렌더링이 발생됨

 

이때 useCallback을 사용할 경우 fetchUser 함수의 참조값을 동일하게 유지시킬 수 있음

api의 옵션으로 사용되는 userId가 변동될 때만 fetchUser에 새로운 함수가 할당되도록 설정하고 그것이 아니면 동일한 함수가 실행되게 되서 무한 루프에 빠지지 않도록 할 수 있음

 

[ useMemo와 useCallback의 사용 ]

사용하지 말아야 할 경우

  1. 연산이 복잡하지 않은 함수에 useCallback을 사용하는 것은 메모리 낭비이므로 간단한 일반 함수들에는 useCallback을 사용하지 않는게 좋음
  2. 단순히 함수 내부에서 setState나 dispatch 함수등을 호출하는 경우에는 useCallback을 사용하지 않는게 좋음
    이미 리액트 자체에서 useState 와 useDispatch에 대한 성능 최적화가 보장되기 때문에 렌더링이 새로 되어도 해당 함수는 재생성되지 않음
  3. useCallback, useMemo의 의존성 배열에 완전히 새로운 객체나 배열을 전달해서는 안됨
    useCallback 내부 함수나 useMemo 내부 값에서 사용하지 않는 props를 전달한다면 메모이제이션을 하는데 소용이 없음
  4. 의도적으로 매번 새로운 함수나 값을 계산해야 한다면 굳이 useCallback이나 useMemo를 사용할 필요가 없음
  5. DOM에서 다른 컴포넌트를 렌더링하지 않는 컴포넌트 (html 태그만 렌더링하는 컴포넌트)에서는 useMemo를 사용할 필요가 없음
  6. div, span, a, img와 같이 호스트 환경 (브라우저 / 모바일)에 속하는 플랫폰 컴포넌트에 전달하는 항목에는 useMemo와 useCallback을 사용할 필요가 없음
    리액트는 해당 컴포넌트들에 함수 참조가 변경되었는지 신경쓰지 않기 때문(ref는 제외)

사용해야 할 경우 

  1. 연산 혹은 처리량이 매우 많아서 렌더링의 문제가 되는 경우 리렌더시 비용 절감을 위해서 useMemo를 사용
  2. 자식 컴포넌트에서 useEffect가 반복적으로 트리거 되거나 무한 루프에 빠질 위험이 있을 때 useMemo, useCallback을 사용
  3. 자식 컴포넌트에 함수를 props로 넘길 때 불필요한 렌더링이 일어난다고 판단된다면 useCallback으로 함수 동등성을 유지
  4. 함수 자체가 매우 복잡하거나 다시 계산하는데 비용이 많이 드는 경우에 useCallback을 사용
  5. 사용자의 입력값이 map 혹은 filter 등을 사용하여 이후 렌더링에서도 동일한 참조를 사용할 가능성이 높을 경우 useMemo를 사용해서 메모이제이션을 적용
  6. 리액트 상위 트리에서 부모가 리렌더링 될 때 자식 컴포넌트까지의 렌더링 전파를 막고 싶을 때 useMemo를 사용
    자식 컴포넌트가 useMemo로 메모이제이션 컴포넌트일 경우 메모이제이션된 props를 사용해 필요한 부분만 리렌더링 할 수 있음
  7. ref 함수를 부수작용(side effect)와 함께 전달하거나 ref로 wrapper 함수를 만들 때 useMemo를 사용
    리액트는 ref 함수가 변경될 때 마다 과거 값을 null로 호출하고 새로운 함수를 호출하기 때문인데 이 경우 ref 함수의 이벤트 리스터가 변경되는 등의 불필요한 작업이 일어날 수 있음

 

[ 결론 ]

새로운 리액트 문서에서 제시하는 권장사항에서 "useMemo, useCallback 혹은 React.memo를 사용하여 캐싱할지 여부를 고려한다면 성능 문제가 발견될 때까지 캐싱을 미루세요"가 있음

 

항상 메모하는 것은 큰 단점이 아니지만 코드의 가독성이 떨어지고 모두 메모하는것이 최적화의 도움이 되지 않기 때문

큰 문제가 아니라면 사용하지 않고 코드를 작성하고 성능문제가 발견되면 useCallback과 useMemo가 꼭 필요한지 현재 적용하는 곳에 적합한지를 고민하고 장단점을 명확하게 파악한후에 프로젝트에 적용시키는것이 중요함