Next.js + TypeScript — App Router, Server Actions 타이핑
Next.js App Router는 TypeScript를 1급 시민으로 지원하며, page, layout, route handler, Server Actions 등 모든 영역에서 타입 안전성을 제공합니다.
Page 컴포넌트 타이핑
Next.js 15+에서는 params와 searchParams가 Promise 입니다.
// 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 타이핑
// 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 타이핑
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 타이핑
// 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와 함께 사용
'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 타이핑
// 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
// 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 타이핑
// app/posts/[slug]/page.tsx
export async function generateStaticParams(): Promise<{ slug: string }[]> {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
Middleware 타이핑
// 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+에서
params와searchParams는 Promise 타입이다 - Server Actions는
'use server'로 선언하고 FormData 또는 일반 매개변수를 받는다 - Route Handler는
NextRequest와NextResponse로 타이핑한다 generateMetadata와generateStaticParams에도 반환 타입을 명시한다- Zod와 결합하면 Server Actions와 Route Handler에서 런타임 검증까지 안전해진다
댓글 로딩 중...