본문으로 건너뛰기
← 블로그로 돌아가기
CS 기초>튜토리얼
TypeScript

TypeScript 입문: 타입으로 버그 줄이기

5분 읽기

undefined is not a function — JavaScript 개발자라면 한 번쯤 이 에러를 봤을 겁니다. 새벽 2시에 이 에러를 만나면 정말 힘이 빠집니다. TypeScript는 이런 종류의 버그를 코드 작성 시점에 잡아줍니다. 실행하기 전에.

TypeScript가 필요한 이유

JavaScript는 유연합니다. 너무 유연해서 문제입니다. 뭘 넣어도 일단 실행은 됩니다. 에러가 나는 건 나중이고, 그 나중이 보통 프로덕션 환경입니다.

function add(a, b) {
    return a + b;
}

add(1, 2);       // 3 — 의도한 결과
add("1", 2);     // "12" — 숫자를 더한 게 아니라 문자열이 합쳐짐
add(true, []);    // "true" — 이건 대체 무슨 결과인지...

타입이 없으니 숫자와 문자열을 더해도, boolean과 배열을 더해도 에러가 나지 않습니다. JavaScript가 알아서 타입을 변환해주기 때문입니다. 친절해 보이지만 사실은 위험합니다. 이런 버그는 테스트에서도 안 잡히다가, 실제 사용자가 이상한 데이터를 입력했을 때 터집니다.

function add(a: number, b: number): number {
    return a + b;
}

add(1, 2);       // 3
add("1", 2);     // 컴파일 에러! — 에디터에서 빨간 줄이 뜸

TypeScript를 쓰면 이 실수를 코드를 치는 순간 바로 확인할 수 있습니다. 배포 후가 아니라, 지금 여기서. 이게 TypeScript를 쓰는 가장 큰 이유입니다.

기본 타입 — 여기서부터 시작

TypeScript의 타입 시스템은 방대하지만, 실무에서 자주 쓰는 건 몇 가지입니다.

// 원시 타입 — 가장 기본
let name: string = "White";
let age: number = 23;
let isActive: boolean = true;

// 배열 — 타입 뒤에 []를 붙입니다
let scores: number[] = [90, 85, 92];
let tags: string[] = ["React", "TS"];

// 객체 — 각 프로퍼티의 타입을 명시
let user: { name: string; age: number } = {
    name: "White",
    age: 23,
};

: string처럼 변수 이름 뒤에 콜론과 타입을 적는 것이 TypeScript의 기본 문법입니다. JavaScript에 이 부분만 추가된 것이라고 생각하면 됩니다. 나머지는 JavaScript와 똑같습니다.

타입을 명시하면 자동 완성이 정확해집니다. user.까지 치면 IDE가 nameage를 제안합니다. 오타로 user.naem이라고 치면 바로 에러가 뜹니다. 코드를 치면서 실시간으로 검증받는 느낌입니다.

타입 추론 — 매번 안 써도 됩니다

사실 TypeScript는 꽤 똑똑합니다. 값을 보고 타입을 자동으로 추론합니다.

let count = 0;           // TypeScript가 number로 추론
let message = "hello";   // string으로 추론
let items = [1, 2, 3];   // number[]로 추론

매번 : number를 쓸 필요가 없습니다. TypeScript가 알아서 파악할 수 있는 경우에는 생략해도 됩니다. 하지만 함수의 매개변수나 복잡한 객체에는 명시적으로 타입을 적는 게 좋습니다. 나중에 코드를 읽는 사람(미래의 나 포함)을 위한 배려입니다.

Interface와 Type — 객체의 설계도

같은 구조의 객체를 여러 번 쓸 때, 매번 { name: string; age: number } 이렇게 쓰면 코드가 지저분해집니다. interface로 이름을 붙여서 분리하면 깔끔합니다.

interface Post {
    title: string;
    date: string;
    tags: string[];
    draft?: boolean;  // ?는 "있어도 되고 없어도 되는" optional 프로퍼티
}

function renderPost(post: Post) {
    console.log(post.title);    // OK
    console.log(post.author);   // 컴파일 에러! Post에 author가 없음
}

// 사용 예시
const myPost: Post = {
    title: "TypeScript 입문",
    date: "2026-02-19",
    tags: ["TypeScript"],
    // draft는 optional이니까 생략 가능
};

interface는 "이 객체는 반드시 이런 모양이어야 해"라고 약속하는 것입니다. 약속을 어기면 컴파일 에러가 발생합니다. 런타임에 터지는 것보다 훨씬 낫습니다.

interfacetype 중 어느 것을 쓸지는 팀 컨벤션에 따릅니다. 일반적인 가이드라인은 이렇습니다:

  • 객체 구조 정의에는 interface
  • 유니온이나 교차 타입에는 type

둘 다 거의 같은 일을 할 수 있어서, 초반에는 하나만 써도 무방합니다.

Union Type — 여러 가능성을 하나로

하나의 변수가 여러 타입 중 하나일 수 있을 때 사용합니다. | 기호로 타입을 연결합니다.

type Status = "loading" | "success" | "error";

function showMessage(status: Status) {
    switch (status) {
        case "loading":
            return "로딩 중...";
        case "success":
            return "완료!";
        case "error":
            return "실패했습니다.";
    }
}

showMessage("loading");  // OK
showMessage("pending");  // 컴파일 에러! "pending"은 Status에 없음

문자열 리터럴 유니온을 쓰면 오타를 컴파일 시점에 잡을 수 있습니다. "sucess"처럼 철자가 틀려도 바로 빨간 줄이 뜹니다. 이 패턴은 React에서 컴포넌트의 상태를 관리할 때 특히 유용합니다.

// 실무에서 자주 보는 패턴
type ButtonVariant = "primary" | "secondary" | "danger";
type Size = "sm" | "md" | "lg";

interface ButtonProps {
    variant: ButtonVariant;
    size: Size;
    children: React.ReactNode;
}

이렇게 하면 <Button variant="priamry" />같은 오타를 에디터가 즉시 잡아줍니다.

Generic — 재사용 가능한 타입

함수나 컴포넌트가 여러 타입에 대해 동작해야 할 때 Generic을 씁니다. 처음 보면 문법이 낯설지만, 개념은 단순합니다.

"이 함수는 어떤 타입이든 받을 수 있는데, 들어온 타입 그대로 나간다"는 뜻입니다.

// T는 "나중에 결정될 타입"을 의미하는 변수명 (보통 T를 씁니다)
function getFirst<T>(arr: T[]): T | undefined {
    return arr[0];
}

getFirst<number>([1, 2, 3]);     // 반환 타입이 number
getFirst<string>(["a", "b"]);    // 반환 타입이 string
getFirst([true, false]);          // TypeScript가 boolean으로 추론

<T>가 없었다면 any를 써야 할 것이고, 그러면 TypeScript를 쓰는 의미가 없어집니다. Generic은 "타입을 유지하면서도 유연하게" 코드를 작성하는 방법입니다.

React에서도 자주 만납니다:

// useState에서 Generic 사용
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);

// 초기값이 없거나 null일 수 있을 때 특히 유용

실전에서 자주 쓰는 패턴

API 응답 타입 정의

백엔드에서 데이터를 가져올 때 타입을 정의해두면 프론트엔드 코드가 훨씬 안전해집니다.

interface ApiResponse<T> {
    data: T;
    status: number;
    message: string;
}

interface User {
    id: number;
    name: string;
    email: string;
}

async function fetchUser(id: number): Promise<ApiResponse<User>> {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
}

// 사용할 때
const response = await fetchUser(1);
console.log(response.data.name);   // 자동 완성 동작!
console.log(response.data.phone);  // 컴파일 에러! User에 phone 없음

API 응답에 타입을 붙이면 response.data.까지만 쳐도 id, name, email이 자동 완성으로 나타납니다. 백엔드와 프론트엔드 사이의 계약서 역할을 하는 셈입니다. API 응답 구조가 바뀌면 프론트엔드 코드에서 컴파일 에러가 나니까, 놓치는 것 없이 전부 수정할 수 있습니다.

Narrowing — 타입을 좁혀가는 기술

TypeScript는 조건문 안에서 타입을 자동으로 좁혀줍니다. 이걸 narrowing이라고 합니다.

function printValue(value: string | number) {
    if (typeof value === "string") {
        // 이 블록 안에서는 value가 string인 걸 TypeScript가 알고 있음
        console.log(value.toUpperCase());  // string 메서드 사용 가능
    } else {
        // 여기서는 자동으로 number
        console.log(value.toFixed(2));     // number 메서드 사용 가능
    }
}

typeof, instanceof, in 연산자를 활용하면 타입 단언(as)을 쓰지 않고도 안전하게 코드를 작성할 수 있습니다. as는 "내가 타입을 알고 있으니 TypeScript는 믿어"라고 강제하는 것이라 가급적 피하는 게 좋습니다.

// 실전 예시: API 에러 처리
interface SuccessResponse {
    status: "success";
    data: User;
}

interface ErrorResponse {
    status: "error";
    message: string;
}

type ApiResult = SuccessResponse | ErrorResponse;

function handleResult(result: ApiResult) {
    if (result.status === "success") {
        // TypeScript가 SuccessResponse로 좁혀줌
        console.log(result.data.name);
    } else {
        // ErrorResponse로 좁혀줌
        console.log(result.message);
    }
}

이 패턴은 실무에서 정말 많이 씁니다. API 호출 결과를 성공/실패로 나눠서 처리할 때, TypeScript가 각 분기에서 어떤 프로퍼티에 접근할 수 있는지 자동으로 알려줍니다.

시작하는 방법

기존 JavaScript 프로젝트에 TypeScript를 도입할 때, 한 번에 전부 바꿀 필요는 없습니다. 이 점이 중요합니다.

tsconfig.json에서 strict: false로 시작하세요. 그리고 파일 하나를 골라 확장자를 .js에서 .ts로 바꿔봅니다. 에러가 뜨면 하나씩 타입을 추가합니다. 타입의 이점을 체감하면 자연스럽게 다음 파일로 넘어가게 되고, 어느 순간 strict: true로 전환하게 됩니다.

# TypeScript 설치
npm install -D typescript

# tsconfig.json 생성
npx tsc --init

# 컴파일
npx tsc

지금 진행 중인 JavaScript 프로젝트가 있다면, 가장 작은 파일 하나의 확장자를 .ts로 바꿔보세요. 그게 첫걸음입니다.

관련 포스트