Next.js App Router는 TypeScript를 1급 시민으로 지원하며, page, layout, route handler, Server Actions 등 모든 영역에서 타입 안전성을 제공합니다.

Page 컴포넌트 타이핑

Next.js 15+에서는 paramssearchParamsPromise 입니다.

TYPESCRIPT
// app/users/[id]/page.tsx
type PageProps = {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ tab?: string }>;
};

export default async function UserPage({ params, searchParams }: PageProps) {
  const { id } = await params;
  const { tab } = await searchParams;

  return (
    <div>
      <h1>사용자 {id}</h1>
      {tab && <p>현재 탭: {tab}</p>}
    </div>
  );
}

Layout 타이핑

TYPESCRIPT
// app/layout.tsx
type LayoutProps = {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
};

export default async function RootLayout({ children, params }: LayoutProps) {
  const { locale } = await params;

  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  );
}

Metadata 타이핑

TYPESCRIPT
import type { Metadata } from 'next';

// 정적 메타데이터
export const metadata: Metadata = {
  title: '내 블로그',
  description: '개발 이야기를 나눕니다',
};

// 동적 메타데이터
type MetadataProps = {
  params: Promise<{ id: string }>;
};

export async function generateMetadata(
  { params }: MetadataProps
): Promise<Metadata> {
  const { id } = await params;
  const post = await getPost(id);

  return {
    title: post.title,
    description: post.summary,
  };
}

Server Actions 타이핑

TYPESCRIPT
// app/actions.ts
'use server';

// 폼 액션 — FormData를 받음
export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  // Zod로 검증하면 더 안전
  const result = UserSchema.safeParse({ name, email });
  if (!result.success) {
    return { error: result.error.flatten().fieldErrors };
  }

  await db.user.create({ data: result.data });
  return { success: true };
}

// 일반 함수 형태의 Server Action
export async function deleteUser(id: number): Promise<{ success: boolean }> {
  await db.user.delete({ where: { id } });
  return { success: true };
}

useActionState와 함께 사용

TYPESCRIPT
'use client';

import { useActionState } from 'react';
import { createUser } from './actions';

type ActionState = {
  error?: Record<string, string[]>;
  success?: boolean;
};

export function CreateUserForm() {
  const [state, formAction, isPending] = useActionState<ActionState, FormData>(
    async (prevState, formData) => {
      const result = await createUser(formData);
      return result;
    },
    {}
  );

  return (
    <form action={formAction}>
      <input name="name" />
      {state.error?.name && <span>{state.error.name[0]}</span>}
      <button disabled={isPending}>
        {isPending ? '저장 중...' : '저장'}
      </button>
    </form>
  );
}

Route Handler 타이핑

TYPESCRIPT
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get('page') || '1');

  const users = await getUsers(page);
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  // Zod로 검증
  const result = UserSchema.safeParse(body);
  if (!result.success) {
    return NextResponse.json(
      { error: result.error.issues },
      { status: 400 }
    );
  }

  const user = await createUser(result.data);
  return NextResponse.json(user, { status: 201 });
}

동적 라우트의 Route Handler

TYPESCRIPT
// app/api/users/[id]/route.ts
type RouteContext = {
  params: Promise<{ id: string }>;
};

export async function GET(
  request: NextRequest,
  { params }: RouteContext
) {
  const { id } = await params;
  const user = await getUser(parseInt(id));

  if (!user) {
    return NextResponse.json(
      { error: '사용자를 찾을 수 없습니다' },
      { status: 404 }
    );
  }

  return NextResponse.json(user);
}

generateStaticParams 타이핑

TYPESCRIPT
// app/posts/[slug]/page.tsx
export async function generateStaticParams(): Promise<{ slug: string }[]> {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

Middleware 타이핑

TYPESCRIPT
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest): NextResponse {
  const token = request.cookies.get('token');

  if (!token && request.nextUrl.pathname.startsWith('/admin')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/admin/:path*'],
};

정리

  • Next.js 15+에서 paramssearchParams는 Promise 타입이다
  • Server Actions는 'use server'로 선언하고 FormData 또는 일반 매개변수를 받는다
  • Route Handler는 NextRequestNextResponse로 타이핑한다
  • generateMetadatagenerateStaticParams에도 반환 타입을 명시한다
  • Zod와 결합하면 Server Actions와 Route Handler에서 런타임 검증까지 안전해진다
댓글 로딩 중...