React Hooks 성능 최적화: useMemo와 useCallback 완벽 가이드
useMemo, useCallback, React.memo를 활용한 React 애플리케이션 성능 최적화 실전 가이드. 중급 개발자를 위한 실용적인 예시와 성능 측정 방법을 소개합니다.
•8 min read•WeakLion
React Hooks 성능 최적화: useMemo와 useCallback 완벽 가이드

React 애플리케이션의 성능을 개선하는 것은 사용자 경험에 직접적인 영향을 미칩니다. 이 글에서는 useMemo와 useCallback을 활용한 실전 최적화 기법과 성능 측정 방법을 알아보겠습니다.
왜 성능 최적화가 필요한가?
React는 기본적으로 매우 빠르지만, 컴포넌트가 불필요하게 재렌더링되면 성능 저하가 발생할 수 있습니다. 특히 다음과 같은 상황에서 최적화가 필요합니다:
- 복잡한 연산이 매 렌더링마다 실행될 때
- 대량의 데이터를 다루는 컴포넌트
- 자식 컴포넌트가 불필요하게 재렌더링될 때
- 이벤트 핸들러가 매번 새로 생성될 때
useMemo: 값의 메모이제이션
useMemo는 계산 비용이 높은 값을 메모이제이션하여 불필요한 재계산을 방지합니다.
기본 사용법
import { useMemo, useState } from 'react'
function ExpensiveComponent({ items }: { items: number[] }) {
const [count, setCount] = useState(0)
// ❌ 나쁜 예: 매 렌더링마다 재계산
const sum = items.reduce((acc, item) => acc + item, 0)
// ✅ 좋은 예: items가 변경될 때만 재계산
const memoizedSum = useMemo(() => {
console.log('계산 실행!')
return items.reduce((acc, item) => acc + item, 0)
}, [items])
return (
<div>
<p>합계: {memoizedSum}</p>
<button onClick={() => setCount(count + 1)}>
카운트: {count}
</button>
</div>
)
}
실전 예시: 필터링과 정렬
function UserList({ users, searchTerm }: Props) {
// 검색어와 사용자 목록이 변경될 때만 필터링
const filteredUsers = useMemo(() => {
return users
.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
)
.sort((a, b) => a.name.localeCompare(b.name))
}, [users, searchTerm])
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
useMemo 사용 시 주의사항
언제 사용해야 하나요?
- 복잡한 계산이 필요한 경우
- 큰 배열이나 객체를 변환하는 경우
- 렌더링 성능 문제가 실제로 발생한 경우
언제 사용하지 말아야 하나요?
- 간단한 계산 (예: 두 수의 합)
- 이미 충분히 빠른 연산
- 메모이제이션 비용이 더 큰 경우
// ❌ 불필요한 useMemo
const doubled = useMemo(() => count * 2, [count])
// ✅ 그냥 계산하세요
const doubled = count * 2
useCallback: 함수의 메모이제이션
useCallback은 함수를 메모이제이션하여 자식 컴포넌트의 불필요한 재렌더링을 방지합니다.
useMemo는 값을 메모이제이션하고, useCallback은 함수를 메모이제이션합니다.
React.memo와 함께 사용하기
import { memo, useCallback, useState } from 'react'
// 자식 컴포넌트를 memo로 감싸기
const ChildComponent = memo(({ onClick }: { onClick: () => void }) => {
console.log('ChildComponent 렌더링')
return <button onClick={onClick}>클릭</button>
})
function ParentComponent() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// ❌ 나쁜 예: 매 렌더링마다 새 함수 생성
const handleClick = () => {
setCount(count + 1)
}
// ✅ 좋은 예: 함수 메모이제이션
const memoizedHandleClick = useCallback(() => {
setCount(prev => prev + 1)
}, []) // 의존성 배열이 비어있으므로 한 번만 생성
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<ChildComponent onClick={memoizedHandleClick} />
<p>Count: {count}</p>
</div>
)
}
실전 예시: 이벤트 핸들러 최적화
interface Todo {
id: string
text: string
completed: boolean
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([])
// useCallback으로 함수 메모이제이션
const handleToggle = useCallback((id: string) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
)
}, [])
const handleDelete = useCallback((id: string) => {
setTodos(prev => prev.filter(todo => todo.id !== id))
}, [])
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
)
}
// memo로 감싼 자식 컴포넌트
const TodoItem = memo(({ todo, onToggle, onDelete }: TodoItemProps) => {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>삭제</button>
</li>
)
})
성능 측정 방법
최적화의 효과를 확인하려면 실제로 측정해야 합니다.
React.memo, useMemo, useCallback을 적용한 결과 70%의 성능 개선을 달성했습니다.
React DevTools Profiler
import { Profiler } from 'react'
function onRenderCallback(
id: string,
phase: "mount" | "update",
actualDuration: number,
) {
console.log(`${id}의 ${phase} 단계: ${actualDuration}ms`)
}
function App() {
return (
<Profiler id="TodoList" onRender={onRenderCallback}>
<TodoList />
</Profiler>
)
}
Performance API 사용
function MeasuredComponent() {
useEffect(() => {
const start = performance.now()
// 무거운 작업 수행
const end = performance.now()
console.log(`실행 시간: ${end - start}ms`)
}, [])
return <div>측정된 컴포넌트</div>
}
Chrome DevTools 활용
- Performance 탭: 전체 렌더링 성능 분석
- React DevTools Profiler: 컴포넌트별 렌더링 시간
- Console의 렌더링 로그: 재렌더링 추적
// 개발 모드에서 렌더링 추적
function Component() {
console.log('Component 렌더링')
return <div>...</div>
}
실전 최적화 패턴
1. 컴포넌트 분리
// ❌ 나쁜 예: 하나의 큰 컴포넌트
function Dashboard() {
const [user, setUser] = useState()
const [posts, setPosts] = useState()
const [comments, setComments] = useState()
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<CommentList comments={comments} />
</div>
)
}
// ✅ 좋은 예: 독립적인 컴포넌트로 분리
const UserProfile = memo(({ user }: Props) => {
// user만 변경될 때 재렌더링
})
const PostList = memo(({ posts }: Props) => {
// posts만 변경될 때 재렌더링
})
2. 상태 끌어올리기 최소화
// ❌ 나쁜 예: 부모 컴포넌트에 모든 상태
function Parent() {
const [childState, setChildState] = useState()
return <Child state={childState} setState={setChildState} />
}
// ✅ 좋은 예: 자식이 자체 상태 관리
function Child() {
const [state, setState] = useState()
return <div>...</div>
}
3. 객체와 배열 의존성 주의
// ❌ 나쁜 예: 매번 새 객체 생성
function Component({ config }) {
const value = useMemo(() => {
return processConfig(config)
}, [{ ...config }]) // 항상 새 객체!
// ✅ 좋은 예: 필요한 값만 의존성에 추가
const value = useMemo(() => {
return processConfig(config)
}, [config.option1, config.option2])
}
마치며
성능 최적화는 측정 후 필요한 곳에만 적용해야 합니다. 모든 곳에 useMemo와 useCallback을 사용하는 것은 오히려 코드를 복잡하게 만들고 메모리를 낭비할 수 있습니다.
핵심 원칙:
- 먼저 측정하라
- 실제 성능 문제가 있는 곳만 최적화하라
- 코드의 가독성도 중요하다
성능 최적화는 끝없는 여정입니다. 사용자 경험을 최우선으로 생각하며, 필요한 곳에 적절한 최적화를 적용하세요! 🚀
💬 댓글
GitHub 계정으로 로그인하여 댓글을 남길 수 있습니다.