React 고급 기술 익히기 - 1편: React의 렌더링 최적화 이해하기
안녕하세요! "React 고급 기술 익히기" 시리즈의 첫 번째 글에 오신 것을 환영합니다. 이 시리즈에서는 React를 더 깊이 이해하고 효율적으로 활용할 수 있는 다양한 고급 기술과 패턴을 다뤄보겠습니다. 오늘은 React 개발의 핵심 중 하나인 렌더링 최적화에 대해 살펴보겠습니다.
왜 렌더링 최적화가 중요할까?
React는 컴포넌트 기반으로 동작하며, 상태(state)나 props가 변경될 때 컴포넌트를 다시 렌더링합니다. 하지만 모든 컴포넌트가 항상 다시 렌더링될 필요는 없습니다. 불필요한 렌더링은 성능 저하를 유발할 수 있기 때문에 이를 방지하는 최적화 전략이 중요합니다.
1. React의 렌더링 원리
React에서 컴포넌트는 다음과 같은 경우에 렌더링됩니다:
- 상태(state)가 변경될 때
- 부모 컴포넌트의 props가 변경될 때
- 부모 컴포넌트가 다시 렌더링될 때
React는 DOM 업데이트를 최소화하기 위해 Virtual DOM을 사용하지만, Virtual DOM 비교 작업(diffing)에도 비용이 듭니다. 따라서 불필요한 렌더링을 줄이는 것이 React 최적화의 핵심입니다.
2. 렌더링 최적화 전략
(1) React.memo
React.memo는 컴포넌트를 **메모이제이션(memoization)**하여 props가 변경되지 않으면 다시 렌더링하지 않도록 합니다.
이는 함수형 컴포넌트에서만 사용할 수 있습니다.
예제:
import React from 'react';
const ChildComponent = React.memo(({ value }) => {
console.log('ChildComponent 렌더링');
return <div>{value}</div>;
});
export default function ParentComponent() {
const [count, setCount] = React.useState(0);
return (
<div>
<ChildComponent value="React.memo 활용" />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
출력 결과:
- 버튼을 클릭해도 ChildComponent는 다시 렌더링되지 않습니다. (props가 변경되지 않으므로)
(2) useCallback
React는 함수형 컴포넌트가 렌더링될 때마다 내부에 선언된 함수를 새로 생성합니다. 이를 방지하기 위해 **useCallback**으로 함수를 메모이제이션할 수 있습니다.
예제:
import React, { useState, useCallback } from 'react';
const Button = React.memo(({ onClick }) => {
console.log('Button 렌더링');
return <button onClick={onClick}>Click Me</button>;
});
export default function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button Clicked');
}, []);
return (
<div>
<Button onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
출력 결과:
- handleClick 함수가 메모이제이션되어 Button 컴포넌트는 불필요한 재렌더링을 방지합니다.
(3) useMemo
useMemo는 값이 자주 변경되지 않는 계산 작업을 메모이제이션하여 성능을 최적화합니다.
예제:
import React, { useState, useMemo } from 'react';
export default function App() {
const [count, setCount] = useState(0);
const expensiveCalculation = (num) => {
console.log('Expensive Calculation');
return num * 2;
};
const result = useMemo(() => expensiveCalculation(count), [count]);
return (
<div>
<p>Result: {result}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
출력 결과:
- count가 변경될 때만 expensiveCalculation 함수가 호출됩니다.
(4) React Profiler로 성능 분석
React Developer Tools에 포함된 Profiler를 사용하면 컴포넌트가 얼마나 자주 렌더링되는지, 어떤 이유로 렌더링되었는지를 확인할 수 있습니다.
Profiler 활성화 방법:
- React Developer Tools 설치.
- Chrome 개발자 도구에서 Profiler 탭 선택.
- 앱 상호작용 후 기록(Record) 버튼으로 렌더링 분석.
(5) 적절한 상태 관리
- 상태를 최소화: 필요 이상으로 많은 상태를 관리하지 않도록 설계합니다.
- 상태를 올바르게 분리: 상태를 관련 있는 컴포넌트끼리만 공유하도록 관리합니다.
- Context API 사용 주의: Context의 값이 변경되면 해당 Context를 사용하는 모든 컴포넌트가 렌더링되므로, 적절히 분리하는 것이 중요합니다.
(6) Code Splitting과 Lazy Loading
- Code Splitting: 필요한 부분의 코드만 로드하여 초기 로드 시간을 줄입니다.
- Lazy Loading: React의 React.lazy를 사용해 컴포넌트를 동적으로 로드합니다.
예제:
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./LazyComponent'));
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
3. 렌더링 최적화 체크리스트
- React.memo를 활용해 불필요한 렌더링 방지.
- 함수나 계산 값은 useCallback과 useMemo로 메모이제이션.
- React Profiler로 성능 병목 현상 분석.
- Context API 사용 시 주의하여 리렌더링 최소화.
- Code Splitting과 Lazy Loading으로 초기 로드 최적화.
결론
React의 렌더링 최적화는 성능 개선과 사용자 경험 향상을 위해 꼭 필요한 과정입니다. React.memo, useCallback, useMemo와 같은 기술은 간단해 보이지만, 복잡한 애플리케이션에서 큰 차이를 만들어낼 수 있습니다.
다음 편에서는 React에서 상태 관리를 최적화하는 방법과 상태 관리 라이브러리 비교를 다룰 예정입니다.