useMemo, useCallback 바르게 알고 사용하기
useMemo란?
useMemo
는 리액트 컴포넌트 내에서 메모이제이션(Memoization
) 기술을 사용하여 성능을 높여줄 수 있도록 지원하는 Hook이다.
첫 문장부터 막막하다… 메모이제이션이 도대체 뭘까?
Memoization(메모이제이션)
메모이제이션은 동일한 계산이 반복되는 경우, 초기의 계산 결과를 메모리에 저장한 뒤, 필요에 따라 활용할 수 있도록 활용하는 기법이다. 즉, 우리는 메모이제이션을 통해 중복 계산을 방지하고 메모리라는 공간을 활용해 계산에 소요되는 시간을 줄일 수 있는 것이다.
따라서 우리는 이러한 메모이제이션 기법을
useMemo()
Hook을 통해 활용할 수 있고, 리액트 컴포넌트의 성능을 최적화 또는 향상시킬 수 있다.
그럼 이제 useMemo에 대해 자세히 알아보도록 하자.
기본 형태
useMemo() Hook의 기본 구조는 아래와 같다.
1 | const memoizedData = useMemo(function, [dependencies]); |
“의존하고 있는 값이 변경될 때만 function이 실행된다”
컴포넌트 최초 렌더링 시에 useMemo
는 선언된 function
을 실행하고 그 결과값을 memoize한다. 그리고 컴포넌트가 리렌더링 됐더라도 뒤에 선언된 의존성 배열(dependencies)에 변화가 없다면 useMemo
는 function
을 실행시키지 않는다. 하지만 만약 dependencies에 변화가 생겼다면 useMemo
는 함수를 실행시켜 새로운 값을 memoize한다.
즉, memoize된 값은, dependency가 변경되지 않으면 유지된다.
이게 useMemo
의 핵심이다. dependencies에 들어가는 것은 주로 props 또는 state가 될 것이므로, 우리는 state나 props가 활용되어 계산되는 특정 값들을 memoize하여 활용할 수 있는 것이다. 예를 들면 state 배열의 길이라던지, 마지막에 저장된 state의 id
값이라던지… 이런 것들을 렌더링마다 매번 계산하지 않아도 된다.
그럼 이제 아래에서 useMemo
의 활용 방법 및 예시를 훑어보도록 하자.
useMemo의 활용
친구의 수를 세는 컴포넌트가 아래와 같이 있다고 가정을 해보자.
1 | export default function Friend() { |
위의 경우에서 우리는 두 개의 state
를 갖고있다. 하나는 친구목록과 하나는 그냥 의미 없는 dummy state
다. 그리고 컴포넌트가 렌더링 되면 friends
라는 state
를 활용하여 친구의 수를 계산하게 된다. 하지만 코드를 실행시켜보면 리렌더링을 클릭했을 때, 친구의 수에는 변화가 전혀 없음에도 친구 수를 다시 계산하고 “친구 수 세기 완료!” 문구가 출력되는 것을 볼 수 있다.
얼마나 비효율적인 일인가? 친구 수를 다시 셀 필요가 없는데도 우리는 친구 수를 렌더링이 될 때마다 계산을 하고 있는 것이다. 이 때문에 우리는 useMemo를 활용해야 하는 것이다.
하지만 위와 같이 firendLength
를 계산받는 코드를 useMemo
를 통해 수정하면, 리렌더링 버튼을 눌러도 친구 목록에 변화가 없는 한, 계산이 불필요하게 실행되지 않는 것이다. (물론 위의 예시는 여러번 해도 상관 없는 가벼운 계산이지만 ^^..)
useCallback
useCallback은 useMemo와 비슷하게, 함수 메모이제이션을 위해 사용되는 Hook이다.
따라서 이러한 useCallback
은 useMemo
와 매우 유사하다. 이에 따라 두 Hook의 차이점을 분명히 짚고 넘어가야 한다.
반환값
Hook 무엇을 반환하는가? useMemo() “값” useCallback() “함수” 인자를 받을 수 있는가?
useMemo
는 값을 저장하기 위한 Hook이기 때문에 함수 안에 인자를 활용할 수 없다. 하지만useCallback
은 함수를 반환하기에 아래와 같이 인자를 활용할 수 있다.1
2
3useCallback(foo(bar), []);
// 하지만 useMemo에서 인자가 사용되어도 React는 이를 무시한다.
근데 그렇다고 하더라도… useCallback
과 useMemo
는 너무 비슷해 보인다. 실제로 useCallback
은 거의 모든 상황에서 useMemo
를 대체할 수 있다. 실제로 리액트 공식 문서에서는 아래와 같이 useMemo
와 useCallback
의 공통점을 인정한다.
그리고 공식문서에 따르면, useMemo
가 조만간 삭제될 수도 있다고 한다…
useCallback의 활용
“컴포넌트가 리렌더링 될 때마다 함수가 재생성되는 것을 방지하고자 사용된다”
즉, 위와 같이 함수가 의존하고 있는 값(state
나 props
)가 변경되지 않으면 이전에 생성해뒀던 함수를 재사용하는 방식으로 메모이제이션을 한다. 하지만… 사실 자바스크립트의 속도는 굉장히 빠르고, 요즘 브라우저의 성능이 굉장히 좋아졌기 때문에, 렌더링마다 함수가 재선언 되는 것은 성능상에 큰 문제가 되지 않는다.
그렇다면 왜, 그리고 언제
useCallback
을 사용해야 할까?
useEffect의 의존성 배열에 함수가 포함될 때
골자부터 설명하자면, 자바스크립트에서 함수는 객체다.
따라서 객체라는 “참조타입 자료형”은 메모리 주소에 대한 비교를 통해 동등성을 비교한다는 점을 기억하도록 하자.
바로 코드를 통해 “왜 useEffect
에서 특정 함수가 의존성 배열에 포함될 때 useCallback
이 필요한지”에 대해 설명해보도록 하겠다.
위와 같은 코드가 존재한다고 가정해보자.
코드의 기본적인 로직은 이하와 같다.
즉, 함수는 객체이기 때문에 컴포넌트가 리렌더링 될 때마다 참조하는 메모리의 주소가 변하게 되어, useEffect
에서의 의존성 배열은 함수가 계속 변경된다고 감지하는 것이다.
따라서, useEffect
와 같은 의존성 배열 내에 특정 함수를 넣고싶다면 반드시 함수를 useCallback
으로 감싸서 메모이제이션을 해줘야 한다.
useCallback과 useMemo를 사용할 때 주의해야 할 점
기본적으로 useMemo, useCallback과 같은 메모이제이션 Hook은 초기 렌더링 비용을 굉장히 많이 차지한다.
실제로, 이 아티클에 따르면 useMemo를 사용했을 경우에는 초기 렌더링에 투자되는 시간 비용이 굉장히 커진다고 한다.
위 사진만 봐도, 특정 함수에 input으로 들어가는 N값이 굉장히 커진다면, 초기 렌더링 비용이 커지는 대신, 그 이후의 비용은 현저히 줄어든다는 것을 확인할 수 있다.
하지만 반대로, useMemo를 사용하지 않았고, 연산이 그렇게 크지 않은(n=1) 경우에는 오히려 초기 렌더링과 리렌더링 사이의 성능 차이가 그렇게 크지 않다. 따라서 useMemo를 연산이 복잡하거나 크지 않은 경우에 사용하게되면 오히려 초기렌더링에 소모되는 시간만 높이는 비효율성을 낳을 수 있는 것이다.
이 때문에, 우리는 useMemo와 useCallback을 사용할 때 반드시 시간적인 효율성을 고려해야 하며,
정말 memoize를 해야할 만큼 복잡한 연산인지 등의 적절성을 반드시 사전에 잘 판단해야 한다.
참조 자료
useMemo, useCallback 바르게 알고 사용하기