useCallback과 React.memo로 컴포넌트 최적화 하기
배운 바를 기록하고 프로젝트에 적용하기 😵
1. useMemo
2. useCallback
3. React.memo
useMemo(( ) => 값, deps)
useMemo는 요약하자면 값을 캐싱하는 함수입니다. 두 번째 인자로 받는 deps 배열에 들어 있는 값 또는 객체가 변하지 않는다면 이전에 만들어 놓은 값을 재사용합니다. 바로 예제를 통해 알아보겠습니다.
users
객체에 active
가 참인 user의 숫자를 계산하는 함수가 있다고 가정하겠습니다. users
객체는 useState로 관리하고 있으며, user
의 이름을 클릭할 경우 해당 user
의 active
속성값을 반전합니다. 그리고 active user의 수를 계산해주는 함수를 작성하고 해당 함수가 호출될 때마다 콘솔창에서 알 수 있도록 “계산 중입니다.” 라고 로그를 찍어주겠습니다.
렌더링된 결과에서 user의 이름을 눌러 active가 잘 반전되는지 확인해보겠습니다.
정상적으로 동작하고 active users의 수도 바뀌는 것을 확인할 수 있습니다. 이번엔 countActiveUsers 함수가 호출되는 지 확인해보겠습니다.
잘 호출되고 있습니다. 2번 호출되는 것은 컴포넌트가 mount될 때 한 번 그리고 user 이름을 클릭했을 때 한 번 호출되기 때문입니다.
지금까지는 정상적인 동작 과정이지만 문제는 해당 컴포넌트에 input과 같이 컴포넌트 내에 다른 state를 업데이트하는 요소가 추가됐을 경우입니다. input을 예로 들겠습니다. 우리는 일반적으로 input의 value를 관리할 state를 만들고 onChange 이벤트 핸들러에 함수를 전달하여 해당 value를 관리합니다.
input value를 변경할 때마다 value를 관리하는 state가 업데이트될 것이고 이로 인해 리렌더링이 발생합니다. 이때 발생한 리렌더링으로 인해 users 객체가 업데이트 되지 않았음에도 countActiveUsers가 매번 호출됩니다.
abc를 입력하는 동안 총 3번 호출됐습니다. countActiveUsers
함수가 워낙 작은 연산이라 예제에서는 영향이 거의 없지만 큰 연산을 하는 함수라면 performance에 영향을 줄지도 모릅니다. 이럴 때 useMemo를 사용하여 값을 캐싱하고 deps를 통해 값이 변할 때를 조절하면 됩니다.
변경 전const activeUsers = countActiveUsers(users);
변경 후const activeUsers = useMemo(() => countActiveUsers(users), [users]);
이젠 input value를 변경해도 users 객체가 업데이트되지 않았기 때문에 countActiveUsers
가 호출되지 않습니다.
useCallback
useMemo와 비슷하지만 useCallback은 함수를 위한 Hook입니다. 값 대신에 함수 자체를 캐싱합니다. 이후에 정리할 React.memo와 함께 useCallback은 컴포넌트 리렌더링 성능 최적화에 유용하게 사용합니다. React.memo와 useCallback으로 컴포넌트 props가 변하지 않았다면 렌더링을 다시 하지 않게끔 해줄 겁니다.
useCallback으로 감싼 함수는 렌더링 될 때마다 함수를 생성을 하긴 하지만 deps에 저장된 객체가 업데이트 됐을 때가 아니라면 생성된 함수를 무시하고 이전에 생성된 함수를 재사용합니다. state 혹은 props를 useCallback 안에서 사용해야 한다면 이 또한 deps에 넣어줘야 합니다.
useMemo나 useCallback에서 deps를 설정해주는 이유는 Hook 내의 함수 호출 타이밍을 조절하기 위함도 있지만, 함수 내에서 사용하는 값의 최신 상태를 보장하기 위함도 있습니다. state
혹은 props
를 함수 내에서 사용하고 있는데 deps에 해당 값이나 객체가 들어있지 않다면 업데이트 이전 값을 참조하고 있을지도 모르기 때문입니다. deps에 특정 값을 넣어서 해당 값이 업데이트될 때 캐싱된 함수를 버리고 새로운 함수를 만들어서 최신 값을 바라보게 해야 합니다.
useCallback 예제는 input에서 사용하는 onChange 함수를 가지고 진행해보겠습니다. 사용 방법은 간단합니다. 함수를 useCallback으로 감싸고 deps를 설정해주면 됩니다.
변경한 onChange 함수는 inputs 객체가 업데이트 되지 않는 이상 캐싱된 함수를 이용하게 될 겁니다. useCallback을 이용하여 캐싱된 함수를 사용하게 만들었다고 해서 눈에 띄는 변화가 생기진 않습니다. (모바일 브라우저조차도 사용자가 이러한 변화를 느끼지 못할 만큼 빠른 연산을 진행하기 때문입니다) 하지만 React.memo와 함께 사용한다면 더 좋은 결과를 만들어 낼 수 있습니다.
React.memo
React.memo는 useMemo, useCallback와 비슷하게 컴포넌트의 props 가 바뀌지 않았다면, 컴포넌트의 리렌더링을 방지할 수 있습니다. 컴포넌트 props 객체의 변경 사항이 없다면 컴포넌트의 리렌더링이 불필요한 상황이니 이전에 렌더링 했던 결과를 재사용하는 방식입니다.
먼저 React.memo가 필요한 상황을 알아보기 위해서 맨 처음 나왔던 BlogSample 예제를 조금 변형하겠습니다.
기존의 예제에 input을 추가하고 input value를 관리할 state
그리고 업데이트할 함수를 작성했습니다. value를 업데이트하면 컴포넌트가 리렌더링 될텐데 value state와 연관이 없는 Users 컴포넌트가 또 다시 렌더링 되는 상황이 마음에 들지 않습니다. 이럴 때 다음과 같이 React.memo를 컴포넌트에 사용하여 props 객체의 변화가 없는 한 이전에 렌더링한 결과를 재사용할 수 있습니다.
정말로 input value가 변할 때 Users 컴포넌트는 렌더링 되지 않는지 확인해 보겠습니다.
예제를 위해 한 곳에서 작성하다 보니 Anonymous로 나왔지만 Users 컴포넌트입니다. BlogSample이 리렌더링 되는 동안 렌더링 되지 않았다는 걸 확인할 수 있습니다.
이번엔 조금 다른 예제입니다. 기존 예제에 user
정보를 props로 받아서 출력하는 User 컴포넌트를 추가하고 handleClick 함수를 useCallback으로 감싼 뒤 deps에 users
객체를 넣어주겠습니다. 그리고 User 컴포넌트가 렌더링 되는지 알기 위해서 props로 받은 id값을 출력해 보겠습니다.
이제 렌더링된 결과에서 user
의 이름을 클릭해 보겠습니다.
결과를 보면 뭔가 이상합니다. 분명 이름이 로키인 user
를 눌렀는데 모든 User 컴포넌트가 리렌더링 됩니다. react devTools profiler로 한번 살펴봐도 모든 컴포넌트가 다시 렌더링 되고 있습니다.
이러한 현상은 handleClick이 deps에 users
객체를 가지고 있기 때문입니다. 이름을 클릭하여 users
객체가 업데이트됐기 때문에 handleClick 함수를 가지고 있는 모든 컴포넌트가 다시 렌더링 됩니다.
우리가 변경한 user
객체는 1개뿐인데 변경하지 않은 user
객체를 가지고 있는 컴포넌트들도 렌더링 되는 상황이 마음에 들지 않습니다.
이 상황을 해결하기 위해 User 컴포넌트도 React.memo로 감싸주고 handleClick deps에 존재하는 users
객체를 지워주겠습니다. users
객체를 지웠으니 state
의 최신 상태를 보장하기 위해 함수형 업데이트를 사용하겠습니다.
함수형 업데이트를 하게 되면, setState
에 등록하는 콜백함수의 파라미터에서 최신 users
를 참조 할 수 있기 때문에 deps에 users
를 넣지 않아도 됩니다.
profiler를 통해 확인해보면 이전과는 다르게 active
상태가 변경된 user
객체를 props로 가지는 컴포넌트만 다시 렌더링 됐습니다.
이러한 방법을 통해 컴포넌트의 성능을 향상시킬 수 있습니다. 다만 모든 컴포넌트에 이런 방식을 적용하는 건 좋지 않을 수도 있습니다. 오히려 더 많은 코드를 실행하는 결과를 맞이할 수도 있으니 최적화가 가능한 컴포넌트에 적용하는 것이 좋습니다.