자바스크립트(JavaScript)로만 프로젝트를 진행하다가 안정성을 높이기 위해 타입스크립트(TypeScript)를 처음 도입했을 때, 기쁨보다 당혹감이 훨씬 컸습니다.

분명 자바스크립트에서는 아무 문제 없이 잘 돌아가던 코드인데, 타입스크립트를 적용하자마자 VS Code 에디터 온 동네에 빨간 줄(Type Error)이 그어지며 화면이 온통 경고 메시지로 뒤덮였기 때문입니다.

당시에는 "개발 속도만 늦어지는 것 같은데 왜 쓸까?" 싶었지만, 타입 에러를 하나씩 해결해 나가면서 런타임(실행 시점)에서 터질 위험한 버그를 코드 작성 단계에서 미리 잡아주는 강력함을 체감하게 되었습니다.

오늘은 제가 타입스크립트 초보 시절 가장 자주 마주치며 식은땀을 흘렸던 대표적인 타입 에러 3가지와 완벽한 해결법을 정리해 보려고 합니다.

1. 에러 1: Object is possibly 'null' or 'undefined'

API 데이터를 받아오거나 DOM 요소를 참조할 때 가장 흔하게 만나는 에러입니다.

TypeScript
 
// ❌ 빨간 줄이 뜨는 과거의 코드
const nameLength = response.user.name.length;
// 🚨 Error: 'response.user' is possibly 'undefined'.

💡 원인 및 해결책

타입스크립트는 response.user 데이터가 서버 응답 지연이나 에러로 인해 undefined 상태일 가능성을 미리 경고해 주는 것입니다. 만약 이 상태로 자바스크립트에서 실행되었다면 런타임에 그 악명 높은 Cannot read properties of undefined 에러가 터졌을 것입니다.

해결을 위해 옵셔널 체이닝(?.)이나 타입 가드(Type Guard)를 활용하면 깔끔하게 해결됩니다.

TypeScript
 
// ⭕ 옵셔널 체이닝으로 안전하게 접근
const nameLength = response.user?.name?.length;

// ⭕ 또는 타입 가드(if문)로 안전성 보장
if (response.user && response.user.name) {
  const nameLength = response.user.name.length;
}

2. 에러 2: Property 'X' does not exist on type 'Y' (이벤트 객체 / 객체 키 접근 에러)

리액트에서 onChange 이벤트를 처리하거나, 객체의 키(Key)로 동적 접근을 시도할 때 자주 발생하는 에러입니다.

TypeScript
 
// ❌ e.target.value에서 빨간 줄이 뜨는 코드
const handleChange = (e) => {
  console.log(e.target.value);
  // 🚨 Error: Parameter 'e' implicitly has an 'any' type.
};

 

타입을 잘 지정해 에러가 안나는 모습

💡 원인 및 해결책

타입스크립트는 매개변수 e가 어떤 이벤트 객체인지 알지 못하므로 암묵적인 any 타입 경고를 보냅니다. 리액트에서 제공하는 이벤트 타입(React.ChangeEvent 등)을 명시해 주어야 합니다.

TypeScript
 
// ⭕ React의 HTMLInputElement 타입을 정확히 명시
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value); // 이제 에러 없이 자동완성까지 지원됨!
};

3. 에러 3: Type 'X' is not assignable to type 'Y' (상태 초기값 타입 불일치)

useState를 사용할 때 초기값 타입과 나중에 넣어줄 API 데이터의 타입이 달라서 발생하는 에러입니다.

TypeScript
 
// ❌ null로 초기화해 두었더니 나중에 객체를 넣을 때 에러 발생
interface UserProfile {
  id: number;
  name: string;
}

const [user, setUser] = useState(null);

// 나중에 API 응답 후 setUser 호출 시 에러!
setUser({ id: 1, name: '김개발' }); 
// 🚨 Error: Argument of type '{ id: number; name: string; }' is not assignable to parameter of type 'null'.

💡 원인 및 해결책

useState(null)로 작성하면 타입스크립트는 user 변수의 타입을 오직 null로만 추론해 버립니다. 제네릭(<T>)을 사용해 "초기엔 null이지만 나중에 UserProfile 객체가 들어올 수 있다"라고 명시해 주어야 합니다.

TypeScript
 
// ⭕ 제네릭과 유니온 타입을 활용해 두 가지 가능성을 모두 열어둠
const [user, setUser] = useState<UserProfile | null>(null);

// 이제 객체를 넣어도 빨간 줄 없이 정상 동작!
setUser({ id: 1, name: '김개발' });

 

useState에 타입추가

4. 회고 및 느낀 점

타입스크립트를 처음 시작했을 때는 일일이 타입을 적어주는 행위가 개발 속도를 갉아먹는 걸림돌처럼 느껴졌습니다.

하지만 수많은 빨간 줄과 싸우고 해결해 나가는 과정에서 "내가 작성하는 코드의 데이터 흐름을 훨씬 객관적이고 안전하게 파악하는 습관"을 가질 수 있게 되었습니다.

지금 타입스크립트의 빨간 줄 때문에 스트레스를 받고 계신 초보 개발자분들이 계시다면, 에러 메시지를 무서워하지 마시고 "타입스크립트가 미래의 런타임 에러를 대신 막아주고 있구나!" 하고 긍정적으로 바라보시면 될 것 같습니다!

타입스크립트(TypeScript)를 처음 접하면 가장 먼저 배우는 단어가 있습니다. 바로 any입니다. 어떤 타입이든 허용한다는 마법 같은 단어지만, 아이러니하게도 any를 남발하는 순간 우리는 타입스크립트를 사용하는 가장 큰 이유인 '타입 안전성'을 포기하게 됩니다.

실무에서 외부 API 응답이나 동적 데이터를 다룰 때, 우리는 데이터의 정확한 형태를 알 수 없는 상황에 직면합니다. 이때 습관적으로 any를 쓰기보다 더 안전하고 견고한 unknown 타입을 선택해야 합니다. 오늘은 왜 unknown이 any보다 우월한지, 그리고 unknown 데이터를 안전하게 요리하는 타입 가드(Type Guard) 기법을 상세히 알아보겠습니다.


1. any: 모든 안전장치를 해제하는 위험한 열쇠

any는 타입스크립트의 타입 검사기(Type Checker)를 완전히 무력화합니다.

TypeScript
 
let value: any = 10;
value.toUpperCase(); // 컴파일 타임에 에러가 발생하지 않음 -> 런타임에 에러 발생!

위 코드에서 value는 숫자지만 any 타입이기 때문에 문자열 메서드인 toUpperCase 호출을 허용합니다. 결국 이 오류는 사용자가 앱을 실행하는 도중 "Uncaught TypeError"라는 이름으로 터지게 됩니다. 사실상 자바스크립트를 쓰는 것과 다를 바 없는 상태가 되는 것이죠.


2. unknown: "무엇인지 모르니 확인하고 쓰세요"

unknown 타입은 any와 마찬가지로 모든 값을 할당받을 수 있습니다. 하지만 결정적인 차이점은 '사용할 때' 발생합니다.

2.1 unknown의 제약 사항

unknown 타입으로 정의된 변수는 타입을 정적으로 확정하기 전까지는 어떠한 동작도 수행할 수 없습니다.

TypeScript
 
let value: unknown = 10;

// 에러 발생: 'value'의 형식이 'unknown'입니다.
value.toUpperCase(); 

// 타입을 확인해야만 사용 가능
if (typeof value === "string") {
  console.log(value.toUpperCase()); // 이제 안전하게 사용 가능!
}

이처럼 unknown은 개발자에게 "이 데이터의 타입을 먼저 검증하라"라고 강제합니다. 컴파일러가 우리 대신 안전장치를 한 번 더 확인해 주는 셈입니다.


3. unknown을 안전하게 다루는 법: 타입 가드(Type Guard)

unknown 데이터의 정체를 밝히기 위해 사용하는 기법이 바로 타입 가드입니다.

3.1 typeof 연산자 (기본 타입 검사)

숫자, 문자열, 불리언 등 자바스크립트의 기본 타입을 확인할 때 사용합니다.

3.2 instanceof 연산자 (클래스/객체 검사)

특정 클래스의 인스턴스인지 확인할 때 유용합니다.

TypeScript
 
function processDate(input: unknown) {
  if (input instanceof Date) {
    console.log(input.toISOString()); // Date 객체임이 보장됨
  }
}

3.3 사용자 정의 타입 가드 (Type Predicates)

가장 강력하고 실무에서 많이 쓰이는 방식입니다. is 키워드를 사용하여 함수가 특정 타입임을 명시적으로 알려줍니다.

TypeScript
 
interface User {
  name: string;
  age: number;
}

// User 타입인지 확인하는 가드 함수
function isUser(target: any): target is User {
  return (
    typeof target === "object" &&
    target !== null &&
    "name" in target &&
    "age" in target
  );
}

function greet(input: unknown) {
  if (isUser(input)) {
    console.log(`안녕하세요, ${input.name}님!`); // 여기서 input은 자동으로 User 타입이 됨
  }
}

4. 실무 예시: API 에러 처리

API 통신 중 발생하는 에러(catch 문의 error 객체)는 기본적으로 unknown 타입(TS 4.4 이상)입니다. 이때 unknown과 타입 가드를 활용하면 매우 견고한 에러 처리가 가능합니다.

TypeScript
 
try {
  await fetchData();
} catch (error: unknown) {
  if (error instanceof AxiosError) {
    // axios 에러인 경우 처리
    console.error(error.response?.data.message);
  } else if (error instanceof Error) {
    // 일반 에러인 경우 처리
    console.error(error.message);
  } else {
    // 그 외의 경우
    console.error("알 수 없는 에러 발생", error);
  }
}

이전처럼 (error as any). message라고 썼을 때 발생할 수 있는 런타임 폭탄을 원천 차단할 수 있습니다.


5. any는 절대 쓰면 안 되나요?

현실적으로 any가 필요한 순간도 있습니다.

  • 마이그레이션 중인 프로젝트에서 타입을 일일이 지정하기 벅찰 때.
  • 타입 정의가 너무 복잡하여 생산성을 심각하게 해칠 때.
  • 제네릭 등으로 도저히 해결 안 되는 복잡한 고차 함수를 짤 때.

하지만 이는 어디까지나 '임시방편'이어야 합니다. any를 썼다면 반드시 TODO 주석을 남기고 나중에 적절한 타입으로 교체해야 합니다.

+ Recent posts