들어가며 — 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 기본 사용
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 서버
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 클라이언트
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배 빠름, 런타임 3~5배
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 예제
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 시대의 주역까지, 다음 글에서 이어진다.
현재 단락 (1/314)
2024~2025년, TypeScript는 단순히 "많이 쓰이는 언어"를 넘어 **사실상의 표준**이 되었다: