useMemo vs useCallback / 언제 쓰고 어떻게 최적화할까?

느린 웹 서비스, 왜 문제였을까요?
2026년 현재, 웹 서비스의 속도는 곧 비즈니스의 생명선과 같아요. 제가 맡았던 프로젝트는 초기 로딩 속도가 너무 느려 사용자 이탈률이 급격히 증가하던 상황이었죠. 라이트하우스(Lighthouse)로 성능 측정을 해보니 점수는 60점대에 머물러 있었고, 특히 메인 번들 파일의 크기가 너무 커서 첫 화면이 뜨는 데만 한참이 걸렸어요.
성능 저하의 주범은 크게 두 가지였어요. 첫째는 불필요하게 거대한 JavaScript 번들이었고, 둘째는 잦은 리렌더링으로 인한 연산 낭비였죠. 이를 해결하기 위해 리액트의 핵심 성능 최적화 기술인 메모이제이션(Memoization)과 코드 스플리팅(Code Splitting)을 전격 도입하기로 했습니다.

메모이제이션으로 렌더링 효율 극대화하기
리액트 컴포넌트는 상태(State)가 변할 때마다 다시 그려지죠. 하지만 데이터가 바뀌지 않았는데도 복잡한 연산을 반복하는 건 엄청난 손해예요. 저는 `useMemo`와 `useCallback`을 사용해 이 문제를 해결했습니다.
복잡한 필터링 로직이나 정렬 연산에는 `useMemo`를 적용하여 이전 계산값을 재사용하게 했고, 자식 컴포넌트에 전달되는 콜백 함수들은 `useCallback`으로 감싸 불필요한 참조 변경을 막았어요. 이 간단한 조치만으로도 컴포넌트가 불필요하게 다시 그려지는 '렌더링 폭포' 현상을 현저히 줄일 수 있었습니다.

코드 스플리팅으로 첫 로딩을 가볍게!
초기 로딩 속도 40% 개선의 일등 공신은 역시 코드 스플리팅이었어요. `React.lazy`와 `Suspense`를 활용해 당장 필요하지 않은 페이지나 모달 컴포넌트들을 별도의 청크(Chunk) 파일로 분리했죠.
기존에는 사용자가 접속하자마자 전체 서비스의 JS 파일을 모두 다운로드해야 했지만, 이제는 현재 보고 있는 페이지에 필요한 코드만 우선적으로 받게 됩니다. 결과는 놀라웠어요. 메인 번들 크기가 줄어들면서 LCP(Largest Contentful Paint) 시간이 단축되었고, 라이트하우스 점수는 수직 상승했습니다.
| 측정 항목 | 최적화 전 | 최적화 후 |
|---|---|---|
| Lighthouse 점수 | 60점 | 95점 |
| 초기 로딩 속도 | 4.8s | 2.1s |
| 메인 번들 크기 | 1.2MB | 450KB |
💡 핵심 요약
1. 메모이제이션 활용: useMemo와 useCallback으로 불필요한 연산과 리렌더링 방지
2. 코드 스플리팅 도입: React.lazy를 통해 초기 번들 크기를 줄여 로딩 속도 40% 개선
3. Lighthouse 점수 상승: 체계적인 최적화를 통해 성능 점수를 60점에서 95점까지 확보
4. 사용자 경험 개선: 빠른 반응 속도로 이탈률을 낮추고 서비스 경쟁력 강화
성능 최적화는 선택이 아닌 필수입니다.
❓ 자주 묻는 질문 (FAQ)
Q1. useMemo를 모든 변수에 다 쓰는 게 좋을까요?
A1. 아니요! 메모이제이션 자체도 메모리 비용이 발생해요. 단순히 값을 할당하는 정도라면 쓰지 않는 게 낫고, 연산이 복잡하거나 자식 컴포넌트의 리렌더링을 막아야 할 때만 쓰세요.
Q2. 코드 스플리팅을 하면 SEO에 문제가 없나요?
A2. 서버 사이드 렌더링(SSR)을 함께 사용하거나, 적절한 Preload 전략을 세우면 SEO에 큰 지장은 없습니다. 오히려 페이지 속도가 빨라져서 검색 순위에 긍정적인 영향을 줄 수 있어요.