Context-Query: React 상태 관리 라이브러리 개발 여정
Context API와 React Query의 장점을 결합한 최적화된 상태 관리 솔루션
Next.js 애플리케이션에서 번들 크기를 최적화하기 위해 Dynamic Import를 사용할 때, React의 lazy와 Next.js의 dynamic 중 어떤 것을 선택해야 할지 혼란스러울 수 있습니다. 두 방식은 비슷해 보이지만 SSR(Server-Side Rendering) 제어에서 결정적인 차이를 보입니다.
이 글에서는 두 방식의 컴파일 동작과 SSR 처리 방식을 비교하고, 각 상황에 맞는 최적의 선택 기준을 제시합니다.
Dynamic Import는 JavaScript 모듈을 비동기적으로 로드하여 초기 번들 크기를 줄이는 표준 기술입니다. 모든 코드를 처음부터 로드하는 대신, 필요한 시점에만 코드를 요청합니다.
// ES6 Dynamic Import (Promise 반환)
const loader = async () => import('./HeavyModule');
loader().then((module) => {
console.log(module.default);
});
// 또는 async/await
const module = await import('./HeavyModule');
console.log(module.default);핵심 특징:
React의 lazy 함수는 Dynamic Import와 Suspense를 결합하여 컴포넌트를 비동기 로드합니다.
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
const WrapComponent = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
};초기 로드:
WrapComponent가 렌더링됨LazyComponent는 아직 로드되지 않았으므로 Suspense의 fallback UI 표시LazyComponent 청크 파일을 HTTP 요청청크 로드 후:
4. 청크 파일 다운로드 완료
5. React가 컴포넌트를 파싱하고 준비
6. Suspense가 fallback 대신 LazyComponent 렌더링
네트워크 최적화:
초기 번들: main.js (300KB)
지연 로드: LazyComponent.chunk.js (150KB)
// lazy 미사용 시: 450KB 한 번에 로드
// lazy 사용 시: 300KB → 필요 시 150KB 추가 로드
클라이언트 컴포넌트 전용:
lazy는 React의 클라이언트 전용 API로, 서버 컴포넌트에서 직접 사용할 수 없습니다.
// [주의] 서버 컴포넌트에서 직접 사용 불가
export default function ServerPage() {
const LazyComponent = lazy(() => import('./LazyComponent')); // 에러!
return <LazyComponent />;
}Suspense 필수:
lazy로 로드한 컴포넌트는 반드시 Suspense로 감싸야 합니다. 그렇지 않으면 에러가 발생합니다.
lazy를 서버 컴포넌트에서 사용하려면 클라이언트 컴포넌트로 감싸야 합니다.
// ClientLazyComponent.tsx
'use client';
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
export const ClientComponent = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
};
// ServerComponent.tsx (서버 컴포넌트)
export default function ServerPage() {
return (
<div>
<ClientComponent /> {/* 클라이언트 컴포넌트로 감싸서 사용 */}
</div>
);
}이 방식은 작동하지만, 서버 컴포넌트에서 직접 Dynamic Import를 사용할 수 없다는 불편함이 있습니다.
Next.js는 서버/클라이언트 컴포넌트 모두에서 사용할 수 있는 dynamic 함수를 제공합니다.
import dynamic from 'next/dynamic';
const LazyComponent = dynamic(() => import('./LazyComponent'), {
ssr: true, // SSR 포함 여부 제어
loading: () => <div>Loading...</div> // 로딩 UI
});
export default function ServerPage() {
return (
<div>
<LazyComponent /> {/* Suspense 없이도 동작 */}
</div>
);
}핵심 특징:
Suspense 없이도 loading 옵션으로 로딩 UI 표시ssr 옵션으로 SSR 포함 여부 제어 가능 (결정적 차이점)| 특징 | React lazy | Next.js dynamic |
|---|---|---|
| 사용 환경 | 클라이언트 컴포넌트만 | 서버/클라이언트 모두 |
| Suspense 필요 | [지원] 필수 | [미지원] 선택적 (loading 옵션) |
| SSR 제어 | [미지원] 불가능 | [지원] 가능 (ssr 옵션) |
| 표준 API | [지원] React 표준 | [미지원] Next.js 전용 |
| 로딩 UI | Suspense fallback | loading 옵션 또는 Suspense |
| 코드 스플리팅 | [지원] 지원 | [지원] 지원 |
| 번들러 독립성 | [지원] 독립적 | [부분] Next.js 의존 |
중요: lazy를 사용해도 서버 컴포넌트 트리에 포함되면 SSR에 포함됩니다.
// ServerPage.tsx (서버 컴포넌트)
export default function ServerPage() {
return (
<ClientComponent> {/* 내부에서 lazy 사용 */}
{/* LazyComponent는 SSR에 포함됨 */}
</ClientComponent>
);
}lazy로 감싸도 코드 스플리팅은 적용되지만, SSR은 여전히 수행됩니다. 서버에서 HTML을 생성할 때 해당 컴포넌트가 포함됩니다.
SSR 결과:
<!-- 서버에서 생성된 HTML에 LazyComponent 포함 -->
<div data-react-component="LazyComponent">
<!-- LazyComponent의 렌더링 결과 -->
</div>dynamic의 ssr: false 옵션을 사용하면 SSR에서 완전히 제외할 수 있습니다.
const ClientOnlyComponent = dynamic(() => import('./HeavyChart'), {
ssr: false,
loading: () => <div>Loading chart...</div>
});
export default function ServerPage() {
return (
<div>
<ClientOnlyComponent />
</div>
);
}SSR 결과:
<!-- 서버에서 생성된 HTML에는 로딩 상태만 포함 -->
<div>
<div>Loading chart...</div>
</div>클라이언트에서 하이드레이션 시점에 비로소 컴포넌트를 로드하고 렌더링합니다.
// [주의] 서버에서 에러 발생 (window is not defined)
const MapComponent = () => {
const map = new window.google.maps.Map(...);
return <div ref={mapRef} />;
};
// [권장] ssr: false로 클라이언트 전용 렌더링
const MapComponent = dynamic(() => import('./MapComponent'), {
ssr: false,
loading: () => <div>Loading map...</div>
});// Chart.js 같은 대용량 라이브러리 (150KB+)
const HeavyChart = dynamic(() => import('./HeavyChart'), {
ssr: false, // SSR에서 제외하여 초기 로드 시간 단축
loading: () => <Skeleton height={400} />
});
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* 초기 HTML에는 Skeleton만 포함 */}
<HeavyChart data={chartData} />
</div>
);
}성능 이점:
// 관리자 페널은 대부분 사용자에게 필요 없음
const AdminPanel = dynamic(() => import('./AdminPanel'), {
ssr: false // 필요한 사용자에게만 클라이언트에서 로드
});
export default function Page({ user }) {
return (
<div>
{user.isAdmin && <AdminPanel />}
</div>
);
}// SSR은 유지하고 코드 스플리팅만 적용
const BlogContent = dynamic(() => import('./BlogContent'), {
ssr: true, // SEO를 위해 SSR 유지
loading: () => <ArticleSkeleton />
});
export default function BlogPost({ slug }) {
return (
<article>
<BlogContent slug={slug} />
</article>
);
}SEO 유지하면서 번들 크기 최적화 달성
필수 사용:
권장 사용:
권장 사용:
허용 사용:
두 방식 모두 동일하게 코드 스플리팅을 수행하므로 번들 크기는 동일합니다.
main.js: 300KB
LazyComponent.chunk.js: 150KB
| 상황 | React lazy | Next.js dynamic (ssr: true) | Next.js dynamic (ssr: false) |
|---|---|---|---|
| 서버 번들 크기 | 포함 | 포함 | 제외 |
| TTFB | 느림 (무거운 컴포넌트 포함) | 느림 | 빠름 |
| SEO | [지원] 좋음 | [지원] 좋음 | [부분] 클라이언트만 |
| FCP | 빠름 (서버 렌더링) | 빠름 | 느림 (로딩 UI만) |
SSR이 필요한 콘텐츠 (SEO 중요):
const BlogPost = dynamic(() => import('./BlogPost'), {
ssr: true // SEO를 위해 SSR 유지
});SSR이 불필요한 인터랙티브 요소:
const CommentSection = dynamic(() => import('./CommentSection'), {
ssr: false // 초기 로드 최적화
});// Before: React lazy
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
// After: Next.js dynamic
import dynamic from 'next/dynamic';
const LazyComponent = dynamic(() => import('./LazyComponent'), {
loading: () => <div>Loading...</div>
// ssr: true (기본값)
});
function Page() {
return <LazyComponent />;
}// Before: lazy + 'use client' wrapper
'use client';
import { lazy, Suspense } from 'react';
const BrowserComponent = lazy(() => import('./BrowserComponent'));
// After: dynamic with ssr: false
import dynamic from 'next/dynamic';
const BrowserComponent = dynamic(() => import('./BrowserComponent'), {
ssr: false
});Next.js에서 Dynamic Import를 사용할 때 SSR 제어가 필요한지 여부가 가장 중요한 선택 기준입니다.
핵심 권장사항:
Next.js 프로젝트라면 dynamic 우선 사용
ssr 옵션으로 유연한 제어브라우저 API 의존 컴포넌트는 ssr: false 필수
SEO가 중요한 콘텐츠는 ssr: true 유지
순수 React 앱이라면 lazy 사용
프로젝트의 요구사항과 성능 목표에 따라 적절한 방식을 선택하여, 최적의 사용자 경험과 개발자 경험을 달성하시기 바랍니다.
Context API와 React Query의 장점을 결합한 최적화된 상태 관리 솔루션
이벤트 루프 제어를 통한 동적 임포트 분석과 Link Preload 헤더 생성 전략
React가 클래스 컴포넌트의 한계를 극복하고 함수형 컴포넌트와 Hooks를 도입한 기술적 배경과 설계 철학 완벽 분석