타입스크립트 | type assertion(타입 단언)을 남발하면 안되는 이유

Soohyeok Kim·2024년 9월 24일
post-thumbnail

타입스크립트는 자바스크립트의 확장 언어로, 정적 타입 시스템을 도입하여 코드의 안정성과 유지 보수성을 향상시킵니다.

그러나 타입스크립트에서 제공하는 타입 단언(Type Assertion) 기능을 남발하면 이러한 장점이 퇴색될 수 있습니다.

타입 단언의 개념과 남발했을 때 발생하는 문제점, 그리고 타입 단언이 왜 존재하며 언제 사용하는 것이 적절할까요

타입 단언(Type Assertion)이란?

타입 단언은 개발자가 특정 값이 어떤 타입임을 as 키워드를 사용하여 컴파일러에게 직접 알려주는 방법입니다.
즉 컴파일러의 타입 추론 결과와 관계없이 개발자가 지정한 타입으로 간주하게 합니다.

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

위 예제에서 someValueany 타입이지만, as string을 통해 문자열로 단언하여 String 프로토타입의 length 프로퍼티에 접근하고 있습니다.
(물론 당연하게도 any타입으로 타입지정을 하지 않아도 someValue는 컴파일러가 string이라고 추론을 합니다.)

그럼 타입 단언은 왜 존재할까?

타입 단언은 타입스크립트의 타입 시스템이 모든 상황을 완벽하게 커버할 수 없기 때문에 존재합니다.

즉 컴파일러가 다룰 데이터가 유니온 타입이거나 해서 타입을 정확히 추론할 수 없어서 개발 안정성이 떨어질 여지가 있는 경우 개발자가 직접 타입 정보를 제공하여 컴파일러를 보조하는 역할을 할 수 있습니다.

중요한 점은 타입 단언은 컴파일 타임에만 존재하며 런타임에는 제거된다는 것입니다.
코드의 실행에는 영향을 주지 않고 오로지 컴파일러에게 타입 정보를 제공하는 용도로만 사용됩니다.

왜 타입 단언을 남발하면 안되는가

1. 타입 안전성의 훼손

타입 단언을 남발하면 타입스크립트을 쓰는 가장 큰 이유 중 하나인 타입 안전성이 훼손됩니다.
잘못된 타입 단언은 컴파일러가 오류를 감지하지 못하게 하여 런타임 에러로 이어질 수 있습니다. (강력한 타입추론 기능을 이용하지 못하게 됩니다.)

interface Human {
  name: string;
  age: number;
}

let human = { name: "김수혁" } as Human;

console.log(human.age.toFixed(27)); // 런타임 에러: Cannot read property 'toFixed' of undefined

이 예시에서 human 객체는 age 프로퍼티가 없음에도 불구하고 Human 타입으로 단언되어 컴파일러는 오류를 감지하지 못하고 age에 대한 자동완성도 제공합니다.
그러나 런타임에서는 ageundefined이기 때문에 에러가 발생합니다.

2. 디버깅과 유지 보수의 어려움

타입 단언은 컴파일러의 타입 체크를 우회하기 때문에 컴파일에러를 발생시키지 않아서
버그가 발생해도 편집기를 들여다보는것으로는 어디서 버그가 발생했는지 알기 쉽지 않습니다. 특히 대규모 프로젝트에서는 작은 타입 오류가 치명적인 문제로 발전할 수 있습니다.

3. 코드의 가독성 저하

이건 2번과 연관되는 부분인데 빈번한 타입 단언은 코드의 가독성을 떨어뜨립니다.
다른 개발자가 코드를 이해하고 유지 보수하기 어려워지며 이는 팀 작업에 부정적인 영향을 미칩니다.
특히 코드가 길고 복잡할수록 중간중간 튀어나오는 타입단언은 코드해석의 피로감을 늘릴 수 있습니다.

타입단언은 언제언제 사용할 수 있나요?

타입 단언은 아래와 같은 상황에서 유용하게 사용될 수 있습니다.

1. DOM 요소 접근 시

타입스크립트는 document.getElementById의 반환 타입을 HTMLElement | null로 추론합니다.
이때 특정 요소가 존재한다고 확신하는 경우 타입 단언을 통해 null이 아님을 명시할 수 있습니다.

const inputElement = document.getElementById("username") as HTMLInputElement;

inputElement.value = "Hello";

2. 써드파티 라이브러리와 통합

Drizzle 라이브러리를 사용할 때를 예시로 들어보겠습니다.

Drizzle ORM을 사용하여 데이터베이스 스키마를 정의할 때 특정 필드를 JSON 타입으로 설정하는 경우가 있습니다.

그러나 Drizzle의 타입 정의는 해당 필드에 대한 구체적인 타입 정보를 제공하지 않을 수 있습니다.

import { pgTable, json } from "drizzle-orm/pg-core";
import { InferSelectModel, eq } from "drizzle-orm";

// drizzle ORM 스키마 정의
const users = pgTable("users", {
  id: uuid("id").primaryKey(),
  name: varchar("name"),
  settings: json("settings"), // JSON 필드
});
                  
// UserSettings 인터페이스 정의
interface UserSettings {
  theme: string;
  notificationsEnabled: boolean;
}

// 함수의 입력 및 반환 타입 정의
interface GetUserParams {
  id: string;
}

interface GetUserResult {
  id: string;
  name: string;
  settings: UserSettings;
}

// 데이터베이스에서 사용자 데이터 가져오기
async function getUser({ id }: GetUserParams): Promise<GetUserResult> {
  const user = await db
  	  .select()
      .from(users)
      .where(eq(users.id, id))
      .execute()
      .then((results) => results[0]);

  // settings 필드의 타입이 any로 추론될 경우
  const settings = user.settings as UserSettings; // UserSettings로 타입 단언

  console.log(settings.theme); // settings의 프로퍼티에 안전하게 접근할 수 있음

  return {
   	id: user.id,
    name: user.name,
    settings, // 타입이 일치하기 때문에 컴파일에러없이 return 가능
  }
}

위 예제에서 settings 필드는 JSON 타입이므로 타입스크립트는 그 내부 구조를 알 수 없어 any로 추론할 수 있습니다.

이때 UserSettings 인터페이스를 정의하고 as UserSettings로 타입 단언하여 컴파일러에게 타입 정보를 제공하면 settings 객체의 프로퍼티에 타입 안전하게 접근할 수 있으며, 컴파일러의 타입 체크도 통과할 수 있습니다.

물론 예시로 이렇게 코드를 해놨을 뿐 drizzle의 schema를 $type기능을 사용해서 settings의 타입을 구체화 시키거나 customType기능을 이용하는등의 방법이 타입추론 기능을 이용할 수 있어서 더 좋습니다.

3. 타입이 확실한 경우

API 호출 등에서 반환된 데이터의 구조를 확실히 알고 있을 때는 타입 단언을 사용하여 코드의 명확성을 높일 수 있습니다.

interface ApiResponse {
  data: string;
}

async function fetchData() {
  const response = await fetch("/api/data");
  
  const result = (await response.json()) as ApiResponse;
  
  console.log(result.data); // data는 string 타입
}

API의 응답 구조가 확실히 ApiResponse 인터페이스를 따른다면 타입 단언을 통해 코드의 가독성과 안전성을 향상시킬 수 있습니다.

4. 일시적으로 타입 체크를 회피하고 싶을 때

타입스크립트 개발에 as any 테크닉(?)이 있습니다.
타입스크립트의 엄격한 타입 체크를 일시적으로 회피하기 위해 사용되는 방법인데
어떤 값에 대해 as any를 사용하면 해당 값은 any 타입으로 간주되어 어떤 프로퍼티나 메서드에도 접근할 수 있게 됩니다.

let value: unknown = getValueFromUnknown();
let result = value as any as SpecificType;

이 방법은 써드파티 라이브러리나 오래된 코드와의 통합 과정에서 타입 정보를 얻기 어려울 때나 복잡할 때 일시적으로 타입 체크를 회피하고자 사용할 수 있습니다.

혹은 프로토타이핑 단계에서 빠르게 개발을 진행하기 위해 엄격한 타입 체크를 일시적으로 무시하고자 할 때 유용할 수 있습니다.

그렇지만 as any를 사용하면 타입스크립트의 타입 시스템을 완전히 무시하게 되어
언젠가 발생할지도 모르는 런타임 오류의 위험이 증가하고 타입 오류를 컴파일 타임에 잡아내지 못하므로 문제 발생 시 디버깅이 어려워 다른 개발자가 코드를 이해하고 수정하기 어려워집니다.

따라서 가능한 한 사용을 피하고 꼭 필요할 때에만 신중하게 사용해야 합니다.

올바르게 사용하려면..

타입 단언은 필요한 경우에만 최소한으로 사용하고 가능한 한 타입스크립트의 타입 추론 기능과 명시적 타입 선언을 활용해야 합니다.

또한 타입 단언을 사용하기 전에 타입 가드를 사용하여 타입 단언을 강제로 사용하지 않고도 안전하게 코드에서 해당 타입을 다룰 수 있게 할 수도 있습니다.

// 타입 가드
function isHTMLInputElement(element: HTMLElement): element is HTMLInputElement {
  return 'value' in element;
}

const element = document.getElementById("username");

if (element && isHTMLInputElement(element)) {
  element.value = "Hello"; // element는 HTMLInputElement 타입으로 추론됨
}

결론

타입 단언은 타입스크립트에서 유용한 도구이지만 남발하면 매우매우 강력한 타입스크립트의 이점을 잃게 됩니다.
타입스크립트의 강력한 타입 시스템을 최대한 활용하여 안정적이고 유지 보수하기 쉬운 코드를 작성하기 위해서는 타입 단언의 사용을 최소화하고 타입 가드를 사용해보며 신중히 사용하면 되겠습니다.

profile
백엔드 개발자

0개의 댓글