TypeScript: 추가 단계 없이 JavaScript 작성 중단

발행: (2026년 2월 17일 오전 06:02 GMT+9)
13 분 소요
원문: Dev.to

Source: Dev.to

위에 제공된 소스 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역하고 싶은 본문을 알려주시면 한국어로 번역해 드리겠습니다.

소개

당신은 이번 분기에 47번째로 Cannot read property 'email' of undefined 에러를 마주하고 있습니다. user 객체는 이메일을 가지고 있어야 했습니다. API 문서에도 이메일이 있다고 적혀 있었고, 6개월 전에 떠난 동료도 분명히 이메일이 있다고 말했습니다.

하지만 그렇지 않습니다. 이제 any가 847번 등장하고 모든 함수가 “뭐든지, 괜찮아” 라는 인자를 유효한 인수로 받아들이는 코드베이스를 디버깅하고 있습니다.

JavaScript Hell에 오신 것을 환영합니다. TypeScript가 탈출구—올바르게 사용한다면.

TypeScript가 무엇인지

TypeScript는 컴파일 타임에 버그를 잡아내는 정적 타입 시스템입니다. 가드레일이 있는 JavaScript라고 할 수 있습니다.

  • 알려줍니다 존재하지 않는 프로퍼티에 접근할 때
  • 자동 완성 객체를, 내부 내용을 알고 있기 때문에
  • 문서화 타입을 통해 코드를 (이제 // user object, has stuff 같은 주석은 필요 없습니다)
  • 두려움 없이 리팩터링 컴파일러가 사용자보다 먼저 경고하기 때문에

논리 검사를 위한 맞춤법 검사기로 생각하세요. 전송하기 전에 빨간 물결선이 표시됩니다.

TypeScript 는 아니다

  • 런타임 안전망 — 타입은 컴파일 후 사라집니다. TypeScript는 런타임에서 잘못된 API 응답으로부터 여러분을 보호해 주지 못합니다.
  • 성능 향상 도구 — JavaScript로 컴파일됩니다. 속도도 동일하고 출력도 동일합니다.
  • 과도한 설계의 변명type NestedGenericFactoryBuilderStrategy, K>는 트릭이 아닙니다.
  • 그저 “타입이 있는 JavaScript” — 올바르게 사용하면 코드 설계 방식을 바꾸며, 단순히 주석을 다는 방식만 바꾸는 것이 아닙니다.

TypeScript를 배우는 데 필요한 모든 것

  • **Cheatsheets**는 구문 관련—타입, 인터페이스, 클래스, 제어 흐름을 위한 것입니다.
  • **The handbook**은 전체적인 흐름을 파악하기 위해 읽으세요.
  • **A solid grasp of tsconfig.json**을 먼저 확실히 이해하고 나서 작업하세요. “TypeScript가 작동하지 않는다”는 경우의 절반은 실제로 “strict: true가 무엇을 하는지 모른다”는 뜻입니다. 존재론적 위기를 피하세요.

Practice (muscle memory)

작은 프로젝트를 하나 선택하세요—CLI 도구, 유틸리티 라이브러리, API 클라이언트 등—그리고 엄격하게 타입을 지정하세요. strict: true 프로젝트 하나에서 얻는 것이 열 개의 튜토리얼보다 더 많습니다.

이 글의 나머지는? 바로 생각하는 부분—“컴파일되는 TypeScript”에서 “보호하는 TypeScript”로 어떻게 진화할지에 대한 내용입니다.

Source:

리팩터링 여정: any에서 타입 안전으로

0 % — 문제점

async function getUser(id: any): Promise {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

const user = await getUser(123);
console.log(user.emial); // typo? TypeScript: LGTM 👍

문제점: 타입 안전이 전혀 없음. any 입력, any 출력.

25 % — 기본 인터페이스

interface User {
  id: number;
  name: string;
  email: string;
}

async function getUser(id: number): Promise {
  const res = await fetch(`/api/users/${id}`);
  return res.json(); // ⚠️ lying to TypeScript
}

const user = await getUser(123);
console.log(user.emial); // ✅ Error: Property 'emial' does not exist

무엇을 하는가: 오타를 잡아준다. 자동 완성이 동작한다. 이제 IDE가 유용해졌다.

문제점: API가 User를 반환한다고 약속하지만, API는 거짓말을 할 수 있다. 런타임 검증이 없음.

50 % — 선택적 속성 및 null 처리

interface User {
  id: number;
  name: string;
  email?: string;               // might not exist
  avatar?: string | null;      // might be null
}

async function getUser(id: number): Promise {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) return null;
  return res.json();
}

const user = await getUser(123);
if (user) {
  console.log(user.email?.toUpperCase()); // safe chaining
}

무엇을 하는가: 현실을 모델링—값이 없거나 null일 수 있다. 엣지 케이스 처리를 강제한다.

문제점: 여전히 res.json()을 맹목적으로 신뢰한다. 응답이 실제로 User와 일치하는지 검증되지 않음.

75 % — API 상태를 위한 구분 유니온

interface User {
  id: number;
  name: string;
  email?: string;
}

type ApiResult =
  | { status: "loading" }
  | { status: "error"; message: string }
  | { status: "success"; data: T };

async function getUser(id: number): Promise> {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      return { status: "error", message: `HTTP ${res.status}` };
    }
    const data = await res.json();
    return { status: "success", data };
  } catch {
    return { status: "error", message: "Network failed" };
  }
}

const result = await getUser(123);

switch (result.status) {
  case "loading":
    showSpinner();
    break;
  case "error":
    showError(result.message); // TS knows `message` exists here
    break;
  case "success":
    showUser(result.data);     // TS knows `data` is User here
    break;
}

무엇을 하는가: 모든 가능한 상태를 모델링한다. TypeScript가 각 분기에서 타입을 좁혀준다. status"error"일 때 data에 접근할 수 없게 된다.

문제점: 여전히 런타임에서 dataUser와 일치한다는 보장이 없다. API가 변경되면 프로덕션에서 문제를 발견하게 된다.

100 % — 타입 가드로 런타임 검증

interface User {
  id: number;
  name: string;
  email?: string;
}

// Type guard: validates at runtime, narrows at compile time
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    "id" in obj &&
    typeof (obj as User).id === "number" &&
    "name" in obj &&
    typeof (obj as User).name === "string"
  );
}

type ApiResult =
  | { status: "error"; message: string }
  | { status: "success"; data: T };

async function getUser(id: number): Promise> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) {
    return { status: "error", message: `HTTP ${res.status}` };
  }
  const data = await res.json();
  if (isUser(data)) {
    return { status: "success", data };
  }
  return { status: "error", message: "Invalid payload" };
}

무엇을 하는가: 런타임에 페이로드가 User와 일치함을 보장한다. 타입 가드는 컴파일러에게 타입을 좁혀주어, 안전성과 자동 완성을 동시에 제공한다.

결과: “any”에서 완전히 타입이 지정되고 검증된 흐름으로 이동했다—코드는 개발 단계에서 빠르게 실패하고, 프로덕션에서도 안전을 보장한다.

Code Example

try {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) {
    return { status: "error", message: `HTTP ${res.status}` };
  }
  const json: unknown = await res.json(); // don't trust it

  if (!isUser(json)) {
    return { status: "error", message: "Invalid user data" };
  }

  return { status: "success", data: json }; // TS knows it's User
} catch {
  return { status: "error", message: "Network failed" };
}

What it does

  • API 응답을 unknown으로 취급 (정직하게)
  • 런타임에 타입 가드로 검증
  • 검증이 통과되면 TypeScript가 User로 좁혀줌
  • 사용자가 보게 되기 전에 API 계약 변경을 감지

Production‑ready. 복잡한 스키마의 경우 직접 작성한 가드 대신 Zod 또는 Valibot 같은 라이브러리를 사용하세요.

현재 상황

FrameworkConventional ChoiceWhy
Next.js / ReactZod, Valibot함수형 스타일, 트리‑쉐이킹 가능, 데코레이터 불필요
NestJSclass-validator + class-transformer데코레이터 기반, NestJS 파이프/DTO와 네이티브 통합

Interface vs. Type vs. Class: The Decision Tree

결정 트리

TL;DR

ConstructUse When
Interface객체 형태 설명, 공개 API, 확장
Type유니언, 인터섹션, 튜플, 매핑된 타입
Class런타임 인스턴스, 프라이빗 상태, instanceof 검사

Quick Reference

Question🚩 Red Flag✅ Benefit
“이것의 타입은 무엇인가요?”any everywhere명시적인 인터페이스
“이것이 null일 수 있나요?”Unchecked .property access옵셔널 체이닝 + null 검사
“이 함수는 무엇을 반환할 수 있나요?”Promise모든 상태에 대한 유니언 타입
“이 API 응답은 안전한가요?”Casting as User런타임 검증 + 타입 가드
“이 객체는 어떤 속성을 가지고 있나요?”console.log to find out자동완성이 알게 됨
“이 리팩터링이 문제를 일으키나요?”“Deploy and pray”병합 전 컴파일러 오류

전체 TypeScript를 사용하면 안 되는 경우

  • 🚫 모든 것을 과도하게 타입 지정 – 한 번만 사용하는 객체에 인터페이스를 만들지 마세요. 인라인 타입이 충분합니다:

    function greet(user: { name: string }) {}
  • 🚫 단순한 문제에 복잡한 제네릭 사용 – 타입 정의가 함수보다 길다면 재고하세요.

  • 🚫 제어할 수 없는 서드파티 API 응답에 타입 지정 – 인터페이스로 거짓말하기보다 런타임 검증을 사용하세요.

  • 🚫 100 % 타입 커버리지를 목표로 삼기 – 경계에서는 any 혹은 unknown이 괜찮습니다. 완벽함은 배포의 적입니다.

TypeScript 커서 규칙 및 AI 가이드

.cursorrules 파일을 만들거나 커서 설정에 규칙을 추가하세요:

# TypeScript Rules

- Never use `any` — use `unknown` and narrow with type guards
- Treat all external data as `unknown`, validate with Zod
- Use discriminated unions for state (loading/error/success)
- Prefer Result pattern over thrown exceptions
- `strict: true` always

예시 AI 프롬프트

Write a TypeScript function to fetch users from `/api/users`.

Requirements:
- Use Zod for response validation
- Return a discriminated union:
  { status: "error"; message: string } |
  { status: "success"; data: User[] }
- Handle network errors and validation failures separately
- Infer the User type from the Zod schema

핵심 요약

TypeScript는 여기저기 콜론과 꺾쇠 괄호를 뿌리는 것이 아니라, 불법적인 상태를 표현할 수 없게 만드는 것입니다.

시작하기:

  1. strict mode 활성화하기.
  2. API 응답을 정직하게 모델링하기.
  3. 상태에 union을 사용하기.
  4. 경계에서 검증하기.

타입을 작성하는 것을 멈추고, 타입으로 설계하기 시작하세요.

0 조회
Back to Blog

관련 글

더 보기 »