포스트

[Frontend] React 리렌더링 최적화: 메모이제이션의 현명한 사용법

[Frontend] React 리렌더링 최적화: 메모이제이션의 현명한 사용법

들어가며

React 애플리케이션의 성능을 최적화하는 데 있어 가장 중요한 요소 중 하하나는 불필요한 리렌더링을 방지하는 것입니다. 이 포스트에서는 React의 메모이제이션 기법들을 깊이 있게 살펴보고, 이를 효과적으로 활용하는 방법에 대해 알아보겠습니다.

목차

  1. React의 리렌더링 이해하기
  2. 메모이제이션 도구들
  3. 실전 예제로 보는 최적화
  4. 성능 측정과 분석
  5. 주의사항과 모범 사례

1. React의 리렌더링 이해하기

1.1 리렌더링이 발생하는 경우

React에서 리렌더링이 발생하는 주요 상황들:

  • 컴포넌트의 상태(state)가 변경될 때
  • 부모 컴포넌트가 리렌더링될 때
  • props가 변경될 때
  • context가 변경될 때

1.2 불필요한 리렌더링의 영향

1
2
3
4
5
6
7
8
9
10
11
12
13
// 부모 컴포넌트의 상태 변경으로 인한 불필요한 자식 컴포넌트 리렌더링
function ParentComponent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      <ExpensiveChild /> {/* count와 무관하지만 매번 리렌더링됨 */}
    </div>
  );
}

2. 메모이제이션 도구들

2.1 React.memo

컴포넌트 자체를 메모이제이션하여 props가 변경되지 않으면 리렌더링을 방지합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 기본적인 React.memo 사용법
const MemoizedComponent = React.memo(function MyComponent(props) {
  return (
    <div>
      <h2>{props.title}</h2>
      <p>{props.description}</p>
    </div>
  );
});

// 커스텀 비교 함수 사용
const MemoizedComponentWithCustomComparison = React.memo(
  MyComponent,
  (prevProps, nextProps) => {
    return prevProps.title === nextProps.title;
    // description이 변경되어도 리렌더링하지 않음
  }
);

2.2 useMemo

계산 비용이 높은 값을 메모이제이션합니다.

1
2
3
4
5
6
7
8
9
10
11
function DataAnalytics({ data }) {
  // 복잡한 데이터 처리를 메모이제이션
  const processedData = useMemo(() => {
    return data.map(item => {
      // 복잡한 계산 수행
      return performExpensiveCalculation(item);
    });
  }, [data]); // data가 변경될 때만 재계산

  return <ChartComponent data={processedData} />;
}

2.3 useCallback

함수를 메모이제이션하여 불필요한 리렌더링을 방지합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SearchComponent({ onSearch }) {
  const [searchTerm, setSearchTerm] = useState('');

  const handleSearch = useCallback(() => {
    onSearch(searchTerm);
  }, [searchTerm, onSearch]); // 의존성이 변경될 때만 함수 재생성

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <SearchButton onClick={handleSearch} />
    </div>
  );
}

3. 실전 예제로 보는 최적화

3.1 데이터 그리드 최적화 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 최적화 전
function DataGrid({ data, onSort, onFilter }) {
  const sortedData = data.sort((a, b) => /* 복잡한 정렬 로직 */);
  
  return (
    <div>
      {sortedData.map(item => (
        <Row key={item.id} data={item} />
      ))}
    </div>
  );
}

// 최적화 후
const DataGrid = React.memo(function DataGrid({ data, onSort, onFilter }) {
  const sortedData = useMemo(() => {
    return data.sort((a, b) => /* 복잡한 정렬 로직 */);
  }, [data]);

  const handleSort = useCallback((column) => {
    onSort(column);
  }, [onSort]);

  return (
    <div>
      {sortedData.map(item => (
        <MemoizedRow key={item.id} data={item} onSort={handleSort} />
      ))}
    </div>
  );
});

const MemoizedRow = React.memo(Row);

3.2 잘못된 최적화 예제

1
2
3
4
5
6
7
8
9
10
11
12
// 불필요한 메모이제이션의 예
function SimpleButton({ onClick }) {
  // 단순 문자열 반환에 불필요한 useMemo 사용
  const buttonText = useMemo(() => "Click me", []); 

  // 단순 함수에 불필요한 useCallback 사용
  const handleClick = useCallback(() => {
    onClick();
  }, [onClick]);

  return <button onClick={handleClick}>{buttonText}</button>;
}

4. 성능 측정과 분석

4.1 React DevTools Profiler 활용

React DevTools의 Profiler를 사용하여 컴포넌트의 리렌더링을 분석하는 방법:

  1. 개발자 도구에서 React DevTools 열기
  2. Profiler 탭 선택
  3. Record 버튼을 눌러 성능 측정 시작
  4. 애플리케이션 사용
  5. 측정 종료 후 결과 분석

4.2 성능 측정 지표

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Profiler } from 'react';

function onRenderCallback(
  id, // 방금 커밋된 Profiler 트리의 "id"
  phase, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
  actualDuration, // 렌더링에 걸린 시간
  baseDuration, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
  startTime, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
  commitTime, // React가 해당 업데이트를 언제 커밋했는지
  interactions // 이 업데이트에 해당하는 상호작용들의 집합
) {
  console.log(`렌더링 시간: ${actualDuration}ms`);
}

function MyComponent() {
  return (
    <Profiler id="MyComponent" onRender={onRenderCallback}>
      <div>
        {/* 컴포넌트 내용 */}
      </div>
    </Profiler>
  );
}

5. 주의사항과 모범 사례

5.1 메모이제이션을 사용해야 하는 경우

  • 복잡한 계산이나 데이터 처리가 필요한 경우
  • 컴포넌트가 자주 리렌더링되는 경우
  • 깊은 중첩 구조에서 성능 문제가 발생하는 경우

5.2 메모이제이션을 피해야 하는 경우

  • 단순한 계산이나 렌더링의 경우
  • 자주 변경되는 데이터나 props를 다루는 경우
  • 메모리 사용량이 중요한 경우

5.3 모범 사례

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 좋은 예시
function UserList({ users, onUserSelect }) {
  // 복잡한 필터링 로직을 메모이제이션
  const filteredUsers = useMemo(() => {
    return users.filter(user => {
      return complexFilteringLogic(user);
    });
  }, [users]);

  // 자주 사용되는 콜백 함수를 메모이제이션
  const handleUserSelect = useCallback((userId) => {
    onUserSelect(userId);
  }, [onUserSelect]);

  return (
    <div>
      {filteredUsers.map(user => (
        <UserItem
          key={user.id}
          user={user}
          onSelect={handleUserSelect}
        />
      ))}
    </div>
  );
}

// UserItem 컴포넌트를 메모이제이션
const UserItem = React.memo(function UserItem({ user, onSelect }) {
  return (
    <div onClick={() => onSelect(user.id)}>
      {user.name}
    </div>
  );
});

결론

React의 메모이제이션 도구들은 강력한 성능 최적화 수단이지만, 무분별한 사용은 오히려 성능 저하를 초래할 수 있습니다. 실제 성능 문제가 발생하는 지점을 정확히 파악하고, 적절한 도구를 선택적으로 적용하는 것이 중요합니다. React DevTools의 Profiler를 활용하여 성능을 측정하고, 이를 바탕으로 최적화를 진행하는 것이 바람직한 접근 방법입니다.

참고 자료

  • React 공식 문서
  • React DevTools 문서
  • Dan Abramov의 블로그 게시물
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.