코드 분할 및 지연 로딩
라우트 단위 코드 분할과 next/dynamic 지연 로딩으로 초기 JavaScript를 줄이고 무거운 UI를 필요할 때 불러옵니다.
웹 애플리케이션의 성능을 최적화하려면 이미지와 폰트 외에도, 페이지를 불러올 때 전송되는 JavaScript 코드의 양을 줄이는 일도 중요합니다. 애플리케이션이 커질수록 JavaScript 번들도 크게 늘어날 수 있으며, 이는 초기 로딩을 늦추고 사용자 경험을 해치는 주된 원인이 됩니다. 코드 분할(code splitting)과 지연 로딩(lazy loading)은 이 문제를 해결하는 핵심 기술이며, Next.js는 이런 기법을 자동으로 적용하거나 개발자가 직접 제어할 수 있는 API를 제공합니다. 이 글에서는 코드 분할과 지연 로딩의 개념, Next.js App Router에서 이를 구현하는 방법, 그리고 관련 최적화 기법을 알아봅니다.
코드 분할(Code Splitting)이란?
코드 분할은 애플리케이션의 JavaScript 코드 전체를 하나의 큰 번들 파일로 만드는 대신, 여러 개의 작은 파일인 청크(chunk)로 나누는 기법입니다. 청크를 나누면 첫 페이지에 필요하지 않은 JavaScript의 다운로드와 파싱을 미룰 수 있습니다. 다만 코드가 나뉘었다고 해서 모든 청크가 사용자의 행동 뒤에만 요청되는 것은 아닙니다.
코드 분할에는 여러 이점이 있습니다. 사용자가 처음 방문한 페이지에 필요한 코드만 내려받으므로 페이지가 더 빨리 표시됩니다. 첫 화면에 꼭 필요한 코드와 나중에 쓸 코드를 나눌 수 있지만, Next.js의 경로 prefetch는 방문하기 전에도 일부 자원을 가져올 수 있습니다. 코드를 바꿨을 때 전체 번들을 다시 내려받지 않고 바뀐 청크만 다시 받으면 되므로, 브라우저 캐싱의 효율도 높아집니다.
Next.js는 라우트 단위로 코드를 자동 분할합니다. 그렇다고 라우트마다 정확히 하나의 독립된 JS 파일이 생기는 것은 아니며, 공통 의존성은 공유 청크에 들어갈 수 있습니다.
지연 로딩(Lazy Loading)이란?
지연 로딩은 특정 컴포넌트나 모듈이 실제로 필요해질 때, 예를 들어 사용자가 특정 영역으로 스크롤하거나 특정 버튼을 눌렀을 때 비동기로 불러오는 기법입니다. 코드 분할과 함께 쓰면 한 페이지 안에서도 중요도가 낮은 컴포넌트의 로딩을 미뤄, 초기 로딩 성능을 더 높일 수 있습니다.
App Router에서 구현
Next.js App Router는 컴포넌트와 라이브러리를 지연 로드하는 여러 방법을 제공합니다. 이 절에서는 개발자가 직접 나누는 방법과 Next.js가 알아서 나누는 부분을 구분해서 봅니다. 직접 나누는 방법은 클라이언트 컴포넌트에서 next/dynamic으로 특정 컴포넌트를 별도 청크로 떼어 내는 것이고, 바로 아래에서 지도 예제로 따라갑니다. 서버 컴포넌트와 라우트 단위의 분할은 Next.js가 자동으로 하므로, 뒤의 세 절에서는 따로 할 일이 없는 부분과 라우트 그룹, 서버 액션이 분할과 어떤 관계인지를 짚습니다.
next/dynamic을 사용한 지연 로딩
Next.js에서 클라이언트 컴포넌트를 지연 로드하는 가장 기본적인 방법은 next/dynamic을 쓰는 것입니다. React의 React.lazy(), Suspense와 비슷하게 동작하지만, Next.js의 서버 렌더링 환경과 더 잘 통합됩니다.
다이내믹 임포트로 지도 컴포넌트를 지연 로드해 보겠습니다. react-leaflet 같은 지도 라이브러리는 용량이 커서, 초기 로딩 때 번들에 포함되지 않게 하고 싶다고 가정합니다. 먼저 지연 로드할 클라이언트 컴포넌트인 src/components/MapComponent.tsx를 만듭니다.
// 이 컴포넌트는 클라이언트 컴포넌트여야 합니다.
"use client";
import { useEffect, useState } from 'react';
interface MapProps {
latitude: number;
longitude: number;
zoom: number;
}
export default function MapComponent({ latitude, longitude, zoom }: MapProps) {
const [mapLoaded, setMapLoaded] = useState(false);
useEffect(() => {
// 실제 지도 라이브러리 로딩 및 초기화 로직
// 여기서는 가상으로 2초 지연을 시뮬레이션합니다.
const timer = setTimeout(() => {
setMapLoaded(true);
console.log('지도 컴포넌트가 로드되었습니다.');
}, 2000); // 지도 라이브러리 로딩 시간 시뮬레이션
return () => clearTimeout(timer);
}, []);
if (!mapLoaded) {
return (
<div style={{ height: '300px', backgroundColor: '#e0e0e0', display: 'flex', justifyContent: 'center', alignItems: 'center', color: '#666', border: '1px dashed #ccc' }}>
지도 로딩 중...
</div>
);
}
return (
<div style={{ height: '300px', backgroundColor: '#f0f8ff', border: '1px solid #007bff', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', color: '#007bff' }}>
<h3>지도 표시 (위도: {latitude}, 경도: {longitude})</h3>
<p>확대: {zoom}</p>
<p>실제 지도 라이브러리가 여기에 렌더링됩니다.</p>
</div>
);
}MapComponent는 실제 지도 대신, 마운트한 뒤 2초가 지나면 mapLoaded를 true로 바꾸는 타이머로 라이브러리 초기화를 흉내 냅니다. useEffect가 돌려주는 정리 함수의 clearTimeout은 2초가 지나기 전에 컴포넌트가 사라지면 타이머를 취소해, 화면에 없는 컴포넌트의 상태를 바꾸지 않게 합니다. 다음으로 "use client" 파일인 src/app/locations/page.tsx에서 MapComponent를 지연 로드합니다. 아래 코드처럼 dynamic에 import()를 돌려주는 함수를 넘기면, 번들러가 그 모듈을 페이지 번들과 다른 청크로 나눕니다.
"use client";
import dynamic from 'next/dynamic';
// ssr: false는 Client Component에서만 선언할 수 있습니다.
// 지도처럼 브라우저 API에 의존하는 라이브러리를 클라이언트에서만 렌더링합니다.
const DynamicMapComponent = dynamic(() => import('@/components/MapComponent'), {
ssr: false,
loading: () => <p style={{ textAlign: 'center', padding: '20px', border: '1px dashed #ccc' }}>지도를 불러오는 중입니다...</p>,
});
export default function LocationsPage() {
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', border: '1px solid #28a745', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}>
<h1 style={{ color: '#28a745', marginBottom: '20px' }}>위치 정보 페이지</h1>
<p style={{ marginBottom: '30px' }}>아래는 지연 로딩된 지도 컴포넌트입니다.</p>
<DynamicMapComponent latitude={37.5665} longitude={126.9780} zoom={10} />
<p style={{ marginTop: '30px', fontSize: '0.9em', color: '#555' }}>
(지도 코드는 메인 번들에서 분리되며, 초기 클라이언트 렌더링 과정에서 별도 청크로 비동기 요청됩니다.)
</p>
</div>
);
}이 예제는 DynamicMapComponent를 곧바로 렌더링하므로 클릭이나 스크롤을 기다리지 않습니다. 모듈이 준비된 뒤의 2초 타이머는 실제 지도 라이브러리나 네트워크를 측정한 결과가 아닙니다.
실행 순서는 모듈 준비, MapComponent 마운트, useEffect의 타이머 등록, 콜백이 mapLoaded를 true로 바꾸는 과정입니다. 모듈이 이미 캐시에 있어 첫 대기 UI가 짧아지더라도, 새로 마운트한 컴포넌트의 mapLoaded는 false에서 시작합니다. 그래서 모듈을 빨리 내려받은 것과 내부의 모의 초기화가 끝난 것은 별개입니다.
원문 지도 예제의 두 로딩 단계
dynamic 모듈 준비 전 UI와 마운트 뒤 mapLoaded=false UI는 서로 다른 대기 상태입니다. 모의 타이머 실행, 언마운트 정리, 새 마운트의 초기화를 추적하면 두 로딩 단계와 취소의 의미가 드러납니다.
NEXT · 12.3
즉시 렌더한 지도 예제에도 두 로딩 UI가 있습니다
LocationsPage는 DynamicMapComponent를 즉시 렌더합니다. 모듈을 기다리는 dynamic의 loading UI와 마운트 후 mapLoaded 상태가 보여 주는 UI를 구분합니다.
ssr: false는 이 Client Component의 서버 사전 렌더를 생략합니다. 클릭 뒤에 불러오는 예제가 아닙니다. setTimeout(2000)은 마운트 후 모의 초기화이며 모듈 전송 시간이나 정확한 완료 시각을 측정한 값이 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- DynamicMapComponent는 페이지에서 즉시 렌더됩니다. 모듈이 준비되기 전에는 dynamic의 loading UI를, 마운트 후에는 내부 mapLoaded 상태에 따른 UI를 표시합니다. useEffect의 setTimeout(2000)은 모의 초기화이며 실제 모듈 전송 시간이나 정확한 완료 시각이 아닙니다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 모듈 준비 전 페이지는 DynamicMapComponent를 즉시 렌더하지만 module 준비 전에는 dynamic loading UI를 표시합니다. | module: 준비 전 · UI: dynamic loading · mapLoaded: 마운트 전 | module 준비 대기를 표시합니다. |
| 첫 마운트 module 준비 뒤 mapLoaded=false로 마운트하고 모의 타이머를 등록합니다. | mapLoaded: false · 타이머 지연: 2000 ms · UI: 내부 초기화 대기 | 대기 이유가 module에서 내부 상태로 바뀝니다. |
| 500 ms에 언마운트 연습의 500 ms 이동에서 정리 함수가 첫 타이머를 취소합니다. | 화면: 언마운트 · 첫 timer: clearTimeout · 옛 callback: 취소 | 첫 마운트의 상태 변경 예약이 제거됩니다. |
| 다시 마운트 3초 뒤 돌아온 새 컴포넌트는 별도 상태와 타이머를 만듭니다. | mapLoaded: false · timer: 새 예약 · 옛 상태: 재사용 안 함 | 새 마운트가 초기 상태에서 시작합니다. |
| 새 callback 실행 새 callback이 실행되면 mapLoaded=true로 바뀝니다. | mapLoaded: true · 2000 ms: 지연값 · 실제 SDK 준비: 별도 신호 필요 | 모의 초기화 화면이 준비 화면으로 바뀝니다. |
원문 지도 예제의 두 로딩 단계
dynamic 모듈 준비 전 UI와 마운트 뒤 mapLoaded=false UI는 서로 다른 대기 상태입니다. 모의 타이머 실행, 언마운트 정리, 새 마운트의 초기화를 추적하면 두 로딩 단계와 취소의 의미가 드러납니다.
NEXT · 12.3
즉시 렌더한 지도 예제에도 두 로딩 UI가 있습니다
LocationsPage는 DynamicMapComponent를 즉시 렌더합니다. 모듈을 기다리는 dynamic의 loading UI와 마운트 후 mapLoaded 상태가 보여 주는 UI를 구분합니다.
ssr: false는 이 Client Component의 서버 사전 렌더를 생략합니다. 클릭 뒤에 불러오는 예제가 아닙니다. setTimeout(2000)은 마운트 후 모의 초기화이며 모듈 전송 시간이나 정확한 완료 시각을 측정한 값이 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- DynamicMapComponent는 페이지에서 즉시 렌더됩니다. 모듈이 준비되기 전에는 dynamic의 loading UI를, 마운트 후에는 내부 mapLoaded 상태에 따른 UI를 표시합니다. useEffect의 setTimeout(2000)은 모의 초기화이며 실제 모듈 전송 시간이나 정확한 완료 시각이 아닙니다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 모듈 준비 전 페이지는 DynamicMapComponent를 즉시 렌더하지만 module 준비 전에는 dynamic loading UI를 표시합니다. | module: 준비 전 · UI: dynamic loading · mapLoaded: 마운트 전 | module 준비 대기를 표시합니다. |
| 첫 마운트 module 준비 뒤 mapLoaded=false로 마운트하고 모의 타이머를 등록합니다. | mapLoaded: false · 타이머 지연: 2000 ms · UI: 내부 초기화 대기 | 대기 이유가 module에서 내부 상태로 바뀝니다. |
| 500 ms에 언마운트 연습의 500 ms 이동에서 정리 함수가 첫 타이머를 취소합니다. | 화면: 언마운트 · 첫 timer: clearTimeout · 옛 callback: 취소 | 첫 마운트의 상태 변경 예약이 제거됩니다. |
| 다시 마운트 3초 뒤 돌아온 새 컴포넌트는 별도 상태와 타이머를 만듭니다. | mapLoaded: false · timer: 새 예약 · 옛 상태: 재사용 안 함 | 새 마운트가 초기 상태에서 시작합니다. |
| 새 callback 실행 새 callback이 실행되면 mapLoaded=true로 바뀝니다. | mapLoaded: true · 2000 ms: 지연값 · 실제 SDK 준비: 별도 신호 필요 | 모의 초기화 화면이 준비 화면으로 바뀝니다. |
위 그림은 곧바로 렌더링하는 지도 예제에도 두 가지 로딩 UI가 있다는 점을 보여 줍니다. LocationsPage가 DynamicMapComponent를 렌더링하면, 모듈이 준비되기 전에는 dynamic의 loading UI가 표시됩니다. 모듈이 준비되면 컴포넌트가 마운트되어 내부의 mapLoaded 상태에 따라 "지도 로딩 중..."을 보여 주고, setTimeout(2000)의 콜백이 실행된 뒤에 모의 지도 화면을 보여 줍니다.
그림의 단계는 모듈 준비 전, 첫 마운트, 500ms에 언마운트, 다시 마운트, 새 콜백 실행의 순서이며, 뒤의 세 단계는 연습 문제 1의 상황입니다. 그림의 설명처럼 ssr: false는 이 클라이언트 컴포넌트의 서버 사전 렌더링을 건너뛸 뿐 클릭 뒤에 불러오는 예제가 아니며, setTimeout(2000)은 마운트 뒤의 모의 초기화이지 모듈 전송 시간이나 정확한 완료 시각을 잰 값이 아닙니다.
dynamic 함수의 주요 옵션은 두 가지입니다. ssr: false는 이 컴포넌트를 서버에서 렌더링하지 않고 클라이언트에서만 렌더링하도록 지정하며, 클라이언트 컴포넌트 안에서만 선언할 수 있습니다. loading은 컴포넌트가 로드되는 동안 보여 줄 React 컴포넌트나 JSX를 정의합니다. React 자체의 지연 로딩을 고른다면 React.lazy로 컴포넌트를 불러오고, Suspense의 fallback으로 로딩 UI를 제공합니다.
무엇을 나눌지는 "첫 화면에 꼭 필요한가", "무거운가", "사용자 행동 뒤에 필요한가"를 차례로 확인하면 판단하기 쉽습니다.
Server Components에서 코드 분할
App Router의 서버 컴포넌트는 기본적으로 자동 코드 분할됩니다. 서버 컴포넌트의 구현 코드는 브라우저 번들에 보내지 않고, 브라우저에는 렌더링 결과와 클라이언트 컴포넌트에 필요한 코드만 전달됩니다. 서버 컴포넌트에서 클라이언트 컴포넌트를 동적으로 import하는 경우의 자동 코드 분할은 지금 지원되지 않으므로, 위 실습처럼 클라이언트 컴포넌트 안에서 dynamic을 선언하는 경우와 구분합니다.
더 깊이: next/dynamic의 정체와 서버 컴포넌트에서의 동작
Next.js의 지연 로딩 안내는 next/dynamic을 React의 React.lazy()와 Suspense를 합친 것으로 설명합니다. App Router와 Pages Router에서 똑같이 동작하므로 두 라우터 사이를 조금씩 옮길 때도 같은 코드를 쓸 수 있습니다. React.lazy()와 Suspense로 불러온 클라이언트 컴포넌트도 기본으로 서버에서 사전 렌더링됩니다. 서버 컴포넌트에서 ssr: false를 쓰면 오류가 나는 규칙은 Next.js 15에서 생겼고, 같은 버전부터 App Router의 next/dynamic은 빈 Suspense를 따로 끼워 넣지 않으며 예전의 suspense 옵션도 없어졌습니다.
서버 컴포넌트를 dynamic으로 불러오는 경우도 있습니다. 이때 지연 로드되는 것은 서버 컴포넌트 자신이 아니라, 그 아래에 있는 클라이언트 컴포넌트들입니다. 서버 컴포넌트의 코드는 처음부터 브라우저로 가지 않으므로 늦출 대상이 그 자식 클라이언트 코드뿐이기 때문입니다. 대신 이 방식은 그 서버 컴포넌트가 쓰는 CSS 같은 정적 자원을 미리 불러오는 데 도움이 됩니다.
라우트 그룹으로 레이아웃과 폴더 구조 나누기
App Router의 라우트 그룹(Route Groups)은 URL 경로에 영향을 주지 않으면서 라우트를 논리적으로 묶는 기능입니다. 라우트별 코드 분할은 괄호 폴더가 있든 없든 Next.js가 자동으로 하며, 라우트 그룹은 URL을 바꾸지 않고 폴더를 정리하거나 서로 다른 레이아웃을 적용할 때 씁니다. 예를 들어 관리자 화면과 일반 사용자 화면의 내비게이션과 권한 규칙이 다르다면 다음처럼 구조를 나눌 수 있습니다.
각 페이지에 필요한 클라이언트 컴포넌트의 JavaScript만 전송되는 것은 자동 라우트 분할의 결과이며, 라우트 그룹이 별도의 청크를 강제해서가 아닙니다. 그룹마다 서로 다른 루트 레이아웃을 두면 두 루트 레이아웃 사이의 이동은 전체 페이지 로드가 될 수 있으므로, 번들 최적화만을 위해 그룹을 늘리지 않습니다.
Server Actions 및 Route Handlers의 코드
서버 액션과 Route Handler의 구현은 서버에서 실행되므로, 이를 브라우저의 지연 로딩 청크와 같은 뜻으로 생각하지 않습니다. 다만 서버 액션을 호출하는 클라이언트 UI에는 그 참조와 호출을 위한 코드가 필요할 수 있습니다.
코드 분할 및 지연 로딩 최적화 팁
컴포넌트 단위로 생각합니다. 큰 컴포넌트나 특정 상호작용 뒤에만 필요한 컴포넌트는 next/dynamic으로 지연 로드하는 것을 고려합니다. 번들 분석 도구로 번들 크기를 분석해 어떤 서드파티 라이브러리가 가장 많은 공간을 차지하는지 확인하고, 불필요한 라이브러리를 없애거나 필요한 부분만 import하는 방법을 찾습니다. Google Lighthouse 같은 성능 감사 도구를 주기적으로 실행해 "Reduce unused JavaScript" 같은 제안을 확인하고 개선합니다.
App Router에서 "use client" 지시어는 클라이언트 모듈 그래프의 진입점을 정하므로, 그 아래 import 의존성이 번들에 얼마나 들어가는지 확인합니다. 따라서 "use client"의 사용 범위를 가능한 한 줄이고, 가능한 부분은 서버 컴포넌트에서 렌더링되게 합니다.
대기 화면의 공간도 신경 씁니다. dynamic의 짧은 로딩 문구와 지도 컴포넌트의 300px 영역은 높이가 다르므로, 레이아웃 이동이 문제라면 대기 UI에도 필요한 공간을 확보합니다. 코드 분할과 지연 로딩은 이미지, 폰트 최적화와 함께 쓸 때 웹 성능 개선 효과가 커집니다.
코드 분할과 지연 로딩은 초기 로딩 시간을 줄이고 웹 애플리케이션 전반의 성능을 높이는 데 꼭 필요한 전략이며, Next.js는 이런 최적화 작업을 감싸 주어 개발자가 더 쉽고 효율적으로 빠른 애플리케이션을 만들게 돕습니다.
정리
- 코드 분할은 JavaScript를 여러 청크로 나누어 첫 페이지에 필요하지 않은 코드의 다운로드와 파싱을 미루며, Next.js는 라우트 단위로 자동 분할합니다.
next/dynamic은 클라이언트 컴포넌트를 지연 로드하며,loading으로 대기 UI를,ssr: false로 서버 사전 렌더링 생략을 지정합니다.ssr: false는 클라이언트 컴포넌트 안에서만 쓸 수 있고, 서버 컴포넌트에서 클라이언트 컴포넌트를 동적 import해도 자동 분할되지 않습니다.- 모듈의 로딩 UI와 컴포넌트 내부의 초기화 상태는 별개이며, 언마운트되면 정리 함수가 타이머를 취소하고 새 마운트는 처음 상태에서 시작합니다.
- 라우트 그룹은 URL을 바꾸지 않고 폴더와 레이아웃을 나누는 기능이며, 라우트별 코드 분할은 그룹과 관계없이 자동으로 일어납니다.
연습 문제
1. 정리 함수와 새 마운트 추적하기
타이머를 등록한 뒤 500ms에 다른 페이지로 이동해 이 컴포넌트가 언마운트되고, 3초 뒤 다시 방문해 새로 마운트했다고 가정합니다. 첫 타이머가 새 화면의 mapLoaded를 바꿀까요? 다시 방문한 화면은 어떤 상태로 시작할까요?
해설 보기
첫 타이머는 언마운트될 때 돌려준 정리 함수의 clearTimeout(timer)로 취소됩니다. 새 마운트는 별도의 상태와 타이머를 만들므로 다시 false에서 시작하고, 이후 새 콜백이 실행되면 true로 바뀝니다.
2000ms는 타이머에 준 지연값일 뿐, 실제 지도가 준비되는 시각을 보장하지 않습니다. 실제 지도 SDK를 붙일 때는 SDK의 준비와 실패 신호를 따로 연결해야 합니다.
2. 서버 컴포넌트에서 ssr: false 쓰기
src/app/locations/page.tsx에서 "use client"를 지우고, 서버 컴포넌트인 채로 dynamic(() => import('@/components/MapComponent'), { ssr: false })를 선언하려고 합니다. 이 코드는 동작할까요? 어떻게 고쳐야 할까요?
해설 보기
동작하지 않습니다. ssr: false는 클라이언트 컴포넌트 안에서만 선언할 수 있으므로, 서버 컴포넌트에서 쓰면 빌드 오류가 납니다. 이 글의 예제처럼 dynamic 선언을 "use client" 파일에 두어야 합니다. 페이지 전체를 클라이언트 컴포넌트로 만들고 싶지 않다면, dynamic 선언만 담은 작은 클라이언트 래퍼 컴포넌트를 만들고 서버 페이지에서 그 래퍼를 렌더링합니다.
3. 클릭한 뒤에만 불러오기
지금 예제는 지도 모듈을 페이지를 열자마자 요청합니다. "지도 보기" 버튼을 누른 뒤에만 지도 모듈을 요청하게 하려면 어떻게 바꾸면 좋을까요?
해설 보기
useState로 지도를 보여 줄지 여부를 관리하고, 버튼을 누르기 전에는 DynamicMapComponent를 렌더링하지 않습니다. 예를 들어 const [showMap, setShowMap] = useState(false);를 두고, 버튼의 onClick에서 setShowMap(true)를 부른 뒤 {showMap && <DynamicMapComponent ... />}처럼 조건부로 렌더링합니다. dynamic으로 만든 컴포넌트는 처음 렌더링될 때 모듈을 요청하므로, 렌더링을 미루면 요청 시점도 버튼을 누른 뒤로 미뤄집니다.
다음 글 메모이제이션 및 리렌더링 최적화에서는 memo, useMemo, useCallback으로 불필요한 다시 렌더링을 줄이는 방법을 알아봅니다.