React 애플리케이션의 성능 최적화는 사용자 경험을 향상시키고 리소스 사용을 효율화하는 중요한 과정입니다. 이 블로그에서는 React 애플리케이션의 성능을 최적화하기 위한 체계적인 워크플로우를 알아보겠습니다.
1. 성능 문제 식별하기
성능 최적화의 첫 단계는 현재 애플리케이션의 성능 문제를 정확히 파악하는 것입니다.
1.1 React DevTools 사용하기
React DevTools의 Profiler 탭을 사용하여 컴포넌트 렌더링 시간을 측정할 수 있습니다.
- Chrome 개발자 도구를 열고 React DevTools의 Profiler 탭으로 이동합니다.
- 녹화 버튼을 클릭하고 애플리케이션을 사용합니다.
- 녹화를 멈추고 결과를 분석합니다.
// 성능 문제가 있는 컴포넌트 예시
const SlowComponent = ({ data }) => {
// 무거운 계산
const processedData = data.map(item => expensiveOperation(item));
return (
<div>
{processedData.map(item => (
<div key={item.id}>{item.value}</div>
))}
</div>
);
};
이 컴포넌트는 매 렌더링마다 무거운 계산을 수행하므로 성능 문제가 발생할 수 있습니다.
1.2 Lighthouse 사용하기
Lighthouse를 사용하여 전반적인 웹 성능을 측정할 수 있습니다.
- Chrome 개발자 도구의 Lighthouse 탭으로 이동합니다.
- 분석하고자 하는 카테고리를 선택하고 "Generate report" 버튼을 클릭합니다.
- 결과를 검토하고 개선이 필요한 영역을 파악합니다.
2. 코드 분할 (Code Splitting) 적용하기
대규모 React 애플리케이션의 경우, 코드 분할을 통해 초기 로딩 시간을 줄일 수 있습니다.
2.1 React.lazy와 Suspense 사용하기
import React, { Suspense, lazy } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
이 방식을 사용하면 HeavyComponent
는 필요한 시점에만 로드됩니다.
3. 메모이제이션 활용하기
불필요한 리렌더링을 방지하기 위해 React의 메모이제이션 기능을 활용할 수 있습니다.
3.1 React.memo 사용하기
const MyComponent = React.memo(({ data }) => {
// 렌더링 로직
});
React.memo
는 props가 변경되지 않았다면 컴포넌트의 리렌더링을 방지합니다.
3.2 useMemo와 useCallback 사용하기
const MyComponent = ({ data }) => {
const expensiveResult = useMemo(() => {
return expensiveComputation(data);
}, [data]);
const handleClick = useCallback(() => {
console.log(data);
}, [data]);
return (
<div>
<p>{expensiveResult}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
};
useMemo
는 계산 비용이 높은 값을, useCallback
은 함수를 메모이제이션합니다.
4. 가상화 (Virtualization) 적용하기
대량의 데이터를 렌더링 할 때는 가상화 기술을 사용하여 성능을 개선할 수 있습니다.
4.1 react-window 사용하기
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const MyList = () => (
<List
height={400}
itemCount={1000}
itemSize={35}
width={300}
>
{Row}
</List>
);
이 방식을 사용하면 화면에 보이는 항목만 렌더링 되어 메모리 사용량과 렌더링 시간이 크게 줄어듭니다.
5. 상태 관리 최적화하기
효율적인 상태 관리는 React 애플리케이션의 성능에 큰 영향을 미칩니다.
5.1 상태 정규화하기
// Before
const state = {
users: [
{ id: 1, name: 'John', posts: [{ id: 1, title: 'Hello' }] }
]
};
// After
const state = {
users: {
1: { id: 1, name: 'John', postIds: [1] }
},
posts: {
1: { id: 1, title: 'Hello' }
}
};
상태를 정규화하면 데이터 업데이트가 더 효율적으로 이루어집니다.
5.2 불변성 유지하기
// Bad
const updateUser = (users, id, updates) => {
users[id] = { ...users[id], ...updates };
return users;
};
// Good
const updateUser = (users, id, updates) => {
return {
...users,
[id]: { ...users[id], ...updates }
};
};
불변성을 유지하면 React가 상태 변화를 더 쉽게 감지할 수 있습니다.
6. 서버 사이드 렌더링 (SSR) 고려하기
초기 로딩 성능을 개선하기 위해 서버 사이드 렌더링을 고려할 수 있습니다.
6.1 Next.js 사용하기
Next.js는 React 애플리케이션의 SSR을 쉽게 구현할 수 있게 해 줍니다.
// pages/index.js
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return { props: { data } };
}
function HomePage({ data }) {
return <div>{data.title}</div>;
}
export default HomePage;
이 방식을 사용하면 서버에서 초기 데이터를 가져와 렌더링 한 HTML을 클라이언트에 전송합니다.
7. 번들 크기 최적화하기
애플리케이션의 번들 크기를 줄이면 초기 로딩 시간을 단축할 수 있습니다.
7.1 Tree Shaking 활용하기
webpack과 같은 번들러의 Tree Shaking 기능을 활용하여 사용하지 않는 코드를 제거할 수 있습니다.
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
},
};
7.2 Code Splitting 사용하기
import()
를 사용하여 동적으로 모듈을 import 할 수 있습니다.
const MyComponent = () => {
const [Module, setModule] = useState(null);
useEffect(() => {
import('./HeavyModule').then(module => setModule(module.default));
}, []);
return Module ? <Module /> : <div>Loading...</div>;
};
8. 이미지 최적화하기
이미지는 웹 성능에 큰 영향을 미치는 요소 중 하나입니다.
8.1 이미지 지연 로딩
import { LazyLoadImage } from 'react-lazy-load-image-component';
const MyImage = ({ src, alt }) => (
<LazyLoadImage
src={src}
alt={alt}
effect="blur"
/>
);
이 방식을 사용하면 이미지가 뷰포트에 들어올 때만 로드됩니다.
8.2 적절한 이미지 포맷 사용하기
WebP와 같은 현대적인 이미지 포맷을 사용하여 이미지 크기를 줄일 수 있습니다.
const MyImage = ({ src, alt }) => (
<picture>
<source srcSet={`${src}.webp`} type="image/webp" />
<img src={`${src}.jpg`} alt={alt} />
</picture>
);
9. 성능 모니터링 및 지속적인 최적화
성능 최적화는 일회성 작업이 아닌 지속적인 과정입니다.
9.1 성능 모니터링 도구 사용하기
- Google Analytics
- New Relic
- Sentry
이러한 도구들을 사용하여 실제 사용자의 성능 지표를 지속적으로 모니터링할 수 있습니다.
9.2 성능 예산 설정하기
성능 예산을 설정하고 이를 CI/CD 파이프라인에 통합하여 성능 저하를 사전에 방지할 수 있습니다.
// lighthouserc.js
module.exports = {
ci: {
assert: {
assertions: {
'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
'interactive': ['error', { maxNumericValue: 5000 }],
},
},
},
};
React 애플리케이션의 성능 최적화는 복잡하지만 체계적인 접근을 통해 효과적으로 수행할 수 있습니다. 이 워크플로우를 따라가면서 다음 단계들을 수행하세요:
- 성능 문제를 정확히 식별합니다.
- 코드 분할을 적용하여 초기 로딩 시간을 줄입니다.
- 메모이제이션을 활용하여 불필요한 리렌더링을 방지합니다.
- 대량의 데이터 렌더링 시 가상화를 적용합니다.
- 상태 관리를 최적화하여 효율적인 데이터 흐름을 만듭니다.
- 필요한 경우 서버 사이드 렌더링을 고려합니다.
- 번들 크기를 최적화하여 로딩 시간을 단축합니다.
- 이미지를 최적화하여 리소스 사용을 효율화합니다.
- 지속적인 성능 모니터링과 최적화를 수행합니다.
이러한 단계들을 꾸준히 실천하면 React 애플리케이션의 성능을 크게 향상시킬 수 있습니다. 성능 최적화는 사용자 경험을 개선하고 비즈니스 목표 달성에 도움을 주는 중요한 과정임을 기억하세요.