Skip to content
Published on

TypeScript 완전 가이드 — 타입 레벨 프로그래밍·Generics·Zod·tRPC·TanStack (Season 2 Ep 4, 2025)

Authors

들어가며 — TypeScript가 정복한 것들

2024~2025년, TypeScript는 단순히 "많이 쓰이는 언어"를 넘어 사실상의 표준이 되었다:

  • Stack Overflow Survey 2024: 웹 개발 3대 언어 (JS, HTML, TS)
  • State of JS 2024: 78%의 프로젝트가 TypeScript 사용
  • npm 다운로드: TS 관련 패키지가 상위 100위 중 60% 차지
  • Next.js·Remix·Astro·Nuxt·Svelte: 모두 TS 퍼스트
  • Node.js 22+: --experimental-strip-types 플래그로 TS 직접 실행
  • Deno·Bun: TS를 변환 없이 네이티브 실행

TypeScript 팀의 책임자 Anders Hejlsberg는 Turbo Pascal → Delphi → C# → TypeScript를 만든 인물. 그 궤적을 이해하면 TS의 설계 철학이 보인다.

이 글은 TS를 타입 시스템으로서 심도 있게 다룬다.


1부 — TypeScript의 설계 철학

1.1 Gradual Typing

"JavaScript를 점진적으로 강하게 타입 지정할 수 있게".

any를 허용하고, 점진 마이그레이션을 가능하게. 이것이 Flow·Elm이 망한 이유이자 TS가 이긴 이유.

1.2 Structural Typing

interface Duck { quack: () => void }
const obj = { quack: () => console.log('quack') };
const d: Duck = obj; // OK — 구조적 호환

"오리처럼 울면 오리다" — Name-based (Java) 아닌 구조 기반.

1.3 Type Erasure

// 컴파일 전
function greet<T extends string>(name: T): T { return name; }

// 컴파일 후 (JS)
function greet(name) { return name; }

타입은 런타임에 존재하지 않는다. 런타임 검증이 필요하면 Zod 같은 별도 도구.


2부 — TypeScript 5.x 핵심 기능 10가지

2.1 const Type Parameters (5.0)

type Hello = <const T>(x: T) => T;
const result = Hello(['a', 'b']); // readonly ['a', 'b']

2.2 Decorators (5.0 표준)

function logged(target: any, ctx: ClassMethodDecoratorContext) {
  return function (this: any, ...args: any[]) {
    console.log(`calling ${String(ctx.name)}`);
    return target.apply(this, args);
  };
}

class Foo {
  @logged
  bar() {}
}

2.3 using 선언 (5.2) — Explicit Resource Management

{
  using file = await fs.open('log');
  // 스코프 벗어나면 자동으로 [Symbol.dispose]() 호출
}

2.4 satisfies 연산자 (4.9)

const config = {
  port: 3000,
  host: 'localhost',
} satisfies Record<string, string | number>;

// config.port는 여전히 number로 좁혀짐
// satisfies는 검증만, 타입 넓히지 않음

2.5 Variadic Tuple Types (4.0)

type Concat<T extends any[], U extends any[]> = [...T, ...U];
type Result = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]

2.6 Template Literal Types (4.1)

type Event = `on${Capitalize<'click' | 'hover'>}`; // 'onClick' | 'onHover'
type Route = `/users/${string}`;

2.7 infer 추론

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

2.8 Discriminated Union

type Result =
  | { ok: true; value: string }
  | { ok: false; error: Error };

function handle(r: Result) {
  if (r.ok) { r.value } // 타입 좁혀짐
  else { r.error }
}

2.9 as const + enum 대체

const Color = { Red: 'red', Blue: 'blue' } as const;
type Color = typeof Color[keyof typeof Color]; // 'red' | 'blue'

Enum은 쓰지 말자 (2025 커뮤니티 합의). Union + as const가 우수.

2.10 NoInfer (5.4)

function createStreetLight<C extends string>(
  colors: C[],
  default_?: NoInfer<C>,
): void {}

createStreetLight(['red', 'yellow'], 'red'); // OK
createStreetLight(['red', 'yellow'], 'blue'); // Error — NoInfer로 막힘

3부 — Generics 실전 패턴 10

3.1 기본

function identity<T>(x: T): T { return x; }
const a = identity(42);       // a: number
const b = identity('hello');  // b: string

3.2 Constraint

function getLength<T extends { length: number }>(x: T): number {
  return x.length;
}

3.3 Default Type

type Ref<T = string> = { current: T };
const r: Ref = { current: 'hi' };

3.4 Keyof + Lookup

function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const name = pluck({ name: 'Alice', age: 30 }, 'name'); // name: string

3.5 Mapped Types

type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

3.6 Conditional Type

type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>;       // false

3.7 Distributive Conditional

type ToArray<T> = T extends any ? T[] : never;
type X = ToArray<string | number>; // string[] | number[]

3.8 infer로 추출

type First<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

3.9 Recursive Type

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

3.10 Branded Type (타입 레벨 다른 ID 구분)

type Brand<T, B> = T & { __brand: B };
type UserID = Brand<string, 'UserID'>;
type OrderID = Brand<string, 'OrderID'>;

function fetchUser(id: UserID) {}
const uid = 'u_123' as UserID;
fetchUser(uid); // OK
// fetchUser('raw string'); // Error

4부 — 타입 레벨 프로그래밍 실전

4.1 String 조작

type CamelCase<S extends string> =
  S extends `${infer Head}_${infer Tail}`
    ? `${Head}${Capitalize<CamelCase<Tail>>}`
    : S;

type A = CamelCase<'user_name_age'>; // 'userNameAge'

4.2 Route 파라미터 추출

type ExtractParams<S extends string> =
  S extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : S extends `${string}:${infer Param}`
    ? Param
    : never;

type R = ExtractParams<'/users/:id/posts/:postId'>;
// 'id' | 'postId'

4.3 SQL 타입 추론 (간이)

type ParseSelect<S extends string> =
  S extends `SELECT ${infer Cols} FROM ${string}`
    ? Cols extends `${infer First}, ${infer Rest}`
      ? First | ParseSelect<`SELECT ${Rest} FROM X`>
      : Cols
    : never;

타입 레벨 프로그래밍이 유용한 순간:

  • API 응답 스키마 자동 추론
  • URL/라우트 타입 안전성
  • SQL·GraphQL 쿼리 검증

주의: 재귀 깊이 제한(~50), 컴파일 시간 증가.


5부 — Zod: 런타임 검증 + 타입 추론

5.1 왜 Zod인가

TS는 런타임에 타입이 없다. API 응답, 폼 입력, LocalStorage 등 신뢰할 수 없는 데이터는 런타임에 검증해야 함.

5.2 기본 사용

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().positive(),
  role: z.enum(['admin', 'user']),
});

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

// 검증
const user = UserSchema.parse(untrustedData);
// 실패 시 ZodError throw, 또는 .safeParse()로 result 반환

5.3 복잡한 스키마

const PostSchema = z.object({
  title: z.string().min(1).max(100),
  tags: z.array(z.string()).min(1).max(5),
  publishedAt: z.coerce.date(), // string -> Date 자동 변환
  author: UserSchema,           // 중첩
}).strict(); // 추가 필드 금지

5.4 2025 대안들

  • Valibot: Zod보다 ~90% 작은 번들 (tree-shakable)
  • ArkType: TS 문법 그대로 런타임 스키마
  • TypeBox: JSON Schema 호환

추천:

  • 서버·복잡한 로직: Zod (생태계 압도적)
  • 번들 크기 민감: Valibot
  • OpenAPI 통합: TypeBox

6부 — tRPC: 타입 안전한 API

6.1 기본 철학

"서버에서 정의한 API 타입이 클라이언트에 자동으로 전파된다."

6.2 서버

import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

export const appRouter = t.router({
  userById: t.procedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return await db.user.findUnique({ where: { id: input.id } });
    }),

  createUser: t.procedure
    .input(z.object({ email: z.string().email() }))
    .mutation(async ({ input }) => {
      return await db.user.create({ data: input });
    }),
});

export type AppRouter = typeof appRouter;

6.3 클라이언트

import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';

export const trpc = createTRPCReact<AppRouter>();

// 컴포넌트
const { data, isLoading } = trpc.userById.useQuery({ id: 'u_1' });
// data 타입이 서버 return 타입으로 자동 추론됨

장점: 스키마 언어 필요 없음. 코드젠 없음. 그냥 TS. 한계: 모노레포·Node.js 환경 전제. 다른 언어 클라이언트 ❌.

다른 언어 필요하면 → gRPC / GraphQL / OpenAPI.


7부 — TanStack: 현대 React의 표준 스택

7.1 TanStack의 구성

라이브러리역할
TanStack Query (React Query)서버 상태 관리·캐시
TanStack Router타입 안전 라우팅
TanStack Table헤드리스 테이블
TanStack Form폼 상태 관리
TanStack Start풀스택 메타 프레임워크 (2024~)

7.2 TanStack Query

const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  staleTime: 5 * 60 * 1000,
  refetchOnWindowFocus: true,
});

핵심 개념:

  • staleTime: 캐시가 신선한 시간 (이 시간 내엔 네트워크 호출 ❌)
  • cacheTime/gcTime: 사용자가 컴포넌트 버린 후 캐시 유지 시간
  • Mutation: POST/PUT/DELETE + Optimistic Update
  • Query Invalidation: 변경 후 관련 쿼리 재검증

7.3 TanStack Router

const rootRoute = createRootRoute();
const usersRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/users/$userId',
  component: UserComponent,
  loader: ({ params }) => fetchUser(params.userId),
});

// URL → 파라미터 → 컴포넌트 props까지 타입 안전

Next.js App Router와의 차이: 완전 타입 안전, 하지만 SSR·빌드 인프라 부족.

7.4 TanStack Start (2024~)

Remix·Next.js와 경쟁하는 풀스택 프레임워크. Vite 기반. 아직 beta지만 급성장 중.


8부 — 2024~2025 런타임 혁명: Bun·Deno·Node

8.1 Node.js 22+ (2024~)

  • --experimental-strip-types: .ts 직접 실행
  • 내장 Test Runner (node --test)
  • 내장 Watch Mode (node --watch)
  • require(esm) 지원

8.2 Bun (1.1+ Production Ready)

  • TS/JSX 네이티브 실행
  • 번들러·테스트러너·패키지 매니저 내장
  • Node.js 호환 99%
  • 속도: npm install 25배 빠름, 런타임 35배
bun install
bun run dev
bun test
bun build ./src/index.ts

8.3 Deno (2.0+)

  • 보안 퍼스트 (--allow-net, --allow-read)
  • TS 네이티브, 표준 라이브러리 풍부
  • deno.json 단일 설정
  • Node.js 호환성 급성장
deno run --allow-net main.ts
deno task dev

8.4 선택 기준

상황추천
기존 Node.js 프로젝트Node 22+
새 풀스택Bun (속도)
보안·표준 중시Deno
ServerlessNode (Lambda 지원)

9부 — Monorepo와 빌드 도구

9.1 Turborepo vs Nx

항목TurborepoNx
학습 곡선낮음높음
기능간결풍부 (generator 등)
Vercel 통합최상부분
추천소~중형대형·복잡

9.2 Turborepo 기본

// turbo.json
{
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": [".next/**"] },
    "test": { "dependsOn": ["build"] },
    "dev": { "cache": false, "persistent": true }
  }
}

turbo build가 워크스페이스 전체를 의존성 그래프대로 병렬 빌드 + 리모트 캐시.

9.3 Biome (2024~, ESLint/Prettier 대체)

  • 10~100배 빠름 (Rust 작성)
  • Lint + Format 통합
  • 단일 설정 파일 (biome.json)
  • 아직 ESLint 룰셋 전체 커버는 아님 (80%+)
bunx @biomejs/biome init
bunx @biomejs/biome check --write ./src

2025 권장 조합:

  • 새 프로젝트 → Biome
  • 복잡한 린트 룰 필요 → ESLint Flat Config
  • 대형 모노레포 → Biome + Turborepo 리모트 캐시

10부 — AI 시대의 TypeScript

10.1 AI와 TS가 찰떡인 이유

  • 타입이 AI의 컨텍스트: Copilot·Cursor는 타입을 읽고 더 정확한 코드 생성
  • Zod 스키마 = AI 입출력 명세: Function calling에 이상적
  • tRPC 라우터 = AI 엔드포인트: 타입 안전 AI API

10.2 Vercel AI SDK 예제

import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const result = await streamText({
  model: openai('gpt-4o'),
  messages,
  tools: {
    weather: {
      description: 'Get weather',
      parameters: z.object({ city: z.string() }),
      execute: async ({ city }) => fetchWeather(city),
    },
  },
});

for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}

10.3 Mastra·LangChain.js (2025)

  • Mastra: TS 퍼스트 에이전트 프레임워크 (Zod 네이티브)
  • LangChain.js: 성숙·범용, 하지만 무거움
  • Vercel AI SDK: UI 스트리밍까지 통합

11부 — TypeScript 마스터 로드맵 6개월

Month 1: 기본 + strict

  • strict mode 기본값 이해
  • Narrowing, Type Guard
  • Utility Types (Pick, Omit, Partial, Record 등)

Month 2: Generics

  • Constraint, Default, Keyof
  • Conditional Type, infer
  • Distributive Conditional

Month 3: 고급 타입

  • Template Literal
  • Recursive Type
  • Branded Type, Phantom Type

Month 4: 런타임 검증

  • Zod 마스터
  • API 레이어 타입 안전
  • tRPC로 풀스택 만들기

Month 5: 실전 패턴

  • Domain-Driven Design
  • Result Monad 패턴
  • 에러 타입 설계

Month 6: 타입 레벨 프로그래밍

  • TypeScript 타입 체조 (typeChallenge)
  • 라이브러리 타입 정의 기여
  • Turborepo 모노레포 구축

12부 — TS 체크리스트 12

  1. structural typing vs nominal typing 차이를 안다
  2. any, unknown, never 차이를 안다
  3. Type vs Interface 선택 기준을 안다
  4. Union 좁히기 (Narrowing) 방법 5가지 이상을 안다
  5. Conditional Type + infer 예시를 쓸 수 있다
  6. Mapped Type으로 자체 유틸리티 만들 수 있다
  7. satisfies vs type annotation 차이를 안다
  8. Branded Type으로 ID 구분 방법을 안다
  9. Zod 스키마 → 타입 추론 흐름을 안다
  10. tRPC 또는 GraphQL Codegen으로 타입 안전 API 만들 수 있다
  11. Turborepo 또는 Nx로 모노레포를 구성할 수 있다
  12. TS 5.x의 주요 기능 5개를 말할 수 있다

13부 — TS 안티패턴 10

  1. any 남발: 타입 안전성 포기. unknown + narrowing
  2. Enum 사용: 번들 크기 증가, tree-shake 안 됨. Union + as const
  3. as Type 강제 캐스팅: 타입 거짓말. satisfies 또는 Guard 사용
  4. @ts-ignore로 해결: @ts-expect-error라도 써서 언젠간 지우게
  5. 타입 대신 주석으로 의도 표현: 타입으로 표현 가능한 건 타입으로
  6. Generics 과용: Container/Factory 외엔 대부분 불필요
  7. Function 타입 사용: 구체적 시그니처 명시
  8. Object 타입: Record<string, unknown> 또는 구체 타입
  9. 거대한 interface: 10개 이상은 분할 신호
  10. TS 설정 strict: false: strict: true 필수. 예외는 마이그레이션 중일 때만

마치며 — TypeScript는 "천천히 강해지는" 언어

TypeScript의 가장 큰 매력은 쓰면 쓸수록 강해진다는 점이다.

처음엔 그냥 JS에 타입 붙이는 도구. 6개월 뒤엔 Generics와 Conditional Type으로 설계. 1년 뒤엔 타입으로 프로그램 자체를 표현. 2년 뒤엔 타입 레벨 프로그래밍으로 불가능해 보이던 것을 가능하게.

그리고 Zod·tRPC·TanStack·Vercel AI SDK가 모두 TypeScript 퍼스트로 설계되어 있어서, TS를 잘할수록 생태계 전체가 강력해진다.

2025년 웹 엔지니어에게 TS는 선택이 아니라 호흡이다.


다음 글 예고 — "Python 완전 가이드: FastAPI·AsyncIO·Pydantic·uv·Polars·AI 엔지니어링 시대"

Season 2 Ep 5는 AI 엔지니어링의 공용어, Python. 다음 글은:

  • Python 3.13의 변화 (free-threading, JIT)
  • FastAPI + Pydantic v2로 타입 안전 API
  • AsyncIO 실전 (TaskGroup, asyncio.timeout)
  • uv: pip/poetry/pyenv 대체의 진짜 등장
  • Polars vs Pandas 2025
  • LangChain·LangGraph·Pydantic AI
  • AI 엔지니어링 스택 총정리

스크립트 언어에서 AI 시대의 주역까지, 다음 글에서 이어진다.