2026년 TypeScript 모범 사례: 더 안전하고 나은 코드 작성하기

2026년 TypeScript 모범 사례: 더 안전하고 나은 코드 작성하기

2026년 필수 TypeScript 모범 사례를 마스터하세요. 고급 타입 패턴, 에러 처리, 성능 최적화, 프로젝트 설정까지 모든 것을 다루는 종합 가이드입니다.

2026년 3월 14일7분 소요

서론: TypeScript가 표준이 된 이유

TypeScript는 전문 JavaScript 개발의 표준으로 확고히 자리잡았습니다. 2026년 기준 새로운 JavaScript 프로젝트의 80% 이상이 TypeScript로 시작됩니다. 컴파일 타임에 버그를 잡고, 타입을 통해 코드 문서화를 개선하며, 더 나은 IDE 지원을 가능하게 하고, 대규모 코드베이스를 유지보수 가능하게 만들기 때문입니다.

하지만 TypeScript를 작성하는 것과 좋은 TypeScript를 작성하는 것은 다릅니다. 이 가이드는 2026년에 더 깔끔하고, 안전하며, 유지보수 가능한 TypeScript 코드를 작성하는 데 도움이 되는 모범 사례를 다룹니다.

1부: 올바른 타입 시스템 기초

any 대신 unknown 사용하기

any 타입은 TypeScript의 탈출구입니다 -- 모든 타입 검사를 비활성화합니다.

// 나쁨: any는 타입 검사를 비활성화
function processData(data: any) {
  return data.name.toUpperCase(); // 에러 없음, 하지만 런타임에 충돌 가능
}

// 좋음: unknown은 타입 확인을 강제
function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    const record = data as { name: string };
    return record.name.toUpperCase(); // 안전!
  }
  throw new Error('잘못된 데이터 형식');
}

명백한 경우 타입 추론 활용하기

// 불필요: TypeScript가 이미 추론함
const name: string = "John";
const count: number = 42;

// 더 나음: TypeScript가 추론하도록
const name = "John";      // string으로 추론
const count = 42;          // number로 추론

// 명시하세요: 함수 매개변수와 반환 타입
function calculateTotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

// 추론이 너무 넓을 때 명시하세요
const status: "active" | "inactive" = "active"; // 단순 string이 아님

const 단언(assertion) 사용하기

// const 단언 없이: 타입은 { name: string, role: string }
const config = { name: "admin", role: "superuser" };

// const 단언 사용: 타입은 { readonly name: "admin", readonly role: "superuser" }
const config = { name: "admin", role: "superuser" } as const;

// enum 대체로 완벽
const HttpStatus = {
  OK: 200,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
} as const;

type StatusCode = typeof HttpStatus[keyof typeof HttpStatus]; // 200 | 404 | 500

선택적 속성 대신 판별 유니온 사용하기

// 나쁨: 모든 것이 선택적, 유효한 상태를 알기 어려움
interface ApiResponse {
  status: string;
  data?: unknown;
  error?: string;
}

// 좋음: 각 상태가 명확하게 정의됨
type ApiResponse =
  | { status: "success"; data: unknown }
  | { status: "error"; error: string; retryAfter?: number }
  | { status: "loading" };

function handleResponse(response: ApiResponse) {
  switch (response.status) {
    case "success":
      console.log(response.data); // TypeScript가 data 존재를 알음
      break;
    case "error":
      console.error(response.error); // TypeScript가 error 존재를 알음
      break;
    case "loading":
      console.log("로딩 중...");
      break;
  }
}

satisfies로 타입 검증하기

type Route = {
  path: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
};

// satisfies 사용: 검증하면서 리터럴 타입도 보존
const route = {
  path: "/api/users",
  method: "GET",
} satisfies Route;
// route.path는 "/api/users" (리터럴 타입 보존!)

2부: 고급 타입 패턴

템플릿 리터럴 타입

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// 결과: "onClick" | "onFocus" | "onBlur"

type CSSUnit = "px" | "rem" | "em" | "%";
type CSSValue = `${number}${CSSUnit}`;

function setWidth(value: CSSValue) { /* ... */ }
setWidth("100px");  // OK
setWidth("2rem");   // OK
setWidth("100");    // 에러: 유효한 CSSValue가 아님

알아야 할 유틸리티 타입

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Partial: 모든 속성을 선택적으로
type UpdateUser = Partial<User>;

// Pick: 특정 속성만 선택
type UserPublic = Pick<User, "id" | "name" | "email">;

// Omit: 특정 속성 제거
type UserWithoutPassword = Omit<User, "password">;

// Record: 특정 키를 가진 객체 타입
type UserRoles = Record<string, "admin" | "user" | "moderator">;

// ReturnType: 함수의 반환 타입 가져오기
function createUser() { return { id: 1, name: "John" }; }
type NewUser = ReturnType<typeof createUser>;

// Awaited: Promise 타입 언래핑
type UserData = Awaited<Promise<User>>; // User

브랜디드 타입으로 타입 안전성 확보

같은 기본 타입의 값을 혼동하는 것을 방지:

// 브랜디드 타입 없이: ID를 쉽게 혼동
function getUser(userId: string) { /* ... */ }
function getOrder(orderId: string) { /* ... */ }
getUser(orderId); // 에러 없음! 하지만 버그.

// 브랜디드 타입 사용: 컴파일러가 실수를 잡아냄
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };

function createUserId(id: string): UserId { return id as UserId; }
function createOrderId(id: string): OrderId { return id as OrderId; }

function getUser(userId: UserId) { /* ... */ }
function getOrder(orderId: OrderId) { /* ... */ }

const userId = createUserId("user_123");
const orderId = createOrderId("order_456");
getUser(orderId); // 에러: 'OrderId'는 'UserId'에 할당할 수 없음
getUser(userId);  // OK!

3부: 에러 처리 모범 사례

예외 대신 Result 타입 사용

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function parseJSON(text: string): Result<unknown, string> {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (e) {
    return { ok: false, error: `잘못된 JSON: ${(e as Error).message}` };
  }
}

// 사용: 호출자가 반드시 두 경우를 처리
const result = parseJSON(userInput);
if (result.ok) {
  console.log(result.value);
} else {
  console.error(result.error);
}

JSON 파싱을 테스트하려면 JSON 포맷터를 사용하세요.

완전한 switch 문

never 타입으로 모든 케이스를 처리했는지 보장:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      // 새 도형을 추가하고 처리를 잊으면 컴파일 에러 발생!
      const _exhaustive: never = shape;
      throw new Error(`처리되지 않은 도형: ${_exhaustive}`);
  }
}

4부: 프로젝트 설정

2026년 권장 tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "resolveJsonModule": true,
    "target": "ES2022",
    "declaration": true,
    "sourceMap": true,
    "outDir": "./dist",
    "esModuleInterop": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

주요 컴파일러 옵션 설명

  • strict: true -- 모든 엄격한 타입 검사 활성화. 필수.
  • noUncheckedIndexedAccess -- 배열 접근이 T | undefined 반환
  • exactOptionalPropertyTypes -- undefined와 "없음"을 구분
  • verbatimModuleSyntax -- 일관된 import/export 구문 강제

5부: 모던 TypeScript 패턴

타입 안전한 API 클라이언트

interface ApiSchema {
  "/users": {
    GET: { response: User[] };
    POST: { body: CreateUserInput; response: User };
  };
  "/users/:id": {
    GET: { response: User };
    PUT: { body: UpdateUserInput; response: User };
    DELETE: { response: void };
  };
}

// 완전한 타입 안전성!
const users = await api("/users", "GET");        // User[]
const user = await api("/users/:id", "GET");     // User

Zod로 런타임 검증

import { z } from "zod";

const UserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  role: z.enum(["admin", "user", "moderator"]),
});

// TypeScript 타입 자동 추론
type User = z.infer<typeof UserSchema>;

// 런타임에 검증
function createUser(input: unknown): User {
  const result = UserSchema.safeParse(input);
  if (!result.success) {
    throw new ValidationError(result.error.message);
  }
  return result.data; // 완전한 타입!
}

6부: 성능 팁

불필요한 타입 단언 피하기

// 나쁨: 타입 단언이 잠재적 문제를 숨김
const data = fetchData() as UserData;

// 좋음: 타입 가드로 데이터 검증
function isUserData(data: unknown): data is UserData {
  return (
    typeof data === "object" &&
    data !== null &&
    "id" in data &&
    "name" in data
  );
}

const data = fetchData();
if (isUserData(data)) {
  // data가 안전하게 UserData로 타입됨
}

효과적인 타입 좁히기

function processValue(value: string | number | null) {
  if (value === null) return "값 없음";

  if (typeof value === "string") {
    return value.toUpperCase(); // string 메서드 사용 가능
  }

  return value.toFixed(2); // number 메서드 사용 가능
}

결론

TypeScript는 단순한 "타입이 있는 JavaScript" 이상입니다. 최대한 활용하면 신뢰할 수 있고 유지보수 가능한 소프트웨어를 구축하는 강력한 도구입니다:

  1. 타입 시스템을 완전히 활용 -- any 대신 unknown, 판별 유니온, 브랜디드 타입
  2. 추론에 맡기기 -- 필요할 때만 타입 명시
  3. 에러를 명시적으로 -- Result 타입, 완전한 검사, 커스텀 에러 클래스
  4. 엄격하게 설정 -- strict: true가 기본
  5. 경계에서 검증 -- 런타임 검증에 Zod 등 사용
  6. 타입도 테스트 -- 타입도 코드베이스의 일부

관련 리소스

관련 글