Next.js 14 App Router 도입기: 변화와 적응
Next.js 14가 출시되면서 프론트엔드 생태계, 특히 React 기반의 웹 개발 환경은 거대한 지각 변동을 맞이했습니다. App Router의 안정화는 단순한 기능 추가를 넘어, 우리가 웹 애플리케이션을 설계하고 구축하는 방식 자체를 재정의했습니다. 본 포스트에서는 실무 프로젝트를 Pages Router에서 App Router로 마이그레이션하며 겪은 구체적인 경험, 그 과정에서 마주한 난관들, 그리고 이를 통해 얻은 확실한 기술적 이점들을 약 3,000자 분량으로 상세히 회고해보고자 합니다.
1. 패러다임의 전환: Server Components (RSC)
가장 근본적인 변화이자 App Router의 핵심은 단연 **React Server Components (RSC)**입니다. 과거 Next.js의 getServerSideProps나 getStaticProps는 페이지 단위의 데이터 패칭을 위한 것이었지만, RSC는 컴포넌트 단위에서 서버 로직을 실행할 수 있게 해줍니다.
1.1 왜 서버 컴포넌트인가?
기존의 클라이언트 사이드 렌더링(CSR) 방식은 모든 자바스크립트 코드를 브라우저로 전송해야 했습니다. 사용자가 보지 않는 컴포넌트나, 상호작용이 필요 없는 정적인 콘텐츠를 렌더링하기 위한 라이브러리 코드까지 모두 다운로드해야 했기 때문에 초기 로딩 속도(TTI) 저하의 원인이 되곤 했습니다.
서버 컴포넌트는 다릅니다. 서버에서 미리 렌더링된 HTML만을 클라이언트로 전송합니다. 이는 다음과 같은 즉각적인 이점을 제공합니다:
- Zero Bundle Size: 서버 컴포넌트 내부에서만 사용되는 외부 라이브러리는 클라이언트 번들에 포함되지 않습니다. 예를 들어, 마크다운 파싱을 위한 무거운 라이브러리를 사용하더라도 브라우저는 결과 HTML만 받게 됩니다.
- Backend Access: 컴포넌트 내부에서 DB에 직접 쿼리를 날리거나, 파일 시스템에 접근하는 것이 가능해집니다. API 엔드포인트를 별도로 만들지 않아도 되는 것입니다.
// app/posts/[id]/page.tsx
import db from '@/lib/db';
async function getPost(id: string) {
// DB 직접 접근 가능
const post = await db.post.findUnique({ where: { id } });
return post;
}
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
2. 직면했던 어려움과 해결책 ('use client'의 경계)
모든 변화에는 진통이 따르듯, App Router 도입 초기에는 혼란스러운 점도 많았습니다. 가장 큰 난관은 서버 컴포넌트와 클라이언트 컴포넌트의 경계를 설정하는 것이었습니다.
2.1 Hooks 사용 불가
서버 컴포넌트는 말 그대로 서버에서 실행되므로, 브라우저의 전유물인 useState, useEffect, useRouter 등의 React Hooks를 사용할 수 없습니다. 처음에는 습관적으로 훅을 작성하다가 에러를 만나는 경우가 허다했습니다.
해결책: 저는 "Island Architecture" 패턴을 적용했습니다. 페이지의 전체적인 골격은 서버 컴포넌트로 구성하되, 버튼 클릭이나 폼 입력처럼 사용자와의 상호작용이 반드시 필요한 부분만 작은 단위의 컴포넌트로 분리하여 상단에 'use client'를 선언했습니다. 이렇게 하면 서버 사이드 렌더링의 이점을 최대한 살리면서도 필요한 인터랙션을 구현할 수 있습니다.
2.2 Context API와 Provider
전역 상태 관리를 위한 Context API 역시 클라이언트 사이드 로직입니다. layout.tsx는 기본적으로 서버 컴포넌트이기 때문에, 이곳에 직접 Context Provider를 감쌀 수 없습니다.
해결책: 별도의 Providers.tsx라는 클라이언트 컴포넌트 래퍼를 만들어, 그 안에서 Provider들을 합성하고 layout.tsx에서는 이 래퍼를 사용하는 방식을 채택했습니다.
// components/Providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export default function Providers({ children }) {
return (
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider>{children}</ThemeProvider>
</QueryClientProvider>
);
}
3. 향상된 라우팅과 레이아웃 시스템
과거 _app.tsx나 _document.tsx에 의존하던 전역 설정이 layout.tsx로 통합되고, 폴더 구조 자체가 라우팅이 되는 방식은 매우 직관적이었습니다.
3.1 중첩 레이아웃 (Nested Layouts)
특정 경로 하위에서만 유지되는 사이드바나 헤더를 만들기 위해 getLayout 패턴을 사용하던 시절은 지났습니다. 이제는 해당 폴더에 layout.tsx를 만들기만 하면, 하위 페이지들은 자동으로 그 레이아웃을 공유합니다. 상태가 유지되는 것은 덤입니다.
3.2 로딩과 에러 처리의 우아함
loading.tsx와 error.tsx 파일 규칙은 개발자 경험(DX)을 극대화해 주었습니다. Suspense나 ErrorBoundary를 직접 구현할 필요 없이, 프레임워크가 파일 시스템을 기반으로 이를 자동으로 처리해 줍니다. 특히 스트리밍(Streaming)을 통해 준비된 UI부터 먼저 보여주는 사용자 경험은 Next.js 14의 백미라고 할 수 있습니다.
4. 결론: 도입할 가치가 있는가?
단언컨대, 도입할 가치는 충분합니다. 초기 학습 비용과 마이그레이션의 수고로움은 분명 존재하지만, 그 결과로 얻게 되는 애플리케이션의 성능 향상과 코드의 유지보수성 증대는 그 비용을 상쇄하고도 남습니다. 물론 모든 프로젝트에 맹목적으로 적용할 필요는 없습니다. SEO가 중요하지 않고 고도의 인터랙션만으로 이루어진 대시보드 같은 서비스라면 기존의 CSR 방식이 더 적합할 수도 있습니다. 하지만 콘텐츠 중심의 서비스, 커머스, 블로그 등을 구축하려 한다면 Next.js 14 App Router는 현재 선택할 수 있는 가장 강력하고 효율적인 무기입니다.