일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 리액트훅
- next-cookies
- msw
- CSR
- Firebase
- 리액트 훅
- Database
- reactquery
- 클래스
- key
- SSR
- lazy()
- react-hook-form
- NextJs
- ErrorBoundary
- 리액트
- 모던자바스크립트
- useLayoutEffect
- 초기마운트
- react
- express
- useEffect
- docker
- react-hook
- Today
- Total
한우의 개발일기
lazy() 알아보기 본문
React의 lazy() 함수 이해하기: 동작 원리와 활용법
React로 대규모 애플리케이션을 개발할 때 마주치는 가장 큰 문제 중 하나는 초기 로딩 시간입니다. 사용자가 애플리케이션에 처음 접속할 때 모든 코드를 한 번에 다운로드하면 초기 로딩 시간이 길어져 사용자 경험이 저하됩니다. 이런 문제를 해결하기 위해 React는 lazy()
함수를 제공합니다.
React.lazy()란?
React.lazy()
는 코드 분할(Code Splitting)을 쉽게 구현할 수 있게 해주는 React의 내장 기능입니다. 코드 분할은 애플리케이션을 여러 개의 작은 번들로 나누고, 필요한 시점에만 해당 번들을 로드하는 기술입니다.
import React, { Suspense, lazy } from 'react';
// 일반적인 임포트
// import MyComponent from './MyComponent';
// lazy()를 사용한 동적 임포트
const MyComponent = lazy(() => import('./MyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>로딩 중...</div>}>
<MyComponent />
</Suspense>
</div>
);
}
위 코드에서 MyComponent
는 애플리케이션이 시작될 때 바로 로드되지 않고, 실제로 렌더링이 필요한 시점에만 로드됩니다. 로딩이 완료되기 전까지는 fallback
으로 지정한 컴포넌트가 표시됩니다.
lazy()의 동작 원리
1. 기본 동작 흐름
lazy()
의 동작 흐름은 다음과 같습니다:
lazy()
는 동적import()
를 인자로 받아 특별한 React 컴포넌트를 반환합니다.- 이 컴포넌트가 처음 렌더링될 때, 지정한 모듈을 비동기적으로 로드합니다.
- 모듈 로딩 중에는 React의 Suspense 메커니즘을 통해 렌더링을 일시 중단합니다.
- 모듈 로딩이 완료되면 내보낸 컴포넌트를 렌더링합니다.
2. Suspense와의 연동 - 왜 반드시 함께 써야 하는가?
lazy()
는 반드시 Suspense
컴포넌트와 함께 사용해야 합니다. 이는 단순한 권장사항이 아니라 기술적 필수사항입니다. 그 이유는 lazy()
의 내부 동작 메커니즘에 있습니다.
Suspense와 lazy()의 동작 원리
Promise 던지기 메커니즘
lazy()
로 생성된 컴포넌트가 렌더링될 때, 해당 모듈이 아직 로드되지 않았다면 Promise 객체를 던집니다(throw).- 일반적인 JavaScript에서 throw는 에러를 발생시키는 데 사용되지만, React의 Suspense 시스템에서는 이 특별한 패턴을 사용하여 "지금은 렌더링할 수 없으니 나중에 다시 시도해달라"는 신호로 활용합니다.
Suspense의 Promise 캐치
Suspense
컴포넌트는 자식 컴포넌트에서 던져진 Promise를 캐치합니다.- Promise가 캐치되면
Suspense
는 자신의fallback
prop에 지정된 컴포넌트를 대신 렌더링합니다.
Promise 해결 후 재시도
- 던져진 Promise가 해결(resolve)되면, React는 자동으로 렌더링을 다시 시도합니다.
- 이번에는 모듈이 로드되었으므로 실제 컴포넌트가 렌더링됩니다.
이 전체 과정을 코드로 표현하면 다음과 같습니다:
<Suspense fallback={<Spinner />}>
<LazyComponent /> {/* 아직 로드되지 않았다면 Promise를 throw */}
</Suspense>
LazyComponent
가 로딩 중일 때 Spinner
컴포넌트가 대신 표시되고, 로딩이 완료되면 자동으로 LazyComponent
가 렌더링됩니다.
Suspense 없이 lazy()를 사용하면 어떻게 될까?
만약 Suspense
없이 lazy()
를 사용하면 다음과 같은 상황이 발생합니다:
lazy()
컴포넌트가 렌더링될 때 Promise를 throw합니다.- 이 Promise를 캐치할
Suspense
가 없으므로 일반적인 예외처리와 동일하게 처리됩니다. - 결과적으로 "[object Promise] was thrown. React doesn't know how to handle this type of thrown value."와 같은 에러가 발생하고 애플리케이션이 중단됩니다.
따라서 lazy()
는 반드시 Suspense
와 함께 사용해야 정상적으로 동작합니다.
3. 내부 동작 방식 간략 설명
lazy()
의 내부 동작은 간략히 다음과 같습니다:
function lazy(importFn) {
// 로딩 상태를 관리하는 객체
const cache = {
status: 'pending', // 'pending', 'fulfilled', 'rejected' 중 하나
result: null // 로드된 컴포넌트 또는 에러
};
// Promise 기반 로딩 시작
importFn().then(
module => {
cache.status = 'fulfilled';
cache.result = module.default; // ES 모듈의 기본 내보내기
},
error => {
cache.status = 'rejected';
cache.result = error;
}
);
// 실제 렌더링 시 호출되는 컴포넌트
return function LazyComponent(props) {
// 상태에 따른 처리
if (cache.status === 'fulfilled') {
// 로딩 완료 - 실제 컴포넌트 렌더링
const Component = cache.result;
return <Component {...props} />;
} else if (cache.status === 'rejected') {
// 로딩 실패 - 에러 발생
throw cache.result;
} else {
// 로딩 중 - Promise를 throw하여 Suspense 발동
throw importFn();
}
};
}
이 구현은 실제 React 내부 구현과 조금 다르지만, 기본 원리는 동일합니다:
- 모듈 로딩 상태 관리
- 로딩 중일 때 Promise를 throw하여 Suspense 활성화
- 로딩 완료 시 실제 컴포넌트 렌더링
React의 실제 구현은 더 복잡하지만, 이 간략화된 버전으로도 핵심 개념을 이해할 수 있습니다.
lazy()의 실제 사용 사례
1. 라우트 기반 코드 분할
가장 일반적인 사용 사례는 라우트 기반 코드 분할입니다
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Contact = lazy(() => import('./routes/Contact'));
const Dashboard = lazy(() => import('./routes/Dashboard'));
function App() {
return (
<Routes>
<Route path="/" element={
<Suspense fallback={<div>로딩 중...</div>}>
<Home />
</Suspense>
} />
<Route path="/about" element={
<Suspense fallback={<div>로딩 중...</div>}>
<About />
</Suspense>
} />
<Route path="/contact" element={
<Suspense fallback={<div>로딩 중...</div>}>
<Contact />
</Suspense>
} />
<Route path="/dashboard" element={
<Suspense fallback={<div>로딩 중...</div>}>
<Dashboard />
</Suspense>
} />
</Routes>
);
}
이렇게 하면 사용자가 특정 페이지에 접근할 때만 해당 페이지의 코드가 로드됩니다.
2. 조건부 컴포넌트 로딩
특정 조건에서만 필요한 무거운 컴포넌트는 조건부로 로딩할 수 있습니다
function AdminPanel({ isAdmin }) {
// 관리자만 볼 수 있는 무거운 컴포넌트
const AdminDashboard = lazy(() => import('./AdminDashboard'));
return (
<div>
{isAdmin ? (
<Suspense fallback={<div>관리자 대시보드 로딩 중...</div>}>
<AdminDashboard />
</Suspense>
) : (
<p>관리자 권한이 필요합니다.</p>
)}
</div>
);
}
3. 사용자 상호작용에 따른 지연 로딩
버튼 클릭과 같은 사용자 상호작용에 따라 컴포넌트를 지연 로딩할 수 있습니다
function App() {
const [showChart, setShowChart] = useState(false);
const Chart = lazy(() => import('./Chart'));
return (
<div>
<button onClick={() => setShowChart(true)}>
차트 보기
</button>
{showChart && (
<Suspense fallback={<div>차트 로딩 중...</div>}>
<Chart />
</Suspense>
)}
</div>
);
}
lazy()의 고급 활용법
1. 사전 로딩 (Preloading)
사용자 경험을 더 향상시키기 위해 미리 컴포넌트를 로드할 수 있습니다. 이는 Suspense와 lazy()의 동작 원리를 활용한 최적화 기법입니다
// 컴포넌트 정의
const DetailView = lazy(() => import('./DetailView'));
// 사전 로딩 함수
const preloadDetailView = () => import('./DetailView');
function ProductList() {
return (
<div>
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onMouseEnter={preloadDetailView} // 마우스를 올렸을 때 미리 로드
/>
))}
{selectedProduct && (
<Suspense fallback={<div>상세 정보 로딩 중...</div>}>
<DetailView product={selectedProduct} />
</Suspense>
)}
</div>
);
}
이 방식이 작동하는 이유는 dynamic import(import()
)가 모듈을 한 번만 로드하기 때문입니다.
그리고 이 모듈은 브라우저에 의해 캐시됩니다
onMouseEnter
에서preloadDetailView
가 호출되면 모듈 로딩이 시작됩니다.- 사용자가 실제로 컴포넌트를 보기로 결정하면, 이미 모듈 로딩이 시작되었거나 완료되었기 때문에 기다리는 시간이 줄어듭니다.
- 모듈이 이미 완전히 로드되었다면, Suspense의 fallback이 전혀 표시되지 않고 즉시 컴포넌트가 렌더링됩니다.
2. 다중 Suspense 경계와 로딩 계층화
복잡한 UI의 경우 여러 개의 Suspense 경계를 사용하여 부분적으로 로딩 상태를 표시할 수 있습니다. 이는 Suspense의 중첩 사용을 통해 세밀한 로딩 제어가 가능함을 보여줍니다
function ProfilePage() {
return (
<div>
<Suspense fallback={<UserHeaderSkeleton />}>
<UserHeader />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<UserPosts />
</Suspense>
<Suspense fallback={<FriendsSkeleton />}>
<UserFriends />
</Suspense>
</div>
);
}
이 패턴이 작동하는 방식
독립적인 로딩 상태: 각 Suspense 경계는 독립적으로 작동하여, 한 컴포넌트의 로딩이 다른 컴포넌트의 렌더링을 차단하지 않습니다.
점진적 UI 렌더링: 준비된 컴포넌트부터 순차적으로 화면에 표시되므로, 사용자는 전체 페이지가 로드될 때까지 기다릴 필요가 없습니다.
컨텍스트별 로딩 UI: 각 컴포넌트에 맞는 스켈레톤 UI를 보여줌으로써 사용자 경험을 향상시킵니다.
중첩된 Suspense도 사용할 수 있습니다
function UserPosts() {
return (
<div>
<h2>사용자 게시물</h2>
<Suspense fallback={<PostListSkeleton />}>
<PostList />
<Suspense fallback={<LoadMoreButtonSkeleton />}>
<LoadMoreButton />
</Suspense>
</Suspense>
</div>
);
}
이 경우 외부 Suspense가 활성화되면 내부 Suspense는 무시됩니다. 내부 Suspense는 외부 컴포넌트가 로드된 후에만 고려됩니다.
3. 로딩 실패 처리
ErrorBoundary를 사용하여 로딩 실패를 처리할 수 있습니다
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
const LazyComponent = lazy(() => import('./HeavyComponent'));
return (
<ErrorBoundary
fallback={<div>컴포넌트를 로드하는 중 오류가 발생했습니다.</div>}
>
<Suspense fallback={<div>로딩 중...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
lazy()를 사용할 때 주의할 점
서버 사이드 렌더링:
lazy()
는 기본적으로 서버 사이드 렌더링을 지원하지 않습니다. SSR에서는@loadable/component
와 같은 대안을 고려하세요.네트워크 환경: 불안정한 네트워크 환경에서는 로딩이 오래 걸릴 수 있습니다. 적절한 로딩 UI와 타임아웃 처리를 고려하세요.
번들 크기 모니터링: Webpack Bundle Analyzer와 같은 도구를 사용하여 분할된 번들의 크기를 모니터링하세요.
중첩된 lazy 컴포넌트: 중첩된 lazy 컴포넌트는 중첩된 Suspense 없이 사용하면 예상치 못한 동작을 보일 수 있습니다.
개발 모드와 프로덕션 모드: 개발 모드에서는 코드 분할의 효과를 제대로 체감하기 어렵습니다. 프로덕션 빌드에서 성능을 확인하세요.
실제 작동 방식 더 자세히 알아보기
Suspense와 lazy()의 내부 메커니즘
React의 lazy()
와 Suspense 메커니즘은 "throw promise" 패턴을 기반으로 합니다. 이 패턴의 핵심은 다음과 같습니다:
- 렌더링 중단: 컴포넌트가 아직 준비되지 않았을 때 Promise 객체를 throw합니다.
- React의 캐치: React는 이 Promise를 캐치하고 가장 가까운 Suspense 경계를 활성화합니다.
- Promise 해결 감지: Promise가 해결되면 React는 자동으로 다시 렌더링을 시도합니다.
- 완료된 컴포넌트 렌더링: 컴포넌트가 준비되면 정상적으로 렌더링합니다.
Suspense의 렌더링 프로세스 이해하기
Suspense가 내부적으로 어떻게 작동하는지 더 구체적으로 살펴보겠습니다:
첫 번째 렌더링 시도:
- React는 컴포넌트 트리를 렌더링합니다.
lazy()
컴포넌트를 만나면 모듈 로딩 상태를 확인합니다.- 모듈이 아직 로드되지 않았다면 Promise를 throw합니다.
Suspense 대응:
- Suspense는 자식 컴포넌트에서 던져진 Promise를 감지합니다.
- 자식 컴포넌트의 렌더링을 중단하고 fallback UI를 렌더링합니다.
- Promise를 구독하여 완료 시점을 모니터링합니다.
모듈 로딩 완료 후:
- Promise가 해결되면 Suspense는 이를 감지합니다.
- React에게 해당 부분을 다시 렌더링하도록 요청합니다.
- 이번에는 모듈이 로드되었으므로 실제 컴포넌트가 렌더링됩니다.
Suspense의 동시 모드(Concurrent Mode) 최적화
React 18에서 도입된 동시 모드에서는 Suspense와 lazy()의 통합이 더욱 최적화되었습니다:
우선순위 기반 렌더링:
- 동시 모드에서는 UI 업데이트에 우선순위를 부여할 수 있습니다.
- Suspense가 활성화된 컴포넌트는 낮은 우선순위로 처리될 수 있어, 중요한 UI 업데이트가 차단되지 않습니다.
자동 중단 및 재시작:
- 더 중요한 업데이트가 발생하면 현재 진행 중인 Suspense 렌더링을 중단하고 나중에 재개할 수 있습니다.
- 이를 통해 애플리케이션의 반응성을 유지할 수 있습니다.
스트리밍 렌더링:
- 서버 사이드 렌더링에서 Suspense를 사용하면 컴포넌트가 준비되는 대로 HTML을 스트리밍할 수 있습니다.
- 이는 초기 로딩 성능을 크게 향상시킬 수 있습니다.
이 메커니즘을 이해하면 lazy()
뿐만 아니라 데이터 패칭과 같은 다른 비동기 작업도 Suspense와 통합할 수 있습니다. React의 Future 기능인 use()
훅은 이런 패턴을 더욱 확장하여 Promise나 Context의 값을 직접 컴포넌트 내에서 사용할 수 있게 합니다.
결론
React의 lazy()
함수는 코드 분할을 통해 애플리케이션 성능을 크게 향상시킬 수 있는 강력한 도구입니다. 초기에는 필요한 코드만 로드하고, 나머지는 필요할 때 로드함으로써 초기 로딩 시간을 단축하고 사용자 경험을 개선할 수 있습니다.
lazy()의 동작 원리를 이해하면 더 효과적으로 활용할 수 있으며, 다양한 패턴과 기법을 통해 애플리케이션의 성능과 사용자 경험을 최적화할 수 있습니다
'React' 카테고리의 다른 글
hydration알아보기 (0) | 2025.04.01 |
---|---|
Suspense 알아보기 (1) | 2025.03.24 |
Errorboundary 내부동작 이해하기 (1) | 2025.03.23 |
useEffect vs useLayoutEffect (0) | 2025.03.21 |
useAsync 파해치기 (0) | 2025.03.18 |