Updated:

React.memo

import { useState, memo } from 'react';

// 일반 컴포넌트
function RegularComponent({ name, age }) {
  console.log('RegularComponent rendered');
  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
}

// memo로 최적화된 컴포넌트
const MemoizedComponent = memo(function MemoizedComponent({ name, age }) {
  console.log('MemoizedComponent rendered');
  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
});

// 커스텀 비교 함수를 사용한 memo
const MemoizedWithCustomCompare = memo(
  function MemoizedWithCustomCompare({ user, onUserClick }) {
    console.log('MemoizedWithCustomCompare rendered');
    return (
      <div onClick={() => onUserClick(user.id)}>
        <p>Name: {user.name}</p>
        <p>Age: {user.age}</p>
      </div>
    );
  },
  (prevProps, nextProps) => {
    // 사용자 정의 비교 로직
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.user.name === nextProps.user.name &&
      prevProps.user.age === nextProps.user.age &&
      prevProps.onUserClick === nextProps.onUserClick
    );
  }
);

// 부모 컴포넌트
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');
  
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setName(name === 'John' ? 'Jane' : 'John')}>
        Toggle Name
      </button>
      
      <h2>Regular Component (always re-renders)</h2>
      <RegularComponent name={name} age={30} />
      
      <h2>Memoized Component (re-renders only when props change)</h2>
      <MemoizedComponent name={name} age={30} />
    </div>
  );
}

코멘트: React.memo(또는 함수형 컴포넌트에서 memo)는 고차 컴포넌트(HOC)로, 컴포넌트의 props가 변경되지 않았을 때 리렌더링을 방지합니다. 이는 다음과 같은 경우에 유용합니다:

  • 컴포넌트가 같은 props로 자주 렌더링될 때
  • 컴포넌트가 렌더링 비용이 많이 들 때
  • 컴포넌트가 순수한 함수형일 때 (props에만 의존)

기본적으로 얕은 비교를 수행하지만, 두 번째 인자로 사용자 정의 비교 함수를 제공할 수 있습니다. 모든 컴포넌트를 memo로 감싸는 것은 오히려 성능을 저하시킬 수 있으므로, 실제 성능 이슈가 있는 경우에만 사용하세요.

useMemo와 useCallback

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

function ExpensiveCalculation({ list, filter }) {
  // 비용이 많이 드는 계산 결과 메모이제이션
  const filteredList = useMemo(() => {
    console.log('Calculating filtered list...');
    return list.filter(item => item.includes(filter));
  }, [list, filter]); // list나 filter가 변경될 때만 재계산
  
  // 복잡한 계산 예시
  const statistics = useMemo(() => {
    console.log('Calculating statistics...');
    
    // 시간이 많이 걸리는 계산 시뮬레이션
    const startTime = performance.now();
    while (performance.now() - startTime < 100) {
      // 인위적인 지연
    }
    
    return {
      count: filteredList.length,
      longestItem: filteredList.reduce(
        (max, item) => (item.length > max.length ? item : max),
        ''
      ),
      averageLength: filteredList.reduce(
        (sum, item) => sum + item.length, 
        0
      ) / (filteredList.length || 1)
    };
  }, [filteredList]);
  
  // 이벤트 핸들러 메모이제이션
  const handleItemClick = useCallback((item) => {
    console.log('Item clicked:', item);
    // 처리 로직...
  }, []); // 의존성이 없으므로 컴포넌트가 리렌더링되어도 함수는 유지됨
  
  return (
    <div>
      <h2>Filtered List ({statistics.count} items)</h2>
      <p>Longest item: {statistics.longestItem}</p>
      <p>Average length: {statistics.averageLength.toFixed(2)} characters</p>
      
      <ul>
        {filteredList.map(item => (
          <li key={item} onClick={() => handleItemClick(item)}>
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
}

// 부모 컴포넌트
function SearchableList() {
  const [filter, setFilter] = useState('');
  const [list, setList] = useState([
    'Apple', 'Banana', 'Cherry', 'Date', 'Elderberry',
    'Fig', 'Grape', 'Honeydew', 'Kiwi', 'Lemon'
  ]);
  const [count, setCount] = useState(0);
  
  // useCallback으로 함수 메모이제이션
  const addItem = useCallback(() => {
    const fruits = ['Mango', 'Nectarine', 'Orange', 'Papaya', 'Quince'];
    const randomFruit = fruits[Math.floor(Math.random() * fruits.length)];
    setList(prevList => [...prevList, randomFruit]);
  }, []);
  
  return (
    <div>
      <div className="controls">
        <input
          type="text"
          value={filter}
          onChange={e => setFilter(e.target.value)}
          placeholder="Filter items..."
        />
        <button onClick={addItem}>Add Random Fruit</button>
        <button onClick={() => setCount(count + 1)}>
          Count: {count} (Unrelated State)
        </button>
      </div>
      
      <ExpensiveCalculation list={list} filter={filter} />
    </div>
  );
}

코멘트: useMemouseCallback은 React의 성능 최적화를 위한 핵심 Hook입니다:

  • useMemo: 계산 비용이 많이 드는 값을 메모이제이션합니다. 의존성 배열의 값이 변경될 때만 재계산됩니다.
  • useCallback: 함수를 메모이제이션합니다. 특히 자식 컴포넌트에 props로 전달되는 함수에 유용합니다.

이러한 Hook은 다음과 같은 경우에 사용하는 것이 좋습니다:

  • 계산 비용이 많이 드는 연산
  • 깊은 컴포넌트 트리로 전달되는 함수
  • React.memo로 최적화된 컴포넌트에 전달되는 함수나 객체

모든 값이나 함수를 메모이제이션하는 것은 오히려 성능을 저하시킬 수 있으므로, 실제 성능 이슈가 있는 경우에만 사용하세요.

가상화 (Virtualization)

import { useState } from 'react';
import { FixedSizeList } from 'react-window';

function VirtualizedList() {
  // 대량의 데이터 생성 (예: 10,000개 항목)
  const [items] = useState(() => 
    Array.from({ length: 10000 }, (_, index) => ({
      id: index,
      text: `Item ${index}`,
      description: `This is the description for item ${index}`
    }))
  );
  
  // 각 항목 렌더링 함수
  const Row = ({ index, style }) => {
    const item = items[index];
    
    return (
      <div 
        style=
      >
        <div>
          <h3>{item.text}</h3>
          <p>{item.description}</p>
        </div>
      </div>
    );
  };
  
  return (
    <div>
      <h2>Virtualized List (10,000 items)</h2>
      <p>Only renders items visible in the viewport</p>
      
      {/* 가상화된 리스트 */}
      <FixedSizeList
        height={400}
        width="100%"
        itemCount={items.length}
        itemSize={80} // 각 항목의 높이
      >
        {Row}
      </FixedSizeList>
    </div>
  );
}

// 그리드 가상화 예시
import { FixedSizeGrid } from 'react-window';

function VirtualizedGrid() {
  const columnCount = 100;
  const rowCount = 100;
  
  // 셀 렌더링 함수
  const Cell = ({ columnIndex, rowIndex, style }) => (
    <div
      style=
    >
      r{rowIndex}, c{columnIndex}
    </div>
  );
  
  return (
    <div>
      <h2>Virtualized Grid (100x100 cells)</h2>
      
      <FixedSizeGrid
        columnCount={columnCount}
        columnWidth={100}
        height={400}
        rowCount={rowCount}
        rowHeight={35}
        width={500}
      >
        {Cell}
      </FixedSizeGrid>
    </div>
  );
}

코멘트: 가상화(Virtualization)는 대량의 데이터를 효율적으로 렌더링하는 기술로, 화면에 보이는 요소만 실제로 렌더링합니다. react-windowreact-virtualized 같은 라이브러리를 사용하여 구현할 수 있습니다. 이 기술은 다음과 같은 경우에 특히 유용합니다:

  • 수천 개의 항목을 포함하는 긴 목록
  • 대규모 테이블이나 그리드
  • 무한 스크롤 UI

가상화를 사용하면 메모리 사용량을 줄이고 렌더링 성능을 크게 향상시킬 수 있습니다. 모바일 기기와 같은 저사양 환경에서 특히 중요합니다.

코드 분할 (Code Splitting)

import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

// 일반적인 정적 import
import Home from './Home';

// 지연 로딩을 위한 동적 import
const About = lazy(() => import('./About'));
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/about">About</Link></li>
            <li><Link to="/dashboard">Dashboard</Link></li>
            <li><Link to="/settings">Settings</Link></li>
          </ul>
        </nav>
        
        {/* Suspense는 지연 로딩 컴포넌트가 로드되는 동안 fallback UI를 표시 */}
        <Suspense fallback={<div>Loading...</div>}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/dashboard" element={<Dashboard />} />
            <Route path="/settings" element={<Settings />} />
          </Routes>
        </Suspense>
      </div>
    </Router>
  );
}

// 컴포넌트 내부에서 조건부 지연 로딩
function ProductPage({ productId }) {
  const ProductDetails = lazy(() => import('./ProductDetails'));
  const ProductReviews = lazy(() => import('./ProductReviews'));
  
  const [activeTab, setActiveTab] = useState('details');
  
  return (
    <div>
      <h1>Product {productId}</h1>
      
      <div className="tabs">
        <button 
          onClick={() => setActiveTab('details')}
          className={activeTab === 'details' ? 'active' : ''}
        >
          Details
        </button>
        <button 
          onClick={() => setActiveTab('reviews')}
          className={activeTab === 'reviews' ? 'active' : ''}
        >
          Reviews
        </button>
      </div>
      
      <Suspense fallback={<div>Loading tab...</div>}>
        {activeTab === 'details' ? (
          <ProductDetails id={productId} />
        ) : (
          <ProductReviews id={productId} />
        )}
      </Suspense>
    </div>
  );
}

코멘트: 코드 분할(Code Splitting)은 애플리케이션을 더 작은 청크로 나누어 필요할 때만 로드하는 기술입니다. React에서는 React.lazySuspense를 사용하여 구현합니다. 이 기술의 장점은 다음과 같습니다:

  • 초기 로딩 시간 단축
  • 필요한 코드만 로드하여 리소스 절약
  • 사용자가 실제로 방문하는 페이지만 다운로드

코드 분할은 다음과 같은 경우에 특히 유용합니다:

  • 라우트 기반 분할 (각 페이지별로 분할)
  • 컴포넌트 기반 분할 (큰 컴포넌트나 라이브러리)
  • 조건부 로딩 (특정 조건에서만 필요한 기능)

불필요한 렌더링 방지

import { useState, useEffect, useRef } from 'react';

function RenderCounter({ id }) {
  const renderCount = useRef(0);
  
  useEffect(() => {
    renderCount.current += 1;
  });
  
  return (
    <div className="render-counter">
      Component {id} rendered {renderCount.current} times
    </div>
  );
}

// 불필요한 렌더링이 발생하는 예
function IneffectiveComponent() {
  const [count, setCount] = useState(0);
  
  // 매 렌더링마다 새로운 함수 생성
  const handleClick = () => {
    console.log('Button clicked');
  };
  
  // 매 렌더링마다 새로운 객체 생성
  const user = { name: 'John', age: 30 };
  
  return (
    <div>
      <h2>Ineffective Component <RenderCounter id="ineffective" /></h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      
      {/* 매 렌더링마다 새로운 props를 받아 리렌더링됨 */}
      <ChildComponent user={user} onClick={handleClick} />
    </div>
  );
}

// 최적화된 버전
function OptimizedComponent() {
  const [count, setCount] = useState(0);
  
  // useCallback으로 함수 메모이제이션
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);
  
  // useMemo로 객체 메모이제이션
  const user = useMemo(() => ({ name: 'John', age: 30 }), []);
  
  return (
    <div>
      <h2>Optimized Component <RenderCounter id="optimized" /></h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      
      {/* 메모이제이션된 props로 불필요한 리렌더링 방지 */}
      <MemoizedChildComponent user={user} onClick={handleClick} />
    </div>
  );
}

// React.memo로 최적화된 자식 컴포넌트
const MemoizedChildComponent = memo(function ChildComponent({ user, onClick }) {
  return (
    <div>
      <RenderCounter id="child" />
      <p>User: {user.name}, {user.age}</p>
      <button onClick={onClick}>Click Me</button>
    </div>
  );
});

// 일반 자식 컴포넌트
function ChildComponent({ user, onClick }) {
  return (
    <div>
      <RenderCounter id="child" />
      <p>User: {user.name}, {user.age}</p>
      <button onClick={onClick}>Click Me</button>
    </div>
  );
}

코멘트: React에서 불필요한 렌더링을 방지하는 것은 성능 최적화의 핵심입니다. 주요 전략은 다음과 같습니다:

  1. React.memo를 사용하여 props가 변경되지 않으면 컴포넌트 리렌더링 방지
  2. useMemo를 사용하여 객체, 배열, 계산 결과 메모이제이션
  3. useCallback을 사용하여 이벤트 핸들러 함수 메모이제이션
  4. 상태 업데이트 로직 최적화 (불필요한 상태 업데이트 방지)
  5. 컴포넌트 구조 재설계 (상태를 적절한 위치에 배치)

렌더링 최적화는 항상 측정 가능한 성능 문제가 있을 때만 적용해야 합니다. 과도한 최적화는 코드를 복잡하게 만들 수 있습니다.

Categories:

Updated:

Leave a comment