TypeScript: Unknown vs Any 타입 차이점과 안전한 타입 가드(Type Guard)
- 원인:
any는 타입 검사 자체를 포기하는 선언이라, 잘못된 속성 접근이 컴파일 시점엔 통과했다가 런타임에 터짐 - 해결: 외부 API 응답처럼 타입을 확신할 수 없는 데이터는
unknown으로 받고,data is T형태의 타입 가드로 검증한 뒤에만 사용 - 주의: 타입 가드 내부에서
as Record<string, unknown>같은 임시 캐스팅으로 속성을 하나씩 검사하는 절차를 생략하면unknown을 쓰는 의미가 없어짐
function isUserProfile(data: unknown): data is UserProfile { ... }
↓ 왜 any가 위험한지 원리 보기
실무에서 대규모 TypeScript 프로젝트를 마이그레이션하거나, 외부 API 또는 레거시 라이브러리를 통합하다 보면 필연적으로 "타입을 확신할 수 없는 데이터"를 다루게 됩니다.
이때 많은 개발자들이 급한 마음에 가장 손쉬운 탈출구인 any를 사용하곤 합니다. 하지만 이는 TypeScript의 가장 큰 무기인 타입 안정성을 스스로 내려놓는 행위이자, 미래의 나에게 런타임 에러라는 시한폭탄을 던지는 것과 같습니다.
any가 왜 위험한지, TypeScript 3.0에서 도입된 unknown이 이를 어떻게 막아주는지, 그리고 실무에서 바로 써먹을 수 있는 사용자 정의 타입 가드(User-Defined Type Guards) 작성 패턴을 정리합니다.
any의 위험성과 unknown의 등장
프로덕션 환경에서 발생한 실제 에러 로그를 분석해 보면, 꽤 많은 비율이 TypeError: Cannot read properties of undefined에서 시작합니다. 그리고 그 이면에는 높은 확률로 any 타입이 자리 잡고 있죠.
any: 타입 시스템의 치명적인 방임
any는 단순히 "모든 타입이 될 수 있다"를 넘어, "타입 검사를 하지 않겠다"는 선언입니다.
// any를 사용해 파싱한 위험한 코드
const responseData: any = JSON.parse('{"id": 1, "username": "admin"}');
// 컴파일 시점에는 무사 통과하지만, 런타임에 터집니다.
// TypeError: responseData.name.toUpperCase is not a function
console.log(responseData.name.toUpperCase());
위 코드에서 responseData는 any이기 때문에 TypeScript 컴파일러는 responseData.name이나 toUpperCase() 호출을 전혀 제제하지 않습니다. 컴파일러의 눈을 가려버렸기 때문에 개발자는 안심하고 배포하지만, 실제 사용자 기기에서 앱이 뻗는 참사가 일어납니다.
unknown: 안전한 봉인과 검증 의무화
반면 unknown은 "어떤 타입인지 모르니, 사용하기 전에 먼저 타입을 확인하라"고 컴파일러가 강력하게 요구합니다.
// unknown으로 안전하게 봉인한 코드
const rawData: unknown = JSON.parse('{"id": 1, "username": "admin"}');
// 컴파일 에러 발생: 'rawData' is of type 'unknown'.
// console.log(rawData.username);
// 명확히 타입을 검증해야만 사용을 허락합니다.
if (typeof rawData === "object" && rawData !== null && "username" in rawData) {
console.log((rawData as { username: string }).username); // 안전하게 접근 가능
}
핵심 차이점 비교 (any vs unknown)
any 타입 (치명적인 방임)
컴파일러가 타입 체크를 완전히 포기합니다. 어떤 프로퍼티에도 마음대로 접근할 수 있고, 함수로 호출할 수도 있습니다. 런타임에 에러가 발생하기 전까지 오류를 감지할 수 없습니다.
unknown 타입 (안전한 봉인)
모든 값을 수용하지만, 타입 검증이 이루어지기 전까지는 연산, 프로퍼티 접근, 함수 호출 등 모든 직접적인 조작을 금지합니다. 개발자가 직접 검증 코드(타입 가드)를 작성하도록 강제합니다.
| 비교 항목 | any |
unknown |
|---|---|---|
| 모든 값 대입 가능 여부 | 가능 | 가능 |
| 속성 접근 & 함수 호출 | 조건 없이 허용 (타입 프리패스) | 타입 좁히기(Narrowing) 없이는 불가능 |
| 다른 타입에 할당 | 가능 (어떤 타입으로든 자동 변환) | any와 unknown 본인을 제외하고는 할당 불가 |
| 권장 사용처 | 신속한 프로토타이핑, 점진적 마이그레이션 | 외부 API 응답 데이터 처리, 동적 입력 데이터 처리 |
실무 중심의 안전한 타입 가드 (Type Guard) 패턴
unknown을 제대로 활용하려면, 컴파일러가 해당 변수의 타입을 확신할 수 있도록 돕는 타입 가드(Type Guard)를 다룰 줄 알아야 합니다. 이때 핵심 무기가 되는 것이 바로 is 키워드를 활용한 사용자 정의 타입 가드(User-Defined Type Guards)입니다.
실무에서 가장 흔히 마주치는 API 응답 데이터 검증 시나리오로 알아봅시다.
실무 시나리오: 사용자 프로필 응답 데이터 검증
외부 서버로부터 받아온 불확실한 사용자 데이터를 파싱하고, 이 데이터가 우리가 정의한 UserProfile 규격에 정확히 부합하는지 런타임에 검사하는 안전한 가드를 설계합니다.
interface UserProfile {
id: string;
email: string;
roles: string[];
settings: {
theme: "light" | "dark";
};
}
// 사용자 정의 타입 가드 구현
function isUserProfile(data: unknown): data is UserProfile {
// 1. 기본 객체 체크
if (typeof data !== "object" || data === null) {
return false;
}
// 객체의 속성을 안전하게 비교하기 위해 임시 타입 캐스팅
const obj = data as Record<string, unknown>;
// 2. 개별 속성 타입 검증
const hasValidId = typeof obj.id === "string";
const hasValidEmail = typeof obj.email === "string" && obj.email.includes("@");
const hasValidRoles = Array.isArray(obj.roles) && obj.roles.every(role => typeof role === "string");
// 3. 중첩 객체 구조 검증
const hasValidSettings =
typeof obj.settings === "object" &&
obj.settings !== null &&
("theme" in obj.settings) &&
((obj.settings as Record<string, unknown>).theme === "light" || (obj.settings as Record<string, unknown>).theme === "dark");
return hasValidId && hasValidEmail && hasValidRoles && hasValidSettings;
}
// 실무 사용처 예시
function processResponse(response: unknown) {
if (isUserProfile(response)) {
// 이 블록 안에서 response는 UserProfile 타입으로 스마트 캐스팅됩니다.
console.log(`반가워요, ${response.email}! 테마 설정: ${response.settings.theme}`);
} else {
// 형식에 어긋난 경우 방어적인 에러 핸들링
console.warn("전달된 응답이 UserProfile 규격에 맞지 않습니다.", response);
}
}
이처럼 data is UserProfile 반환 타입을 설정해두면, 함수가 true를 반환할 때 TypeScript는 해당 변수를 무조건 UserProfile 타입으로 취급해 줍니다. 번거로워 보일 수 있지만, 이렇게 한 번 작성해둔 타입 가드는 예측 불가능한 외부 데이터로부터 애플리케이션의 런타임 무결성을 지키는 안전장치가 되어줍니다.
레거시 API를 붙이는 작업에서 응답 타입을 그냥 any로 두고 넘어간 적이 있는데, API 쪽에서 필드 이름을 username에서 userName으로 바꾼 걸 아무도 눈치채지 못하고 몇 주 뒤 사용자 화면에서 조용히 undefined가 노출되고 나서야 알았습니다. 그 뒤로 외부 API 응답만큼은 무조건 unknown으로 받고 타입 가드를 거치도록 룰을 정했습니다.
함께 읽어보면 좋은 공식 레퍼런스
- - TypeScript Handbook: Advanced Types (Narrowing)
- - MDN Web Docs: JavaScript typeof operator
- - TypeScript Handbook: Release Notes for TS 3.0 (unknown Type)
TypeScript 프로젝트를 점진적으로 이전할 때는 any를 쓰더라도, 새로 만드는 유틸리티나 API 데이터 핸들러에는 unknown을 써보시길 권합니다. 처음 설계할 때 타입을 정교하게 잡아두면 나중에 프로덕션 버그가 줄어듭니다. 관련 글은 [호마다의 IT 개발 입문 블로그] 에서 확인하실 수 있습니다.
댓글
댓글 쓰기