
`as`로 타입을 뭉갰더니 런타임이 터졌습니다 (Type Assertion의 배신)
빨간 줄을 없애려고 습관적으로 `as unknown as Type`을 쓰시나요? `as`가 사실 컴파일러의 눈을 가리는 행위인 이유와 Type Guard를 통한 올바른 해결법.

빨간 줄을 없애려고 습관적으로 `as unknown as Type`을 쓰시나요? `as`가 사실 컴파일러의 눈을 가리는 행위인 이유와 Type Guard를 통한 올바른 해결법.
로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

서로 다른 인터페이스를 연결해주는 변환기. 레거시 시스템과 신규 시스템을 이어주는 가장 강력한 디자인 패턴.

객체지향의 거장 로버트 마틴(Uncle Bob)이 정립한 5가지 설계 원칙. SRP, OCP, LSP, ISP, DIP가 무엇인지, 왜 지켜야 하는지, 실제 타입스크립트 예제로 정리해본다.

나만 알아보는 코드는 쓰레기입니다. 변수명 짓기부터 함수 쪼개기, 그리고 주석을 달지 말아야 하는 이유까지. 6개월 뒤의 나를 살리는 리팩토링의 기술.

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는 흔적도 없이 사라지고, 그냥 쌩 데이터만 남습니다.
이걸 "공항 검색대 눈 가리개"에 비유하니 이해가 됐습니다.
요원은 눈이 가려져서 가방을 못 봅니다. 그래서 통과(Compile Success)는 시켜줍니다. 하지만 비행기 안에서 가방이 터지는(Runtime Error) 건 막을 수 없습니다.
as는 문제를 해결하는 게 아니라, 경고 메시지를 꺼버리는 위험한 버튼입니다.
as를 없애려면 Type Guard(타입 가드)를 써야 합니다.
컴파일러에게 "이거 봐, 진짜 맞지?"라고 증명해 보이는 과정입니다.
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 추천)
}
}
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 문으로 빠져서 우아하게 에러 처리를 할 수 있습니다.
매번 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를 씁니다.
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를 쓰세요.
쇼핑몰 프로젝트에서 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)를 만들 때만 숨겨서 써야 합니다. 비즈니스 로직에 나오면 안 됩니다.
! (Non-null Assertion)도 as인가요?네, user!.name은 user as User의 동생입니다.
"이거 절대 null 아니야!"라고 컴파일러에게 소리치는 거죠.
하지만 코드가 수정되면서 null이 될 수도 있습니다.
! 대신 ?. (Optional Chaining)이나 if 문을 쓰는 습관을 들이세요.