Next.js 14 도입기: App Router와 함께한 2개월

odada·2025년 1월 2일
0

next.js

목록 보기
5/12

Next.js 14 도입기: App Router와 함께한 2개월

2024년 1월, Next.js 14를 실무 프로젝트에 도입한지 약 2개월이 지났습니다. 기존 pages 디렉토리 구조에서 app 디렉토리로 마이그레이션하면서 느낀 장점들과 주의할 점들을 공유하고자 합니다.

1. App Router의 명확한 구조화

이전 구조의 문제점

기존 pages 구조에서는 다음과 같은 불편함이 있었습니다:

pages/
  index.tsx
  about.tsx
  users/
    [id].tsx
  api/
    users.ts
    posts.ts
src/
  components/
  hooks/
  api/
    types.ts
    client.ts

API 정의와 타입이 여러 곳에 분산되어 있어 관리가 어려웠습니다. 특히 pages/apisrc/api 폴더가 별도로 존재하면서 혼란스러웠죠.

App Router의 개선된 구조

app/
  (routes)/
    page.tsx
    about/
      page.tsx
    users/
      [id]/
        page.tsx
  api/
    users/
      route.ts
    posts/
      route.ts
  _components/
  _hooks/
  _lib/
    api/
      client.ts
      types.ts

이렇게 구조를 변경하면서 얻은 장점들:

  1. 라우팅 관련 코드 집중화

    • 페이지와 API 라우트가 app 디렉토리 아래 통합
    • 연관된 코드들의 물리적 거리 감소
  2. 명확한 컨벤션

    • page.tsx: 페이지 컴포넌트
    • route.ts: API 엔드포인트
    • layout.tsx: 레이아웃 컴포넌트
    • loading.tsx: 로딩 UI
    • error.tsx: 에러 처리
  3. 직관적인 API 구조

    // app/api/users/route.ts
    import { NextResponse } from 'next/server';
    
    export async function GET() {
      // 핸들러 로직
      return NextResponse.json({ users: [] });
    }
    
    export async function POST(request: Request) {
      const body = await request.json();
      // 처리 로직
      return NextResponse.json({ success: true });
    }

2. Server Components의 자연스러운 활용

기본이 되는 서버 컴포넌트

모든 컴포넌트는 기본적으로 서버 컴포넌트로 동작합니다. 이는 다음과 같은 이점을 제공했습니다:

  1. 초기 번들 사이즈 감소

    • 클라이언트로 전송되는 JavaScript 양 감소
    • 특히 큰 라이브러리 사용시 효과적
  2. 데이터 페칭 간소화

    // app/users/page.tsx
    async function UsersPage() {
      const users = await db.users.findMany();  // 직접 DB 접근
      return (
        <ul>
          {users.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      );
    }
  3. 환경 변수 접근 용이성

    • .env 파일의 SERVER_ 변수들도 안전하게 사용

클라이언트 컴포넌트와의 결합

필요한 부분만 선택적으로 클라이언트 컴포넌트로 전환:

// app/_components/UserForm.tsx
'use client';

export function UserForm() {
  const [name, setName] = useState('');
  
  return (
    <form>
      <input 
        value={name} 
        onChange={e => setName(e.target.value)} 
      />
    </form>
  );
}

3. Server Actions로 간소화된 폼 처리

기존에는 API 라우트를 만들고, fetch로 요청하는 과정이 필요했습니다:

// Before: pages/api/submit.ts
export default async function handler(req, res) {
  if (req.method !== 'POST') return res.status(405).end();
  // ... 처리 로직
}

// Before: components/Form.tsx
const handleSubmit = async (data) => {
  const res = await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify(data)
  });
}

Server Actions를 사용하면 훨씬 간단해집니다:

// After: app/_lib/actions.ts
'use server'

export async function submitForm(data: FormData) {
  const name = data.get('name');
  // 직접 처리 로직 구현
}

// After: app/_components/Form.tsx
export function Form() {
  return (
    <form action={submitForm}>
      <input name="name" />
      <button type="submit">제출</button>
    </form>
  );
}

4. 개선된 API 라우트

HTTP 메서드별 핸들러 분리

// app/api/posts/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const query = searchParams.get('q');
  
  // 조회 로직
  return NextResponse.json({ posts: [] });
}

export async function POST(request: Request) {
  const body = await request.json();
  
  // 생성 로직
  return NextResponse.json({ id: 'new-post' });
}

동적 라우트 처리 개선

// app/api/posts/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const post = await db.post.findUnique({
    where: { id: params.id }
  });
  
  if (!post) {
    return NextResponse.json(
      { error: 'Not found' },
      { status: 404 }
    );
  }
  
  return NextResponse.json(post);
}

5. 주의할 점들

1. 서버/클라이언트 컴포넌트 구분

  • 'use client' 지시어의 전략적 사용
  • 큰 컴포넌트를 작게 분리하여 클라이언트 코드 최소화

2. 데이터 캐싱 전략

// 캐시 제어
fetch(url, { next: { revalidate: 3600 } });  // 1시간
fetch(url, { cache: 'no-store' });  // 항상 최신

3. TypeScript 설정

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "next"
      }
    ]
  }
}

6. 앞으로의 과제

  1. 더 섬세한 캐싱 전략

    • Partial Prerendering 도입 검토
    • 동적/정적 라우트 최적화
  2. 성능 모니터링

    • Core Web Vitals 추적
    • 서버 컴포넌트 분할 전략
  3. 배포 파이프라인

    • 증분 정적 재생성 자동화
    • Edge Runtime 활용

마치며

Next.js 14의 App Router는 단순한 디렉토리 구조 변경이 아닌, 웹 개발 패러다임의 변화를 가져왔습니다. 서버와 클라이언트의 경계가 자연스럽게 섞이면서, 더 효율적인 개발이 가능해졌습니다.

특히 API 통합과 Server Components의 기본 채택은 코드베이스를 더 깔끔하고 관리하기 쉽게 만들어주었습니다. 마이그레이션이 쉽지는 않았지만, 투자할 만한 가치가 있었다고 확신합니다.

다음에는 Partial Prerendering을 도입하면서 겪은 경험을 공유하도록 하겠습니다.

0개의 댓글