1. 개요

any 와 unknown 은 둘 다 “어떤 타입이든 담을 수 있는 타입” 처럼 보인다. 실제로 값을 대입하는 방향으로는 둘 다 뭐든 받는다.

let a: any = 42
let u: unknown = 42

그런데 그 값을 꺼내 쓰는 방향에서는 완전히 다르게 행동한다. 이 차이를 모르고 “귀찮으니까 any 쓰지 뭐” 로 넘어가면, TypeScript 를 쓰는 의미의 상당 부분을 그냥 버리는 셈이다.


2. 핵심 내용

2-1. any: 타입 체크를 끈다

any 타입 값은 어떤 프로퍼티에 접근하든, 어떤 메서드를 호출하든 컴파일러가 검사하지 않는다.

let a: any = 42
a.toUpperCase() // 컴파일 통과. 런타임에서 "a.toUpperCase is not a function"
a.foo.bar.baz() // 이것도 컴파일 통과

any 는 이 값에 대해서는 타입 시스템을 아예 꺼버리는 것과 같다. 자바스크립트를 그대로 쓰는 것과 다를 게 없다. 심지어 any 는 전염된다. any 값을 다른 변수에 대입하면 그 변수도 any 가 되고, 함수 인자로 넘기면 그 함수 내부에서도 타입 정보가 사라진다.

2-2. unknown: 확인 전엔 아무것도 못 한다

unknown 도 뭐든 담을 수 있지만, 꺼내 쓰려면 먼저 무슨 타입인지 증명해야 한다.

let u: unknown = 42
u.toUpperCase() // Error: 개체가 'unknown' 형식입니다
u + 1 // Error: 'unknown' 형식에 산술 연산을 적용할 수 없습니다

아무 프로퍼티 접근도, 연산도 그냥은 안 된다. 2편에서 본 좁히기를 거쳐야 한다.

function handle(u: unknown) {
  if (typeof u === "string") {
    console.log(u.toUpperCase()) // 이제 string 으로 좁혀져서 OK
  }
}

unknown 은 “지금은 무슨 타입인지 모른다” 를 정직하게 표현하는 타입이고, 그 무지를 컴파일러가 강제로 인정하게 만든다. 뭘 하려면 먼저 확인부터 하라는 게 unknown 의 계약이다.

2-3. 왜 unknown 이 있어야 했나

unknown 이 등장하기 전(TypeScript 3.0 이전)에는 “타입을 모르는 외부 값” 을 표현할 방법이 any 밖에 없었다. JSON.parse 의 반환 타입, 외부 API 응답, catch 절의 에러 객체가 전부 any 였다. 문제는 이게 타입 시스템의 구멍을 그대로 프로젝트 곳곳에 심는다는 것이다. JSON.parse(text) 의 결과를 별 확인 없이 .name 으로 접근하다가 실제로는 배열이 와서 런타임 에러가 나는 식이다. unknown 은 “값이 들어오는 건 막지 않지만, 쓰기 전엔 반드시 확인하게” 만들어서 이 구멍을 막는다.

TypeScript 4.4 부터는 catch 절의 에러 타입도 기본이 any 대신 unknown 으로 바뀌었다(useUnknownInCatchVariables). 지금은 catch (e) 를 쓰면 e 가 unknown 이라 바로 e.message 에 접근하면 에러가 나고, 아래처럼 확인을 거쳐야 한다.

try {
  riskyCall()
} catch (e) {
  if (e instanceof Error) {
    console.log(e.message) // Error 로 좁혀졌으니 OK
  } else {
    console.log("알 수 없는 에러", e)
  }
}

옛날 코드에서 catch (e: any) 를 흔히 보는 이유가 이거다. any 시절 습관이 남아있거나, 확인 과정이 귀찮아서 명시적으로 any 로 되돌린 경우다.

2-4. 외부 입력은 unknown 으로 받는 게 기본이어야 한다

API 응답, 폼 입력, 로컬스토리지에서 읽은 값처럼 “내 코드 밖에서 온 값” 은 전부 unknown 으로 받는 게 원칙이다.

function parseUser(json: string): unknown {
  return JSON.parse(json)
}
 
function isUser(v: unknown): v is { id: number; name: string } {
  return (
    typeof v === "object" &&
    v !== null &&
    typeof (v as any).id === "number" &&
    typeof (v as any).name === "string"
  )
}
 
const parsed = parseUser(rawJson)
if (isUser(parsed)) {
  console.log(parsed.name) // 검증을 통과한 뒤에만 안전하게 접근
}

JSON.parse 의 실제 시그니처는 any 를 반환하지만, 그걸 그대로 쓰지 않고 unknown 으로 감싸서 강제로 검증을 거치게 만드는 패턴이다. 검증 함수를 만드는 방식은 다음 편에서 좀 더 다룬다.

2-5. 정리하면

anyunknown
아무 타입이나 대입 가능OO
확인 없이 프로퍼티 접근O (위험)X (컴파일 에러)
다른 타입 변수에 대입O, 전염됨X, 좁힌 뒤에만 가능
쓰는 상황정말 마지막 수단외부 입력, 아직 모르는 타입을 정직하게 표현

any 를 쓰는 순간 그 지점부터 TypeScript 는 자바스크립트가 된다. unknown 을 쓰면 “여기는 아직 타입을 모른다” 는 사실이 코드에 남고, 컴파일러가 그걸 확인 없이는 못 쓰게 강제한다. 대부분의 “일단 any” 는 unknown + 타입 가드로 바꿀 수 있다.


3. 마무리

요약

  • any 는 타입 체크를 끈다. 확인 없이 아무 프로퍼티나 접근해도 컴파일은 통과하고 런타임에서 터진다.
  • unknown 은 뭐든 담을 수 있지만 꺼내 쓰려면 반드시 좁히기를 거쳐야 한다.
  • JSON.parse 결과, API 응답, catch 의 에러 객체처럼 외부에서 오는 값은 unknown 으로 받는 게 기본이어야 한다. TypeScript 4.4 부터 catch 기본 타입도 unknown 이다.
  • “일단 any” 로 넘어가고 싶은 지점은 대부분 unknown + 타입 가드로 바꿀 수 있다.

다음은 7편이다. is 키워드로 런타임 검증과 컴파일 타임 타입을 잇는 법을 다룬다.