- 공유 링크 만들기
- X
- 이메일
- 기타 앱
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
React
Next.js
React 성능 최적화 2026 실전 가이드
React 성능 최적화 완전 가이드 2026
불필요한 리렌더링 제거 · memo · useMemo · useCallback · 가상화 — 실무 최적화 총정리
✦ 리렌더링 최적화
✦ React Profiler 활용
✦ 실무 코드 예시
React 앱이 느려지는 가장 흔한 원인은 불필요한 리렌더링입니다. 상태 하나를 바꿨을 때 관련 없는 컴포넌트까지 모두 다시 렌더링된다면, 앱이 커질수록 성능이 급격히 나빠집니다. 이 글에서는 React Profiler로 문제를 진단하는 방법부터 memo, useMemo, useCallback, 가상화까지 실무에서 바로 적용할 수 있는 최적화 기법을 정리합니다.
왜 React가 느려지는가
React는 상태(state)나 props가 변경될 때 해당 컴포넌트와 그 모든 자식 컴포넌트를 다시 렌더링합니다. 이 과정 자체는 빠르지만, 복잡한 연산을 수행하거나 자식이 수백 개인 경우 성능 문제가 발생합니다.
| 성능 문제 유형 | 원인 | 해결책 |
|---|---|---|
| 불필요한 자식 리렌더링 | 부모 상태 변경 | React.memo |
| 비싼 계산 매 렌더마다 실행 | 렌더 함수 내 복잡한 로직 | useMemo |
| 함수 참조 변경으로 자식 리렌더 | 렌더마다 새 함수 생성 | useCallback |
| 수천 개 리스트 렌더링 | 전체 DOM 생성 | 가상화 (react-virtual) |
| 전역 상태 변경으로 과도한 리렌더 | Context 남용 | Context 분리 또는 Zustand |
React Profiler로 병목 찾기
최적화 전에 반드시 어디가 느린지 먼저 파악해야 합니다. Chrome DevTools의 React DevTools Profiler 탭을 활용하세요.
Profiler 사용 방법은 다음과 같습니다. 먼저 React DevTools 확장을 설치합니다. 이후 DevTools → Profiler 탭 → Record 버튼을 클릭합니다. 느린 동작을 재현한 뒤 Stop을 클릭합니다. Flamegraph에서 렌더링 시간이 긴(노란색·빨간색) 컴포넌트를 확인합니다.
💡 Profiler 핵심 팁
"Why did this render?" 기능(설정에서 활성화)을 켜면 각 컴포넌트가 왜 리렌더됐는지 이유를 표시합니다.
"Why did this render?" 기능(설정에서 활성화)을 켜면 각 컴포넌트가 왜 리렌더됐는지 이유를 표시합니다.
React.memo — 컴포넌트 메모이제이션
React.memo는 컴포넌트를 감싸서 props가 변경되지 않으면 리렌더링을 건너뜁니다.
// ❌ 문제: 부모가 리렌더될 때마다 ProductCard도 리렌더
function ProductCard({ product }) {
return <div>{product.name}</div>
}
// ✅ 해결: props가 같으면 리렌더 건너뜀
const ProductCard = React.memo(function ProductCard({ product }) {
return <div>{product.name}</div>
})
// 커스텀 비교 함수
const ProductCard = React.memo(
function ProductCard({ product }) { ... },
(prevProps, nextProps) => prevProps.product.id === nextProps.product.id
)
⚠️ React.memo 주의사항
객체나 함수를 props로 전달하면 매 렌더마다 새로운 참조가 생성되어 React.memo가 무용지물이 됩니다.
객체나 함수를 props로 전달하면 매 렌더마다 새로운 참조가 생성되어 React.memo가 무용지물이 됩니다.
useMemo와 useCallback을 함께 사용해야 합니다.
useMemo — 값 메모이제이션
useMemo는 비용이 큰 연산 결과를 메모이제이션합니다.
import { useMemo } from 'react'
function ProductList({ products, sortBy, filterText }) {
const filteredMemo = useMemo(() =>
products
.filter(p => p.name.includes(filterText))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1),
[products, sortBy, filterText]
)
const options = useMemo(() => ({ sortBy, filterText, pageSize: 20 }), [sortBy, filterText])
return <MemoizedChild options={options} />
}
useCallback — 함수 메모이제이션
useCallback은 함수 자체를 메모이제이션합니다. React.memo로 감싼 자식에게 콜백을 전달할 때 필수입니다.
import { useCallback, useState } from 'react'
function TodoList() {
const [todos, setTodos] = useState([])
const handleToggleMemo = useCallback((id) => {
setTodos(prev => prev.map(t => t.id === id ? {...t, done: !t.done} : t))
}, [])
return todos.map(todo => <MemoTodoItem key={todo.id} todo={todo} onToggle={handleToggleMemo} />)
}#fab387;">1 : -1),
[products, sortBy, filterText] // 의존성
)
// React.memo와 함께 — 객체 props 안정화
const options = useMemo(() => ({
sortBy,
filterText,
pageSize: 20
}), [sortBy, filterText])
return <MemoizedChild options={options} />
}
useCallback — 함수 메모이제이션
useCallback은 함수 자체를 메모이제이션합니다. React.memo로 감싼 자식에게 콜백을 전달할 때 필수입니다.
import { useCallback, useState } from 'react'
function TodoList() {
const [todos, setTodos] = useState([])
const [filter, setFilter] = useState('all')
// ❌ filter 상태가 바뀔 때마다 새 함수 생성 → MemoTodoItem 리렌더
const handleToggle = (id) => {
setTodos(prev => prev.map(t => t.id === id ? {...t, done: !t.done} : t))
}
// ✅ todos가 바뀔 때만 새 함수 생성
const handleToggleMemo = useCallback((id) => {
setTodos(prev => prev.map(t => t.id === id ? {...t, done: !t.done} : t))
}, []) // 함수형 업데이트 사용 시 의존성 없음
return todos.map(todo =>
)
}
상태 구조 최적화
상태를 어디에, 어떻게 배치하느냐가 리렌더링 범위를 결정합니다. 상태는 그것을 실제로 사용하는 컴포넌트에 최대한 가깝게 배치해야 합니다.
// ❌ 상태를 상위 컴포넌트에 두면 자식 모두 리렌더
function App() {
const [searchText, setSearchText] = useState('')
return (
{/* 검색과 무관한데 searchText 변경 시 리렌더 */}
)
}
// ✅ 상태를 사용하는 컴포넌트 내부로 이동 (State Colocation)
function SearchSection() {
const [searchText, setSearchText] = useState('')
return
}
function App() {
return (
{/* 검색 상태가 여기 격리됨 */}
{/* 이제 리렌더 안 됨 */}
)
}
가상화 — 긴 목록 성능 해결
수백~수천 개의 항목을 렌더링할 때는 화면에 보이는 항목만 DOM에 렌더링하는 가상화 기법을 사용하세요. @tanstack/react-virtual이 현재 가장 많이 쓰입니다.
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
function VirtualList({ items }) {
const parentRef = useRef(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // 각 항목 예상 높이(px)
})
return (
{virtualizer.getVirtualItems().map(vItem => (
{items[vItem.index].name}
))}
)
}
React 19 Compiler — 자동 최적화
React 19에서 실험적으로 도입된 React Compiler(구 React Forget)는 빌드 타임에 자동으로 메모이제이션 코드를 삽입합니다. 이 기능이 안정화되면 memo, useMemo, useCallback을 수동으로 추가하는 작업 대부분이 필요 없어질 전망입니다.
💡 React Compiler 2026 현황
2025년 말부터 Meta 프로덕션에서 사용 중입니다. Next.js 15에서도 opt-in으로 활성화할 수 있습니다(
2025년 말부터 Meta 프로덕션에서 사용 중입니다. Next.js 15에서도 opt-in으로 활성화할 수 있습니다(
experimental.reactCompiler: true). 단, 아직 RC 단계이므로 신규 프로젝트에서 먼저 테스트한 뒤 적용하시기를 권장드립니다. 기존 코드의 메모이제이션 패턴과 충돌할 수 있습니다.
자주 묻는 질문 (FAQ)
Q. 모든 컴포넌트에 React.memo를 쓰면 더 빨라지나요?
A. 아닙니다. React.memo 자체도 props 비교 비용이 발생합니다. 렌더링 비용보다 비교 비용이 더 클 수 있습니다. 간단한 텍스트만 렌더링하는 컴포넌트에는 오히려 역효과입니다. React Profiler로 실제로 불필요한 리렌더가 발생하는 컴포넌트에만 선별적으로 적용하세요.
Q. useMemo와 useCallback의 차이는 무엇인가요?
A. useMemo는 값을 메모이제이션합니다. 예를 들어 필터링된 배열, 계산된 객체 등입니다. useCallback은 함수를 메모이제이션합니다. 본질적으로 useCallback(fn, deps)은 useMemo(() => fn, deps)와 동일합니다. 값을 캐싱할 때는 useMemo, 함수를 캐싱할 때는 useCallback을 사용하면 됩니다.
Q. Context API 때문에 성능이 느려지는 경우 어떻게 해야 하나요?
A. Context 값이 변경되면 해당 Context를 구독하는 모든 컴포넌트가 리렌더됩니다. 해결책은 크게 두 가지입니다. 첫째, Context를 기능별로 분리하세요. 예를 들어 AuthContext, ThemeContext, CartContext처럼 나누면 각 Context의 변경이 관련 없는 컴포넌트에 영향을 주지 않습니다. 둘째, 전역 상태가 복잡해진다면 Zustand 같은 외부 상태 관리 라이브러리로 전환하세요. Zustand는 구독한 상태 슬라이스가 변경될 때만 리렌더를 발생시킵니다.
Q. 리스트 렌더링 시 key를 잘못 쓰면 어떤 문제가 생기나요?
A. key는 React가 어떤 항목이 변경·추가·삭제됐는지 식별하는 기준입니다. index를 key로 쓰면 항목 순서가 바뀔 때 React가 잘못된 DOM을 재사용하여 의도치 않은 상태 버그가 발생합니다. 항상 각 항목의 고유 ID(예: user.id, post.slug)를 key로 사용하세요. 가상화와 함께 쓸 때도 고유 key가 필수입니다.
Q. React 19 Compiler를 쓰면 기존 memo, useMemo, useCallback을 제거해야 하나요?
A. 반드시 제거할 필요는 없습니다. React Compiler는 기존 수동 메모이제이션 코드와 공존할 수 있습니다. 다만 컴파일러가 자동으로 최적화하는 코드에 중복으로 memo를 적용하면 약간의 오버헤드가 생길 수 있습니다. Compiler를 도입할 때는 eslint-plugin-react-compiler를 활용해 불필요한 수동 메모이제이션을 점진적으로 제거하시기 바랍니다.
🔗 관련 포스트
- React Hook과 memo 완전 가이드
- Next.js 성능 최적화 가이드 2026
- Zustand vs Jotai vs Redux 2026 — 상태 관리 완전 비교
- Vitest vs Jest 2026 — 마이그레이션 완전 가이드
| 항목 | ||
|---|---|---|
| 렌더 최적화 | React.memo 사용 | - |
| 상태 관리 | Context 최적화 | - |
| 번들 분석 | next/bundle-analyzer | - |
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
댓글
댓글 쓰기