리액트는 선언적입니다. 상태(State)가 변하면 컴포넌트는 다시 그려집니다. 이 과정은 리액트의 핵심 철학이지만, 서비스 규모가 커지면 '불필요한 리렌더링'이 앱의 반응성을 떨어뜨리는 주범이 됩니다. 오늘 포스트에서는 성능 최적화의 두 기둥인 useMemo와
useCallback을 언제 사용해야 하고, 언제 지양해야 하는지 깊이 있게 분석해 보겠습니다.
1. 리액트의 렌더링 매커니즘 이해하기
최적화를 시작하기 전, 우리는 리액트가 언제 렌더링을 수행하는지 알아야 합니다. 기본적으로 리액트 컴포넌트는 부모 컴포넌트가 렌더링되면 자식 컴포넌트도 무조건 다시 렌더링됩니다. 비록 자식의 Props가 바뀌지 않았더라도 말이죠.
여기서 우리는 값의 참조 동일성(Referential Equality)이라는 문제에 직면합니다. 자바스크립트에서 객체(Object)나 함수(Function)는 렌더링 시마다 새로운 메모리 주소에 할당됩니다. 리액트는 shallow compare를 통해 이전과 현재의 Props를 비교하는데, 매번 새로 생성되는
객체는 리액트 입장에서 "새로운 값"으로 인식되어 리렌더링을 유발합니다.
2. useMemo: 값의 재사용
useMemo는 연산 비용이 많이 드는 계산 결과값을 메모리에 저장(Memoization)해두고 재사용할 때 사용합니다. 의존성 배열(Dependency Array)의 값이 변하지 않았다면, 리액트는 함수를 다시 실행하지 않고 이전에 저장된 값을 반환합니다.
const expensiveValue = useMemo(() => {
return performHardCalculation(data);
}, [data]); // data가 변경될 때만 재계산
언제 사용해야 할까요?
- 무거운 연산: 데이터 필터링, 정렬, 복잡한 통계 계산 등 CPU 소모가 큰 작업을 할 때.
- 참조 동일성 유지: 하위 컴포넌트에 객체 형태의 Props를 전달할 때, 해당 객체가 리렌더링마다 새로 생성되어 자식의
React.memo를 무력화하는 경우.
3. useCallback: 함수의 재사용
useCallback은 함수 자체를 메모이제이션합니다. useMemo가 리턴값을 저장한다면, useCallback은 함수 정의를 저장하는 것이죠.
const handleClick = useCallback(() => {
console.log("Clicked:", id);
}, [id]); // id가 바뀔 때만 새로운 함수를 생성
useCallback 그 자체로는 성능을 개선하지 않습니다. 오히려 함수를 정의하고 의존성 배열을 비교하는 추가 연산이 발생합니다. 이 훅이 진짜 힘을 발휘하는 순간은 React.memo를 사용하는 자식 컴포넌트에 함수를 props로 넘겨줄 때입니다.
4. React.memo와의 환상적인 궁합
최적화의 완성은 React.memo와의 조합입니다. React.memo는 컴포넌트 수준에서 메모이제이션을 수행하며, Props가 변하지 않았다면 렌더링 결과물을 재사용합니다. 이때 useCallback으로 감싸지 않은 함수를 Props로 넘기면, 부모가 렌더링될 때마다 자식도 '무조건'
리렌더링되므로 React.memo가 무용지물이 됩니다.
5. 최적화의 함정: Premature Optimization
도널드 크누스는 "조기 최적화(Premature Optimization)는 모든 악의 근원이다"라고 말했습니다. 모든 함수와 모든 값을 메모이제이션하는 것은 코드 가독성을 해치고, 의존성 배열 관리 난이도를 높이며, 오히려 메모리 점유율을 높입니다.
최적화가 필요 없는 경우:
- 단순한 기본 타입(String, Number) 값을 계산할 때.
- 자식 컴포넌트가
React.memo로 감싸져 있지 않아, 어차피 부모와 함께 리렌더링될 때. - 컴포넌트의 렌더링 비용이 충분히 가벼울 때.
마치며: 측정하고 최적화하라
진정한 최적화는 감이 아닌 데이터에 기반해야 합니다. Chrome 개발자 도구의 React Profiler 탭을 열어보세요. 어떤 컴포넌트가 가장 오래 걸리는지, 어떤 값 때문에 리렌더링이 발생하는지 시각적으로 확인하고 필요한 곳에만 useMemo와
useCallback을 적용하는 지혜가 필요합니다.
작은 디테일의 차이가 eleslog.work와 같은 고성능 웹 앱을 만듭니다. 여러분의 코드에서도 불필요하게 낭비되고 있는 렌더링 사이클은 없는지 오늘 한번 점검해 보시는 건 어떨까요?