2026년 TypeScript 모범 사례: 더 안전하고 나은 코드 작성하기
2026년 TypeScript 모범 사례: 더 안전하고 나은 코드 작성하기
2026년 필수 TypeScript 모범 사례를 마스터하세요. 고급 타입 패턴, 에러 처리, 성능 최적화, 프로젝트 설정까지 모든 것을 다루는 종합 가이드입니다.
2026년 3월 14일7분 소요
서론: TypeScript가 표준이 된 이유
TypeScript는 전문 JavaScript 개발의 표준으로 확고히 자리잡았습니다. 2026년 기준 새로운 JavaScript 프로젝트의 80% 이상이 TypeScript로 시작됩니다. 컴파일 타임에 버그를 잡고, 타입을 통해 코드 문서화를 개선하며, 더 나은 IDE 지원을 가능하게 하고, 대규모 코드베이스를 유지보수 가능하게 만들기 때문입니다.
하지만 TypeScript를 작성하는 것과 좋은 TypeScript를 작성하는 것은 다릅니다. 이 가이드는 2026년에 더 깔끔하고, 안전하며, 유지보수 가능한 TypeScript 코드를 작성하는 데 도움이 되는 모범 사례를 다룹니다.
1부: 올바른 타입 시스템 기초
any 대신 unknown 사용하기
any 타입은 TypeScript의 탈출구입니다 -- 모든 타입 검사를 비활성화합니다.
// 나쁨: any는 타입 검사를 비활성화
function processData(data: any) {
return data.name.toUpperCase(); // 에러 없음, 하지만 런타임에 충돌 가능
}
// 좋음: unknown은 타입 확인을 강제
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'name' in data) {
const record = data as { name: string };
return record.name.toUpperCase(); // 안전!
}
throw new Error('잘못된 데이터 형식');
}
명백한 경우 타입 추론 활용하기
// 불필요: TypeScript가 이미 추론함
const name: string = "John";
const count: number = 42;
// 더 나음: TypeScript가 추론하도록
const name = "John"; // string으로 추론
const count = 42; // number로 추론
// 명시하세요: 함수 매개변수와 반환 타입
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// 추론이 너무 넓을 때 명시하세요
const status: "active" | "inactive" = "active"; // 단순 string이 아님
const 단언(assertion) 사용하기
// const 단언 없이: 타입은 { name: string, role: string }
const config = { name: "admin", role: "superuser" };
// const 단언 사용: 타입은 { readonly name: "admin", readonly role: "superuser" }
const config = { name: "admin", role: "superuser" } as const;
// enum 대체로 완벽
const HttpStatus = {
OK: 200,
NOT_FOUND: 404,
SERVER_ERROR: 500,
} as const;
type StatusCode = typeof HttpStatus[keyof typeof HttpStatus]; // 200 | 404 | 500
선택적 속성 대신 판별 유니온 사용하기
// 나쁨: 모든 것이 선택적, 유효한 상태를 알기 어려움
interface ApiResponse {
status: string;
data?: unknown;
error?: string;
}
// 좋음: 각 상태가 명확하게 정의됨
type ApiResponse =
| { status: "success"; data: unknown }
| { status: "error"; error: string; retryAfter?: number }
| { status: "loading" };
function handleResponse(response: ApiResponse) {
switch (response.status) {
case "success":
console.log(response.data); // TypeScript가 data 존재를 알음
break;
case "error":
console.error(response.error); // TypeScript가 error 존재를 알음
break;
case "loading":
console.log("로딩 중...");
break;
}
}
satisfies로 타입 검증하기
type Route = {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE";
};
// satisfies 사용: 검증하면서 리터럴 타입도 보존
const route = {
path: "/api/users",
method: "GET",
} satisfies Route;
// route.path는 "/api/users" (리터럴 타입 보존!)
2부: 고급 타입 패턴
템플릿 리터럴 타입
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// 결과: "onClick" | "onFocus" | "onBlur"
type CSSUnit = "px" | "rem" | "em" | "%";
type CSSValue = `${number}${CSSUnit}`;
function setWidth(value: CSSValue) { /* ... */ }
setWidth("100px"); // OK
setWidth("2rem"); // OK
setWidth("100"); // 에러: 유효한 CSSValue가 아님
알아야 할 유틸리티 타입
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Partial: 모든 속성을 선택적으로
type UpdateUser = Partial<User>;
// Pick: 특정 속성만 선택
type UserPublic = Pick<User, "id" | "name" | "email">;
// Omit: 특정 속성 제거
type UserWithoutPassword = Omit<User, "password">;
// Record: 특정 키를 가진 객체 타입
type UserRoles = Record<string, "admin" | "user" | "moderator">;
// ReturnType: 함수의 반환 타입 가져오기
function createUser() { return { id: 1, name: "John" }; }
type NewUser = ReturnType<typeof createUser>;
// Awaited: Promise 타입 언래핑
type UserData = Awaited<Promise<User>>; // User
브랜디드 타입으로 타입 안전성 확보
같은 기본 타입의 값을 혼동하는 것을 방지:
// 브랜디드 타입 없이: ID를 쉽게 혼동
function getUser(userId: string) { /* ... */ }
function getOrder(orderId: string) { /* ... */ }
getUser(orderId); // 에러 없음! 하지만 버그.
// 브랜디드 타입 사용: 컴파일러가 실수를 잡아냄
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
function createUserId(id: string): UserId { return id as UserId; }
function createOrderId(id: string): OrderId { return id as OrderId; }
function getUser(userId: UserId) { /* ... */ }
function getOrder(orderId: OrderId) { /* ... */ }
const userId = createUserId("user_123");
const orderId = createOrderId("order_456");
getUser(orderId); // 에러: 'OrderId'는 'UserId'에 할당할 수 없음
getUser(userId); // OK!
3부: 에러 처리 모범 사례
예외 대신 Result 타입 사용
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseJSON(text: string): Result<unknown, string> {
try {
return { ok: true, value: JSON.parse(text) };
} catch (e) {
return { ok: false, error: `잘못된 JSON: ${(e as Error).message}` };
}
}
// 사용: 호출자가 반드시 두 경우를 처리
const result = parseJSON(userInput);
if (result.ok) {
console.log(result.value);
} else {
console.error(result.error);
}
JSON 파싱을 테스트하려면 JSON 포맷터를 사용하세요.
완전한 switch 문
never 타입으로 모든 케이스를 처리했는지 보장:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// 새 도형을 추가하고 처리를 잊으면 컴파일 에러 발생!
const _exhaustive: never = shape;
throw new Error(`처리되지 않은 도형: ${_exhaustive}`);
}
}
4부: 프로젝트 설정
2026년 권장 tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"target": "ES2022",
"declaration": true,
"sourceMap": true,
"outDir": "./dist",
"esModuleInterop": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
주요 컴파일러 옵션 설명
strict: true-- 모든 엄격한 타입 검사 활성화. 필수.noUncheckedIndexedAccess-- 배열 접근이T | undefined반환exactOptionalPropertyTypes--undefined와 "없음"을 구분verbatimModuleSyntax-- 일관된 import/export 구문 강제
5부: 모던 TypeScript 패턴
타입 안전한 API 클라이언트
interface ApiSchema {
"/users": {
GET: { response: User[] };
POST: { body: CreateUserInput; response: User };
};
"/users/:id": {
GET: { response: User };
PUT: { body: UpdateUserInput; response: User };
DELETE: { response: void };
};
}
// 완전한 타입 안전성!
const users = await api("/users", "GET"); // User[]
const user = await api("/users/:id", "GET"); // User
Zod로 런타임 검증
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "user", "moderator"]),
});
// TypeScript 타입 자동 추론
type User = z.infer<typeof UserSchema>;
// 런타임에 검증
function createUser(input: unknown): User {
const result = UserSchema.safeParse(input);
if (!result.success) {
throw new ValidationError(result.error.message);
}
return result.data; // 완전한 타입!
}
6부: 성능 팁
불필요한 타입 단언 피하기
// 나쁨: 타입 단언이 잠재적 문제를 숨김
const data = fetchData() as UserData;
// 좋음: 타입 가드로 데이터 검증
function isUserData(data: unknown): data is UserData {
return (
typeof data === "object" &&
data !== null &&
"id" in data &&
"name" in data
);
}
const data = fetchData();
if (isUserData(data)) {
// data가 안전하게 UserData로 타입됨
}
효과적인 타입 좁히기
function processValue(value: string | number | null) {
if (value === null) return "값 없음";
if (typeof value === "string") {
return value.toUpperCase(); // string 메서드 사용 가능
}
return value.toFixed(2); // number 메서드 사용 가능
}
결론
TypeScript는 단순한 "타입이 있는 JavaScript" 이상입니다. 최대한 활용하면 신뢰할 수 있고 유지보수 가능한 소프트웨어를 구축하는 강력한 도구입니다:
- 타입 시스템을 완전히 활용 --
any대신unknown, 판별 유니온, 브랜디드 타입 - 추론에 맡기기 -- 필요할 때만 타입 명시
- 에러를 명시적으로 -- Result 타입, 완전한 검사, 커스텀 에러 클래스
- 엄격하게 설정 --
strict: true가 기본 - 경계에서 검증 -- 런타임 검증에 Zod 등 사용
- 타입도 테스트 -- 타입도 코드베이스의 일부
관련 리소스
- 2026년 웹 개발 트렌드 -- 최신 웹 기술
- 2026년 개발자를 위한 AI 도구 -- TypeScript를 이해하는 AI 도구
- JSON 포맷터 -- JSON 데이터 포맷 및 검증
- 정규식 테스터 -- TypeScript 검증용 정규식 테스트
- UUID 생성기 -- TypeScript 프로젝트용 UUID 생성