React 애플리케이션에서 성능 최적화는 매우 중요하다. 컴포넌트가 복잡해지고 상태나 props의 변경이 빈번해질수록 불필요한 연산이 반복될 가능성이 높아진다. 이러한 문제를 해결하기 위한 도구로 React는 useMemo와 useCallback 훅을 제공한다. 이 두 훅은 주로 계산 비용이 많이 드는 연산을 memoization하여 불필요한 연산을 방지하는 데 사용된다. 특히, 클래스 컴포넌트에서 사용하던 shouldComponentUpdate 메서드를 대체할 수 있는 기능을 제공한다.
useMemo: 복잡한 계산 결과를 memoization
useMemo는 memoization을 통해 계산 비용이 많이 드는 연산을 최적화한다. memoization이란 동일한 계산이 반복될 때, 이전에 계산된 결과를 저장해두고 이를 재사용하여 불필요한 계산을 피하는 기법이다. useMemo는 컴포넌트가 리렌더링될 때마다 그 값을 다시 계산하는 대신, 의존성 배열에 명시된 값이 변경될 때만 해당 계산을 다시 실행한다.
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
위 코드에서 useMemo는 getFilteredTodos 함수의 결과를 memoization한다. 즉, todos나 filter가 변경되지 않으면, getFilteredTodos 함수는 다시 호출되지 않고 이전에 계산된 값을 반환한다. 이를 통해 연산 비용이 큰 함수의 반복 실행을 피할 수 있다. 그러나 useMemo는 첫 번째 렌더링을 더 빠르게 만들지는 않는다. 첫 렌더링 시에는 여전히 계산이 이루어지기 때문에, 주로 업데이트 시 불필요한 계산을 방지하는 데 효과적이다.
useMemo를 사용하는 경우
useMemo는 주로 비용이 큰 연산이나 리스트 필터링과 같은 작업에서 사용된다. 특히, 다음과 같은 경우에 유용하다.
- 배열의 필터링, 정렬 등 복잡한 연산이 여러 번 수행되는 경우
- 렌더링 성능을 개선하고 싶을 때
- 특정 상태나 props가 변경되지 않으면 동일한 결과를 재사용하고 싶을 때
function TodoList({ todos, filter }) {
const visibleTodos = useMemo(() => {
console.time('filter array');
const result = getFilteredTodos(todos, filter);
console.timeEnd('filter array');
return result;
}, [todos, filter]);
return (
<ul>
{visibleTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
위 예시에서 useMemo는 todos와 filter가 변경될 때만 getFilteredTodos 함수를 호출하고, 그 외에는 이전에 계산된 visibleTodos 값을 사용한다.
그러나 useMemo는 캐싱하는 과정과 메모리를 많이 사용하기에 무조건적으로 쓴다고 좋아지는 것은 아니다. 이럴때 console.time을 이용해 연산 시간을 측정하여 useMemo를 사용하는 게 좋을지 판단에 도움을 받을 수 있다.
console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');
예를 들어 위 코드를 실행하면 콘솔에 "filter array: 0.15ms"와 같은 로그가 나타난다. 전체 기록된 시간이 유의미한 값으로 누적된다면 (예: 1ms 이상), 그 계산을 memoization하는 게 좋을 수 있다.
useCallback: 이벤트 핸들러를 memoization
useCallback은 함수를 memoization하는 데 사용된다. 컴포넌트가 리렌더링될 때마다 함수도 다시 생성되는데, 함수가 다시 생성되면 하위 컴포넌트로 전달된 함수가 동일한 로직을 가지더라도 매번 새로운 함수로 인식된다. 이로 인해 불필요한 렌더링이 발생할 수 있다. useCallback을 사용하면 이러한 문제를 방지할 수 있다.
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
useCallback은 두 번째 인자로 전달된 의존성 배열의 값이 변경되지 않는 한, 같은 함수 인스턴스를 반환한다. 이를 통해 불필요한 함수 생성과 불필요한 하위 컴포넌트의 리렌더링을 방지할 수 있다.
useCallback을 사용하는 경우
useCallback은 주로 컴포넌트가 동일한 함수 인스턴스를 필요로 할 때 사용된다. 특히, 다음과 같은 경우에 유용하다.
- 함수가 하위 컴포넌트로 props로 전달될 때
- 함수가 리렌더링을 트리거하지 않고 동일한 로직을 유지해야 할 때
function ParentComponent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<ChildComponent increment={increment} />
<p>Count: {count}</p>
</div>
);
}
function ChildComponent({ increment }) {
console.log('Child component re-rendered');
return <button onClick={increment}>Increment</button>;
}
위 예시에서 useCallback을 사용하여 increment 함수가 매번 새로운 함수로 생성되지 않도록 했다. 이로 인해 ChildComponent는 불필요한 리렌더링을 방지할 수 있게 된다.
'React' 카테고리의 다른 글
React Hook을 사용할 때 실수들 (2): useEffect (2) | 2024.09.10 |
---|---|
React Hook을 사용할 때 실수들 (1): useState (1) | 2024.09.10 |
React Hook 이해하기 (3): useRef (0) | 2024.09.10 |
React Hook 이해하기 (2): useEffect (0) | 2024.09.10 |
React Hook 이해하기 (1): useState (0) | 2024.09.10 |