들어가기
웹페이지를 만들 때 랜딩 페이지의 로딩 시간은 중요하다.
페이지 로드 속도가 느려지면 그만큼 사용자 이탈률도 높아진다.
이때 문제를 해결하기 위해서 CDN 캐싱 등 여러 방법을 이용할 수 있지만,
코드 레벨에서의 가장 대표적인 방법은 dynamic import이다.
용어 정리
용어부터 짚고 넘어가면 좋을 것 같다.
초기 로딩 시간 줄이기에 대해 검색하면 code splitting, dynamic import, lazy loading 키워드들을 보게된다.
이를 정리하면 다음과 같다:
dynamic import는 lazy loading을 하기 위한 수단이고, lazy loading을 하기 위해서는 code splitting이 선행되어야 한다.
즉 코드 레벨에서 dynamic import를 사용하면 번들링 때 code splitting이 되고, 런타임 때 lazy loading이 된다.
1. 번들링 시: import() 구문을 만나면 번들러(Webpack 등)는 이 모듈을 별도의 청크로 분리하여(Code Splitting) 저장한다.
2. 런타임 시: 실제 코드가 실행될 때, Dynamic Import가 해당 청크를 필요한 시점에 불러오면서(Lazy Loading) 초기 로딩 부담을 줄인다.
Nextjs, React에서의 lazy loading
Nextjs를 사용한다면 next/dynamic 에서 제공하는 dynamic 함수를 이용해 lazy loading 할 수 있다.
dynamic 함수는 구현 코드를 뜯어보면 React.lazy()와 Suspense를 이용하고 있음을 알 수 있다.(내부 Loadable 함수 참고)
또한 옵션으로 클라이언트 컴포넌트의 `ssr`(클라이언트 컴포넌트는 ssr과 csr 두 곳에서 렌더링 되므로), Suspense fallback을 편하게 이용할 수 있는 `loading` 옵션을 제공한다.
// dynamic 함수 사용 예시
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
// Client Components:
const ComponentA = dynamic(() => import('../components/A'))
const ComponentB = dynamic(() => import('../components/B'))
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
export default function ClientComponentExample() {
const [showMore, setShowMore] = useState(false)
return (
<div>
{/* Load immediately, but in a separate client bundle */}
<ComponentA />
{/* Load on demand, only when/if the condition is met */}
{showMore && <ComponentB />}
<button onClick={() => setShowMore(!showMore)}>Toggle</button>
{/* Load only on the client side */}
<ComponentC />
</div>
)
}
React를 사용한다면 직접 lazy 함수와 Suspense를 이용해 구현할 수 있다. 공식문서
import { lazy } from 'react';
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));
...
<Suspense fallback={<Loading />}>
<h2>Preview</h2>
<MarkdownPreview />
</Suspense>
위 두 사용 예시를 보면 사용 방식이 동일하다는 것을 알 수 있다.
const ComponentA = dynamic(() => import('../components/A'))
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));
이때 () => import('파일경로') 구문이 바로 dynamic import에 해당한다.
dynamic import와 번들러의 역할
동적 import 키워드는 필요한 파일을 메모리에 올리고, 해당 파일이 없다면 서버에 요청하는 역할을 한다.
예를 들어 런타임 때 코드를 한 줄씩 읽어가면서 동적 import 키워드를 만나게 될 텐데, 만약 번들러가 빌드 시 파일들을 청크로 분리하지 않는다면 초기 로딩 시 브라우저는 결국 한 덩어리로 된 빌드 파일을 받아올 수밖에 없다. 이때 동적 import()의 장점은 고작 메모리에 올리는 시간을 절약하는 데 그친다.
하지만 실제로 번들러는 빌드 시 동적 import() 구문을 만나면 해당 파일을 청크로 분리한다(code splitting).
그 결과 빌드 파일이 여러 개의 청크로 나눠지게 된다.
이제 브라우저가 동적 import 키워드를 만나면 그 청크가 필요할 때만 서버에 요청하여 가져오게 된다.
즉, 초기 JS 로딩 파일 크기가 줄어들어 초기 로딩 시간을 줄일 수 있다.
사용 예시
무거운 모달 컴포넌트를 클릭 시 dynamic import 하여 초기 로딩 속도를 최적화할 수 있다.
초기 로딩 시점에 반드시 필요하지 않은,
예를 들어 모달, 팝업 등 무거운 UI 요소 등을 Dynamic Import로 로드 시점을 조절하여 성능을 최적화해 보자.
파일 구조
components/
├── HeavyModal.js // 모달 컴포넌트 (첫 화면 로딩 후에 필요)
└── LightComponent.js // 첫 화면에서 항상 필요한 컴포넌트
pages/
└── index.js // 메인 페이지
// components/HeavyModal.js
export default function HeavyModal() {
return (
<div style={{ padding: '20px', background: 'lightgray' }}>
<h2>난 엄청 무거운 모달 컴포넌트</h2>
</div>
);
}
index.js에서 동적 import로 모달 로드 설정
// pages/index.js
import { useState } from 'react';
import dynamic from 'next/dynamic';
import LightComponent from '@/components/LightComponent';
// `HeavyModal`는 필요할 때만 로드되도록 설정
const HeavyModal = dynamic(() => import('@/components/HeavyModal'), {
loading: () => <p>Loading modal...</p>, // 로딩 중에 표시할 UI
ssr: false, // 서버사이드 렌더링 비활성화
});
export default function Home() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<h1>Welcome to Next.js!</h1>
<LightComponent />
<button onClick={() => setShowModal(true)}>
Open Heavy Modal
</button>
{/* 모달이 필요한 경우에만 로드 */}
{showModal && <HeavyModal />}
</div>
);
}
서버 컴포넌트 내에서 Dynamic Import로 클라이언트 컴포넌트 로드
서버 컴포넌트 내에 포함된 클라이언트 컴포넌트가 초기 시점에 필요하긴 하지만 크기가 커서 초기 로딩에 부담을 줄 때,
dynamic import를 이용해 초기 로딩 속도를 개선할 수 있다.
// ServerComponent.js (서버 컴포넌트)
import dynamic from 'next/dynamic';
// 무거운 클라이언트 컴포넌트를 동적 로드
const HeavyClientComponent = dynamic(() => import('./HeavyClientComponent'), {
ssr: false,
});
export default function ServerComponent() {
return (
<div>
<h1>Welcome to the Server Component!</h1>
<HeavyClientComponent />
</div>
);
}
'프론트엔드' 카테고리의 다른 글
웹사이트 성능 개선하기: 성능 지표 (0) | 2024.12.30 |
---|---|
ErrorBoundary 필기 노트 (1) | 2024.02.19 |
[컨셉비] 2. 1차 스프린트 과정 정리 (4) | 2024.02.13 |
[컨셉비] 1. 프론트엔드 중간 합류와 각오 (2) | 2024.01.27 |
css 속성들을 이해하면서 Skeleton 만들기 (0) | 2024.01.26 |