Chapter 12

실전 패턴

프로덕션 레벨 TypeScript 설계와 패턴

이 챕터는 고급 내용입니다. 처음 보시는 분은 가볍게 훑어보고, Chapter 1~11을 충분히 익힌 뒤 다시 돌아오세요!


Design Patterns Type Safety Best Practices Production

프로젝트 설정 Best Practices

엄격한 tsconfig.json으로 시작하세요


{
  "compilerOptions": {
    // 엄격 모드 - 반드시 활성화! 타입 실수를 사전에 방지
    "strict": true,
    "noUncheckedIndexedAccess": true,    // 배열/객체 접근 시 undefined 가능성 체크
    "noImplicitOverride": true,          // 부모 메서드 재정의 시 override 키워드 필수
    "exactOptionalPropertyTypes": true,  // 선택적 속성의 타입을 더 정확하게 체크

    // 모듈 설정
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "isolatedModules": true,

    // 출력 설정
    "outDir": "./dist",
    "declaration": true,
    "sourceMap": true,

    // 코드 품질
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,

    // 경로 별칭
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@models/*": ["src/models/*"],
      "@utils/*": ["src/utils/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
        
팁: "strict": true 하나로 strictNullChecks, noImplicitAny, strictFunctionTypes 등 7개 옵션이 활성화됩니다.

타입 안전한 API 클라이언트


// API 엔드포인트 스키마(구조)를 타입으로 미리 정의
// 각 URL별로 어떤 HTTP 메서드를 지원하고, 요청/응답 타입이 무엇인지 명시
interface ApiSchema {
  '/users': {
    GET: { response: User[]; query: { page?: number; limit?: number } };
    POST: { response: User; body: CreateUserDto };
  };
  '/users/:id': {
    GET: { response: User; params: { id: string } };
    PUT: { response: User; params: { id: string }; body: UpdateUserDto };
    DELETE: { response: void; params: { id: string } };
  };
  '/posts': {
    GET: { response: Post[]; query: { authorId?: number } };
    POST: { response: Post; body: CreatePostDto };
  };
}

// 엔드포인트에서 사용 가능한 HTTP 메서드 추출
type ApiMethod<Path extends keyof ApiSchema> = keyof ApiSchema[Path];

// 특정 엔드포인트+메서드의 응답 타입 자동 추출
type ApiResponse<
  Path extends keyof ApiSchema,
  Method extends ApiMethod<Path>
> = ApiSchema[Path][Method] extends { response: infer R } ? R : never;
        

장점

API 스키마를 한 곳에서 정의하면, 클라이언트 코드 전체에서 엔드포인트/메서드/요청/응답 타입이 자동으로 강제됩니다.

API 클라이언트 - 구현과 사용


class TypedApiClient {
  constructor(private baseUrl: string) {} // API 서버 주소 저장

  // GET 요청 - Path에 따라 query 타입과 응답 타입이 자동 결정됨
  async get<Path extends keyof ApiSchema>(
    path: Path,        // '/users' 같은 API 경로
    options?: ApiSchema[Path] extends { GET: { query: infer Q } }
      ? { query?: Q }  // 해당 경로의 GET 쿼리 파라미터 타입
      : never
  ): Promise<ApiResponse<Path, 'GET'>> {  // 해당 경로의 GET 응답 타입
    const url = new URL(`${this.baseUrl}${path as string}`);
    if (options && 'query' in options) {
      const query = options.query as Record<string, string>;
      Object.entries(query).forEach(([k, v]) =>
        url.searchParams.set(k, String(v))
      );
    }
    const res = await fetch(url.toString());
    return res.json();
  }

  async post<Path extends keyof ApiSchema>(
    path: Path,
    body: ApiSchema[Path] extends { POST: { body: infer B } } ? B : never
  ): Promise<ApiResponse<Path, 'POST'>> {
    const res = await fetch(`${this.baseUrl}${path as string}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    return res.json();
  }
}
        

const api = new TypedApiClient('https://api.example.com');

// 자동 완성과 타입 검증이 작동!
const users = await api.get('/users', { query: { page: 1 } });
//    ^? User[]

const newPost = await api.post('/posts', {
  title: '새 글',   // CreatePostDto 타입 강제
  content: '내용',
});
//    ^? Post
        

빌더 패턴 (Builder Pattern)

빌더 패턴이란: 복잡한 객체를 한 번에 만들지 않고, 단계별로 조립하여 생성하는 디자인 패턴입니다. 레고 블록을 하나씩 끼워 맞추듯, 메서드를 체이닝하여 객체를 완성합니다.


interface QueryConfig {
  table: string;
  fields: string[];
  conditions: string[];
  orderBy?: string;
  limit?: number;
}

class QueryBuilder {
  private config: Partial<QueryConfig> = {}; // 설정을 점진적으로 채움

  from(table: string): this {        // 1단계: 테이블 지정
    this.config.table = table;
    return this;                      // this를 반환하여 체이닝 가능
  }

  select(...fields: string[]): this { // 2단계: 조회할 필드 지정
    this.config.fields = fields;
    return this;
  }

  where(condition: string): this {    // 3단계: 조건 추가 (여러 번 호출 가능)
    if (!this.config.conditions) this.config.conditions = [];
    this.config.conditions.push(condition);
    return this;
  }

  orderBy(field: string): this {      // 4단계: 정렬 기준 (선택사항)
    this.config.orderBy = field;
    return this;
  }

  limit(count: number): this {        // 5단계: 결과 개수 제한 (선택사항)
    this.config.limit = count;
    return this;
  }

  build(): string {                   // 최종: 모은 설정으로 SQL 생성
    const { table, fields, conditions, orderBy, limit } = this.config;
    if (!table || !fields?.length) {
      throw new Error('table과 fields는 필수입니다.');
    }
    let sql = `SELECT ${fields.join(', ')} FROM ${table}`;
    if (conditions?.length) sql += ` WHERE ${conditions.join(' AND ')}`;
    if (orderBy) sql += ` ORDER BY ${orderBy}`;
    if (limit) sql += ` LIMIT ${limit}`;
    return sql;
  }
}
        

const query = new QueryBuilder()
  .from('users')
  .select('id', 'name', 'email')
  .where('age > 18')
  .where('active = true')
  .orderBy('name')
  .limit(10)
  .build();
// SELECT id, name, email FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10
        

상태 머신 (State Machine): Discriminated Union

상태 머신이란: 객체가 가질 수 있는 모든 상태와 상태 간 전이 규칙을 미리 정의하는 패턴입니다. 예를 들어 주문은 "대기 → 확정 → 배송 → 완료" 순서로만 진행되어야 합니다.


// 주문 상태를 discriminated union(구별된 유니온)으로 정의
// 각 상태마다 가지고 있는 데이터가 다릅니다
type OrderState =
  | { status: 'pending'; createdAt: Date }
  | { status: 'confirmed'; confirmedAt: Date; estimatedDelivery: Date }
  | { status: 'shipped'; trackingNumber: string; shippedAt: Date }
  | { status: 'delivered'; deliveredAt: Date; signature: string }
  | { status: 'cancelled'; cancelledAt: Date; reason: string };

// 상태 전이 함수 - 유효한 전이만 허용 (pending에서만 confirm 가능)
function confirmOrder(
  order: Extract<OrderState, { status: 'pending' }>
): Extract<OrderState, { status: 'confirmed' }> {
  return {
    status: 'confirmed',
    confirmedAt: new Date(),
    estimatedDelivery: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
  };
}

function cancelOrder(
  order: Extract<OrderState, { status: 'pending' | 'confirmed' }>,
  reason: string
): Extract<OrderState, { status: 'cancelled' }> {
  return {
    status: 'cancelled',
    cancelledAt: new Date(),
    reason,
  };
}
        

// 상태별 렌더링 - 완전한 패턴 매칭
function renderOrder(order: OrderState): string {
  switch (order.status) {
    case 'pending':    return `주문 대기 중 (${order.createdAt})`;
    case 'confirmed':  return `확정됨. 배송 예정: ${order.estimatedDelivery}`;
    case 'shipped':    return `배송 중: ${order.trackingNumber}`;
    case 'delivered':  return `배송 완료. 서명: ${order.signature}`;
    case 'cancelled':  return `취소됨: ${order.reason}`;
  } // exhaustive - 빠진 상태가 있으면 컴파일 에러!
}
        

타입 안전한 이벤트 이미터


// 이벤트 맵 정의 - 이벤트 이름과 전달할 데이터 타입을 연결
interface AppEvents {
  'user:login': { userId: string; timestamp: Date };
  'user:logout': { userId: string };
  'post:created': { postId: string; title: string };
  'error': { code: number; message: string };
}

// 제네릭 이벤트 이미터 - 이벤트 이름에 맞는 데이터 타입을 자동 강제
class TypedEventEmitter<Events extends Record<string, any>> {
  private listeners = new Map<string, Set<Function>>(); // 이벤트별 리스너 저장

  // 이벤트 구독 - 특정 이벤트 발생 시 실행할 함수 등록
  on<E extends keyof Events>(
    event: E,                              // 이벤트 이름
    listener: (data: Events[E]) => void    // 해당 이벤트의 데이터 타입에 맞는 콜백
  ): () => void {
    const key = event as string;
    if (!this.listeners.has(key)) {
      this.listeners.set(key, new Set());
    }
    this.listeners.get(key)!.add(listener);

    // 구독 해제 함수 반환 (호출하면 리스너 제거)
    return () => this.listeners.get(key)?.delete(listener);
  }

  // 이벤트 발행 - 등록된 모든 리스너에게 데이터 전달
  emit<E extends keyof Events>(event: E, data: Events[E]): void {
    const key = event as string;
    this.listeners.get(key)?.forEach(listener => listener(data));
  }
}
        

const emitter = new TypedEventEmitter<AppEvents>();

// 자동 완성: 이벤트 이름과 데이터 타입이 연결됨
emitter.on('user:login', (data) => {
  console.log(data.userId);    // OK: string
  console.log(data.timestamp); // OK: Date
  // console.log(data.name);   // 컴파일 에러!
});

emitter.emit('post:created', {
  postId: '123',
  title: '새 글',
  // content: '...'  // 컴파일 에러! AppEvents에 없는 필드
});
        

리포지토리 패턴 (Repository Pattern)

리포지토리 패턴이란: 데이터를 저장하고 조회하는 로직을 하나의 인터페이스로 추상화하는 패턴입니다. 실제 저장소가 DB인지, 파일인지, API인지 상관없이 동일한 방식으로 데이터에 접근할 수 있습니다.


// 기본 엔티티 (모든 데이터가 공통으로 가지는 필드)
interface BaseEntity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

// 제네릭 Repository 인터페이스 - 어떤 데이터 타입이든 CRUD 가능
interface Repository<T extends BaseEntity> {
  findById(id: string): Promise<T | null>;        // 하나 조회
  findAll(filter?: Partial<T>): Promise<T[]>;     // 목록 조회
  create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T>; // 생성
  update(id: string, data: Partial<Omit<T, 'id'>>): Promise<T>;       // 수정
  delete(id: string): Promise<boolean>;            // 삭제
  count(filter?: Partial<T>): Promise<number>;     // 개수 조회
}
        

// 구체적 엔티티
interface User extends BaseEntity {
  name: string;
  email: string;
  role: 'admin' | 'user';
}

// 구현
class UserRepository implements Repository<User> {
  async create(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>) {
    // data는 { name: string; email: string; role: 'admin' | 'user' } 타입
    const user: User = {
      id: crypto.randomUUID(),
      createdAt: new Date(),
      updatedAt: new Date(),
      ...data,
    };
    // DB에 저장하는 로직...
    return user;
  }
  // ... 나머지 메서드 구현
}
        

Result 타입 (Either 패턴)

예외 대신 타입으로 성공/실패를 표현하는 패턴


// Result 타입 정의 - 성공 또는 실패를 타입으로 표현
// try/catch 대신 반환값으로 에러를 전달하여, 에러 처리를 빼먹을 수 없게 함
type Result<T, E = Error> =
  | { ok: true; value: T }    // 성공: 값을 담고 있음
  | { ok: false; error: E };  // 실패: 에러 정보를 담고 있음

// 성공 결과 생성 함수
function Ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

// 실패 결과 생성 함수
function Err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}
        

// 에러 타입 정의
type ValidationError = { field: string; message: string };
type NotFoundError = { entity: string; id: string };
type UserError = ValidationError | NotFoundError;

function parseAge(input: string): Result<number, ValidationError> {
  const age = parseInt(input, 10);
  if (isNaN(age)) {
    return Err({ field: 'age', message: '숫자가 아닙니다' });
  }
  if (age < 0 || age > 150) {
    return Err({ field: 'age', message: '유효한 나이가 아닙니다' });
  }
  return Ok(age);
}

// 사용
const result = parseAge('25');
if (result.ok) {
  console.log(result.value);  // number - 안전하게 접근
} else {
  console.log(result.error.message); // string - 에러 정보
}
        

Result 타입 - 체이닝


// Result에 변환/체이닝 메서드 추가
class ResultHelper {
  // map: 성공 값을 변환 (실패면 그대로 통과)
  static map<T, U, E>(
    result: Result<T, E>,
    fn: (value: T) => U
  ): Result<U, E> {
    if (result.ok) return Ok(fn(result.value));
    return result;  // 실패면 에러를 그대로 전달
  }

  // flatMap: 성공 값으로 다음 Result를 생성 (체이닝)
  static flatMap<T, U, E>(
    result: Result<T, E>,
    fn: (value: T) => Result<U, E>
  ): Result<U, E> {
    if (result.ok) return fn(result.value);
    return result;  // 실패면 에러를 그대로 전달
  }

  // unwrapOr: 성공이면 값, 실패면 기본값 반환
  static unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
    return result.ok ? result.value : defaultValue;
  }
}
        

// 실전 사용: 검증 체이닝
function validateUser(input: unknown): Result<User, ValidationError> {
  const nameResult = validateName(input);
  const emailResult = ResultHelper.flatMap(
    nameResult,
    (name) => validateEmail(input).ok
      ? Ok({ ...input, name } as User)
      : validateEmail(input)
  );
  return emailResult;
}

// 에러가 throw되지 않음 - 컴파일러가 모든 경로를 강제
const userResult = validateUser(rawInput);
const userName = ResultHelper.unwrapOr(
  ResultHelper.map(userResult, u => u.name),
  '알 수 없는 사용자'
);
        

브랜디드 타입 (Branded Type) / Opaque 타입

같은 string이나 number지만 의미적으로 구분되어야 하는 값을 타입으로 보호합니다. 예: UserId와 PostId는 둘 다 string이지만 절대 혼용하면 안 됩니다.


// Brand 유틸리티 타입 - 원래 타입에 "브랜드 태그"를 추가
type Brand<T, B> = T & { readonly __brand: B };

// 도메인 타입 정의 - 같은 string/number지만 서로 다른 "브랜드"
type UserId = Brand<string, 'UserId'>;  // string이지만 UserId 전용
type PostId = Brand<string, 'PostId'>;  // string이지만 PostId 전용
type Email = Brand<string, 'Email'>;    // string이지만 Email 전용
type KRW = Brand<number, 'KRW'>;       // number이지만 원화 전용
type USD = Brand<number, 'USD'>;       // number이지만 달러 전용

// 생성 함수 (검증 포함) - 유효한 값만 브랜드 타입으로 변환
function createUserId(id: string): UserId {
  if (!id.startsWith('user_')) throw new Error('Invalid user ID');
  return id as UserId;
}

function createEmail(email: string): Email {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    throw new Error('Invalid email');
  }
  return email as Email;
}

function createKRW(amount: number): KRW {
  return Math.round(amount) as KRW;  // 원화는 정수
}
        

// 타입 시스템이 혼용을 방지!
function getUser(id: UserId): Promise<User> { /* ... */ }
function getPost(id: PostId): Promise<Post> { /* ... */ }

const userId = createUserId('user_abc');
const postId = 'post_123' as PostId;

getUser(userId);   // OK
getUser(postId);   // 컴파일 에러! PostId는 UserId가 아님
getUser('abc');    // 컴파일 에러! string은 UserId가 아님

// 통화 혼용 방지
function payInKRW(amount: KRW): void { /* ... */ }
const priceKRW = createKRW(50000);
const priceUSD = 35.99 as USD;
payInKRW(priceKRW);  // OK
payInKRW(priceUSD);  // 컴파일 에러! USD는 KRW가 아님
        

타입 안전한 상태 관리

Redux 스타일의 타입 안전한 액션/리듀서 패턴


// 액션 타입 정의 - "앱에서 발생할 수 있는 모든 행동" 목록
type Action =
  | { type: 'SET_USER'; payload: User }            // 사용자 설정
  | { type: 'SET_LOADING'; payload: boolean }       // 로딩 상태 변경
  | { type: 'ADD_TODO'; payload: { text: string } } // 할 일 추가
  | { type: 'TOGGLE_TODO'; payload: { id: number } } // 완료/미완료 토글
  | { type: 'REMOVE_TODO'; payload: { id: number } }; // 할 일 삭제

// 상태 타입 - 앱의 전체 상태를 하나의 객체로 관리
interface AppState {
  user: User | null;
  loading: boolean;
  todos: Array<{ id: number; text: string; done: boolean }>;
}

const initialState: AppState = {
  user: null,
  loading: false,
  todos: [],
};
        

// 리듀서 - 모든 액션의 payload 타입이 자동 추론
function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
      //                       ^? User (자동 추론)
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
      //                          ^? boolean
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload.text,  // ^? { text: string }
          done: false,
        }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.payload.id ? { ...t, done: !t.done } : t
        ),
      };
    case 'REMOVE_TODO':
      return {
        ...state,
        todos: state.todos.filter(t => t.id !== action.payload.id),
      };
  }
}
        

액션 크리에이터 헬퍼


// 타입 안전한 액션 크리에이터 생성기
// 액션 타입 이름을 넣으면, 해당 payload 타입에 맞는 생성 함수를 반환
function createAction<T extends Action['type']>(type: T) {
  return (
    payload: Extract<Action, { type: T }>['payload'] // 해당 타입의 payload만 허용
  ): Extract<Action, { type: T }> => {
    return { type, payload } as Extract<Action, { type: T }>;
  };
}

// 액션 크리에이터 정의
const setUser = createAction('SET_USER');
const setLoading = createAction('SET_LOADING');
const addTodo = createAction('ADD_TODO');
const toggleTodo = createAction('TOGGLE_TODO');
        

// 사용 - payload 타입이 자동으로 검증됨!
const action1 = setUser({ id: '1', name: '홍길동', email: 'a@b.com' });
const action2 = setLoading(true);
const action3 = addTodo({ text: 'TypeScript 공부' });
const action4 = toggleTodo({ id: 1 });

// 타입 에러!
setLoading('true');       // boolean이어야 함
addTodo({ title: '...' }); // text 필드가 필요
toggleTodo({ id: '1' });  // number이어야 함
        
실무: 이 패턴은 Zustand, Redux Toolkit의 createSlice에서 내부적으로 사용되는 원리와 동일합니다.

서드파티 라이브러리 활용

타입 있는 라이브러리


// 자체 타입 포함
import axios from 'axios';
// package.json에
// "types": "./index.d.ts"

// 별도 @types 필요
import express from 'express';
// npm i -D @types/express
            

타입 없는 라이브러리


// 방법 1: declare module
declare module 'old-lib' {
  export function doStuff(
    input: string
  ): number;
}

// 방법 2: 임시 any 처리
declare module 'untyped-lib';
// 모든 import가 any
            

// 방법 3: 래퍼를 만들어 타입 안전성 확보
// wrappers/old-lib.ts
import oldLib from 'old-lib'; // any

interface OldLibApi {
  process(data: string[]): Promise<ProcessResult>;
  configure(options: OldLibOptions): void;
}

export function createOldLib(): OldLibApi {
  return {
    process: (data) => oldLib.process(data),
    configure: (options) => oldLib.configure(options),
  };
}
// 이제 앱 코드에서는 타입이 보장되는 래퍼만 사용
        
원칙: any는 경계(boundary)에만 두고, 내부 코드는 항상 타입 안전하게 유지하세요.

JavaScript에서 마이그레이션

점진적으로 TypeScript를 도입하는 실전 전략

단계별 마이그레이션

단계 작업 설정
1단계 tsconfig 추가, allowJs 활성화 "allowJs": true, "strict": false
2단계 .js를 .ts로 리네이밍 (에러 무시) // @ts-ignore 또는 // @ts-expect-error
3단계 핵심 모듈부터 타입 추가 shared types, API 계층 우선
4단계 strict 모드 점진적 활성화 "strict": true, 옵션 하나씩

// 1단계 tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,              // JS 파일 허용
    "checkJs": false,             // JS 파일 타입 체크 안 함
    "strict": false,              // 느슨하게 시작
    "noImplicitAny": false,       // any 허용
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}
        
팁: // @ts-expect-error로 임시 무시한 곳은 TODO 주석과 함께 기록하세요. 나중에 검색하여 점진적으로 수정합니다.

성능 고려사항

컴파일 성능 최적화


// tsconfig.json - 빌드 속도 향상
{
  "compilerOptions": {
    "incremental": true,            // 증분 컴파일 (변경된 부분만 다시 컴파일)
    "tsBuildInfoFile": ".tsbuildinfo", // 빌드 캐시 파일 위치
    "skipLibCheck": true,           // .d.ts 체크 건너뛰기 (빌드 속도 향상)
    "isolatedModules": true         // 파일 단위 컴파일 가능 (번들러 호환)
  }
}
          

런타임 성능에 영향을 주는 패턴

패턴 비용 대안
enum (숫자/문자열) 런타임 코드 생성 as const 객체 또는 union 타입
namespace IIFE 래퍼 생성 ES Modules
과도한 class prototype 체인 오버헤드 인터페이스 + 함수
데코레이터 메타데이터 reflect-metadata 런타임 필요할 때만 사용
핵심: TypeScript의 타입은 컴파일 후 완전히 제거됩니다. interface, type, type assertion은 런타임 비용이 0입니다.

제로 비용 추상화


// 이 모든 타입 코드는 컴파일 후 완전히 사라집니다!
// 런타임 (Runtime, 프로그램 실행 시점) 비용: 0
interface Config {
  apiUrl: string;
  timeout: number;
  retries: number;
}

type ReadonlyConfig = Readonly<Config>;
type PartialConfig = Partial<Config>;
type ConfigKeys = keyof Config;

function processConfig<T extends Config>(config: T): T {
  return config; // 컴파일 후: function processConfig(config) { return config; }
}
        

// 반면, 이것들은 런타임 코드를 생성합니다
// enum -> 런타임 객체 생성
enum Direction { Up, Down, Left, Right }

// 대안: as const (런타임 비용 최소)
const Direction2 = {
  Up: 'UP',
  Down: 'DOWN',
  Left: 'LEFT',
  Right: 'RIGHT',
} as const;

type Direction2 = typeof Direction2[keyof typeof Direction2];
// 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
        

원칙

interfacetype을 최대한 활용하세요. 타입 시스템이 강력할수록, 런타임 검증 코드는 줄어듭니다.

TypeScript 완전 정복 - 전체 과정 요약

이 챕터의 패턴들이 어렵게 느껴졌다면 정상입니다! Chapter 1~8의 기초가 탄탄해지면 자연스럽게 이해됩니다.

기초 (Ch 1-4)

  • Ch 1 - 타입 시스템, 왜 TypeScript인가
  • Ch 2 - 기본 타입, 유니온, 인터섹션
  • Ch 3 - 함수 타입, 오버로딩
  • Ch 4 - 인터페이스, 클래스, 접근 제어

중급 (Ch 5-8)

  • Ch 5 - 제네릭, 제약 조건
  • Ch 6 - 타입 가드, 타입 좁히기
  • Ch 7 - 유틸리티 타입, 매핑 타입
  • Ch 8 - 조건부 타입, Template Literal

고급 (Ch 9-12)

  • Ch 9 - 모듈, 네임스페이스, .d.ts
  • Ch 10 - 데코레이터, 메타프로그래밍
  • Ch 11 - 비동기, Promise<T>, 에러 처리
  • Ch 12 - 실전 패턴, 프로덕션 설계

마무리 조언:
1. strict: true를 항상 사용하세요
2. any를 피하고, unknown을 사용하세요
3. 타입을 먼저 설계하고, 구현을 시작하세요
4. 컴파일러를 동료로 대하세요

← 전체 목차로 돌아가기