본문 바로가기
Front-end

React useCallback

by 춘_춘 2023. 12. 4.

 

메모제이션이란?

메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야할 때, 이전에 계산한 값을 메모를 저장해놓음으로써, 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. 메모이제이션를 사용하면 동일한 결과를 불필요하게 다시 계산하지 않고, 캐시된 결과를 반환할 수 있다. 따라서, useCallback, useMemo와 같은 메모이제이션 훅을 통해 성능을 향상시키고 코드의 복잡성을 줄일 수 있다.

useCallback이란?

리액트의 렌더링 성능을 위해 제공되는 훅이다.

훅을 사용하면 컴포넌트가 렌더링 될때마다 함수를 생성해서 자식 컴포넌트의 속성으로 주게 넘겨주게 된다.

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

 

위에서 메모이제이션된 함수는 콜백 함수의 의존성이 변경되었을 때에만 변경된다. 이는 불필요한 렌더링을 방지하기 위해 (ex. shouldComponentUpdate를 사용하여) 참조의 동일성을 보장하거나, 자식 컴포넌트에 의존적인 콜백 함수를 전달할 때 유용하다.

참고로, useCallback(fn, deps) useMemo(() => fn, deps) 같다. useCallback 의존성 배열에 있는 상태나 props 변경되지 않는다면, 해당 함수는 다시 생성되지 않는다.

 

함수 메모이제이션

앞서 말한 useCallback 사용법을 정리하자면 아래와 같다. 첫번째 인자로 넘긴 함수를, 두번째 인자로 넘긴 의존성 배열내의 값이 변경되기 전까지 저장하고 재사용할 수 있게 해준다.

const memoizedFunction = useCallback(함수, 배열);

만약 useCallback을 사용하지 않는다면, 아래와 같은 함수는 컴포넌트가 렌더링 될 때마다 새롭게 생성된다.

const sum = () => x + y;

하지만, useCallback 사용하면 컴포넌트가 다시 렌더링 되더라도, 해당 함수가 의존하고 있는 값들이 바뀌지 않는다면 함수를 새로 생성하지 않고 기존 함수를 계속 반환한다.

const add = useCallback(() => x + y, [x, y]);

사실 컴포넌트를 렌더링 할 때마다 함수를 새로 선언하는 것은 성능상 큰 영향을 끼치지 않는다. 따라서, 모든 함수마다 useCallback을 사용하는 것은 큰 의미가 없고, 오히려 유지 보수를 어렵게 하거나 성능을 해칠 수 있다. useCallback의 의미있는 사용법을 알기 위해서는 자바스크립트의 함수 동등성에 대해서 알아야 한다.

 

함수 동등성

자바스크립트에서 함수는 객체로 취급이 되기때문에, 함수를 동일하게 만들어도 메모리 주소가 다르면 다른 함수로 간주한다. 바로 메모리 주소에 의한 참조 비교가 일어나기 때문인데, 콘솔창에서 아래와 같이 동일한 코드의 함수를 작성하시고 === 연산자로 비교를 해보면 false 반환된다.

> const add1 = () => x + y;
undefined
> const add2 = () => x + y;
undefined
> add1 === add2
false

 

만약 특정 함수를 다른 함수의 인자로 넘기거나, 자식 컴포넌트의 props로 넘길 때 함수의 참조가 달라서 예상하지 못한 성능 문제가 생길 수 있다.

경우, useCallback 이용해 함수를 특정 조건이 변경되지 않는 이상 재생성하지 못하게 제한하여 함수 동등성을 보장할 있다. (만약 리액트가 함수가 동등하지 않다고 판단한다면 상황에 따라 성능이 악화되거나, 무한루프에 빠지는 등의 문제를 겪을 있다.)

 

정리하자면, useCallback은 리액트 코드를 최적화하고 메모리 소비를 줄일 수 있는 좋은 방법을 제공한다. 함수의 동등성을 유지하게 하여 필요없는 성능 악화나 무한루프를 방지할 수도 있다. 그 중 특히, 계산 비용이 많이 들거나 외부 데이터 소스에 크게 의존하는 기능에 가장 적합하다. 필요한 때에 적절하게 사용하면 성능을 향상시키고 코드의 복잡성을 줄이는 데 도움이 될 수 있다.

 

useCallback 주의할 점

useCallback 훅으로 함수 재생성을 방지하고, 참조 동등성을 보장하여 성능을 향상시킬 순 있다. 하지만 모든 함수마다 useCallback을 사용하는 것은 오히려 성능을 악화시키고 가독성을 해칠 수 있다.

가끔 React 컴포넌트 내에서 선언하는 모든 함수에 useCallback를 사용하는 경우가 있다. 일반적으로 소프트웨어의 성능 최적화에는 그에 상응하는 대가가 있는데, (예를 들어 코드가 복잡해지거나 메모리를 사용하거나, 유지보수가 어려워지는 등) 모든 함수에 useCallback을 사용하는 것은 오히려 성능을 악화시킬 수 있다.

따라서, useCallback를 사용하기 전에 실질적으로 얻을 수 있는 성능 이점이 어느 정도인지 반드시 예상을 해보고 사용하는 것이 좋다고 한다.

 

 

useCallback을 사용하지 말아야 할 경우

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

 

🟢 useCallback을 사용해야 하는 경우

  • 자식 컴포넌트에서 useEffect가 반복적으로 트리거 되거나, 무한 루프에 빠질 위험이 있을 때 useCallback을 사용하자
  • 자식 컴포넌트에 함수를 props로 넘길 때, 불필요한 렌더링이 일어난다고 판단된다면 useCallback으로 함수 동등성을 유지해주자.
  • 함수 자체가 매우 복잡하거나, 다시 계산하는데 비용이 많이 드는 경우에 useCallback을 사용하자.

 

참고자료

- https://yceffort.kr/2022/04/best-practice-useCallback-useMemo

- https://db2dev.tistory.com/entry/React-%EB%98%91%EB%98%91%ED%95%98%EA%B2%8C-useCallback-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

- https://velog.io/@khy226/useMemo%EC%99%80-useCallback-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0

'Front-end' 카테고리의 다른 글

2023년 기술 블로그 회고  (1) 2024.01.01
Web Worker  (0) 2024.01.01
Redux-saga  (0) 2024.01.01
Redux vs Zustand  (0) 2023.12.13
코드 스플리팅(Code Splitting)  (0) 2023.12.02