useMemo, useCallback 바르게 알고 사용하기

useMemo란?

useMemo는 리액트 컴포넌트 내에서 메모이제이션(Memoization) 기술을 사용하여 성능을 높여줄 수 있도록 지원하는 Hook이다.

첫 문장부터 막막하다… 메모이제이션이 도대체 뭘까?

Memoization(메모이제이션)

메모이제이션은 동일한 계산이 반복되는 경우, 초기의 계산 결과를 메모리에 저장한 뒤, 필요에 따라 활용할 수 있도록 활용하는 기법이다. 즉, 우리는 메모이제이션을 통해 중복 계산을 방지하고 메모리라는 공간을 활용해 계산에 소요되는 시간을 줄일 수 있는 것이다.

따라서 우리는 이러한 메모이제이션 기법을 useMemo() Hook을 통해 활용할 수 있고, 리액트 컴포넌트의 성능을 최적화 또는 향상시킬 수 있다.

그럼 이제 useMemo에 대해 자세히 알아보도록 하자.

기본 형태

useMemo() Hook의 기본 구조는 아래와 같다.

1
const memoizedData = useMemo(function, [dependencies]);

의존하고 있는 값이 변경될 때만 function이 실행된다

컴포넌트 최초 렌더링 시에 useMemo는 선언된 function을 실행하고 그 결과값을 memoize한다. 그리고 컴포넌트가 리렌더링 됐더라도 뒤에 선언된 의존성 배열(dependencies)에 변화가 없다면 useMemofunction을 실행시키지 않는다. 하지만 만약 dependencies에 변화가 생겼다면 useMemo는 함수를 실행시켜 새로운 값을 memoize한다.

즉, memoize된 값은, dependency가 변경되지 않으면 유지된다.

이게 useMemo의 핵심이다. dependencies에 들어가는 것은 주로 props 또는 state가 될 것이므로, 우리는 state나 props가 활용되어 계산되는 특정 값들을 memoize하여 활용할 수 있는 것이다. 예를 들면 state 배열의 길이라던지, 마지막에 저장된 state의 id값이라던지… 이런 것들을 렌더링마다 매번 계산하지 않아도 된다.

그럼 이제 아래에서 useMemo의 활용 방법 및 예시를 훑어보도록 하자.


useMemo의 활용

친구의 수를 세는 컴포넌트가 아래와 같이 있다고 가정을 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export default function Friend() {
const [friends, setFriends] = useState([0, 1, 2, 3, 4]);
const [dummy, setDummy] = useState(0);
// 1. 컴포넌트 렌더링마다 이 컴포넌트는 친구의 수를 센다.
const friendLength = friendCount(friends);
// 3. 친구 추가 버튼을 누르면 임의의 친구 0이 추가된다.
const handleAddFriend = () => {
setFriends([...friends, 0]);
};
// 4. 리렌더링 버튼을 클릭하면 dummy state가 변경되며 리렌더링을 야기한다.
const handleRerender = () => setDummy((el) => el + 1);
return (
<div>
당신의 친구는 총<span> {friendLength}</span>명입니다.
<button onClick={handleAddFriend}>친구 추가</button>
<button onClick={handleRerender}>리렌더링</button>
</div>
);
}

function friendCount(friend) {
// 2. 친구의 수를 세고 아래와 같은 문구를 출력하며 친구 수를 리턴한다.
console.log('친구 수 세기 완료!');
return friend.length;
}

위의 경우에서 우리는 두 개의 state를 갖고있다. 하나는 친구목록과 하나는 그냥 의미 없는 dummy state다. 그리고 컴포넌트가 렌더링 되면 friends라는 state를 활용하여 친구의 수를 계산하게 된다. 하지만 코드를 실행시켜보면 리렌더링을 클릭했을 때, 친구의 수에는 변화가 전혀 없음에도 친구 수를 다시 계산하고 “친구 수 세기 완료!” 문구가 출력되는 것을 볼 수 있다.

얼마나 비효율적인 일인가? 친구 수를 다시 셀 필요가 없는데도 우리는 친구 수를 렌더링이 될 때마다 계산을 하고 있는 것이다. 이 때문에 우리는 useMemo를 활용해야 하는 것이다.

하지만 위와 같이 firendLength를 계산받는 코드를 useMemo를 통해 수정하면, 리렌더링 버튼을 눌러도 친구 목록에 변화가 없는 한, 계산이 불필요하게 실행되지 않는 것이다. (물론 위의 예시는 여러번 해도 상관 없는 가벼운 계산이지만 ^^..)


useCallback

useCallback은 useMemo와 비슷하게, 함수 메모이제이션을 위해 사용되는 Hook이다.

따라서 이러한 useCallbackuseMemo와 매우 유사하다. 이에 따라 두 Hook의 차이점을 분명히 짚고 넘어가야 한다.

  1. 반환값

    Hook 무엇을 반환하는가?
    useMemo() “값”
    useCallback() “함수”
  2. 인자를 받을 수 있는가?

    useMemo는 값을 저장하기 위한 Hook이기 때문에 함수 안에 인자를 활용할 수 없다. 하지만 useCallback은 함수를 반환하기에 아래와 같이 인자를 활용할 수 있다.

    1
    2
    3
    useCallback(foo(bar), []);

    // 하지만 useMemo에서 인자가 사용되어도 React는 이를 무시한다.

근데 그렇다고 하더라도… useCallbackuseMemo는 너무 비슷해 보인다. 실제로 useCallback은 거의 모든 상황에서 useMemo를 대체할 수 있다. 실제로 리액트 공식 문서에서는 아래와 같이 useMemouseCallback의 공통점을 인정한다.

그리고 공식문서에 따르면, useMemo가 조만간 삭제될 수도 있다고 한다…

useCallback의 활용

“컴포넌트가 리렌더링 될 때마다 함수가 재생성되는 것을 방지하고자 사용된다”

즉, 위와 같이 함수가 의존하고 있는 값(stateprops)가 변경되지 않으면 이전에 생성해뒀던 함수를 재사용하는 방식으로 메모이제이션을 한다. 하지만… 사실 자바스크립트의 속도는 굉장히 빠르고, 요즘 브라우저의 성능이 굉장히 좋아졌기 때문에, 렌더링마다 함수가 재선언 되는 것은 성능상에 큰 문제가 되지 않는다.

그렇다면 왜, 그리고 언제 useCallback을 사용해야 할까?

useEffect의 의존성 배열에 함수가 포함될 때

골자부터 설명하자면, 자바스크립트에서 함수는 객체다.
따라서 객체라는 “참조타입 자료형”은 메모리 주소에 대한 비교를 통해 동등성을 비교한다는 점을 기억하도록 하자.

바로 코드를 통해 “왜 useEffect에서 특정 함수가 의존성 배열에 포함될 때 useCallback이 필요한지”에 대해 설명해보도록 하겠다.

위와 같은 코드가 존재한다고 가정해보자.
코드의 기본적인 로직은 이하와 같다.

즉, 함수는 객체이기 때문에 컴포넌트가 리렌더링 될 때마다 참조하는 메모리의 주소가 변하게 되어, useEffect에서의 의존성 배열은 함수가 계속 변경된다고 감지하는 것이다.

따라서, useEffect와 같은 의존성 배열 내에 특정 함수를 넣고싶다면 반드시 함수를 useCallback으로 감싸서 메모이제이션을 해줘야 한다.


useCallback과 useMemo를 사용할 때 주의해야 할 점

기본적으로 useMemo, useCallback과 같은 메모이제이션 Hook은 초기 렌더링 비용을 굉장히 많이 차지한다.

실제로, 이 아티클에 따르면 useMemo를 사용했을 경우에는 초기 렌더링에 투자되는 시간 비용이 굉장히 커진다고 한다.

위 사진만 봐도, 특정 함수에 input으로 들어가는 N값이 굉장히 커진다면, 초기 렌더링 비용이 커지는 대신, 그 이후의 비용은 현저히 줄어든다는 것을 확인할 수 있다.

하지만 반대로, useMemo를 사용하지 않았고, 연산이 그렇게 크지 않은(n=1) 경우에는 오히려 초기 렌더링과 리렌더링 사이의 성능 차이가 그렇게 크지 않다. 따라서 useMemo를 연산이 복잡하거나 크지 않은 경우에 사용하게되면 오히려 초기렌더링에 소모되는 시간만 높이는 비효율성을 낳을 수 있는 것이다.

이 때문에, 우리는 useMemo와 useCallback을 사용할 때 반드시 시간적인 효율성을 고려해야 하며,
정말 memoize를 해야할 만큼 복잡한 연산인지 등의 적절성을 반드시 사전에 잘 판단해야 한다.


참조 자료

무지성 useMemo, 멈춰!

React Hooks: useCallback 사용법


useMemo, useCallback 바르게 알고 사용하기

https://hoonjoo-park.github.io/react/useMemoCallback/

Author

Hoonjoo

Posted on

2022-06-11

Updated on

2022-06-11

Licensed under

Comments