React를 사용하면서 다양한 hooks api들을 사용하게 되는데 그 중에 최적화를 위해 사용하는 useMemo, useCallback에 대해서 정리해보려고 한다.
먼저, useMemo 함수에 대해서 알아보기 전에 메모이제이션(memoization) 개념에 대해서 알고 가야한다. memoization이란 기존에 수행한 연산이 결과값을 어딘가에 저장해두고 동일한 입력들이 들어오면 재활용하는 프로그래밍 기법을 말한다. memoization을 적절히 적용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화 할 수 있다.
useMemo와 useCallback은 각각 메모이제이션된 값, 함수를 반환한다.
- 자신의 state가 변경될 때
- 부모가 컴포넌트로부터 전달받은 props가 변경될 때
- 부모 컴포넌트가 리렌더링 될 때
https://ko.reactjs.org/docs/hooks-reference.html#usememo
메모이제이션된 '함수'을 반환한다.
function Component() {
const [count, setCount] = React.useState(0);
const onClick = () => console.log("I'm clicked!");
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<Button onClick={onClick} />
</>
)
}
다음과 같이 숫자를 클릭하면 숫자가 1씩 증가하는 앱이 있다. 아래에는 Button이라는 컴포넌트가 있다.
숫자를 클릭했을 때 Button 컴포넌트는 어떻게 될까?
function Button() {
return (
<button>클릭</button>
);
}
Button 컴포넌트는 props를 가지고 있지 않음에도 React의 리렌더링 조건 중 '3. 부모 컴포넌트가 리렌더링 될 때'라는 조건에 따라 버튼이 클릭될 때마다 리렌더링 된다.
간단한 앱에서는 버튼에 불필요한 리렌더링이 많이 일어나도 성능에 문제가 생기지 않겠지만, Button이라는 컴포넌트가 복잡하고 고연산을 담당하는 컴포넌트였다면? 리렌더링을 최대한 줄이는 게 좋을 것이다.
onClick 함수를 useCallback으로 감싸주게 되면, 불필요한 리렌더링을 막을 수 있다.
const onClick = useCallback(() => console.log("I'm clicked!"), [])
useCallback을 사용함으로써 deps가 바뀔 경우를 제외하고 불필요한 리렌더링을 하지 않는다.
여기서 deps란 useCallback 함수에 비어있는 2번 째 인자인데, 위의 onClick 함수는 아무런 의존성이 없기 때문에 비어있지만 함수 내부에 의존하는 상태 값이 있다면 아래와 같이 deps에 명시해야 한다.
const handleClick = React.useCallback(
() => console.log('current count :' + count),
[count])
만약 deps에 명시해두지 않는다면, 바뀐 count 값을 인지하지 못하고 이전 값을 계속 출력하게 된다.
메모이제이션된 '값'을 반환한다.
useMemo도 useCallback과 매우 유사하게 최적화에 사용된다. 단지, useCallback은 함수를 반환하는 반면, useMemo는 값을 반환한다.
function Component() {
const [count, setCount] = React.useState(0)
const doubleCount = count * 2
console.log(doubleCount)
return (
<>
<button onClick={() => setCount(count + 1)}>Up</button>
</>
)
}
버튼을 클릭할 때마다 doubleCount, 두배로 계산한 값을 출력하게 된다. 하지만 만약에 count값과 무관하게 컴포넌트가 재렌더링 되는 상황이 생겼다면, doubleCount는 불필요한 연산을 하게 되는 것이다. 컴포넌트의 상태값이 많고 복잡한 연산의 경우 최적화가 좋지 않다.
const doubleCount = useMemo(() => count * 2, [count])
위처럼 useMemo를 사용하면 deps인 count가 변경될 때에만 연산하므로 최적화 측면에서 좋다. useCallback과 마찬가지로 deps에 의존값을 반드시 명시해야 한다.
useMemo와 useCallback을 적절한 곳에 사용하면 컴포넌트 렌더링 최적화에 큰 도움이 될 수 있다.
그러나, 무분별하게 사용하게 되면 오히려 이전보다 사용하는 코드와, 메모이제이션용 메모리가 추가로 필요하게 되어 성능적으로 더 안좋아질 수도 있으니 적절하게 사용하자.