반응형
Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
Tags
- 코테
- javascript
- 리눅스
- 회고
- array
- 생각일기
- 네트워크
- mysql
- MongoDB
- 생각정리
- 생각로그
- nest.js
- 트러블슈팅
- 알고리즘
- typescript
- js
- mongo
- til
- next.js
- Java
- 피드백
- 기록
- WIL
- 자바스크립트
- 주간회고
- CS
- Grafana
- react
- mongoose
- Git
Archives
- Today
- Total
코딩일상
[Next.js] dynamicImport?? 란 선 구현 후 배움 본문
반응형

Next.js Dynamic Import 완벽 가이드
목차
Dynamic Import란?
코드 분석
const NaverMap = dynamic(() => import("@/components/map/NaverMap"), {
ssr: false,
loading: () => <LoadingSpinner message='로딩 중' />,
});
한 줄 요약: "필요할 때만 가져오기"
쉬운 비유
일반 import (정적 import):
여행 갈 때 모든 짐을 다 들고 출발
├─ 수영복 (바다 안 가도 챙김)
├─ 등산화 (산 안 가도 챙김)
├─ 스키복 (겨울 아닌데도 챙김)
└─ 가방 무거움! 😫
→ 처음부터 다 가져가니까 느림
dynamic import (동적 import):
필요한 것만 그때그때 가져오기
├─ 바다 가면 → 수영복 배송 받기
├─ 산 가면 → 등산화 배송 받기
└─ 가방 가벼움! 😊
→ 필요할 때만 가져오니까 빠름
왜 사용하는가?
1. 초기 로딩 속도 개선 (가장 중요!)
일반 import 방식:
// ❌ 나쁜 예
import NaverMap from "@/components/map/NaverMap";
function HomePage() {
const [showMap, setShowMap] = useState(false);
return (
<div>
<button onClick={() => setShowMap(true)}>
지도 보기
</button>
{showMap && <NaverMap />}
</div>
);
}
문제점:
페이지 로드 과정:
1. HTML 다운로드
2. JavaScript 다운로드 ← NaverMap 코드 포함 (큼!)
├─ 내 코드: 50KB
├─ NaverMap 라이브러리: 200KB 😱
└─ 총: 250KB
3. JavaScript 실행
4. 페이지 표시
→ 지도 안 볼 수도 있는데 무조건 다운로드!
→ 250KB를 다 받아야 페이지가 뜸!
dynamic import 방식:
// ✅ 좋은 예
const NaverMap = dynamic(() => import("@/components/map/NaverMap"), {
ssr: false,
loading: () => <LoadingSpinner message='로딩 중' />,
});
function HomePage() {
const [showMap, setShowMap] = useState(false);
return (
<div>
<button onClick={() => setShowMap(true)}>
지도 보기
</button>
{showMap && <NaverMap />}
</div>
);
}
개선점:
페이지 로드 과정:
1. HTML 다운로드
2. JavaScript 다운로드 ← 내 코드만! (작음!)
└─ 내 코드: 50KB
3. JavaScript 실행
4. 페이지 표시 ✅ (빠름!)
사용자가 "지도 보기" 클릭하면:
5. NaverMap 코드 다운로드 (200KB)
6. 지도 표시
→ 필요할 때만 다운로드!
→ 초기 로딩은 50KB만!
비교: 실제 숫자로 보기
시나리오: 쇼핑몰 메인 페이지
일반 import:
┌─────────────────────────────────────┐
│ 초기 다운로드: 500KB │
│ - 상품 목록: 100KB │
│ - 지도 (안 보일 수도): 200KB ❌ │
│ - 리뷰 모달 (안 열 수도): 150KB ❌ │
│ - 기타: 50KB │
│ │
│ 로딩 시간 (4G): 3초 │
└─────────────────────────────────────┘
dynamic import:
┌─────────────────────────────────────┐
│ 초기 다운로드: 150KB ✅ │
│ - 상품 목록: 100KB │
│ - 기타: 50KB │
│ │
│ 로딩 시간 (4G): 0.9초 ✅ │
│ │
│ 필요할 때: │
│ - 지도 클릭 시: +200KB │
│ - 리뷰 클릭 시: +150KB │
└─────────────────────────────────────┘
→ 초기 로딩 70% 빠름!
ssr: false의 의미
SSR이란?
SSR (Server-Side Rendering): 서버에서 미리 HTML을 만들어서 보내기
일반적인 과정:
클라이언트 요청
↓
서버에서 React 컴포넌트 실행
↓
HTML 생성
↓
클라이언트로 전송
↓
브라우저에 표시
왜 지도는 ssr: false가 필요한가?
문제 상황:
// NaverMap 컴포넌트 내부
function NaverMap() {
useEffect(() => {
// window 객체 사용
const map = new window.naver.maps.Map(...);
}, []);
}
서버에서 실행하면:
Node.js 서버 환경:
├─ window 객체 없음! ❌
├─ document 객체 없음! ❌
├─ navigator 객체 없음! ❌
└─ DOM API 없음! ❌
→ 에러 발생!
ReferenceError: window is not defined
비유로 이해하기:
서버 = 주방 (요리하는 곳)
브라우저 = 식당 (먹는 곳)
지도 = 테이블에서 먹는 음식
문제:
- 주방에는 테이블이 없음!
- 테이블 세팅은 식당에서만 가능!
해결:
- 주방(서버)에서는 안 만들고
- 식당(브라우저)에 가서 만들기
→ ssr: false
ssr: false 동작 방식
const NaverMap = dynamic(() => import("@/components/map/NaverMap"), {
ssr: false, // 서버에서는 렌더링 안 함!
});
실행 과정:
1. 서버에서:
└─ NaverMap 컴포넌트 건너뜀
└─ 빈 공간 또는 loading 컴포넌트만 렌더링
2. HTML 생성:
<div id="map-container">
<!-- 비어있음 또는 로딩 스피너 -->
</div>
3. 클라이언트로 전송
4. 브라우저에서:
└─ NaverMap 코드 다운로드
└─ window 객체 사용 가능! ✅
└─ 지도 렌더링
언제 ssr: false를 써야 할까?
필수적으로 써야 하는 경우:
✅ 브라우저 API를 사용하는 라이브러리
- 지도 (Naver, Google, Kakao Map)
- 차트 (일부 차트 라이브러리)
- 캔버스 애니메이션
- localStorage 사용
- Web API (Geolocation, Camera 등)
예시:
const Chart = dynamic(() => import("./Chart"), {
ssr: false
});
const WebcamCapture = dynamic(() => import("./Webcam"), {
ssr: false
});
선택적으로 쓰는 경우:
⚠️ 초기 로딩 최적화를 위해
- 무거운 컴포넌트
- 사용자가 클릭해야 보이는 것
- 스크롤 내려야 보이는 것
예시:
const HeavyModal = dynamic(() => import("./Modal"), {
ssr: false // 모달은 클릭해야 보이니까
});
const Footer = dynamic(() => import("./Footer"), {
ssr: false // 맨 아래 있으니까 나중에
});
loading의 역할
기본 개념
loading: () => <LoadingSpinner message='로딩 중' />
비유: 음식 기다릴 때 보는 "조리 중" 표시
레스토랑:
주문 → "조리 중입니다" 표시 → 음식 나옴
웹페이지:
클릭 → <LoadingSpinner /> 표시 → 컴포넌트 로드됨
loading 없으면?
// ❌ loading 옵션 없음
const NaverMap = dynamic(() => import("@/components/map/NaverMap"), {
ssr: false,
});
사용자 경험:
사용자: "지도 보기" 클릭!
↓
[ 아무 일도 안 일어남... ] ← 5초 동안 빈 화면
↓
갑자기 지도 나타남!
→ 고장난 건가? 😰
→ 클릭이 안 된 건가?
→ 다시 클릭! (중복 클릭)
loading 있으면?
// ✅ loading 옵션 있음
const NaverMap = dynamic(() => import("@/components/map/NaverMap"), {
ssr: false,
loading: () => <LoadingSpinner message='로딩 중' />,
});
사용자 경험:
사용자: "지도 보기" 클릭!
↓
[🔄 로딩 중...] ← 즉시 피드백!
↓
지도 나타남!
→ 기다리는 중이구나! 😊
→ 안심하고 기다림
좋은 loading 컴포넌트 만들기
간단한 버전:
loading: () => (
<div style={{ textAlign: 'center', padding: '50px' }}>
로딩 중...
</div>
)
괜찮은 버전:
loading: () => (
<div className="flex justify-center items-center h-96">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" />
<p className="ml-4">지도를 불러오는 중...</p>
</div>
)
프로페셔널 버전:
// LoadingSpinner.tsx
function LoadingSpinner({ message }: { message: string }) {
return (
<div className="flex flex-col items-center justify-center h-96 bg-gray-50 rounded-lg">
{/* 스피너 애니메이션 */}
<div className="relative">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-gray-200" />
<div className="animate-spin rounded-full h-16 w-16 border-4 border-blue-500 border-t-transparent absolute top-0" />
</div>
{/* 메시지 */}
<p className="mt-4 text-gray-600 font-medium">{message}</p>
{/* 진행 표시 (선택) */}
<div className="mt-2 w-48 h-1 bg-gray-200 rounded-full overflow-hidden">
<div className="h-full bg-blue-500 animate-progress" />
</div>
</div>
);
}
// 사용
const NaverMap = dynamic(() => import("@/components/map/NaverMap"), {
ssr: false,
loading: () => <LoadingSpinner message="지도를 불러오는 중입니다..." />,
});
실제 타이밍
사용자 행동 타임라인:
0ms: "지도 보기" 클릭
↓
1ms: <LoadingSpinner /> 즉시 표시 ✅
↓
[네트워크 요청 시작]
↓
200-1000ms: 지도 코드 다운로드 중
↓ (이 시간 동안 로딩 스피너 보임)
↓
1000ms: 지도 코드 다운로드 완료
↓
1050ms: 지도 초기화 및 렌더링
↓
1100ms: 지도 표시 ✅
→ 사용자는 1100ms 기다림
→ 하지만 1ms부터 피드백 받음!
→ 체감 대기 시간 짧음!
실전 예시
예시 1: 지도 컴포넌트
// pages/store/[id].tsx
import dynamic from "next/dynamic";
// 동적 import로 지도 로드
const NaverMap = dynamic(() => import("@/components/map/NaverMap"), {
ssr: false, // 브라우저에서만 실행
loading: () => <LoadingSpinner message='지도를 불러오는 중입니다' />,
});
export default function StorePage() {
return (
<div>
<h1>매장 정보</h1>
{/* 다른 정보는 즉시 표시 */}
<StoreInfo />
<StorePhotos />
{/* 지도는 필요할 때만 */}
<section>
<h2>찾아오시는 길</h2>
<NaverMap
lat={37.5665}
lng={126.9780}
/>
</section>
</div>
);
}
장점:
1. 초기 로딩 빠름
- 매장 정보, 사진 먼저 보임
- 지도는 스크롤 내리면 로드
2. 서버 에러 없음
- window.naver를 서버에서 안 씀
3. 사용자 경험 좋음
- 로딩 중 표시로 안심
예시 2: 모달 컴포넌트
// components/ProductDetail.tsx
import dynamic from "next/dynamic";
// 리뷰 모달은 클릭해야 보이니까 동적 로드
const ReviewModal = dynamic(() => import("./ReviewModal"), {
loading: () => (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div className="bg-white p-8 rounded-lg">
<p>리뷰를 불러오는 중...</p>
</div>
</div>
),
});
export default function ProductDetail() {
const [showReviews, setShowReviews] = useState(false);
return (
<div>
<ProductInfo />
<button onClick={() => setShowReviews(true)}>
리뷰 보기
</button>
{/* 클릭했을 때만 모달 코드 다운로드 */}
{showReviews && (
<ReviewModal onClose={() => setShowReviews(false)} />
)}
</div>
);
}
효과:
일반 import:
- 초기 번들: 300KB
- 리뷰 모달 코드 포함 (100KB)
dynamic import:
- 초기 번들: 200KB ✅
- 리뷰 클릭 시: +100KB
→ 초기 로딩 33% 빠름!
예시 3: 무거운 에디터
// pages/write.tsx
import dynamic from "next/dynamic";
// Quill 에디터는 매우 무거움 (200KB+)
const QuillEditor = dynamic(() => import("react-quill"), {
ssr: false, // window 객체 사용
loading: () => (
<div className="border rounded-lg p-4 h-64 flex items-center justify-center">
<p>에디터를 불러오는 중...</p>
</div>
),
});
export default function WritePage() {
const [content, setContent] = useState("");
return (
<div>
<h1>글쓰기</h1>
{/* 에디터만 동적 로드 */}
<QuillEditor
value={content}
onChange={setContent}
/>
<button>저장</button>
</div>
);
}
예시 4: 조건부 로딩
// 사용자 권한에 따라 다른 컴포넌트
const AdminPanel = dynamic(() => import("@/components/AdminPanel"), {
loading: () => <Skeleton />,
});
const UserDashboard = dynamic(() => import("@/components/UserDashboard"), {
loading: () => <Skeleton />,
});
export default function Dashboard({ user }) {
if (user.role === 'admin') {
return <AdminPanel />; // 관리자만 이 코드 다운로드
}
return <UserDashboard />; // 일반 유저는 이 코드만
}
성능 비교
실제 측정 데이터
테스트 페이지: 쇼핑몰 상품 상세
컴포넌트 구성:
- 상품 정보: 50KB
- 이미지 갤러리: 100KB
- 리뷰 모달: 150KB
- 네이버 지도: 200KB
- Q&A 섹션: 80KB
총: 580KB
일반 import (모두 정적):
초기 로딩:
┌─────────────────────────────────┐
│ 다운로드: 580KB │
│ 시간 (4G): 4.8초 │
│ FCP: 2.1초 │
│ LCP: 4.8초 │
│ Lighthouse 점수: 62점 │
└─────────────────────────────────┘
사용자가 안 볼 수도 있는 것까지 다 다운로드! ❌
dynamic import (최적화):
초기 로딩:
┌─────────────────────────────────┐
│ 다운로드: 150KB ✅ │
│ (상품정보 + 이미지만) │
│ │
│ 시간 (4G): 1.2초 ✅ │
│ FCP: 0.5초 ✅ │
│ LCP: 1.2초 ✅ │
│ Lighthouse 점수: 94점 ✅ │
└─────────────────────────────────┘
필요할 때만 추가 다운로드:
- 리뷰 클릭: +150KB (1초)
- 지도 스크롤: +200KB (1.5초)
- Q&A 클릭: +80KB (0.6초)
→ 초기 로딩 75% 빠름!
→ 점수 32점 향상!
주의사항
1. 너무 많이 쓰지 말기
// ❌ 나쁜 예: 모든 것을 dynamic으로
const Header = dynamic(() => import("./Header"));
const Footer = dynamic(() => import("./Footer"));
const Button = dynamic(() => import("./Button"));
const Text = dynamic(() => import("./Text"));
→ 너무 많은 네트워크 요청
→ 오히려 느려질 수 있음!
// ✅ 좋은 예: 큰 것만 dynamic으로
import Header from "./Header"; // 작고 항상 필요
import Footer from "./Footer"; // 작고 항상 필요
const HeavyChart = dynamic(() => import("./Chart")); // 크고 선택적
const Map = dynamic(() => import("./Map")); // 크고 선택적
기준:
dynamic import 추천:
✅ 파일 크기 100KB 이상
✅ 조건부로만 보이는 것
✅ 스크롤 해야 보이는 것
✅ 클릭해야 보이는 것
✅ 브라우저 API 사용
일반 import 추천:
✅ 파일 크기 10KB 이하
✅ 항상 보이는 것
✅ 첫 화면에 있는 것
2. SEO 고려하기
// ⚠️ 주의: SEO 중요한 콘텐츠는 ssr: true (기본값)
const BlogPost = dynamic(() => import("./BlogPost"), {
// ssr: false 하면 안 됨!
// 검색엔진이 내용을 못 봄!
});
// ✅ SEO 중요하면 일반 import 또는 ssr: true
import BlogPost from "./BlogPost";
SEO가 중요한 것:
❌ dynamic으로 하면 안 되는 것:
- 블로그 본문
- 상품 설명
- 메타 정보
- 주요 콘텐츠
✅ dynamic으로 해도 되는 것:
- 댓글 섹션
- 관련 상품 (하단)
- 모달
- 지도
3. loading 컴포넌트 크기
// ❌ 나쁜 예: loading이 너무 무거움
loading: () => (
<HeavyAnimatedComponent /> // 200KB
)
→ 로딩 컴포넌트가 더 무거움!
→ 본말전도!
// ✅ 좋은 예: loading은 가볍게
loading: () => (
<div className="animate-spin">🔄</div> // 1KB
)
정리
핵심 3가지
1. Dynamic Import = 필요할 때만 가져오기
└─ 초기 로딩 빠르게!
2. ssr: false = 브라우저에서만 실행
└─ window, document 사용하는 것
3. loading = 기다리는 동안 보여줄 것
└─ 사용자 경험 개선!
언제 사용할까?
반드시 써야 하는 경우:
✅ 지도 (Naver, Google, Kakao)
✅ 차트 라이브러리 (일부)
✅ 브라우저 API 사용
✅ localStorage/sessionStorage 사용
추천하는 경우:
✅ 100KB 이상 컴포넌트
✅ 모달, 팝업
✅ 조건부 렌더링
✅ 스크롤 해야 보이는 것
템플릿
// 복사해서 쓰세요!
// 브라우저 API 사용 (지도, 차트 등)
const Component = dynamic(() => import("./Component"), {
ssr: false,
loading: () => <LoadingSpinner message="로딩 중..." />,
});
// 단순 코드 스플리팅 (무거운 컴포넌트)
const HeavyComponent = dynamic(() => import("./Heavy"), {
loading: () => <Skeleton />,
});
// 조건부 컴포넌트
const Modal = dynamic(() => import("./Modal"));
이제 이해되셨나요? 😊
반응형
'개발 공부' 카테고리의 다른 글
| [supabse] custom schema 사용시 발생 에러 error code name PGRST106 (0) | 2025.05.21 |
|---|---|
| [grafana] 대시보드 별 유저 권한 부여 하는 법 정리 (1) | 2024.10.23 |
| [grafana] export csv 한글깨짐 현상 해결법 (0) | 2024.10.17 |
| [FE] 프론트 엔드 에서는 어떤식으로 디자인 패턴을 가져갈까? (0) | 2024.05.27 |
| User-Agent 란??? 무엇인가 (0) | 2023.06.10 |
Comments
