Next.js App Router 핵심 성능 최적화: Server Components와 Dynamic Import 실전 가이드
Next.js 13 버전부터 도입되어 이제 표준으로 자리 잡은 App Router는 기존 Pages Router 방식과 비교해 프론트엔드 아키텍처 및 번들링 메커니즘에서 혁신적인 변화를 일으켰습니다.
하지만 'use client' 지시어를 최상단에 습관적으로 붙이거나 컴포넌트 경계를 제대로 설계하지 않으면, 클라이언트 번들 사이즈가 폭증하여 오히려 기존 방식보다 로딩 성능과 구글 Lighthouse 점수가 악화되는 문제가 자주 발생합니다.
Next.js App Router 환경에서 초기 번들 파일 용량을 대폭 줄이고 렌더링 성능을 극대화하는 핵심 3가지 성능 최적화 전략을 가이드합니다.
1. 서버 컴포넌트(Server Components)의 이점 극대화
App Router의 모든 컴포넌트는 기본적으로 **React Server Components(RSC)**로 동작합니다. 서버 컴포넌트는 오직 서버에서만 실행되어 HTML 스트림으로 변환되므로, 대형 외부 라이브러리 코드가 클라이언트 자바스크립트 번들에 포함되지 않는 엄청난 이점을 지닙니다.
- 최적화 수칙:
state,useEffect, 이벤트 핸들러(onClick) 등 유저 상호작용이 필요한 말단 컴포넌트에만 핀포인트로'use client'를 선언하세요.- 무거운 마크다운 파서(gray-matter, remark), 날짜 변환 라이브러리(date-fns, dayjs) 등은 서버 컴포넌트 내에서만 수용하여 클라이언트 다운로드 용량을 0KB로 유지하세요.
2. 'use client' 경계(Boundary) 하향 이동 및 Leaf 노드 배치
상위 레이아웃이나 페이지 전체에 'use client'를 선언하면 그 하위의 모든 자식 컴포넌트까지 전부 클라이언트 컴포넌트로 강제 전환됩니다.
- 바람직한 패턴:
- 페이지 전체가 아닌, 상호작용이 일어나는 버튼, 모달, 포맷 폼 태그 등의 최하단 잎새 노드(Leaf Node) 컴포넌트로
'use client'위치를 최대한 끌어내리세요. - 만약 클라이언트 컴포넌트가 서버 컴포넌트를 감싸야 하는 구조라면,
childrenprop 패스스루 방식을 활용하여 서버 컴포넌트 속성을 유지하세요.
- 페이지 전체가 아닌, 상호작용이 일어나는 버튼, 모달, 포맷 폼 태그 등의 최하단 잎새 노드(Leaf Node) 컴포넌트로
// ❌ 나쁜 예시: 페이지 전체를 Client Component로 선언
'use client';
export default function BadPage() {
const [open, setOpen] = useState(false);
return <div><HeavyServerContent /></div>;
}
// ✅ 좋은 예시: Interactivity가 필요한 부분만 별도 분리
import InteractiveButton from '@/components/InteractiveButton';
import HeavyServerContent from '@/components/HeavyServerContent';
export default function GoodPage() {
return (
<div>
<HeavyServerContent />
<InteractiveButton />
</div>
);
}
3. next/dynamic을 활용한 무거운 라이브러리 Lazy Loading
차트(Recharts, Chart.js), 에디터(Tiptap, Quill), 3D 뷰어(Three.js)와 같이 대용량 자바스크립트 라이브러리가 필요한 컴포넌트는 최초 페이지 로딩 시점에 받아올 필요가 없습니다.
Next.js의 dynamic() 함수를 활용하면 해당 요소가 실제로 뷰포트에 나타나거나 사용자가 클릭할 때 필요시 동적으로 번들을 로드합니다.
import dynamic from 'next/dynamic';
// 차트 컴포넌트를 동적으로 로드하며, SSR 렌더링 과정에서 제외시킴
const DynamicAnalyticsChart = dynamic(
() => import('@/components/AnalyticsChart'),
{
loading: () => <div className="h-64 animate-pulse bg-gray-100 rounded-lg" />,
ssr: false, // 클라이언트 측에서만 렌더링
}
);
export default function DashboardPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold">대시보드 리포트</h1>
<DynamicAnalyticsChart />
</div>
);
}
✍️ 글을 마치며
Next.js App Router의 성능 핵심은 **"클라이언트로 전송하는 자바스크립트 코드의 양을 극단적으로 다이어트하는 것"**에 있습니다.
습관적으로 붙이던 'use client' 선언을 점검하고, 무거운 인터랙션 컴포넌트는 dynamic 동적 임포트로 지연 로딩 처리하여 유저에게 날아갈 듯 빠른 모던 웹 브라우징 경험을 선사해 보시기 바랍니다!