1편에서 "TypeScript 무조건 Yes" 라고 말했다. create-next-app 이 자동 세팅해주니 그 자체는 손도 안 댄다. 그런데 Next.js 의 진짜 타입 안전은 그보다 한 단계 더 — Server Action 인자 타입, Route Handler 응답 타입, fetch 결과 타입까지 풀스택 연결.
이번 편은 그 한 단계 더 — tsconfig 설정, Server/Client 컴포넌트 타입, 폼·API 의 타입 공유 패턴.
1. tsconfig 권장 옵션
create-next-app 기본 + 권장 추가:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "preserve",
"strict": true, // ★ 필수
"noUncheckedIndexedAccess": true, // ★ 추천: arr[i] 가 T|undefined
"noUnusedLocals": false, // 개발 중 마찰 줄이려 false
"skipLibCheck": true,
"esModuleInterop": true,
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
핵심 2개 — strict: true 와 noUncheckedIndexedAccess: true. 둘 다 켜야 런타임 사고가 컴파일 타임에 잡힌다.
2. Server Component 타입 — params 와 searchParams
Next 15 부터 둘 다 Promise.
// app/blog/[slug]/page.tsx
type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export default async function BlogPost({ params, searchParams }: Props) {
const { slug } = await params;
const { lang } = await searchParams;
// ...
}
searchParams 의 값이 string | string[] | undefined 인 게 헷갈리기 쉬움. URL 의 ?key=a&key=b 가 배열로 오니까 항상 처리 분기.
3. Server Action 타입 — Zod 로 양쪽 보장
10편 폼 챕터의 패턴 재방문 — TypeScript 와 Zod 의 시너지.
// actions.ts
'use server';
import { z } from 'zod';
const Schema = z.object({
title: z.string().min(2),
body: z.string().min(10),
authorId: z.string().uuid(),
});
type FormState = {
ok: boolean;
errors?: Record<string, string[]>;
data?: z.infer<typeof Schema>;
};
export async function createPost(prev: FormState, fd: FormData): Promise<FormState> {
const parsed = Schema.safeParse(Object.fromEntries(fd));
if (!parsed.success) {
return { ok: false, errors: parsed.error.flatten().fieldErrors };
}
await db.posts.create({ data: parsed.data });
return { ok: true, data: parsed.data };
}
z.infer<typeof Schema> 가 핵심 — 런타임 검증 스키마에서 TypeScript 타입을 자동 추출. 한 곳 정의 → 양쪽 자동. 옛 방식의 type + 런타임 검증 따로 적던 중복이 사라진다.
4. Route Handler 응답 타입 — 클라이언트와 공유
API 가 반환하는 모양을 어디서 정의할지 — 답은 lib/api-types.ts 같은 공유 파일.
// lib/api-types.ts
import { z } from 'zod';
export const PostSchema = z.object({
id: z.string().uuid(),
title: z.string(),
body: z.string(),
createdAt: z.string().datetime(),
});
export type Post = z.infer<typeof PostSchema>;
export const PostListResponse = z.object({
data: z.array(PostSchema),
total: z.number(),
});
export type PostListResponse = z.infer<typeof PostListResponse>;
// app/api/posts/route.ts
export async function GET(): Promise<Response> {
const posts = await db.posts.findMany();
const body: PostListResponse = { data: posts, total: posts.length };
return Response.json(body);
}
// 클라이언트
import { PostListResponse } from '@/lib/api-types';
const res = await fetch('/api/posts');
const json: PostListResponse = await res.json();
주의 — 서버에서 보낸 타입이 자동 안전하지 않음 — 위 json: PostListResponse 는 단순 캐스팅. 실제 응답이 다를 수 있다. 진짜 안전하려면 클라이언트도 Zod 로 다시 parse. 또는 다음 절의 tRPC 도입.
5. tRPC — 진짜 타입 안전 풀스택
Next + TS 의 정점. 서버 함수 시그니처가 클라이언트로 자동 전파, 별도 fetch·타입 동기화 코드 0.
// server/routers/posts.ts
import { router, publicProcedure } from '@/server/trpc';
import { z } from 'zod';
export const postsRouter = router({
list: publicProcedure
.input(z.object({ page: z.number().default(1) }))
.query(async ({ input }) => {
return db.posts.findMany({ skip: (input.page - 1) * 20, take: 20 });
}),
create: publicProcedure
.input(z.object({ title: z.string().min(2), body: z.string() }))
.mutation(async ({ input }) => db.posts.create({ data: input })),
});
// 클라이언트
'use client';
import { trpc } from '@/lib/trpc';
const { data } = trpc.posts.list.useQuery({ page: 1 });
// data 의 타입이 자동으로 Post[] — fetch·타입 정의·검증 코드 0줄
fetch 의 url·메서드·body 직렬화·역직렬화·타입 동기화 — 다 사라진다. 리팩토링이 양방향 — 서버에서 필드 하나 빼면 클라이언트 전부 빨간 줄.
대안 — Server Actions 만으로 충분 — 같은 사이트 안 폼·버튼이면 9·10편의 Server Action 으로 타입 안전 가능. tRPC 는 외부에서 API 호출하거나 모바일 앱이 있을 때 강력. 작은 사이드 프로젝트는 Server Action + fetch + Zod 로도 충분. 회사 코드에서 자주 만난다.
요약 — 24편 좌표
여기까지 정리. tsconfig 의 strict 와 noUncheckedIndexedAccess 둘 다 켜기. Server Component 의 params·searchParams 는 Promise (Next 15). Zod + z.infer 로 런타임·컴파일 타입 한 곳 정의. Route Handler 응답은 공유 타입 파일, 진짜 안전은 클라에서도 Zod parse. 풀스택 자동 타입은 tRPC. 다음 편에서 성능 측정·최적화.
다음 편 예고 — 성능 측정과 최적화
Lighthouse·Web Vitals·번들 분석. 25편.