본문으로 건너뛰기

함수 하나씩 검사하며 만들기

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

목차

타입 설계가 끝났다면 이제 함수를 한 번에 하나씩 만들고, 함수를 하나 완성할 때마다 npx tsc로 검사 오류가 0인지 확인한 뒤 작은 값으로 불러 기대한 출력이 나오는지 맞춰 봅니다. 모든 함수를 다 짜 놓고 마지막에 한꺼번에 검사하면 오류가 여러 줄에 겹쳐 어디서부터 꼬였는지 찾기 어렵지만, 함수 하나마다 검사와 시험을 거치면 방금 쓴 몇 줄만 보면 됩니다.

앞 레슨에서 설계한 네 가지 타입(Drink · Food · Order · Summary) 위에 cost → parseRaw · isOrder → summarize를 차례로 쌓아 올리고, 완성된 코드를 역할에 따라 세 파일(types.ts · cost.ts · main.ts)로 나누어 봅니다.

앞 레슨 코드 펼치기

타입 먼저 설계하기에서 작성한 네 가지 타입 선언입니다.

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단계. 14~16번 줄: subtotal(items)은 Item[]을 받아 reduce로 각 품목의 price * qty를 더해 합계(number)를 돌려줍니다.

1계산 함수 cost부터 만들고 바로 시험하기

먼저 이미 확인을 통과한 Order 하나를 받아 금액을 계산하는 cost 함수부터 만듭니다. 앞 레슨의 타입 선언에 이어 적습니다.

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

switch (o.kind)로 갈래를 나누고 default에 never 검사를 넣습니다.

// 2. 함수 하나씩 검사
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;
  }
}

이어서 라테 한 잔(4500 + 500)을 넣어 바로 시험합니다.

console.log(
  cost({
    kind: "drink",
    price: 4500,
    extra: 500,
  }),
);
실행 결과
5000

검사 오류가 0이고 손으로 셈한 5000이 그대로 찍혔습니다. cost 상자 하나가 확실히 동작함을 확인했으니 다음 상자로 넘어갑니다.

2바깥 값 확인 함수와 합계 함수 이어 붙이기

다음은 바깥에서 온 JSON 문자열을 안전하게 읽는 parseRaw와 Order 모양인지 확인하는 isOrder입니다(6단원 바깥 값 확인하기). 각 갈래를 깔끔하게 확인하기 위해 isDrink와 isFood로 나누어 작성합니다.

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

먼저 음료인지 확인하는 isDrink와 음식인지 확인하는 isFood를 이어 씁니다.

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"
  );
}
// 3. 바깥 값 확인
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"
  );
}

두 함수를 합쳐 isOrder와 parseRaw를 만듭니다.

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;
  }
}

이제 확인 함수와 cost를 합쳐 전체 줄 배열을 정산하는 summarize 함수를 완성합니다.

function cost(o: Order) {
  return o.kind === "drink"
    ? o.price + o.extra
    : o.price;
}
function summarize(
  lines: string[],
): Summary {
  let total = 0;
  let skipped = 0;
  for (const s of lines) {
    const v = parseRaw(s);
    if (isOrder(v)) {
      total += cost(v);
    } else {
      skipped += 1;
    }
  }
  return { total, skipped };
}

먼저 isOrder에 글자 가격("2000")을 넣어 false로 걸러지는지 시험하고, 이어서 정상 주문 두 줄로 summarize를 시험합니다.

const bad = parseRaw(
  '{"kind":"food",' +
    '"price":"2000"}',
);
console.log(isOrder(bad));
const res = summarize([
  '{"kind":"drink",' +
    '"price":4500,"extra":500}',
  '{"kind":"food",' +
    '"price":2000}',
]);
console.log(
  res.total,
  res.skipped,
);
실행 결과
false
7000 0

3파일 셋으로 나누기 (types.ts · cost.ts · main.ts)

한 파일에서 모든 함수가 잘 돌아가는 것을 확인했다면, 10단원 모듈과 타입 내보내기에서 배운 대로 역할에 따라 파일을 나눌 수 있습니다.

  • types.ts: Drink, Food, Order, Summary 앞에 export를 붙여 내보냅니다.
  • cost.ts: ./types.ts에서 import type으로 Order만 가져오고 export function cost를 내보냅니다.
  • main.ts: ./types.ts에서 타입들을, ./cost.ts에서 cost 함수를 가져와 정산을 실행합니다.
// cost.ts 예시 (주석으로 파일 구분)
// import type {
//   Order,
// } from "./types.ts";
// export function cost(
//   o: Order,
// ) { ... }

타입만 가져오는 줄에는 반드시 import type을 쓰고, 파일 경로 끝에는 .ts 확장자를 붙입니다.

예제

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

costDrink 함수를 작성할 때 실수로 음료 추가 금액 o.extra를 더하지 않고 뺐습니다.

interface Drink {
  kind: "drink";
  price: number;
  extra: number;
}
function costDrink(o: Drink) {
  return o.price - o.extra;
}
console.log(
  costDrink({
    kind: "drink",
    price: 4500,
    extra: 500,
  }),
);

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

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

수에서 수를 빼는 계산도 결과 타입은 똑같이 number이므로 타입 검사는 오류 0으로 통과합니다. 하지만 라테(4500 + 500 = 5000원)를 넣었을 때 4000이 찍히므로, 작은 값으로 시험해 보아야만 +를 -로 잘못 친 실수를 바로 잡을 수 있습니다.

자주 틀리는 곳

틀린 생각: 함수 다섯 개를 끝까지 다 작성한 뒤에 npx tsc를 한 번만 돌리면 된다.

함수를 여러 개 한꺼번에 짜고 검사하면 앞 함수의 반환 타입 실수가 뒤 함수들의 호출 자리까지 연쇄 오류를 일으켜 터미널에 오류가 열 줄씩 쏟아집니다.

맞는 규칙: 함수를 하나 작성할 때마다 npx tsc로 검사 오류 0을 확인하고 다음 함수로 넘어갑니다.

틀린 생각: 만드는 도중 나오는 검사 오류는 일단 as나 any로 덮어 두고 나중에 고치면 된다.

중간에 v as Order나 v: any로 덮어 두면 당장 빨간 줄은 사라지지만, 그 뒤에 만드는 모든 함수가 잘못된 모양을 믿고 짜여 나중에 처음부터 다시 고쳐야 합니다.

맞는 규칙: 만드는 도중 만난 타입 오류는 덮지 않고 그 자리에서 확인 조건(typeof, in)이나 타입 표기를 고쳐 해결합니다.

틀린 생각: npx tsc 검사 오류가 0이면 방금 만든 함수가 올바르게 동작한다는 뜻이다.

예제에서 보았듯 o.price + o.extra 자리에 o.price - o.extra를 적어도, 또는 isDrink에서 v.kind !== "drink"로 반대로 적어도 타입 검사는 오류 0으로 통과합니다(검사를 지나면 맞다는 생각).

맞는 규칙: 검사기는 타입이 맞는지만 볼 뿐 계산식이 맞는지는 모릅니다(검사 규칙 1). 함수를 하나 만들 때마다 npx tsc(오류 0) 뒤에 작은 시험 값 한 줄을 넣어 손으로 셈한 값과 실행 결과를 반드시 대조합니다.

확인 문제

1. 함수를 여러 개 한꺼번에 다 짜지 않고 하나 만들 때마다 npx tsc와 작은 시험 값을 돌려 보는 까닭은 무엇인가요?

힌트

오류가 났을 때 살펴봐야 하는 코드 줄 수와 타입 검사가 계산식까지 확인해 주는지(검사를 지나면 맞다는 생각)를 함께 떠올려 보세요.

정답

오류가 생겼을 때 방금 작성한 함수 몇 줄에서 원인을 바로 찾을 수 있고, 타입 검사만으로는 잡히지 않는 계산 실수(+를 -로 잘못 쓰는 등)까지 즉시 확인할 수 있기 때문입니다(검사를 지나면 맞다는 생각).

2. 2절의 isFood 함수에서 "price" in v 뒤에 typeof v.price === "number" 확인을 빼먹으면 어떤 잘못된 JSON 글이 통과해 버릴까요?

힌트

"price" in v는 price 속성이 있다는 것만 확인할 뿐 값의 종류는 확인하지 않습니다.

정답

price 자리에 숫자 대신 문자열 "2000"이 들어온 객체가 isFood를 true로 통과해 버립니다. 그러면 나중에 total += cost(v)를 할 때 숫자 덧셈 대신 문자열 이어 붙이기가 일어나 합계가 망가집니다.

3. cost.ts 파일에서 types.ts에 선언된 Order 타입만 가져올 때 import { Order }처럼 type 키워드를 빼먹으면 어떤 오류가 나나요?

힌트

10단원 모듈과 타입 내보내기에서 배운 verbatimModuleSyntax 규칙과 타입 지우기 뒤의 모습을 떠올려 보세요.

정답

타입 검사 때는 TS1484 오류가 나고, Node로 바로 실행할 때는 types.ts에서 Order가 지워져 SyntaxError로 멈춥니다. 따라서 반드시 import type { Order }⁠로 가져와야 합니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 2절의 블록들을 main.ts에 차례로 붙여 넣고 npx tsc와 node main.ts로 false와 7000 0이 찍히는지 확인해 보세요. 여유가 있다면 types.ts와 main.ts 두 파일로 나누어 import type으로도 돌려 보세요. 다음 레슨 마무리 — 새 갈래·옮기는 길·다음 걸음에서는 깨진 주문 줄을 섞어 넣고 새 갈래 Gift를 추가해 봅니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

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

타입스크립트 목차로