"일단 빨간 줄만 없애면 되는 거 아니야?"
API 응답 타입을 정의하기 귀찮아서, 혹은 데이터가 확실하다고 생각해서 이렇게 짰습니다.
// 서버 응답이 User 타입일 거라고 확신함
const user = response.data as User;
console.log(user.address.city);
VSCode의 빨간 줄은 사라졌고, 빌드도 성공했습니다. 그런데 배포 후 Sentry에서 에러 알람이 쉴 새 없이 울렸습니다.
Uncaught TypeError: Cannot read properties of undefined (reading 'city')
알고 보니 특정 상황에서 서버가 address 필드를 null로 보내고 있었습니다.
하지만 저는 as User로 "무조건 User 타입이야!"라고 우겼기 때문에,
TypeScript는 "그래, 네가 그렇다면 그렇겠지" 하고 검사를 포기해버린 것입니다.
처음엔 뭐가 이해가 안 갔나? (강제 형 변환?)
저는 as 키워드가 C나 Java의 Casting(형 변환)인 줄 알았습니다.
데이터를 실제로 그 타입으로 바꿔주는 줄 알았죠.
하지만 TypeScript의 as는 Type Assertion(단언)입니다.
"나를 믿어(Trust me), 내가 너보다 더 잘 알아"라고 컴파일러에게 거짓말을 하는 행위입니다.
런타임에는 아무런 영향을 주지 않습니다.
JS로 변환되면 as는 흔적도 없이 사라지고, 그냥 쌩 데이터만 남습니다.
어떤 포인트에서 이해가 됐나? (눈 가리개 비유)
이걸 "공항 검색대 눈 가리개"에 비유하니 이해가 됐습니다.
- Compiler: 공항 보안 검색 요원입니다. 위험한 물건(타입 불일치)이 있는지 꼼꼼히 검사합니다.
- Safe Code: 엑스레이를 통과해서 안전함이 증명된 가방입니다.
- as Keyword: 요원에게 "이 가방 안전하니까 검사하지 말고 그냥 통과시켜!"라고 소리치며 눈을 가리는 행위입니다.
요원은 눈이 가려져서 가방을 못 봅니다. 그래서 통과(Compile Success)는 시켜줍니다. 하지만 비행기 안에서 가방이 터지는(Runtime Error) 건 막을 수 없습니다.
as는 문제를 해결하는 게 아니라, 경고 메시지를 꺼버리는 위험한 버튼입니다.
해결 과정 - Type Guard 사용하기
as를 없애려면 Type Guard(타입 가드)를 써야 합니다.
컴파일러에게 "이거 봐, 진짜 맞지?"라고 증명해 보이는 과정입니다.
1. in 연산자나 typeof 사용
function printCity(user: unknown) {
// ❌ 나쁜 예: 일단 우기기
// console.log((user as User).address.city);
// ✅ 좋은 예: 증명하기
if (typeof user === 'object' && user !== null && 'address' in user) {
// 여기 들어오면 TS는 user가 object고 address가 있다는 걸 앎
const addr = (user as any).address; // (복잡한 객체는 Zod 추천)
}
}
2. User Defined Type Guard (is 키워드)
가장 강력한 무기입니다. 검사 로직을 함수로 분리합니다.
interface User {
name: string;
address: { city: string };
}
// 이 함수가 true를 리턴하면, user는 진짜 User 타입임
function isUser(target: unknown): target is User {
return (
typeof target === 'object' &&
target !== null &&
'address' in target
);
}
// 사용
const response = await fetch('/api/user');
const data = await response.json();
if (isUser(data)) {
console.log(data.address.city); // 안전함! 자동완성 됨!
} else {
console.error("데이터 형식이 잘못됨", data); // 에러 처리 가능
}
이제 서버가 이상한 데이터를 보내면, 런타임 에러가 나는 대신 else 문으로 빠져서 우아하게 에러 처리를 할 수 있습니다.
깊이 파고들기 - Zod - 런타임 검증의 끝판왕
매번 isUser 함수를 짜는 건 귀찮고 실수하기 쉽습니다.
이럴 때 Zod 같은 Schema Validation 라이브러리를 씁니다.
import { z } from 'zod';
// 스키마 정의 (런타임 코드)
const UserSchema = z.object({
name: z.string(),
address: z.object({
city: z.string()
})
});
// 타입 자동 추론 (User 인터페이스 안 만들어도 됨!)
type User = z.infer<typeof UserSchema>;
// 데이터 검증
const result = UserSchema.safeParse(response.data);
if (result.success) {
console.log(result.data.address.city); // 100% 안전
} else {
console.error(result.error); // 왜 틀렸는지 상세히 알려줌
}
Zod는 들어오는 데이터(JSON)를 실제로 전수 검사해서,
타입스크립트 타입과 런타임 값을 완벽하게 일치시켜 줍니다.
저는 이제 API 응답 처리할 때 as를 아예 안 쓰고 무조건 Zod를 씁니다.
Application: as가 허용되는 유일한 예외
as를 써도 되는 경우가 딱 하나 있습니다.
"내가 컴파일러보다 확실히 더 잘 아는 경우"인데, 주로 DOM Element나 상수 값을 다룰 때입니다.
// HTML에 #app이 무조건 있다고 확신할 때
const root = document.getElementById('app') as HTMLElement;
// const user = {} as User; (이건 절대 안 됨! 초기화 덜 된 객체!)
하지만 document.getElementById조차도 null일 가능성(오타 등)이 있으므로,
가능하면 if (!root) throw Error를 쓰는 게 더 좋습니다.
satisfies 연산자 (TS 4.9의 축복) 자세히 살펴보기
TypeScript 4.9에서 as를 대체할 강력한 무기인 satisfies가 등장했습니다.
이 녀석은 "타입 검사"는 하되, "타입 추론"은 좁게 유지해줍니다.
as vs : Type vs satisfies
// 1. Type Annotation (넓은 타입)
const palette: Record<string, string | number[]> = {
red: [255, 0, 0],
green: "#00ff00",
};
// ❌ 에러! palette.red가 string 배열인지 string인지 모름 (Union Type)
// palette.red.map(...) // TS Error
// 2. as (거짓말)
const palette2 = {
red: [255, 0, 0],
green: "#00ff00",
} as Record<string, string | number[]>;
// ❌ 실제 값과 상관없이 타입을 강제함. 실수하면 런타임 에러.
// 3. satisfies (완벽)
const palette3 = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Record<string, string | number[]>;
// ✅ 성공!
// TS는 palette3가 규칙을 지켰는지 검사하면서도,
// red가 'number[]'라는 구체적인 타입을 기억함!
palette3.red.map(x => x * 2);
palette3.green.toUpperCase();
이제 설정 파일이나 테마 객체를 정의할 때 as 대신 무조건 satisfies를 쓰세요.
8. Case Study: ID 충돌의 악몽 (Branded Types)
쇼핑몰 프로젝트에서 UserId와 OrderId가 둘 다 string이었습니다.
함수에 인자 순서를 바꿔 넣는 실수를 자주 했습니다.
function cancelOrder(userId: string, orderId: string) { ... }
// 실수로 순서 바꿈
cancelOrder(orderId, userId); // TS는 둘 다 string이라서 에러 안 냄!
이걸 막으려고 as를 써서 가짜 타입을 남발하려다가, Branded Types(Nominal Typing) 패턴을 도입했습니다.
// 유령 속성(__brand)을 이용해 서로 다른 타입인 척 함
type UserId = string & { __brand: 'UserId' };
type OrderId = string & { __brand: 'OrderId' };
// 검증 함수 (Type Guard)
function createUserId(id: string): UserId {
return id as UserId; // 여기서만 유일하게 as 사용 허용!
}
const myUser = createUserId("user_123");
const myOrder = "order_123" as OrderId;
// cancelOrder(myOrder, myUser); // 🚨 컴파일 에러 발생!
원시 타입(Primitive Type)에 의미를 부여해서 실수를 원천 봉쇄했습니다.
as는 이런 저수준 라이브러리(Utility)를 만들 때만 숨겨서 써야 합니다. 비즈니스 로직에 나오면 안 됩니다.
9. FAQ: ! (Non-null Assertion)도 as인가요?
네, user!.name은 user as User의 동생입니다.
"이거 절대 null 아니야!"라고 컴파일러에게 소리치는 거죠.
하지만 코드가 수정되면서 null이 될 수도 있습니다.
! 대신 ?. (Optional Chaining)이나 if 문을 쓰는 습관을 들이세요.