- Authors

- Name
- Youngju Kim
- @fjvbn20031
들어가며 — 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 |
| Serverless | Node (Lambda 지원) |
9부 — Monorepo와 빌드 도구
9.1 Turborepo vs Nx
| 항목 | Turborepo | Nx |
|---|---|---|
| 학습 곡선 | 낮음 | 높음 |
| 기능 | 간결 | 풍부 (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
- structural typing vs nominal typing 차이를 안다
any,unknown,never차이를 안다- Type vs Interface 선택 기준을 안다
- Union 좁히기 (Narrowing) 방법 5가지 이상을 안다
- Conditional Type +
infer예시를 쓸 수 있다 - Mapped Type으로 자체 유틸리티 만들 수 있다
satisfiesvs type annotation 차이를 안다- Branded Type으로 ID 구분 방법을 안다
- Zod 스키마 → 타입 추론 흐름을 안다
- tRPC 또는 GraphQL Codegen으로 타입 안전 API 만들 수 있다
- Turborepo 또는 Nx로 모노레포를 구성할 수 있다
- TS 5.x의 주요 기능 5개를 말할 수 있다
13부 — TS 안티패턴 10
any남발: 타입 안전성 포기.unknown+ narrowing- Enum 사용: 번들 크기 증가, tree-shake 안 됨. Union +
as const as Type강제 캐스팅: 타입 거짓말.satisfies또는 Guard 사용@ts-ignore로 해결:@ts-expect-error라도 써서 언젠간 지우게- 타입 대신 주석으로 의도 표현: 타입으로 표현 가능한 건 타입으로
- Generics 과용: Container/Factory 외엔 대부분 불필요
Function타입 사용: 구체적 시그니처 명시Object타입:Record<string, unknown>또는 구체 타입- 거대한 interface: 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 시대의 주역까지, 다음 글에서 이어진다.