[Frontend] React 리렌더링 최적화: 메모이제이션의 현명한 사용법
[Frontend] React 리렌더링 최적화: 메모이제이션의 현명한 사용법
들어가며
React 애플리케이션의 성능을 최적화하는 데 있어 가장 중요한 요소 중 하하나는 불필요한 리렌더링을 방지하는 것입니다. 이 포스트에서는 React의 메모이제이션 기법들을 깊이 있게 살펴보고, 이를 효과적으로 활용하는 방법에 대해 알아보겠습니다.
목차
- React의 리렌더링 이해하기
- 메모이제이션 도구들
- 실전 예제로 보는 최적화
- 성능 측정과 분석
- 주의사항과 모범 사례
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를 사용하여 컴포넌트의 리렌더링을 분석하는 방법:
- 개발자 도구에서 React DevTools 열기
- Profiler 탭 선택
- Record 버튼을 눌러 성능 측정 시작
- 애플리케이션 사용
- 측정 종료 후 결과 분석
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 라이센스를 따릅니다.