티스토리 뷰

 

TypeScript를 사용하는 궁극적인 목적은 에러를 사전에 방지하고 안전한 코드를 작성하는 것입니다. 이를 위해 가장 먼저 선행되어야 할 것은 **컴파일러의 엄격한 검사(strict: true)**이며, 그 다음은 도메인을 정확하게 반영하는 타입 설계 철학, 마지막으로 **런타임 데이터의 무결성(Zod)**을 확보하는 것입니다.

0. 시작하기 전에: strict: true 설정의 중요성

TypeScript의 진정한 힘은 tsconfig.json에서 strict: true 옵션을 켰을 때 발휘됩니다. 이 설정은 단순히 "까다로운 검사"가 아니라, 타입 시스템이 논리적으로 결함 없이 작동하기 위한 필수 조건입니다.

왜 모든 엄격 모드 옵션을 켜야 하는가?

  1. noImplicitAny: 타입을 명시하지 않아 암묵적으로 any가 되는 것을 막습니다. any는 타입 시스템의 구멍이며, 이를 방지해야만 코드 전체의 타입 추적이 가능해집니다.
  2. strictNullChecks: null과 undefined를 엄격히 구분합니다. 런타임 에러의 가장 흔한 원인인 "Cannot read property of undefined"를 컴파일 단계에서 99% 차단합니다.
  3. strictFunctionTypes: 함수의 매개변수와 반환 타입이 올바르게 상속되거나 할당되는지 체크하여 예기치 못한 함수 실행 에러를 방지합니다.

1. 타입 설계 철학: 도메인 모델을 타입으로 표현하기

단순히 에러를 막는 것을 넘어, 타입은 비즈니스 로직의 명세서가 되어야 합니다. 훌륭한 타입 설계는 "불가능한 상태를 표현 불가능하게(Make Illegal States Unrepresentable)" 만드는 것입니다.

1.1. 구별된 유니온 (Discriminated Unions)

도메인의 상태에 따라 가질 수 있는 데이터가 다르다면, 이를 하나의 거대한 객체가 아닌 유니온 타입으로 분리하세요.

// 나쁜 예: 모든 상태가 옵셔널이어서 런타임에 체크가 어려움
interface Order {
  status: 'pending' | 'shipped' | 'delivered';
  trackingNumber?: string;
  deliveryDate?: Date;
}

// 좋은 예: 상태에 따라 필요한 데이터가 강제됨
type Order =
  | { status: 'pending' }
  | { status: 'shipped'; trackingNumber: string }
  | { status: 'delivered'; trackingNumber: string; deliveryDate: Date };

function processOrder(order: Order) {
  if (order.status === 'shipped') {
    // 여기서 trackingNumber는 반드시 존재함이 보장됨
    console.log(order.trackingNumber);
  }
}

1.2. 브랜디드 타입 (Branded Types)

단순한 string이나 number라도 도메인적 의미가 다르다면 서로 섞이지 않게 보호해야 합니다.

type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };

function getOrder(id: OrderId) { /* ... */ }

const myUserId = "user_123" as UserId;
// getOrder(myUserId); // Error: UserId를 OrderId에 할당할 수 없음

2. any를 지양하는 습관: 왜 unknown인가?

코드에 any가 등장하는 순간, TypeScript는 해당 변수에 대한 타입 검사를 포기합니다. 가장 좋은 대안은 unknown입니다.

2.1. any vs unknown

  • any: "무엇이든 할 수 있다." (타입 체크 안 함)
  • unknown: "무엇인지 모르니, 확인 전까지는 아무것도 할 수 없다." (안전한 타입 체크 강제)
// any의 위험성
const valueAny: any = "hello";
valueAny.toFixed(); // OK (런타임 에러 발생!)

// unknown의 안전함
const valueUnknown: unknown = "hello";
if (typeof valueUnknown === "string") {
  console.log(valueUnknown.toUpperCase()); // OK (타입 좁히기 성공)
}

3. 왜 Zod인가? (TypeScript의 한계)

TypeScript는 아무리 설계를 잘해도 "이 데이터는 이 타입일 것이다"라고 가정할 뿐입니다. 외부 API나 유저 입력은 통제할 수 없습니다.

interface User {
  name: string;
  age: number;
}

// API 응답으로 { name: "John", age: "30" }이 온다면?
// TS 컴파일러는 이를 잡을 수 없으며, 런타임에 로직이 깨집니다.
const userData: User = await fetch('/api/user').then(res => res.json());

Zod는 데이터를 "믿는" 대신 "검증"하여 정적 타입과 실제 데이터 사이의 간극을 메웁니다.

4. Zod 기초: 스키마 정의 및 검증

4.1. 기본 타입 정의

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(20),
  email: z.string().email(),
  age: z.number().int().positive(),
  isPremium: z.boolean().default(false),
});

// 데이터 검증
const result = UserSchema.safeParse(inputData);

if (result.success) {
  // result.data는 완벽하게 검증된 User 타입입니다.
}

5. 실무 활용: Next.js API Routes 적용

API로 들어오는 데이터(req.body)를 Zod로 가드하는 패턴입니다.

import type { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';

const ContactSchema = z.object({
  email: z.string().email(),
  message: z.string().min(10),
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const validation = ContactSchema.safeParse(req.body);

  if (!validation.success) {
    return res.status(400).json({ error: validation.error.format() });
  }

  const { email, message } = validation.data; // 안전한 타입 데이터
  res.status(200).json({ success: true });
}

요약: 3단계 보안 및 설계 체계

  1. 엄격한 규칙 (strict: true): 프로젝트 내부의 타입 논리를 세웁니다.
  2. 정교한 설계 (도메인 중심 타입): 불가능한 상태를 방지하고 의미론적 안전성을 확보합니다.
  3. 실행 시점 검증 (Zod): 외부 데이터가 약속을 지키는지 최종 확인합니다.

이 체계를 갖추는 것이 현대적인 TypeScript 개발의 표준이며, 가장 견고한 소프트웨어를 만드는 길입니다.

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함