본문 바로가기

컴포넌트 최적화

2. 컴포넌트 최적화

React 애플리케이션의 성능을 향상시키기 위한 다양한 최적화 기법을 배워보겠습니다. React.memo, useMemo, useCallback 등을 활용해서 불필요한 렌더링을 방지하고 애플리케이션을 더 빠르게 만드는 방법을 알아봅시다.

2.1 React.memo로 컴포넌트 메모이제이션

기본 React.memo 사용법

import { useState, memo } from 'react';

// 최적화되지 않은 컴포넌트
function ExpensiveComponent({ name, count }) {
  console.log(`ExpensiveComponent 렌더링됨: ${name}`);
  
  // 복잡한 계산 시뮬레이션
  const expensiveValue = (() => {
    let result = 0;
    for (let i = 0; i < 100000; i++) {
      result += Math.random();
    }
    return result.toFixed(2);
  })();

  return (
    <div >
      <h3>{name}</h3>
      <p>카운트: {count}</p>
      <p>복잡한 계산 결과: {expensiveValue}</p>
    </div>
  );
}

// React.memo로 최적화된 컴포넌트
const OptimizedComponent = memo(function OptimizedComponent({ name, count }) {
  console.log(`OptimizedComponent 렌더링됨: ${name}`);
  
  const expensiveValue = (() => {
    let result = 0;
    for (let i = 0; i < 100000; i++) {
      result += Math.random();
    }
    return result.toFixed(2);
  })();

  return (
    <div >
      <h3>{name}</h3>
      <p>카운트: {count}</p>
      <p>복잡한 계산 결과: {expensiveValue}</p>
    </div>
  );
});

function MemoExample() {
  const [parentCount, setParentCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  return (
    <div }>
      <h2>React.memo 비교 예제</h2>
      
      <div }>
        <button
          onClick={() => setParentCount(prev => prev + 1)}
          
        >
          부모 상태 변경 ({parentCount})
        </button>
        
        <button
          onClick={() => setChildCount(prev => prev + 1)}
          
        >
          자식 카운트 변경 ({childCount})
        </button>
      </div>

      <div }>
        <div>
          <h3>최적화되지 않은 컴포넌트</h3>
          <ExpensiveComponent name="컴포넌트 1" count={childCount} />
        </div>
        
        <div>
          <h3>React.memo로 최적화된 컴포넌트</h3>
          <OptimizedComponent name="컴포넌트 2" count={childCount} />
        </div>
      </div>

      <div >
        <h4>💡 테스트 방법</h4>
        <ol }>
          <li>개발자 도구 콘솔을 열어주세요</li>
          <li>"부모 상태 변경" 버튼을 클릭해보세요</li>
          <li>파란색(최적화되지 않은) 컴포넌트는 매번 렌더링되지만</li>
          <li>녹색(최적화된) 컴포넌트는 props가 변경되지 않아 렌더링되지 않습니다</li>
        </ol>
      </div>
    </div>
  );
}

커스텀 비교 함수

import { useState, memo } from 'react';

const UserCard = memo(function UserCard({ user, theme }) {
  console.log(`UserCard 렌더링됨: ${user.name}`);
  
  return (
    <div >
      <h3>{user.name}</h3>
      <p>나이: {user.age}</p>
      <p>이메일: {user.email}</p>
      <p>마지막 로그인: {user.lastLogin}</p>
    </div>
  );
});

// 커스텀 비교 함수를 사용한 최적화
const OptimizedUserCard = memo(function OptimizedUserCard({ user, theme }) {
  console.log(`OptimizedUserCard 렌더링됨: ${user.name}`);
  
  return (
    <div >
      <h3>{user.name} ⚡</h3>
      <p>나이: {user.age}</p>
      <p>이메일: {user.email}</p>
      <p>마지막 로그인: {user.lastLogin}</p>
    </div>
  );
}, (prevProps, nextProps) => {
  // lastLogin을 제외하고 비교 (자주 변경되는 값 무시)
  return (
    prevProps.user.name === nextProps.user.name &&
    prevProps.user.age === nextProps.user.age &&
    prevProps.user.email === nextProps.user.email &&
    prevProps.theme === nextProps.theme
  );
});

function CustomComparisonExample() {
  const [theme, setTheme] = useState('light');
  const [updateCount, setUpdateCount] = useState(0);
  
  const user = {
    name: '김개발',
    age: 25,
    email: 'kim@example.com',
    lastLogin: new Date().toLocaleString() // 매번 변경되는 값
  };

  return (
    <div }>
      <h2>커스텀 비교 함수 예제</h2>
      
      <div }>
        <button
          onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
          
        >
          테마 변경 ({theme})
        </button>
        
        <button
          onClick={() => setUpdateCount(prev => prev + 1)}
          
        >
          강제 업데이트 ({updateCount})
        </button>
      </div>

      <div }>
        <div>
          <h3>기본 React.memo</h3>
          <UserCard user={user} theme={theme} />
        </div>
        
        <div>
          <h3>커스텀 비교 함수</h3>
          <OptimizedUserCard user={user} theme={theme} />
        </div>
      </div>

      <div >
        <h4>🔍 동작 원리</h4>
        <ul }>
          <li><strong>기본 React.memo:</strong> lastLogin이 매번 변경되어 항상 재렌더링</li>
          <li><strong>커스텀 비교:</strong> lastLogin을 무시하고 중요한 props만 비교</li>
          <li><strong>성능 향상:</strong> 불필요한 렌더링 방지로 성능 개선</li>
        </ul>
      </div>
    </div>
  );
}

2.2 useMemo로 값 메모이제이션

복잡한 계산 최적화

import { useState, useMemo } from 'react';

function ExpensiveCalculation({ items, multiplier }) {
  console.log('ExpensiveCalculation 컴포넌트 렌더링');
  
  // useMemo 없이 매번 계산
  const expensiveValue = (() => {
    console.log('복잡한 계산 실행 중...');
    let result = 0;
    
    // 복잡한 계산 시뮬레이션
    for (let i = 0; i < items.length; i++) {
      for (let j = 0; j < 1000000; j++) {
        result += items[i].value * multiplier;
      }
    }
    
    return result;
  })();

  // useMemo로 최적화된 계산
  const optimizedValue = useMemo(() => {
    console.log('최적화된 계산 실행 중...');
    let result = 0;
    
    for (let i = 0; i < items.length; i++) {
      for (let j = 0; j < 1000000; j++) {
        result += items[i].value * multiplier;
      }
    }
    
    return result;
  }, [items, multiplier]); // items나 multiplier가 변경될 때만 재계산

  return (
    <div >
      <div >
        <h3>❌ 최적화 없음</h3>
        <p>계산 결과: {expensiveValue.toLocaleString()}</p>
        <small>매번 계산 실행</small>
      </div>
      
      <div >
        <h3>✅ useMemo 사용</h3>
        <p>계산 결과: {optimizedValue.toLocaleString()}</p>
        <small>의존성 변경시만 계산</small>
      </div>
    </div>
  );
}

function MemoExample() {
  const [items, setItems] = useState([
    { id: 1, value: 10 },
    { id: 2, value: 20 },
    { id: 3, value: 30 }
  ]);
  const [multiplier, setMultiplier] = useState(2);
  const [unrelatedState, setUnrelatedState] = useState(0);

  const addItem = () => {
    const newItem = {
      id: items.length + 1,
      value: Math.floor(Math.random() * 50) + 1
    };
    setItems(prev => [...prev, newItem]);
  };

  const removeItem = (id) => {
    setItems(prev => prev.filter(item => item.id !== id));
  };

  return (
    <div }>
      <h2>useMemo 최적화 예제</h2>
      
      <div }>
        <button
          onClick={addItem}
          
        >
          아이템 추가
        </button>
        
        <button
          onClick={() => setMultiplier(prev => prev + 1)}
          
        >
          배수 증가 ({multiplier})
        </button>
        
        <button
          onClick={() => setUnrelatedState(prev => prev + 1)}
          
        >
          관련없는 상태 변경 ({unrelatedState})
        </button>
      </div>

      <div }>
        <h3>아이템 목록</h3>
        <div }>
          {items.map(item => (
            <div
              key={item.id}
              
            >
              <span>값: {item.value}</span>
              <button
                onClick={() => removeItem(item.id)}
                
              >
                ×
              </button>
            </div>
          ))}
        </div>
      </div>

      <ExpensiveCalculation items={items} multiplier={multiplier} />
      
      <div >
        <h4>🧪 테스트해보기</h4>
        <ol }>
          <li>개발자 도구 콘솔을 열어주세요</li>
          <li>"관련없는 상태 변경" 버튼을 클릭해보세요</li>
          <li>최적화되지 않은 계산은 매번 실행되지만, useMemo는 실행되지 않습니다</li>
          <li>"아이템 추가"나 "배수 증가"를 클릭하면 둘 다 다시 계산됩니다</li>
        </ol>
      </div>
    </div>
  );
}

2.3 useCallback으로 함수 메모이제이션

함수 참조 최적화

import { useState, useCallback, memo } from 'react';

// 자식 컴포넌트 - 함수를 prop으로 받음
const ChildComponent = memo(function ChildComponent({ name, onClick }) {
  console.log(`ChildComponent 렌더링: ${name}`);
  
  return (
    <div >
      <h4>{name}</h4>
      <button
        onClick={onClick}
        
      >
        클릭하세요
      </button>
    </div>
  );
});

function CallbackExample() {
  const [count, setCount] = useState(0);
  const [unrelatedState, setUnrelatedState] = useState(0);

  // useCallback 없는 함수 - 매번 새로운 함수 생성
  const handleClick1 = () => {
    setCount(prev => prev + 1);
  };

  // useCallback으로 최적화된 함수 - 의존성 배열이 비어있으므로 한 번만 생성
  const handleClick2 = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  // 의존성이 있는 useCallback
  const handleClick3 = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  // 복잡한 함수 최적화
  const handleComplexOperation = useCallback((multiplier) => {
    return (value) => {
      console.log(`복잡한 연산 실행: ${value} × ${multiplier}`);
      return value * multiplier;
    };
  }, []);

  return (
    <div }>
      <h2>useCallback 최적화 예제</h2>
      
      <div }>
        <p>현재 카운트: <strong>{count}</strong></p>
        <button
          onClick={() => setUnrelatedState(prev => prev + 1)}
          
        >
          관련없는 상태 변경 ({unrelatedState})
        </button>
      </div>

      <div }>
        <div>
          <h3>❌ useCallback 없음</h3>
          <ChildComponent
            name="컴포넌트 1"
            onClick={handleClick1}
          />
        </div>

        <div>
          <h3>✅ useCallback 사용</h3>
          <ChildComponent
            name="컴포넌트 2"
            onClick={handleClick2}
          />
        </div>

        <div>
          <h3>⚠️ 의존성 있는 useCallback</h3>
          <ChildComponent
            name="컴포넌트 3"
            onClick={handleClick3}
          />
        </div>
      </div>

      <div >
        <h4>🔍 동작 원리</h4>
        <ul }>
          <li><strong>컴포넌트 1:</strong> 매번 새로운 함수가 생성되어 항상 재렌더링</li>
          <li><strong>컴포넌트 2:</strong> 함수가 메모이제이션되어 불필요한 렌더링 방지</li>
          <li><strong>컴포넌트 3:</strong> count 의존성으로 인해 count 변경시 재렌더링</li>
        </ul>
      </div>
    </div>
  );
}

실전 useCallback 활용

import { useState, useCallback, useMemo } from 'react';

const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete, onEdit }) {
  console.log(`TodoItem 렌더링: ${todo.id}`);
  
  return (
    <div >
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
        }
      />
      
      <span
        
      >
        {todo.text}
      </span>
      
      <div }>
        <button
          onClick={() => onEdit(todo.id, prompt('새 텍스트:', todo.text))}
          
        >
          수정
        </button>
        <button
          onClick={() => onDelete(todo.id)}
          
        >
          삭제
        </button>
      </div>
    </div>
  );
});

function OptimizedTodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'React 공부하기', completed: false },
    { id: 2, text: 'useState 이해하기', completed: true },
    { id: 3, text: '컴포넌트 최적화하기', completed: false }
  ]);
  const [filter, setFilter] = useState('all'); // all, active, completed
  const [newTodo, setNewTodo] = useState('');

  // useCallback으로 최적화된 이벤트 핸들러들
  const handleToggle = useCallback((id) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []);

  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  const handleEdit = useCallback((id, newText) => {
    if (newText && newText.trim()) {
      setTodos(prev => prev.map(todo =>
        todo.id === id ? { ...todo, text: newText.trim() } : todo
      ));
    }
  }, []);

  const handleAdd = useCallback(() => {
    if (newTodo.trim()) {
      const newId = Math.max(...todos.map(t => t.id), 0) + 1;
      setTodos(prev => [...prev, {
        id: newId,
        text: newTodo.trim(),
        completed: false
      }]);
      setNewTodo('');
    }
  }, [newTodo, todos]);

  // useMemo로 필터링된 todos 최적화
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);

  // useMemo로 통계 정보 최적화
  const stats = useMemo(() => {
    return {
      total: todos.length,
      completed: todos.filter(t => t.completed).length,
      active: todos.filter(t => !t.completed).length
    };
  }, [todos]);

  return (
    <div >
      <h2>최적화된 할 일 앱</h2>
      
      {/* 새 할 일 추가 */}
      <div >
        <input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
          placeholder="새로운 할 일을 입력하세요"
          
        />
        <button
          onClick={handleAdd}
          
        >
          추가
        </button>
      </div>

      {/* 필터 버튼들 */}
      <div >
        {['all', 'active', 'completed'].map(filterType => (
          <button
            key={filterType}
            onClick={() => setFilter(filterType)}
            
          >
            {filterType === 'all' ? '전체' : 
             filterType === 'active' ? '미완료' : '완료'}
          </button>
        ))}
      </div>

      {/* 통계 정보 */}
      <div >
        <div }>
          <div }>
            {stats.total}
          </div>
          <div }>전체</div>
        </div>
        <div }>
          <div }>
            {stats.completed}
          </div>
          <div }>완료</div>
        </div>
        <div }>
          <div }>
            {stats.active}
          </div>
          <div }>미완료</div>
        </div>
      </div>

      {/* 할 일 목록 */}
      <div>
        {filteredTodos.length === 0 ? (
          <div >
            {filter === 'all' ? '할 일이 없습니다.' :
             filter === 'active' ? '미완료된 할 일이 없습니다.' :
             '완료된 할 일이 없습니다.'}
          </div>
        ) : (
          filteredTodos.map(todo => (
            <TodoItem
              key={todo.id}
              todo={todo}
              onToggle={handleToggle}
              onDelete={handleDelete}
              onEdit={handleEdit}
            />
          ))
        )}
      </div>

      <div >
        <h4>✨ 적용된 최적화 기법</h4>
        <ul }>
          <li><strong>React.memo:</strong> TodoItem 컴포넌트 불필요한 렌더링 방지</li>
          <li><strong>useCallback:</strong> 이벤트 핸들러 함수 메모이제이션</li>
          <li><strong>useMemo:</strong> 필터링된 todos와 통계 정보 캐싱</li>
          <li><strong>키 최적화:</strong> 안정적인 key 사용으로 리스트 렌더링 최적화</li>
        </ul>
      </div>
    </div>
  );
}

2.4 성능 측정과 프로파일링

React DevTools Profiler 활용 팁

import { useState, Profiler } from 'react';

function ExpensiveComponentTree() {
  const [count, setCount] = useState(0);
  
  // Profiler 콜백 함수
  const onRenderCallback = (id, phase, actualDuration) => {
    console.log('Profiler 측정 결과:', {
      컴포넌트: id,
      단계: phase, // mount, update
      렌더링시간: `${actualDuration}ms`
    });
  };

  return (
    <div }>
      <h2>성능 프로파일링 예제</h2>
      
      <button
        onClick={() => setCount(prev => prev + 1)}
        
      >
        카운트 증가 ({count})
      </button>

      <Profiler id="ExpensiveTree" onRender={onRenderCallback}>
        <div>
          <h3>무거운 컴포넌트 트리</h3>
          {Array.from({ length: 100 }, (_, i) => (
            <div
              key={i}
              
            >
              아이템 {i + 1} - 카운트: {count}
            </div>
          ))}
        </div>
      </Profiler>

      <div >
        <h4>📊 성능 측정 도구</h4>
        <ul }>
          <li><strong>React DevTools Profiler:</strong> 컴포넌트 렌더링 시간 측정</li>
          <li><strong>브라우저 Performance 탭:</strong> 전체 성능 분석</li>
          <li><strong>console.time/timeEnd:</strong> 특정 코드 실행 시간 측정</li>
        </ul>
      </div>
    </div>
  );
}

2.5 최적화 주의사항과 Best Practices

언제 최적화를 적용해야 할까?

import { useState, memo, useMemo, useCallback } from 'react';

// ❌ 과도한 최적화 - 불필요함
const OverOptimizedComponent = memo(function OverOptimizedComponent({ text }) {
  const processedText = useMemo(() => {
    return text.toUpperCase(); // 간단한 연산까지 메모이제이션
  }, [text]);

  const handleClick = useCallback(() => {
    console.log('클릭됨'); // 간단한 함수까지 메모이제이션
  }, []);

  return (
    <div onClick={handleClick}>
      {processedText}
    </div>
  );
});

// ✅ 적절한 최적화 - 필요한 곳에만
function WellOptimizedComponent({ items, onItemClick }) {
  // 복잡한 계산만 useMemo 사용
  const expensiveCalculation = useMemo(() => {
    return items.reduce((acc, item) => {
      // 복잡한 로직
      return acc + Math.pow(item.value, 2);
    }, 0);
  }, [items]);

  // 자식 컴포넌트에 전달되는 함수만 useCallback 사용
  const handleItemClick = useCallback((id) => {
    onItemClick(id);
  }, [onItemClick]);

  return (
    <div>
      <h3>계산 결과: {expensiveCalculation}</h3>
      {items.map(item => (
        <ExpensiveChildComponent
          key={item.id}
          item={item}
          onClick={handleItemClick}
        />
      ))}
    </div>
  );
}

const ExpensiveChildComponent = memo(function ExpensiveChildComponent({ item, onClick }) {
  // 복잡한 렌더링 로직이 있는 경우에만 memo 사용
  return (
    <div
      onClick={() => onClick(item.id)}
      
    >
      {item.name} - {item.description}
    </div>
  );
});

function OptimizationBestPractices() {
  const [items] = useState([
    { id: 1, name: '아이템 1', description: '설명 1', value: 10 },
    { id: 2, name: '아이템 2', description: '설명 2', value: 20 },
    { id: 3, name: '아이템 3', description: '설명 3', value: 30 }
  ]);

  const handleItemClick = (id) => {
    console.log('아이템 클릭:', id);
  };

  return (
    <div }>
      <h2>최적화 Best Practices</h2>
      
      <div }>
        <WellOptimizedComponent
          items={items}
          onItemClick={handleItemClick}
        />
      </div>

      <div >
        <h3>🎯 최적화 가이드라인</h3>
        
        <h4>언제 React.memo를 사용할까?</h4>
        <ul }>
          <li>✅ 컴포넌트가 자주 렌더링되고 props가 자주 바뀌지 않는 경우</li>
          <li>✅ 렌더링 비용이 높은 컴포넌트</li>
          <li>❌ props가 매번 바뀌는 컴포넌트</li>
          <li>❌ 렌더링 비용이 낮은 간단한 컴포넌트</li>
        </ul>

        <h4>언제 useMemo를 사용할까?</h4>
        <ul }>
          <li>✅ 복잡한 계산이나 데이터 변환</li>
          <li>✅ 큰 배열의 필터링이나 정렬</li>
          <li>✅ 객체나 배열을 새로 생성하는 경우</li>
          <li>❌ 간단한 계산 (문자열 연결, 기본 산술 연산)</li>
        </ul>

        <h4>언제 useCallback을 사용할까?</h4>
        <ul }>
          <li>✅ 자식 컴포넌트의 props로 전달되는 함수</li>
          <li>✅ useEffect의 의존성으로 사용되는 함수</li>
          <li>✅ 다른 훅의 의존성 배열에 포함되는 함수</li>
          <li>❌ 컴포넌트 내부에서만 사용되는 간단한 함수</li>
        </ul>

        <h4>⚠️ 주의사항</h4>
        <ul }>
          <li>측정하지 않은 최적화는 조기 최적화가 될 수 있음</li>
          <li>React DevTools Profiler로 실제 성능 문제를 확인 후 적용</li>
          <li>의존성 배열을 정확히 명시해야 함</li>
          <li>과도한 메모이제이션은 오히려 메모리를 낭비할 수 있음</li>
        </ul>
      </div>
    </div>
  );
}

2.6 정리

React 컴포넌트 최적화의 핵심 기법들을 학습했습니다:

  1. React.memo로 불필요한 컴포넌트 렌더링 방지
  2. useMemo로 복잡한 계산 결과 캐싱
  3. useCallback으로 함수 참조 최적화
  4. 성능 측정을 통한 최적화 포인트 파악
  5. Best Practices와 적절한 최적화 시점 판단

최적화는 성능 문제가 실제로 발생했을 때 적용하는 것이 좋습니다. 모든 컴포넌트를 무분별하게 최적화하는 것보다는, React DevTools Profiler 등을 활용해서 실제 성능 병목지점을 찾아 집중적으로 최적화하는 것이 효과적입니다.

다음 장에서는 React의 State와 이벤트 처리에 대해 더 깊이 알아보겠습니다.

export { MemoExample, CustomComparisonExample, CallbackExample, OptimizedTodoApp, ExpensiveComponentTree, OptimizationBestPractices };

컴포넌트 최적화 - 리엑트 베이스캠프 | 위니버시티