React Hooks 성능 최적화: useMemo와 useCallback 완벽 가이드

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

8 min readWeakLion

React Hooks 성능 최적화: useMemo와 useCallback 완벽 가이드

React Hooks Performance Optimization

React 애플리케이션의 성능을 개선하는 것은 사용자 경험에 직접적인 영향을 미칩니다. 이 글에서는 useMemouseCallback을 활용한 실전 최적화 기법과 성능 측정 방법을 알아보겠습니다.

왜 성능 최적화가 필요한가?

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 vs 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 활용

  1. Performance 탭: 전체 렌더링 성능 분석
  2. React DevTools Profiler: 컴포넌트별 렌더링 시간
  3. 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])
}

마치며

성능 최적화는 측정 후 필요한 곳에만 적용해야 합니다. 모든 곳에 useMemouseCallback을 사용하는 것은 오히려 코드를 복잡하게 만들고 메모리를 낭비할 수 있습니다.

핵심 원칙:

  1. 먼저 측정하라
  2. 실제 성능 문제가 있는 곳만 최적화하라
  3. 코드의 가독성도 중요하다

성능 최적화는 끝없는 여정입니다. 사용자 경험을 최우선으로 생각하며, 필요한 곳에 적절한 최적화를 적용하세요! 🚀

💬 댓글

GitHub 계정으로 로그인하여 댓글을 남길 수 있습니다.

댓글을 불러오는 중...