Frontend/Typescript

TypeScript 완벽 보안 가이드: strict 모드, any 지양, 그리고 Zod

미니임 2026. 2. 23. 10:01

 

TypeScript를 사용하는 궁극적인 목적은 에러를 사전에 방지하고 안전한 코드를 작성하는 것입니다. 이를 위해 가장 먼저 선행되어야 할 것은 **컴파일러의 엄격한 검사(strict: true)**이며, 그 다음은 any를 제거하는 습관, 마지막으로 **런타임 데이터의 무결성(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. any를 지양하는 습관: 왜 unknown인가?

코드에 any가 등장하는 순간, TypeScript는 해당 변수에 대한 타입 검사를 포기합니다. 이는 TypeScript를 사용하는 이유 자체를 무색하게 만듭니다. 가장 좋은 대안은 unknown입니다.

1.1. any vs unknown

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

// unknown의 안전함
const valueUnknown: unknown = "hello";
// valueUnknown.toUpperCase(); // Error: 'valueUnknown' is of type 'unknown'.

if (typeof valueUnknown === "string") {
  console.log(valueUnknown.toUpperCase()); // OK (타입 좁히기 성공)
}

1.2. any를 제거하는 리팩토링 팁

  1. 점진적 교체: 당장 모든 타입을 정의하기 어렵다면 any 대신 unknown으로 먼저 바꿉니다. 컴파일러가 에러를 띄워 당신이 타입을 확인하도록 강제할 것입니다.
  2. 사용자 정의 타입 가드(Type Guards): is 키워드를 사용하여 데이터의 실체를 증명합니다.
  3. as const 사용: 리터럴 타입의 추론을 강화하여 string 대신 구체적인 값을 타입으로 사용합니다.
// 타입 가드 예시
function isUser(obj: unknown): obj is { name: string } {
  return typeof obj === "object" && obj !== null && "name" in obj;
}

const data: unknown = fetchExternalData();
if (isUser(data)) {
  console.log(data.name); // 안전하게 접근 가능
}

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

TypeScript는 strict: true 설정을 하고 any를 쓰지 않더라도 "이 변수는 이 타입일 것이다"라고 가정하고 코드를 짭니다. 하지만 외부에서 들어오는 데이터는 통제할 수 없습니다.

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

// strict 모드가 켜져 있어도, API 응답으로 { name: "John", age: "30" } (문자열 나이)이 온다면?
// TS 컴파일러는 이를 잡을 수 없으며, 실행 중에 age.toFixed() 호출 시 에러가 발생합니다.
const userData: User = await fetch('/api/user').then(res => res.json());

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

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

Zod를 사용하면 먼저 스키마를 정의하고, 해당 스키마를 통해 데이터를 파싱(Parsing)합니다.

3.1. 기본 타입 정의

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),         // UUID 형식의 문자열
  name: z.string().min(2).max(20), // 2~20자 사이의 문자열
  email: z.string().email(),     // 이메일 형식
  age: z.number().int().positive(), // 양의 정수
  isPremium: z.boolean().default(false), // 기본값 설정
});

// 데이터 검증
const result = UserSchema.safeParse({
  id: "550e8400-e29b-41d4-a716-446655440000",
  name: "Kim",
  email: "test@example.com",
  age: 25,
});

if (result.success) {
  console.log("검증 성공:", result.data);
} else {
  // strict 모드와 결합 시 에러 객체의 타입도 안전하게 추론됩니다.
  console.error("검증 실패:", result.error.format());
}

4. infer를 이용한 TypeScript와의 연결

Zod의 가장 큰 장점 중 하나는 정의한 스키마로부터 TypeScript 타입을 자동으로 추출할 수 있다는 점입니다. 이를 통해 중복 선언을 방지할 수 있습니다.

// 스키마로부터 타입 추출
type UserType = z.infer<typeof UserSchema>;

// 이제 UserType은 아래와 동일하게 동작합니다.
/*
type UserType = {
  id: string;
  name: string;
  email: string;
  age: number;
  isPremium: boolean;
}
*/

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

API로 들어오는 데이터(request body)를 Zod로 가드(Guard)하는 패턴입니다. strict: true와 함께 사용하면 req.body의 불확실성을 완벽히 제거할 수 있습니다.

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

const ContactSchema = z.object({
  email: z.string().email(),
  message: z.string().min(10, "메시지는 최소 10자 이상이어야 합니다."),
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') return res.status(405).end();

  // req.body는 기본적으로 any 타입이지만, Zod를 통해 안전한 타입을 획득합니다.
  const validation = ContactSchema.safeParse(req.body);

  if (!validation.success) {
    return res.status(400).json({ 
      error: "유효하지 않은 요청입니다.",
      details: validation.error.flatten().fieldErrors 
    });
  }

  // validation.data는 이제 완벽하게 타입이 추론된 안전한 데이터입니다.
  const { email, message } = validation.data;
  
  res.status(200).json({ success: true });
}

요약: 3단계 보안 체계 구축

  1. 엄격한 규칙 (strict: true): 프로젝트 내부의 타입 논리를 세웁니다.
  2. 신중한 타입 선택 (any 지양): unknown을 사용하여 모르는 데이터에 대한 방어적 코딩을 습관화합니다.
  3. 실행 시점 검증 (Zod): 외부 데이터가 코드의 타입 약속을 지키는지 런타임에 최종 확인합니다.

이 세 가지를 병행하는 것이 현대적인 TypeScript 개발의 표준이며, 가장 견고한 애플리케이션을 만드는 방법입니다.

반응형