어떤 값이 들어올지 모를 때 무슨 타입을 사용해야 할까 (any vs unknown)

BangDori·2024년 3월 7일
0

같은 서버에서 오는 응답의 형태는 대체로 통일되어 있어서 API 응답 값은 하나의 Response 타입으로 묶을 수 있다.

interface Response<T> {
  data: T;
  status: string;
  errorCode?: string;
  errorMessage?: string;
}

타입스크립트의 제네릭 타입을 활용하여, 응답 데이터의 타입을 지정해주고 data와 status를 받는다. 그리고 옵셔널 체이닝으로 에러가 발생할 경우 혹은 API 통신이 실패할 경우 errorCode와 errorMessage 값을 응답으로 받아오기 때문에 옵셔널 체이닝을 이용하여 Response 타입을 설정해주었다.

🤔 Response 타입을 잘 설정한 것 같은데.. any 타입이랑 unknown 타입이 갑자기 왜 나오는건가요?

현재 Response 타입에는 큰 문제가 없어보인다. 하지만 만약 다음과 같이 data에 내부에 어떠한 응답을 명확히 할 수 없는 경우, 값의 형식이 달라지더라도 로직에 영향을 주지 않는 경우에는 어떻게 타입을 지정해주어야 할까?

interface CartData {
  cartItems: CartItem[];
  forPass: unknown; // 프론트 로직에서 사용해야 하는 값! (할인 코드 등 존재)
}

타입스크립트 공식 문서와 책에서는 해당 타입을 특정할 수 없을때에는 any 타입이 아닌 unknown 타입을 사용하라고 말을 한다. 그 이유는 unknown 타입이 any 타입보다 더 안전하기 때문이라고 하는데,,, 왜 그럴까? 한 번 알아보자.

unknown 타입

공식 문서만큼 정확한 설명지도 없으니까 TypeScript Documentation - unknown을 먼저 확인해보았다.

공식 문서에서는 unknown 타입에 대해서 다음과 같이 설명하고 있다.

The unknown type represents any value. This is similar to the any type, but is safer because it’s not legal to do anything with an unknown value

unknown 타입은 모든 값을 나타냅니다. 이 값은 any 타입과 유사하지만, unknown 값으로 어떠한 작업을 하는데 있어 합법적이지 않기 때문에 더 안전합니다.

이 말을 다시 생각해보면 다음과 같이 풀어쓸 수 있을 것 같다. unknown 타입은 any 타입과 유사하게 특정한 타입이 존재하는 것은 아니지만, any 타입처럼 어떠한 작업을 함부로 진행할 수 없다는 의미인 것 같다. 내 번역이 어색해서 이해가 잘 안되는 것 같기도하고, 말 100번 하는 것 보다는 코드를 직접 보는게 더 잘 이해가 될 것 같으니 코드로 다시 한번 생각해보자!

위 코드를 살펴보면 any 타입을 가지는 anyX 변수의 경우에는 string 타입에서 지원해주는 toupperCase를 적용했음에도 에러가 발생하지 않았지만, unknown 타입을 가지는 unknownX 변수의 경우 에러가 발생했음을 나타내고 있다.

이 부분은 각 타입에 대한 할당 가능성과 관련된 이야기인데, 타입스크립트 핸드북에서 assignability table을 확인할 수 있었다.

테이블을 확인해보니 any 타입의 경우에는 never타입에 할당하는 것을 제외하고는 모든 타입에 대해 할당받을 수도 할당할 수도 있다. 하지만 unknown 타입의 경우 any 타입과 의미 자체는 비슷하지만, 어떤 타입도 할당받을 수 있지만 any 타입을 제외하고는 다른 타입에 할당할 수 없다.

여기서 다시 한번 생각해볼만한 부분이 있다! "바로 any 타입을 제외하고는 다른 타입에 할당할 수 없다"는 부분이다. 이를 통해 우리는 unknown 타입이 any 타입보다 더 안전하다는 것을 알 수 있다. 아래 소스코드를 확인해보자.

let a: any;
a.toUpperCase();

let u: unknown;
u.toUpperCase(); // 🚨 error! 'u' is of type 'unknown'

어떤 타입이 들어올 지 모르기 때문에 u 변수는 다음과 같이 에러를 발생시키는 것을 확인할 수 있다. 그렇기에 우리는 u 타입에 대해 타입 좁히기를 적용하여 타입을 유추할 수 있다.

let u: unknown;

if (typeof u === "string") {
  u.toUpperCase(); // ✅
}

if (typeof u === "number") {
  u.toFixed(2); // ✅
}

🤔 any 타입도 타입 좁히기로 해결할 수 있지 않나요?

물론! any 타입도 타입 좁히기를 통해 unknown 타입처럼 사용할 수 있습니다.

let a: any;

if (typeof a === "string") {
  a.toUpperCase(); // ✅
}

if (typeof a === "number") {
  a.toFixed(2); // ✅
}

하지만 여기서 문제는 여기서 발생합니다!

let a: any;
a.toUpperCase(); // ✅

a 변수가 어떤 타입을 가지는지 알 수 없음에도 toUpperCase를 허용하고 있다는 것은 타입스크립트의 목적에 부합하지 않기 때문에 이를 반드시 방지하여야 합니다!

타입스크립트는 코드에 목적을 명시하고 목적에 맞지 않는 타입의 변수나 함수들에서 에러를 발생시켜 버그를 사전에 제거한다.

a가 어떤 타입이 들어오는지를 알 수 없다는 사실은 코드를 방치해두는 것과 같으며, 사전에 컴파일과정에서 목적에 맞지 않는 타입 에러를 확인하고 방지할 수 없습니다. 그렇기에 우리는 any 타입이 아닌 unknown 타입과 타입 좁히기를 이용하여 타입스크립트의 목적에 맞게 다뤄야 합니다!

개인적인 생각

근데 위 예제 코드는 내가 프로젝트에서 사용했었던 코드이거나, 혼자서 간단하게 만들어본 코드인데 unknown 타입을 사용해야 하는 상황에 어떤 것이 있는지 명확하게 와닿지 않는다.

특히 변수나 함수에는 모두 하나 이상의 타입이 존재하기 때문에, 이를 명확하게 하지 못한다는 것이 이해가 안된다. 단순히 다음과 같은 코드를 보더라도 타입을 정의할 수 있다.

const unknowns: (number | string | boolean | undefined | {})[] = [
  1,
  "hi",
  false,
  undefined,
  {},
];

그럼 unknown 타입이 언제 정확히 사용되는 걸까? 아직 타입스크립트를 이용한 프로젝트를 많이 진행해보지 않은터라 마주해본 경험이 없는 것 같다. 다음에는 프로젝트를 해보면서 unknown 타입이 사용되는 상황이 있다면 꼭 마주해보고 싶다.

참고

profile
Happy Day 😊❣️

0개의 댓글