본문으로 건너뛰기

마무리 — 새 갈래·옮기는 길·다음 걸음

타입스크립트 목차작은 프로그램 만들기레슨 51 / 51

목차

프로그램이 완성된 뒤에도 바깥 파일에서 깨진 주문 줄이 섞여 들어오거나, 기프트 카드(Gift) 같은 새 주문 갈래가 더해지기도 합니다. 확인 함수(isOrder)와 판별 유니언·never를 세워 두었다면 깨진 줄은 멈춤 없이 건너뛰고, 새 갈래를 더했을 때는 검사기가 고칠 switch 자리를 빠짐없이 알려 줍니다.

마지막 레슨에서는 깨진 줄이 섞인 입력으로 영수증을 찍어 보고, 새 갈래 Gift를 추가한 뒤 기존 자바스크립트 코드를 타입스크립트로 옮기는 차례와 다음 걸음을 정리합니다.

앞 레슨 코드 펼치기

함수 하나씩 검사하며 만들기에서 완성한 타입 선언입니다.

interface Drink {
  kind: "drink";
  price: number;
  extra: number;
}
interface Food {
  kind: "food";
  price: number;
}
type Order = Drink | Food;
interface Summary {
  total: number;
  skipped: number;
}

6단계 중 1단계. 1~11번 줄: Payment에 "gift" 갈래를 더했지만, payBroken의 switch 문은 기존 "cash"와 "card" 두 갈래만 처리합니다.

1깨진 주문 줄을 건너뛰고 영수증 찍기

실제 주문 기록에는 끊긴 JSON 글('{"kind":')이나 글자 가격 줄이 섞일 수 있습니다. 앞 레슨의 parseRaw와 isOrder가 이를 어떻게 거르는지 확인합니다.

interface Drink {
  kind: "drink";
  price: number;
  extra: number;
}
interface Food {
  kind: "food";
  price: number;
}
type Order = Drink | Food;
interface Summary {
  total: number;
  skipped: number;
}
function isDrink(
  v: unknown,
): v is Drink {
  return (
    typeof v === "object" &&
    v !== null &&
    "kind" in v &&
    v.kind === "drink" &&
    "price" in v &&
    typeof v.price ===
      "number" &&
    "extra" in v &&
    typeof v.extra === "number"
  );
}
function isFood(
  v: unknown,
): v is Food {
  return (
    typeof v === "object" &&
    v !== null &&
    "kind" in v &&
    v.kind === "food" &&
    "price" in v &&
    typeof v.price === "number"
  );
}
function isOrder(
  v: unknown,
): v is Order {
  return (
    isDrink(v) || isFood(v)
  );
}
function parseRaw(
  s: string,
): unknown {
  try {
    return JSON.parse(s);
  } catch {
    return null;
  }
}
function cost(o: Order) {
  switch (o.kind) {
    case "drink":
      return o.price + o.extra;
    case "food":
      return o.price;
    default:
      const left: never = o;
      return left;
  }
}

이제 정상 주문 두 줄과 깨진 줄 하나를 넣어 영수증을 찍어 봅니다.

const lines = [
  '{"kind":"drink",' +
    '"price":4500,"extra":500}',
  '{"kind":',
  '{"kind":"food",' +
    '"price":2000}',
];
let total = 0;
let skip = 0;
for (const s of lines) {
  const v = parseRaw(s);
  if (isOrder(v)) {
    total += cost(v);
  } else skip += 1;
}
console.log(`합계 ${total}원`);
console.log(`건너뜀 ${skip}건`);
실행 결과
합계 7000원
건너뜀 1건

둘째 줄 '{"kind":'에서 JSON.parse가 던진 오류는 parseRaw가 잡아 null로 바꾸고, isOrder(null)이 false를 돌려주어 프로그램이 멈추지 않고 건너뜀 1건으로 기록됩니다.

2새 갈래 Gift 더하고 never가 짚어 주는 곳 고치기

기프트 카드(kind: "gift", amount: number) 갈래를 새로 더합니다. 타입만 더하고 cost의 switch를 안 고치면 어떻게 될까요?

interface Drink {
  kind: "drink";
  price: number;
  extra: number;
}
interface Food {
  kind: "food";
  price: number;
}
interface Gift {
  kind: "gift";
  amount: number;
}
type Order =
  | Drink
  | Food
  | Gift;
function cost(o: Order) {
  switch (o.kind) {
    case "drink":
      return o.price + o.extra;
    case "food":
      return o.price;
    default:
      const left: never = o;
      return left;
  }
}
검사 결과 오류 1개
main.ts(25,13): error TS2322: Type 'Gift' is not assignable to type 'never'.

default 갈래의 const left: never = o; 줄(25번 줄)에서 Gift 갈래가 처리되지 않았다고 정확히 짚어 줍니다.

검사기가 알려 준 자리에 case "gift": return o.amount;⁠를 보태면 다시 검사 오류가 0이 됩니다.

interface Drink {
  kind: "drink";
  price: number;
  extra: number;
}
interface Food {
  kind: "food";
  price: number;
}
interface Gift {
  kind: "gift";
  amount: number;
}
type Order =
  | Drink
  | Food
  | Gift;
function cost(o: Order) {
  switch (o.kind) {
    case "drink":
      return o.price + o.extra;
    case "food":
      return o.price;
    case "gift":
      return o.amount;
    default:
      const left: never = o;
      return left;
  }
}
const items: Order[] = [
  {
    kind: "drink",
    price: 4500,
    extra: 500,
  },
  { kind: "food", price: 2000 },
  {
    kind: "gift",
    amount: 5000,
  },
];
let sum = 0;
for (const it of items) {
  sum += cost(it);
}
console.log(`합계 ${sum}원`);
실행 결과
합계 12000원

3가진 JS 코드를 TS로 옮기는 차례와 다음 걸음

만들어 둔 자바스크립트 프로그램(예: 할 일 앱)을 타입스크립트로 옮길 때는 아래 네 걸음을 차례로 밟습니다.

단계할 일확인 기준
1. 파일 하나씩 .ts로가장 작은 파일 하나만 확장자를 .ts로 바꾸고 tsconfig.json을 둔다바꾼 파일에서 npx tsc 실행
2. 데이터 모양 먼저프로그램이 다루는 핵심 객체 모양을 interface와 판별 유니언으로 먼저 적는다타입 선언 문법 오류 0
3. 매개변수와 빈 그릇함수 매개변수(TS7006)와 빈 배열·빈 객체(let list: Todo[] = [])⁠에 타입을 붙인다함수마다 npx tsc 오류 0
4. 바깥 경계와 strictJSON.parse나 querySelector 자리에 확인 함수·instanceof를 넣고 strict 검사를 통과시킨다전체 프로젝트 검사 오류 0 + 실행 확인

열한 단원을 통해 일곱 가지 검사 규칙과 설계법을 모두 익혔습니다. 다음 걸음으로는 props와 상태(useState)를 타입으로 설계하는 React, 또는 서버와 브라우저의 데이터 경계를 잇는 Next.js와 Node 백엔드로 나아갈 수 있습니다.

예제

검사 결과와 실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.

할 일 앱의 자바스크립트 코드를 .ts로 옮기면서, 할 일 모양 Todo를 선언하고 완료되지 않은 할 일만 세는 countLeft 함수를 적었습니다.

interface Todo {
  title: string;
  done: boolean;
}
function countLeft(
  list: Todo[],
): number {
  let left = 0;
  for (const t of list) {
    if (!t.done) left += 1;
  }
  return left;
}
const todos: Todo[] = [
  { title: "우유", done: true },
  { title: "운동", done: false },
];
console.log(countLeft(todos));

검사를 지나갈까요? 실행하면 무엇이 찍힐까요?

검사 결과와 실행 결과 보기
검사 결과 오류 없음
실행 결과
1

interface Todo로 모양을 먼저 적고 매개변수에 list: Todo[]를 붙였으므로 검사 오류 0으로 통과하고, 완료되지 않은 할 일 개수인 1이 출력됩니다.

자주 틀리는 곳

틀린 생각: 기존 자바스크립트 폴더의 모든 .js 파일을 한꺼번에 .ts로 바꾸고 시작해야 한다.

파일 열 개를 한꺼번에 .ts로 바꾸면 검사 오류가 쏟아져 어디부터 고칠지 찾기 어렵습니다.

맞는 규칙: 다른 파일에 의존하지 않는 작은 유틸리티나 데이터 파일 하나부터 .ts로 바꾸어 검사 오류 0을 만든 뒤 하나씩 넓혀 갑니다.

틀린 생각: 처음부터 noUncheckedIndexedAccess 같은 추가 엄격 옵션까지 한꺼번에 다 켜고 옮긴다.

첫날부터 추가 옵션을 다 켜면 핵심 타입보다 부수적인 인덱스 오류를 고치는 데 시간을 쓰게 됩니다.

맞는 규칙: 먼저 과목 기본 설정("strict": true)을 통과시킨 뒤, 프로그램이 안정되면 noUncheckedIndexedAccess 같은 추가 옵션을 하나씩 켜서 빈틈을 조입니다.

틀린 생각: 옮기는 도중 귀찮은 자리는 일단 any나 as로 두고 나중에 고치면 된다.

'임시'로 적은 any와 as는 빨간 줄이 사라져 나중에 어디에 적었는지 잊어버리기 쉽습니다(검사 규칙 6).

맞는 규칙: 아직 정확한 모양을 모르는 바깥 값은 any 대신 **unknown**으로 적어 두어야, 나중에 그 값을 쓰는 자리에서 검사기가 확인 함수를 짜도록 일깨워 줍니다.

확인 문제

1. 1절에서 둘째 줄 '{"kind":'처럼 문법이 깨진 JSON 글이 들어왔을 때 프로그램이 멈추지 않고 건너뜀 1건으로 넘어갈 수 있었던 까닭은 무엇인가요?

힌트

parseRaw 안의 try ... catch와 isOrder(null)의 결과를 차례로 따라가 보세요.

정답

parseRaw가 try ... catch로 SyntaxError를 잡아 null을 돌려주고, 이어서 isOrder(null)이 false를 돌려주어 else 갈래(skip += 1)로 안전하게 넘어갔기 때문입니다.

2. 2절에서 type Order = Drink | Food⁠에 | Gift를 더했을 때, cost 함수의 어느 줄이 왜 오류를 내어 고칠 곳을 알려 주었나요?

힌트

switch (o.kind)의 마지막 default 갈래에 적힌 타입을 떠올려 보세요.

정답

default 갈래의 const left: never = o; 줄에서 Gift를 never에 넣을 수 없다는 TS2322 오류가 났습니다. case "gift"를 아직 적지 않아 default까지 내려온 o의 지금 타입이 Gift가 되었기 때문입니다.

3. 기존 자바스크립트 코드를 타입스크립트로 옮길 때 아직 정확한 모양을 다 파악하지 못한 외부 입력 자리가 있다면, any와 unknown 가운데 무엇을 적어 두어야 왜 안전할까요?

힌트

나중에 그 변수의 속성을 읽으려 할 때 검사기가 알려 주는지(TS18046) 생각해 보세요.

정답

unknown을 적어 두어야 안전합니다. any를 적어 두면 검사가 완전히 꺼져 잊어버리기 쉽지만, unknown을 적어 두면 그 값을 확인 없이 쓰려 할 때마다 검사기가 TS18046으로 알려 주어 확인 함수를 빠뜨리지 않게 해 줍니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 2절의 완성 코드에 무료 쿠폰 갈래(kind: "coupon")를 하나 더 추가해 보고, npx tsc가 가리키는 줄을 따라 case "coupon": return 0;⁠을 보태 검사 오류 0으로 통과시켜 보세요. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

타입스크립트 목차로