Prologue: 늘 불안했던 타입스크립트의 우회로
타입스크립트를 사용해 규모 있는 프로젝트를 작업하면서 자주 겪었던 모순적인 감정이 있었습니다. "타입을 안전하게 지키고 싶어서 규칙을 정하지만, 그 규칙 때문에 오히려 내 코드의 구체적인 형태를 잃어버리거나, 반대로 컴파일러의 눈을 가리기 위해 위험한 우회로를 찾게 된다"는 것이었습니다.
특히 라우터 경로 정의, 테마 설정 객체, 또는 복잡한 API 스키마를 선언할 때 이런 현상이 심했습니다.
그동안 타입스크립트에서 객체의 타입을 제어하기 위해 제가 썼던 방법은 보통 두 가지였습니다.
- 타입 지정(Type Annotation):
const theme: Theme = { ... }처럼 명시적으로 못 박기. - 타입 단언(Type Assertion):
const theme = { ... } as Theme로 컴파일러에게 강제하기.
하지만 두 방법 모두 치명적인 한계가 있었습니다. 타입 지정은 객체의 구체적인 타입을 뭉뚱그려(Upcasting) 추론의 정밀함을 떨어뜨렸고, 타입 단언은 오타나 누락된 속성이 있어도 컴파일러가 입을 꾹 닫아버려 버그를 걸러내지 못했습니다.
"타입의 안정성도 검증받으면서, 객체가 원래 가지고 있던 구체적인 값 정보도 그대로 살릴 수는 없을까?"
이 고민을 단번에 해결해 준 기능이 바로 타입스크립트 4.9 버전에 도입된 satisfies 연산자였습니다.
Concept: Satisfies 연산자가 해결한 딜레마
satisfies 연산자의 핵심 사상은 **"이 객체가 특정 타입의 요구 사항을 충족하는지(Satisfy) 검증하되, 객체의 실제 추론된 가장 구체적인 타입(Specific Type)은 그대로 유지한다"**는 것입니다.
이것이 왜 대단한 변화인지 이해하기 위해 아주 흔한 예시인 '색상 테마 설정' 객체를 살펴보겠습니다.
type Color = string | { r: number; g: number; b: number };
type Theme = Record<string, Color>;
테마의 색상은 단순 문자열("red", "#ffffff")일 수도 있고, RGB 객체({ r: 255, g: 0, b: 0 })일 수도 있는 유연한 구조입니다.
1. 타입 지정(Annotation)의 문제점
const palette: Theme = {
primary: "blue",
danger: { r: 255, g: 0, b: 0 }
};
// 에러 발생! 'string | { r: number; g: number; b: number }' 타입에 'toUpperCase'가 없을 수 있음
palette.primary.toUpperCase();
palette 변수의 타입을 Theme로 정의해 버린 순간, 타입스크립트는 palette.primary가 문자열이라는 사실을 망각하고 더 넓은 타입인 Color로 뭉뚱그려 버립니다. 개발자는 문자열이라는 걸 뻔히 아는데도 문자열 메서드를 쓰려면 번거로운 타입 가드(Type Guard)를 거쳐야 합니다.
2. 타입 단언(Assertion)의 위험성
const palette = {
primary: "blue",
dannger: { r: 255, g: 0, b: 0 } // 실수로 오타 발생! (danger -> dannger)
} as Theme;
// 컴파일러는 오타를 잡아내지 못하고 그냥 통과시킵니다.
as Theme는 컴파일러에게 "내가 다 알아서 할 테니 묻지도 따지지도 말고 Theme 타입으로 취급해"라고 강제하는 치트키입니다. 오타가 있어도 그냥 넘어가고 런타임에 가서야 에러가 터집니다.
3. Satisfies의 완벽한 해결책
const palette = {
primary: "blue",
danger: { r: 255, g: 0, b: 0 }
} satisfies Theme;
// 1. 타입 안전성: 만약 danger 속성을 누락하거나 잘못 적으면 컴파일 에러를 뿜어줍니다.
// 2. 타입 구체화: primary가 문자열이라는 추론 정보가 살아있어 에러 없이 호출 가능합니다.
palette.primary.toUpperCase(); // OK!
이 세 가지 차이를 비교하고 나서 머리가 맑아졌습니다. satisfies는 **"검사는 하되, 타입은 바꾸지 마라"**라는 단순하면서도 아주 강력한 제어 방식을 타입스크립트 생태계에 가져다준 것입니다.
Deep Dive: 3대 선언 방식 비교 분석
세 방식이 내부적으로 어떻게 작동하는지 명확하게 표로 비교해 보았습니다.
| 비교 항목 | 타입 지정 (: T) | 타입 단언 (as T) | Satisfies (satisfies T) |
|---|---|---|---|
| 타입 정합성 검사 | 수행함 (완벽히 일치해야 함) | 건너뜀 (안전하지 않음) | 수행함 (적합 여부 검증) |
| 추론 타입 결정 | 지정된 타입 T로 상향 통일됨 | 강제로 지정된 타입 T로 간주됨 | 우변의 리터럴 값을 바탕으로 가장 구체적으로 추론됨 |
| 추가 속성 허용 여부 | 잉여 속성 검사로 차단됨 | 허용됨 (타입 검사 우회) | 허용되나, 원래 속성 타입에 맞아야 함 |
특히 만족스러운 부분은 **잉여 속성 검사(Excess Property Checking)**의 우아함입니다. 타입 지정에서는 정의되지 않은 임의의 키값을 객체에 넣으면 에러를 뿜지만, satisfies는 큰 틀의 구조만 맞추면 유연하게 추가 속성도 그 자체의 고유 타입으로 추론해 냅니다.
Application: 실무 설정 파일 및 상태 스토어 리팩토링
이 기능을 내 서비스의 '내비게이션 메뉴 라우팅 설정' 객체에 직접 적용해 보았습니다. 기존에는 메뉴 항목마다 필요한 권한(Role) 리스트가 있고, 각 메뉴 클릭 시 이동할 경로가 정의되어 있었습니다.
// 라우트 정의 공통 타입
type Role = 'admin' | 'user' | 'guest';
interface RouteItem {
path: string;
allowedRoles: Role[];
}
type RouteConfig = Record<string, RouteItem>;
기존에는 이 설정을 타입 지정으로 관리하다 보니, 특정 페이지 경로를 직접 문자열로 자동완성 받지 못하고 매번 수동으로 확인해야 했습니다.
// 리팩토링 후 satisfies 적용 코드
const APP_ROUTES = {
home: { path: '/', allowedRoles: ['guest', 'user'] },
dashboard: { path: '/dashboard', allowedRoles: ['user'] },
adminSettings: { path: '/admin/settings', allowedRoles: ['admin'] }
} satisfies RouteConfig;
// 이제 APP_ROUTES의 구체적인 키값이 그대로 추론됩니다!
// 1. 자동완성 지원
// APP_ROUTES.home.path -> '/'로 구체적으로 추론되어 다른 함수 인자로 넘길 때도 자동완성 가능
// 2. 안전한 오타 방지
// allowedRoles에 'administrator' 같은 잘못된 롤을 적으면 즉시 컴파일러가 잡아냄
이 작업을 마치고 나니 개발 생산성이 크게 향상되었습니다.
- API URL 목록이나 다국어 사전 키값 목록 등 유연하면서도 정밀해야 하는 설정 파일들을 작성할 때 더 이상 임의의 캐스팅(
as any나as const)을 남발하지 않게 되었습니다. - 객체의 키 이름이나 타입 오타를 안전하게 방지하면서도 개발 도구의 자동완성(IntelliSense)을 극한으로 누릴 수 있게 되었습니다.
Summary: 엄격함과 유연함의 아름다운 타협
사학 논문을 쓸 때도 무작정 가혹한 고증 잣대를 들이대서 서술의 깊이를 깎아먹거나, 반대로 고증 없이 상상력에만 의존하는 방식 모두 실패하기 마련입니다. 타입스크립트 작성도 마찬가지였습니다. 컴파일러의 족쇄에 묶여 정밀함을 희생하는 엄격함과, 그냥 실행만 되게 만드는 느슨함 사이에서 늘 방황했습니다.
satisfies 연산자는 엄격한 구조적 안전성과 유연한 타입 추론이라는 모순된 두 가치를 가장 조화롭게 타협시킨 결과물입니다.
자바스크립트 본연의 유연하고 구체적인 구조를 보존하면서도 타입 시스템의 안전망을 덧씌우는 이 간단한 키워드 하나가 코드 퀄리티를 한 단계 올려주었습니다.
프로젝트 전반에 걸쳐 단순 설정값이나 테마 선언 파일들에서 : T 또는 as T를 걷어내고 satisfies T로 변경하는 작업은 강력하게 추천하고 싶은 현대 타입스크립트의 가장 표준적인 리팩토링 습관입니다.