React.lazy()는 동적 import를 사용하여 컴포넌트를 필요한 시점에 불러오는 기능입니다. 이를 통해 초기 번들 크기를 줄이고 애플리케이션의 초기 로딩 성능을 개선할 수 있습니다.
특징
- 사용자가 보는 부분만 먼저 렌더링하고 나머지 부분은 나중에 렌더링을 한다.
- lazy 컴포넌트를 사용할 때는 반드시 Suspense 컴포넌트로 감싸야 합니다.
- 코드 스플리팅
- 큰 번들을 작은 청크로 분할하여 필요할 때만 로드할 수 있다
- 사용자가 실제로 필요로 할 때까지 코드 로딩을 지연한다
내부적으로 동작 방법
1. promise 기반 동작이 코드가 실행되면 다음과 같은 과정이 일어난다.
const LazyComponent = React.lazy(() => import('./MyComponent'));
이 코드가 실행되면 다음과 같은 과정이 일어난다.
// React.lazy의 간단화된 내부 구현
function lazy(factory) {
let status = 'pending';
let result;
const lazyComponent = {
$$typeof: REACT_LAZY_TYPE,
_payload: {
_status: status,
_result: null,
_factory: factory
},
_init: function(payload) {
if (payload._status === 'pending') {
const factory = payload._factory;
try {
const thenable = factory();
payload._status = 'resolved';
payload._result = thenable.then(resolved => {
payload._result = resolved.default || resolved;
});
} catch (error) {
payload._status = 'rejected';
payload._result = error;
}
}
return payload._result;
}
};
return lazyComponent;
}
2. 상태 관리 : React.lazy는 내부적으로 세 가지 상태를 관리합니다
- pending: 초기 상태
- resolved: 컴포넌트 로드 성공
- rejected: 컴포넌트 로드 실패
// 내부적인 상태 처리 예시
function resolveComponent(lazyComponent) {
const payload = lazyComponent._payload;
const init = lazyComponent._init;
switch (payload._status) {
case 'pending':
// 컴포넌트 로딩 시작
return init(payload);
case 'resolved':
// 이미 로드된 컴포넌트 반환
return payload._result;
case 'rejected':
// 에러 처리
throw payload._result;
default:
throw new Error('Unexpected status');
}
}
3. Suspense와의 연동
// Suspense의 간단화된 동작 방식
function Suspense({ children, fallback }) {
try {
return children;
} catch (promise) {
if (promise instanceof Promise) {
return fallback;
}
throw promise;
}
}
4. 웹팩과의 통합
// 웹팩이 생성하는 청크 코드 예시
// 0.chunk.js
export default function MyComponent() {
return <div>Lazy Loaded!</div>;
}
// main.js
__webpack_require__.e(0).then(__webpack_require__.bind(null, './MyComponent'))
5. 실제 렌더링 프로세스
// 렌더링 프로세스의 간단화된 예시
function renderLazyComponent(lazyComponent) {
const Component = resolveComponent(lazyComponent);
if (Component instanceof Promise) {
// Suspense로 fallback을 보여줌
throw Component;
}
// 컴포넌트 렌더링
return <Component />;
}
6. 청크 로딩 메커니즘
// 동적 import가 변환되는 방식
const LazyComponent = React.lazy(() => import('./MyComponent'));
// 웹팩에 의해 다음과 같이 변환됨
const LazyComponent = React.lazy(() =>
__webpack_require__.e('MyComponent')
.then(__webpack_require__.bind(null, './MyComponent'))
);
주요 동작 특징
- 메모이제이션
- 한 번 로드된 컴포넌트는 캐시되어 재사용된다
- 불필요한 재로딩을 방지할 수 있다
- 청크 관리
- 웹팩이 코드를 자동으로 청크로 분할한다
- 각 청크는 고유한 ID를 가진다
- 에러 처리
// 내부 에러 처리 메커니즘
function handleLazyError(error, retries = 3) {
if (retries > 0) {
return new Promise(resolve => {
setTimeout(() => {
resolve(loadComponent(retries - 1));
}, 1000);
});
}
throw error;
}
4. 성능 최적화
- 코드 스플리팅으로 초기 번들 크기 감소
- 필요한 시점에 필요한 코드만 로드
기본적인 사용방법
// 일반적인 import
import OrdinaryComponent from './OrdinaryComponent';
// lazy 로딩을 사용한 import
const LazyComponent = React.lazy(() => import('./LazyComponent'));
import React, { Suspense } from 'react';
function MyApp() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
라우팅에서 활용
import { BrowserRouter, Route, Routes } from 'react-router-dom';
const Home = React.lazy(() => import('./routes/Home'));
const About = React.lazy(() => import('./routes/About'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
조건부 렌더링에서의 활용
const AdminPanel = React.lazy(() => import('./AdminPanel'));
function App() {
const [isAdmin, setIsAdmin] = useState(false);
return (
<div>
{isAdmin && (
<Suspense fallback={<div>Loading admin panel...</div>}>
<AdminPanel />
</Suspense>
)}
</div>
);
}