리액트를 조금 공부하고 나면 React.memo, useMemo, useCallback 같은 최적화 도구가 눈에 들어옵니다. 그런데 이 도구들은 생각보다 늦게 꺼내는 편이 좋습니다. 구조가 정리되지 않은 상태에서 메모이제이션부터 넣으면 코드만 복잡해지고, 실제로는 별 문제를 해결하지 못하는 경우가 많습니다.
React 성능 최적화의 출발점은 함수 하나를 감싸는 일이 아니라, “왜 이 컴포넌트가 자주 렌더링되는가”를 이해하는 일입니다. 즉, 도구보다 흐름이 먼저입니다.
이 글에서는 아래 내용을 정리합니다.
- 렌더링 최적화를 시작하기 전에 먼저 볼 것
- 불필요한 리렌더를 판단하는 기준
memo,useMemo,useCallback을 사용하는 감각
핵심은 최적화는 기본 구조가 단순할수록 더 잘 먹힌다는 점입니다.
React에서 렌더링 최적화는 무엇인가
렌더링 최적화는 화면이 다시 그려지는 횟수나 비용을 줄여서 사용자 경험을 부드럽게 만드는 작업입니다. 하지만 모든 렌더링이 문제는 아닙니다. React는 원래 상태가 바뀌면 다시 렌더링하도록 설계되어 있습니다.
중요한 건 아래 두 가지입니다.
- 이 렌더링이 실제로 느린가?
- 이 렌더링이 불필요한가?
이 둘을 구분하지 못하면 정상적인 업데이트까지 문제로 오해하게 됩니다.
최적화 전에 먼저 확인할 것
대부분의 경우 아래 세 가지가 먼저입니다.
- state가 너무 상위에 있지 않은가
- 큰 컴포넌트가 너무 많은 책임을 가지지 않는가
- 파생 값을 불필요하게 state로 저장하고 있지 않은가
예를 들어 검색 입력 state가 페이지 최상단에 있어서 페이지 전체가 매 타이핑마다 다시 렌더링된다면, useMemo보다 상태 위치를 조정하는 편이 더 효과적일 수 있습니다. 이런 감각은 React state와 props 가이드와도 연결됩니다.
불필요한 리렌더는 어떻게 생길까
대표적인 경우는 아래와 같습니다.
- 부모가 자주 렌더링되면서 자식도 함께 다시 렌더링됨
- 매번 새 객체나 새 함수를 만들어 props로 전달함
- 비싼 계산을 렌더링 때마다 반복함
- 리스트가 크고 각 아이템이 무거움
예를 들어 아래 코드는 렌더링마다 새 객체를 만듭니다.
<Chart options={{ theme: 'dark', showLegend: true }} />
이 자체가 항상 문제는 아니지만, Chart가 무거운 컴포넌트라면 다시 계산되는 비용이 커질 수 있습니다.
React.memo는 언제 쓰면 좋을까
React.memo는 props가 같으면 컴포넌트 재렌더링을 건너뛰도록 도와줍니다. 따라서 아래 조건이 함께 맞을 때 효과가 큽니다.
- 자식 컴포넌트가 비교적 무겁다
- 같은 props로 자주 다시 렌더링된다
- props 안정성을 어느 정도 유지할 수 있다
const ProductRow = React.memo(function ProductRow({ product, onSelect }) {
return <li onClick={() => onSelect(product.id)}>{product.name}</li>;
});
하지만 부모가 매번 새 onSelect 함수를 만들고 있다면, memo 효과가 줄어들 수 있습니다. 그래서 memo는 혼자 쓰기보다 props 구조를 함께 보는 편이 좋습니다.
useMemo와 useCallback은 어떻게 봐야 할까
useMemo는 비싼 계산 결과를 기억하고, useCallback은 함수 참조를 기억합니다. 둘 다 남용하기 쉬운 도구입니다.
예를 들어 정렬 비용이 큰 리스트라면 useMemo가 도움이 될 수 있습니다.
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => a.price - b.price);
}, [products]);
반대로 아주 가벼운 계산이라면 useMemo를 넣는 비용과 복잡성이 더 클 수 있습니다.
useCallback도 마찬가지입니다.
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);
이 함수가 정말 자식 최적화에 영향을 주는지, 아니면 단순히 “최적화해 보이는 코드”인지 먼저 구분하는 편이 좋습니다.
초보자가 자주 하는 실수
1. 문제를 측정하지 않고 최적화부터 하기
느린 지점이 어디인지 모르는데 도구부터 넣으면, 나중에 왜 넣었는지도 설명하기 어려워집니다.
2. 모든 컴포넌트에 memo 붙이기
코드 양은 늘어나는데 이득이 거의 없는 경우가 많습니다. 최적화는 선택적으로 적용할수록 좋습니다.
3. state 구조 문제를 메모이제이션으로 덮기
상태 위치가 과도하게 위에 있거나 컴포넌트 책임이 섞여 있으면, 메모이제이션으로도 한계가 있습니다.
4. 리스트 최적화를 늦게 보기
실제 사용자 체감은 긴 리스트, 복잡한 카드, 잦은 필터링에서 많이 나타납니다. 체감 성능 이슈라면 여기부터 보는 편이 좋습니다.
공부용으로 해보기 좋은 실습
아래 순서로 작은 상품 목록 앱을 만들어보면 좋습니다.
- 검색어 입력으로 목록 필터링 만들기
- 정렬 옵션 추가하기
- 개별 카드에 선택 상태 넣기
- 일부러 큰 리스트로 늘려보기
- 어디서 렌더링 비용이 커지는지 관찰하기
그다음 memo, useMemo, useCallback을 하나씩 적용해보면 “무조건 쓰는 도구”가 아니라 “필요할 때 쓰는 도구”라는 감각이 생깁니다.
FAQ
Q. 렌더링이 일어나면 무조건 나쁜 건가요?
아닙니다. 상태가 바뀌면 렌더링되는 것은 React의 정상 동작입니다.
Q. useMemo는 많이 쓸수록 좋은가요?
그렇지 않습니다. 계산 비용보다 관리 비용이 커질 수 있습니다.
Q. 성능 공부는 언제 시작하는 게 좋나요?
컴포넌트 분리, state 흐름, effect 감각이 어느 정도 잡힌 뒤 시작하면 훨씬 이해가 잘 됩니다.
Read Next
- 아직 구조 설계가 더 중요하게 느껴진다면 React 컴포넌트 설계 가이드부터 다시 정리해보세요.
- 반복되는 상태 로직을 줄이면서 구조를 단순화하고 싶다면 React 커스텀 훅 가이드를 함께 보면 좋습니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis vs RabbitMQ vs Kafka 개발자를 위한 미들웨어 트러블슈팅 허브 글입니다. Redis, RabbitMQ, Kafka 중 어떤 증상부터 먼저 봐야 하는지와 어떤 문제 패턴이 각 시스템에 가까운지 정리합니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Kafka consumer lag가 계속 늘 때: 트러블슈팅 가이드 Kafka consumer lag가 계속 늘어날 때 무엇부터 봐야 하는지 정리합니다. poll 주기, 처리 속도, rebalance, consumer 설정까지 실전 기준으로 다룹니다.
- Kafka Rebalancing Too Often 가이드 Kafka consumer group에서 rebalance가 너무 자주 일어날 때 membership flapping, poll timing, protocol, assignment churn을 어떤 순서로 봐야 하는지 설명하는 실전 가이드입니다.
- Docker container가 계속 재시작될 때: 먼저 확인할 것들 exit code, command failure, environment mistake, health check 관점에서 Docker restart loop를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.