Next.js 교재 · 24편 · TypeScript

TypeScript 통합 — 타입 안전 Next.js

한 언어로 풀스택, 한 타입 시스템으로 풀스택. Next + TS 가 만나면 결정적 시너지.

TypeScript 가 서버·클라이언트 경계를 가로지르는 컨셉 일러스트

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: truenoUncheckedIndexedAccess: 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편 좌표

여기까지 정리. tsconfigstrictnoUncheckedIndexedAccess 둘 다 켜기. Server Component 의 params·searchParams 는 Promise (Next 15). Zod + z.infer 로 런타임·컴파일 타입 한 곳 정의. Route Handler 응답은 공유 타입 파일, 진짜 안전은 클라에서도 Zod parse. 풀스택 자동 타입은 tRPC. 다음 편에서 성능 측정·최적화.

다음 편 예고 — 성능 측정과 최적화

Lighthouse·Web Vitals·번들 분석. 25편.

© 2026 주나이테크(주) @JUNAITECH