TypeScript 실무 패턴 2026 — 바로 쓰는 고급 패턴 완전 가이드
실무에서 즉시 활용할 수 있는 TypeScript 고급 패턴을 코드 예제와 함께 완전 정리했습니다. Discriminated Union, 브랜드 타입, Zod 검증, 제네릭 패턴, 유틸리티 타입 활용까지 포함합니다.
TypeScript의 힘: 런타임 에러를 컴파일 타임에 잡는다
TypeScript는 JavaScript에 정적 타입을 추가해 버그를 배포 전에 잡고, 대규모 코드베이스의 유지보수성을 높입니다. 기본 사용법을 넘어 실무에서 차이를 만드는 고급 패턴을 정리했습니다.
1. Discriminated Union — API 응답 처리의 핵심
태그(판별자)를 이용해 타입을 좁히는 패턴입니다. API 응답, 상태 관리, 이벤트 시스템에서 가장 유용합니다.
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: string; statusCode: number }
| { status: "loading" };
function handleResponse<T>(res: ApiResponse<T>): T | null {
switch (res.status) {
case "success":
return res.data; // T 타입 자동 추론
case "error":
console.error(`${res.statusCode}: ${res.error}`);
return null;
case "loading":
return null;
}
}
React 상태 관리에 적용
type FetchState<T> =
| { type: "idle" }
| { type: "loading" }
| { type: "success"; data: T }
| { type: "error"; message: string };
function UserProfile({ userId }: { userId: string }) {
const [state, setState] = useState<FetchState<User>>({ type: "idle" });
// 렌더링 시 각 상태를 완전히 처리
if (state.type === "loading") return <Spinner />;
if (state.type === "error") return <Error message={state.message} />;
if (state.type === "success") return <Profile user={state.data} />;
return null;
}
2. 브랜드 타입 — 의미 있는 원시 타입
같은 string이나 number라도 서로 다른 의미로 구분합니다.
// 브랜드 타입 정의
type UserId = string & { __brand: "UserId" };
type PostId = string & { __brand: "PostId" };
type Amount = number & { __brand: "Amount" }; // 원화 금액
type Percent = number & { __brand: "Percent" }; // 0~100
// 생성 함수 (유효성 검사 포함)
function createAmount(value: number): Amount {
if (value < 0) throw new Error("금액은 0 이상이어야 합니다.");
return value as Amount;
}
function createPercent(value: number): Percent {
if (value < 0 || value > 100) throw new Error("퍼센트는 0~100이어야 합니다.");
return value as Percent;
}
// 사용 — 타입 혼용 방지
function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }
const userId = "abc" as UserId;
const postId = "xyz" as PostId;
getUser(userId); // OK
getUser(postId); // 컴파일 에러 — PostId를 UserId로 넘길 수 없음
3. satisfies 연산자 — 타입 체크 + 추론 유지
// 문제: 명시적 타입 주석은 리터럴 추론을 잃음
const routes: Record<string, string> = {
home: "/",
about: "/about",
};
routes.home; // string 타입 (리터럴 "/" 아님)
// 해결: satisfies로 체크하면서 리터럴 유지
const routes = {
home: "/",
about: "/about",
blog: "/blog",
} satisfies Record<string, string>;
routes.home; // "/" 리터럴 타입 유지
// routes.invalid // 컴파일 에러 (Record 타입 준수 검증)
설정 객체에 활용
type LogLevel = "debug" | "info" | "warn" | "error";
const config = {
logLevel: "info",
maxRetries: 3,
timeout: 5000,
} satisfies {
logLevel: LogLevel;
maxRetries: number;
timeout: number;
};
// config.logLevel은 "info" 리터럴 (LogLevel 아님)
// 하지만 "invalid" 를 넣으면 컴파일 에러
4. Template Literal Types — 문자열 타입 안전성
// CSS 단위
type CSSUnit = `${number}${"px" | "rem" | "em" | "%"}`;
const padding: CSSUnit = "16px"; // OK
const margin: CSSUnit = "1.5rem"; // OK
// const invalid: CSSUnit = "16vw"; // 에러
// 이벤트 이름 패턴
type EventName<T extends string> = `on${Capitalize<T>}`;
type ButtonEvents = EventName<"click" | "hover" | "focus">;
// "onClick" | "onHover" | "onFocus"
// API 엔드포인트 타입
type ApiEndpoint = `/api/${string}`;
function callApi(endpoint: ApiEndpoint) { /* ... */ }
callApi("/api/users"); // OK
// callApi("/users"); // 에러
5. 조건부 타입 — 입력에 따른 출력 타입
type Endpoint = "/users" | "/posts" | "/comments";
type User = { id: string; name: string };
type Post = { id: string; title: string };
type Comment = { id: string; content: string };
type ResponseOf<E extends Endpoint> =
E extends "/users" ? User[] :
E extends "/posts" ? Post[] :
E extends "/comments" ? Comment[] :
never;
async function fetchApi<E extends Endpoint>(
endpoint: E
): Promise<ResponseOf<E>> {
const res = await fetch(endpoint);
return res.json();
}
const users = await fetchApi("/users"); // User[] 타입 자동 추론
const posts = await fetchApi("/posts"); // Post[] 타입 자동 추론
유용한 유틸리티 타입 활용
// 선택적 → 필수 변환
type Required<T> = { [K in keyof T]-?: T[K] };
// 깊은 Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// 특정 키만 선택
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// Nullable 추가
type Nullable<T> = { [K in keyof T]: T[K] | null };
6. const Assertion — 리터럴 타입 추출
// 배열에서 유니온 타입 추출
const ROLES = ["admin", "user", "moderator"] as const;
type Role = (typeof ROLES)[number];
// "admin" | "user" | "moderator"
// 객체에서 값 타입 추출
const STATUS = {
ACTIVE: "active",
INACTIVE: "inactive",
PENDING: "pending",
} as const;
type Status = (typeof STATUS)[keyof typeof STATUS];
// "active" | "inactive" | "pending"
// 함수에서 사용
function setRole(role: Role) { /* ... */ }
setRole("admin"); // OK
// setRole("guest"); // 에러
7. Zod — 런타임 + 컴파일 타임 동시 검증
import { z } from "zod";
// 스키마 정의 한 번 → TypeScript 타입 자동 추론
const UserSchema = z.object({
name: z.string().min(1, "이름은 필수입니다."),
email: z.string().email("이메일 형식이 올바르지 않습니다."),
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "user", "moderator"]),
});
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age: number; role: "admin" | "user" | "moderator" }
// API 핸들러에서 검증
export async function POST(req: Request) {
const body = await req.json();
const result = UserSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: { code: "VALIDATION_ERROR", details: result.error.flatten() } },
{ status: 422 }
);
}
// result.data는 완전히 타입 안전
const user: User = result.data;
}
복잡한 스키마 구성
const PaginationSchema = z.object({
cursor: z.string().optional(),
limit: z.number().int().min(1).max(100).default(20),
});
const PostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
tags: z.array(z.string()).max(5),
publishedAt: z.string().datetime().optional(),
});
// 스키마 합성
const CreatePostSchema = PostSchema.extend({
authorId: z.string().uuid(),
});
8. 제네릭 컴포넌트 — 타입 안전한 재사용
// 타입 안전한 범용 리스트 컴포넌트
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
emptyState?: React.ReactNode;
}
function List<T>({
items,
renderItem,
keyExtractor,
emptyState = <p>데이터가 없습니다.</p>,
}: ListProps<T>) {
if (items.length === 0) return <>{emptyState}</>;
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// T가 자동 추론됨
<List
items={users} // T = User
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
9. Type Guard — 런타임 타입 검사를 타입 시스템과 연결
// 커스텀 Type Guard 함수
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isNonNull<T>(value: T | null | undefined): value is T {
return value != null;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof (value as User).id === "string"
);
}
// 활용
const results = [1, null, 3, undefined, 5];
const validNumbers = results.filter(isNonNull); // number[] (null/undefined 제거)
const apiResponse: unknown = await fetch("/api/user").then(r => r.json());
if (isUser(apiResponse)) {
console.log(apiResponse.name); // User 타입으로 안전하게 접근
}
10. Builder 패턴 — 복잡한 객체 생성
class QueryBuilder<T extends Record<string, unknown>> {
private filters: Array<(item: T) => boolean> = [];
private sortField?: keyof T;
private sortOrder: "asc" | "desc" = "asc";
private limitCount?: number;
where(fn: (item: T) => boolean): this {
this.filters.push(fn);
return this;
}
sortBy(field: keyof T, order: "asc" | "desc" = "asc"): this {
this.sortField = field;
this.sortOrder = order;
return this;
}
limit(count: number): this {
this.limitCount = count;
return this;
}
execute(data: T[]): T[] {
let result = data.filter((item) =>
this.filters.every((fn) => fn(item))
);
if (this.sortField) {
const field = this.sortField;
result = result.sort((a, b) => {
const aVal = a[field];
const bVal = b[field];
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return this.sortOrder === "asc" ? cmp : -cmp;
});
}
if (this.limitCount !== undefined) {
result = result.slice(0, this.limitCount);
}
return result;
}
}
// 사용
const result = new QueryBuilder<User>()
.where((u) => u.age >= 20)
.where((u) => u.role === "developer")
.sortBy("name")
.limit(10)
.execute(users);
실무 적용 요약
| 패턴 | 주요 사용 사례 |
|---|---|
| Discriminated Union | API 응답, 상태 관리, 이벤트 핸들링 |
| 브랜드 타입 | ID, 금액, 퍼센트 등 의미 있는 원시 타입 |
| satisfies | 설정 객체, 라우트 맵, 컬러 팔레트 |
| Template Literal | CSS 단위, 이벤트 이름, API 경로 |
| 조건부 타입 | 입력에 따라 출력 타입이 달라지는 함수 |
| const Assertion | 상수 배열/객체에서 유니온 타입 추출 |
| Zod | API 입력 검증, 폼 검증, 환경 변수 검증 |
| 제네릭 컴포넌트 | 재사용 가능한 UI 컴포넌트 |
| Type Guard | 외부 데이터 타입 좁히기 |
| Builder 패턴 | 복잡한 쿼리, 설정 빌더 |
관련 글: REST API 설계 베스트 프랙티스 · Supabase 사이드프로젝트 시작하기 · Next.js SSG 완전 가이드
관련 글
Next.js SSG 완벽 가이드 2026 — App Router 정적 사이트 생성 실전
Next.js App Router에서 SSG를 제대로 활용하는 방법. generateStaticParams, revalidate, 동적 라우트 정적 생성, Core Web Vitals 최적화까지 실전 코드로 정리했습니다.
REST API 설계 베스트 프랙티스 2026 — 실무 즉시 적용 가이드
실무에서 바로 적용할 수 있는 REST API 설계 원칙과 패턴 완전 가이드. URL 설계, HTTP 메서드, 상태 코드, 에러 응답, 페이지네이션, 버전 관리, 인증, Rate Limiting, OpenAPI 문서화까지 정리했습니다.
Turborepo 모노레포 가이드 2026 — pnpm 워크스페이스 실전 설정
Turborepo와 pnpm 워크스페이스로 모노레포를 구성하는 실전 가이드. 태스크 파이프라인, 원격 캐싱, 공유 패키지 설정까지 처음부터 단계별로 설명합니다.