TypeScript 실무 타입 해설팩 01

자료 유형: 무료 실무 해설팩

TypeScript 문법은 조금 알지만 API, 상태, optional 값에서 자주 막히는 학습자를 위한 실무 해설팩입니다. 타입 문법 암기가 아니라 실제 화면 버그를 줄이는 타입 설계 기준을 제공합니다.

이 팩의 약속

이 자료는 타입 이름을 외우게 하지 않는다. 실제 프론트엔드에서 자주 생기는 런타임 버그를 TypeScript로 어떻게 줄이는지 설명한다.

  • API 상태를 여러 boolean으로 나눠 모순 상태를 만드는 문제
  • 외부 JSON에 as User를 바로 붙이는 문제
  • optional 값을 기본값 없이 렌더링하는 문제
  • 제네릭 응답 타입을 만들지 않아 API마다 타입이 흩어지는 문제

예제 1. API 상태 모델링

연결 문제: https://nst21c.com/?task=tsDiscriminatedUnion#labApp

흔한 실패 코드

type UserListState = {
  loading: boolean;
  error: string | null;
  users: User[] | null;
};

이 구조는 loading: true, error: "실패", users: [...]가 동시에 가능하다. 화면에서는 로딩 스피너, 에러 메시지, 목록이 동시에 보이는 식으로 깨질 수 있다.

개선 코드

type UserListState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; users: User[] }
  | { status: "empty" }
  | { status: "error"; message: string };

function getUserListMessage(state: UserListState) {
  switch (state.status) {
    case "loading":
      return "사용자 목록을 불러오는 중입니다.";
    case "success":
      return `${state.users.length}명의 사용자를 찾았습니다.`;
    case "empty":
      return "조건에 맞는 사용자가 없습니다.";
    case "error":
      return state.message;
    default:
      return "검색 조건을 입력하세요.";
  }
}

실무 화면 예

  • 검색 결과
  • 관리자 목록
  • 상품 목록
  • 대시보드 요약 카드

코드 리뷰 체크포인트

  • 상태 이름 하나로 화면 분기가 가능한가?
  • 성공 상태가 아닐 때 users에 접근하지 못하게 되어 있는가?
  • 빈 목록과 에러를 구분하는가?

예제 2. unknown JSON 검증

연결 문제: https://nst21c.com/?task=tsUnknownError#labApp

흔한 실패 코드

const user = (await response.json()) as User;
return user.name.toUpperCase();

서버가 { nickname: "Kim" }을 보내도 TypeScript는 막지 못한다. as User는 런타임 검증이 아니라 컴파일러에게 믿으라고 말하는 것이다.

개선 코드

type User = {
  id: number;
  name: string;
};

function isUser(value: unknown): value is User {
  if (typeof value !== "object" || value === null) return false;
  const candidate = value as Record<string, unknown>;
  return typeof candidate.id === "number" && typeof candidate.name === "string";
}

async function loadUser(response: Response) {
  const data: unknown = await response.json();
  if (!isUser(data)) {
    throw new Error("사용자 응답 형식이 올바르지 않습니다.");
  }
  return data;
}

실무 화면 예

  • 외부 API 응답
  • localStorage 복원
  • URL query 파싱
  • CSV/엑셀 import

코드 리뷰 체크포인트

  • 외부 데이터는 unknown으로 받은 뒤 좁히는가?
  • 필수 필드의 존재와 타입을 확인하는가?
  • 실패했을 때 호출부가 처리할 수 있는 에러를 던지는가?

예제 3. optional 필드와 기본값

연결 문제: https://nst21c.com/?task=tsOptional#labApp

흔한 실패 코드

type Profile = {
  id: number;
  name: string;
  nickname?: string;
};

function getDisplayName(profile: Profile) {
  return profile.nickname.toUpperCase();
}

nickname은 없을 수 있다. 그런데 바로 메서드를 호출하면 런타임 오류가 난다.

개선 코드

function getDisplayName(profile: Profile) {
  return (profile.nickname ?? profile.name).toUpperCase();
}

실무 화면 예

  • 닉네임 없는 사용자 프로필
  • 썸네일 없는 상품 카드
  • 소개글 없는 작성자 카드
  • 선택하지 않은 필터 값

코드 리뷰 체크포인트

  • optional 필드에 접근하기 전에 기본값을 정했는가?
  • ||??의 차이를 알고 쓰는가?
  • 화면에 빈 값이 보일 때 사용자에게 자연스러운 대체 문구가 있는가?

예제 4. 제네릭 API 응답 타입

연결 문제: https://nst21c.com/?task=tsApiResponse#labApp

흔한 실패 코드

type UserResponse = {
  data: User;
  error: string | null;
};

type ProductResponse = {
  data: Product;
  error: string | null;
};

API마다 같은 구조를 복사하면 나중에 meta, status, message 같은 공통 필드가 바뀔 때 여러 타입을 다 고쳐야 한다.

개선 코드

type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

type UserResponse = ApiResponse<User>;
type ProductResponse = ApiResponse<Product>;

function getUserName(response: UserResponse) {
  if (!response.ok) return "사용자를 불러오지 못했습니다.";
  return response.data.name;
}

실무 화면 예

  • 목록 API
  • 상세 API
  • 검색 API
  • 저장/수정 API

코드 리뷰 체크포인트

  • 반복되는 응답 구조를 제네릭으로 묶었는가?
  • 성공/실패 응답을 타입으로 구분하는가?
  • 실패 상태에서 data를 읽을 수 없게 되어 있는가?

5일 복습 루틴

1일차: API 상태 모델링 문제를 풀고 boolean 상태를 union으로 바꾼다. 2일차: unknown JSON 문제를 풀고 검증 함수를 작성한다. 3일차: optional 필드 문제를 풀고 기본값 기준을 정한다. 4일차: ApiResponse 제네릭 문제를 풀고 성공/실패를 분리한다. 5일차: 네 예제를 하나의 사용자 목록 화면 타입으로 합친다.

이 팩 다음에 보낼 후속 안내

TypeScript 팩을 받은 사용자는 React에서 이 타입을 실제 UI 상태와 연결하는 연습이 필요하다. 후속 메일에서는 React form validation, loading/error/retry 화면으로 연결한다.