Next.js App Router, 그 고통과 성장의 기록: 마이그레이션 경험기
Next.js 13/14 App Router로의 마이그레이션은 단순한 업데이트가 아닌 패러다임 전환입니다. 이 글에서 App Router의 핵심 개념부터 실질적인 코드 예제, 마주했던 어려움과 해결책까지 저의 경험을 공유합니다. RSC 기반의 새로운 Next.js 세계로 떠나볼까요?
Next.js App Router, 그 고통과 성장의 기록: 마이그레이션 경험기
안녕하세요, 시니어 프론트엔드 개발자이자 테크 블로거입니다. 오늘은 많은 개발자분들이 궁금해하고, 또 한편으로는 두려워하는 주제인 Next.js App Router 마이그레이션 경험에 대해 이야기해보려 합니다.
Next.js 13에서 처음 소개된 App Router는 React Server Components (RSC)를 기반으로 완전히 새로운 접근 방식을 제시하며, Next.js 14에서는 더욱 안정화되었습니다. pages 디렉토리 기반의 기존 방식에 익숙했던 저에게도 이번 마이그레이션은 단순한 업데이트를 넘어선 패러다임의 전환이었습니다. 이 글을 통해 App Router의 핵심 개념부터 실제 마이그레이션 과정에서 마주쳤던 난관, 그리고 최종적으로 얻게 된 이점들까지 솔직하게 공유하고자 합니다.
왜 App Router로 마이그레이션해야 하는가?
솔직히 처음에는 "굳이?"라는 생각이 들었습니다. 하지만 App Router가 가져다줄 잠재적인 이점들을 고려했을 때, 그 고통을 감수할 가치가 충분하다고 판단했습니다.
주요 이점들은 다음과 같습니다:
- 성능 향상: RSC 덕분에 클라이언트 번들 크기가 줄어들고, 초기 로딩 속도가 빨라집니다. 서버에서 데이터를 미리 가져와 렌더링하므로 사용자 경험이 향상됩니다.
- 단순해진 데이터 Fetching:
async/await컴포넌트를 통해 서버 컴포넌트 내에서 직접 데이터를 가져올 수 있어,getServerSideProps나getStaticProps같은 별도의 데이터 fetching 함수 없이도 직관적으로 코드를 작성할 수 있습니다. - 개발자 경험 개선 (DX): 라우팅, 레이아웃, 데이터 Fetching 등이 컴포넌트 레벨에서 통합되어 코드 응집도가 높아집니다.
- SEO 최적화: 서버에서 렌더링된 HTML을 제공하여 검색 엔진 크롤링에 유리합니다.
App Router 마이그레이션, 그 고통과 성장의 기록
저의 프로젝트는 Next.js 12 (혹은 13 초반 pages 방식) 기반으로 운영되고 있었습니다. 이를 Next.js 14의 App Router 방식으로 전환하는 과정은 마치 새로운 프레임워크를 배우는 것과 같았습니다.
1. app 디렉토리 구조 이해하기
가장 먼저 익숙해져야 했던 것은 새로운 파일 시스템 기반의 라우팅이었습니다.
기존 pages 디렉토리가 app 디렉토리로 대체되며, 각 라우트 세그먼트가 폴더로 표현되고, 그 안에 page.tsx, layout.tsx 등의 특별한 파일들이 배치됩니다.
1// app/dashboard/layout.tsx 2import Navbar from '@/components/Navbar'; 3import Sidebar from '@/components/Sidebar'; 4 5export default function DashboardLayout({ 6 children, 7}: { 8 children: React.ReactNode; 9}) { 10 return ( 11 <> 12 <Navbar /> 13 <div className="flex"> 14 <Sidebar /> 15 <main className="flex-1 p-4">{children}</main> 16 </div> 17 </> 18 ); 19}
1// app/dashboard/page.tsx 2import { getUserData } from '@/lib/api'; // 서버에서만 실행되는 함수 3 4export default async function DashboardPage() { 5 const userData = await getUserData(); // 서버 컴포넌트에서 직접 데이터 fetching 6 7 return ( 8 <div> 9 <h1>환영합니다, {userData.name}님!</h1> 10 <p>오늘의 할 일: {userData.todos.length}개</p> 11 {/* ... */} 12 </div> 13 ); 14}
위 예시에서 볼 수 있듯이, DashboardLayout은 dashboard 경로의 모든 페이지에 적용되는 레이아웃이며, DashboardPage는 해당 경로의 실제 콘텐츠를 렌더링합니다. layout.tsx는 기본적으로 Server Component로 동작합니다.
2. Client Component와 Server Component의 분리
App Router의 핵심은 React Server Components (RSC) 입니다. 기본적으로 모든 컴포넌트는 Server Component로 간주되며, 클라이언트 측 상호작용 (useState, useEffect, onClick 등)이 필요한 경우에만 파일 상단에 "use client" 지시어를 추가하여 Client Component로 명시해야 합니다.
1// app/components/Counter.tsx 2"use client"; // 이 컴포넌트는 클라이언트에서 렌더링됩니다. 3 4import { useState } from 'react'; 5 6export default function Counter() { 7 const [count, setCount] = useState(0); 8 9 return ( 10 <div> 11 <p>현재 카운트: {count}</p> 12 <button onClick={() => setCount(count + 1)}>증가</button> 13 </div> 14 ); 15}
이러한 분리는 초기에는 혼란스러웠지만, 결국 어디서 어떤 코드가 실행될지 명확하게 구분할 수 있게 해주어 번들 최적화에 큰 도움이 되었습니다. 중요한 점은, Client Component 내에서 Server Component를 직접 import 할 수는 없다는 것입니다. 대신, Server Component에서 Client Component를 자식으로 전달하는 패턴을 사용해야 합니다.
3. 데이터 Fetching의 변화
기존에는 getServerSideProps나 getStaticProps를 사용했지만, App Router에서는 Server Component 내에서 async/await를 사용하여 데이터를 직접 가져올 수 있습니다. 이는 정말 혁신적이었습니다.
1// app/products/[id]/page.tsx 2import { getProductDetails } from '@/lib/api'; // 서버에서만 실행되는 함수 3 4interface ProductPageProps { 5 params: { id: string }; 6} 7 8export default async function ProductPage({ params }: ProductPageProps) { 9 const product = await getProductDetails(params.id); // 서버에서 직접 데이터 fetching 10 11 if (!product) { 12 return <div>상품을 찾을 수 없습니다.</div>; 13 } 14 15 return ( 16 <div> 17 <h1>{product.name}</h1> 18 <p>{product.description}</p> 19 <p>가격: {product.price}원</p> 20 {/* 클라이언트 상호작용이 필요한 부분은 Client Component로 분리 */} 21 {/* <AddToCartButton productId={product.id} /> */} 22 </div> 23 ); 24}
데이터 fetching 로직이 UI 컴포넌트와 함께 co-locate 되어 개발 편의성이 크게 향상되었습니다. 또한, Next.js는 자동으로 fetch 요청을 캐싱하고 중복 요청을 제거해주어 성능 최적화에도 기여합니다.
흔히 저지르는 실수와 해결책
마이그레이션 과정에서 제가 겪었던 몇 가지 흔한 실수들을 공유합니다.
"use client"누락: 클라이언트 상호작용 (useState, useEffect, onClick 등)이 필요한 컴포넌트에"use client"를 명시하지 않아 에러가 발생합니다.- 해결책: 클라이언트 측 API를 사용하는 컴포넌트에는 반드시
"use client"를 추가하세요.
- 해결책: 클라이언트 측 API를 사용하는 컴포넌트에는 반드시
- Server Component에서 Client Component import: Server Component에서 Client Component를 직접 import하는 것은 가능하지만, Client Component 내부에서 Server Component를 import하는 것은 불가능합니다.
- 해결책: Client Component가 Server Component를 필요로 한다면, Server Component에서 Client Component를 자식으로 받아 렌더링하는 패턴을 고려해보세요. (e.g.,
<ClientComponent><ServerComponentAsChild /></ClientComponent>는 안되고,<ServerComponent><ClientComponent /></ServerComponent>는 가능하며,<ServerComponent><ClientComponent><ServerComponentAsGrandchild /></ClientComponent></ServerComponent>또한 불가능)
- 해결책: Client Component가 Server Component를 필요로 한다면, Server Component에서 Client Component를 자식으로 받아 렌더링하는 패턴을 고려해보세요. (e.g.,
- 환경 변수 사용 혼동:
NEXT_PUBLIC_접두사가 없는 환경 변수는 Server Component에서만 접근 가능합니다. Client Component에서 접근하려 하면undefined가 됩니다.- 해결책: 클라이언트 측에서 필요한 환경 변수는 반드시
NEXT_PUBLIC_접두사를 붙여야 합니다.
- 해결책: 클라이언트 측에서 필요한 환경 변수는 반드시
App Router, 장단점 비교
| 장점 (Pros) | 단점 (Cons) |
|---|---|
| 성능 향상: RSC로 클라이언트 번들 최소화 | 높은 학습 곡선: RSC 패러다임 이해 필요 |
단순한 데이터 Fetching: async/await 컴포넌트 | 기존 라이브러리 호환성 문제: 일부 라이브러리 미지원 |
| 향상된 개발자 경험 (DX): Co-location, 통합된 라우팅 | 디버깅의 어려움: 서버/클라이언트 경계 이해 필요 |
| 스트리밍 및 Suspense: 사용자 경험 개선 | 초기 설정 복잡성: app 디렉토리 구조 및 규칙 |
| SEO 최적화: 서버 렌더링 HTML | 마이그레이션 비용: 기존 프로젝트 전환 시 시간 소요 |
💡 핵심 정리: Next.js App Router로의 마이그레이션은 단순히 코드를 옮기는 작업이 아니라, React Server Components (RSC)의 철학을 이해하고 서버와 클라이언트의 경계를 명확히 구분하는 패러다임 전환입니다. 초기에는 혼란과 어려움이 따르지만, 성능 향상, 개발자 경험 개선, 그리고 더욱 효율적인 데이터 Fetching이라는 큰 보상을 안겨줄 것입니다.
App Router는 Next.js의 미래이며, 웹 개발의 새로운 방향을 제시하고 있습니다. 저의 경험이 여러분의 App Router 여정에 작은 도움이 되기를 바랍니다.
- ✅ Server Component와 Client Component의 역할 명확히 구분
- ✅
app디렉토리 기반의 라우팅 및 파일 컨벤션 숙지 - ✅
async/await를 활용한 Server Component 내 데이터 Fetching 적극 활용 - ✅
NEXT_PUBLIC_환경 변수 사용 규칙 준수
다음 단계
- 🚀 공식 Next.js 문서 (v14 기준)를 정독하며 App Router의 세부 기능들을 익혀보세요.
- 🛠️ 작은 토이 프로젝트를 만들어 App Router의 개념들을 직접 적용해보세요.
- 💬 커뮤니티 (Discord, GitHub Discussions)에 참여하여 궁금증을 해결하고 경험을 공유하세요!
Happy Coding! ✨