본문으로 건너뛰기

타입 먼저 설계하기

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

목차

작은 프로그램을 처음부터 만들 때는 함수를 바로 짜기보다 프로그램이 다룰 데이터의 모양(타입)을 먼저 설계합니다. 문제를 입력부터 출력까지 다섯 상자로 나누고 가운데에 놓인 갈래 타입(Drink · Food · Order · Summary)을 먼저 고정해 두면, 이후 함수를 하나씩 만들 때마다 검사기가 빈틈을 즉시 알려 줍니다.

이번 11단원에서는 세 레슨에 걸쳐 하루 카페 주문 기록(JSON 글)을 읽어 확인하고, 갈래별 합계와 건너뛴 줄 수를 찍는 정산 프로그램을 처음부터 끝까지 완성합니다. 첫 걸음으로 문제를 다섯 단계로 쪼개고 네 가지 핵심 타입을 먼저 세웁니다.

6단계 중 1단계. 1~8번 줄: 음료 사이즈를 "S" | "M" | "L"로 제한하고, 품목 한 줄의 모양을 Item으로 선언합니다.

1문제를 다섯 상자로 나누기

우리가 만들 정산 프로그램이 해야 할 일을 실행 결과부터 먼저 정합니다. 바깥에서 들어온 JSON 주문 줄들을 읽었을 때, 정상 주문은 금액을 더하고 깨진 줄은 건너뛰어 아래와 같이 찍혀야 합니다.

합계 7000원
건너뜀 1건

빈 파일에서 이 프로그램을 한꺼번에 짜려 하면 어디서부터 손을 대야 할지 막막합니다. 그래서 일을 다섯 상자로 먼저 나눕니다.

상자하는 일입력과 출력 타입
1. 바깥 글하루 주문 기록을 JSON 문자열 배열로 받는다string[]
2. 확인 함수JSON을 풀어 실제 Order 모양인지 확인한다string → unknown → v is Order
3. 갈래 타입음료·음식 주문과 정산 결과의 모양을 정의한다Drink · Food · Order · Summary
4. 계산 함수확인된 주문의 금액을 구하고 합계를 모은다Order → number, string[] → Summary
5. 출력합계 금액과 건너뛴 줄 수를 형식에 맞춰 찍는다Summary → 화면 출력

상자는 다섯이지만 코드를 쓰는 차례는 3번(갈래 타입)이 맨 앞입니다. Order와 Summary가 먼저 정의되어 있어야 확인 함수(v is Order)와 계산 함수(o: Order)의 머리(매개변수와 반환 타입)를 적을 수 있기 때문입니다.

2네 가지 타입 먼저 적어 보기

다섯 상자 가운데 중심이 되는 네 가지 타입(Drink, Food, Order, Summary)을 먼저 코드로 적습니다.

첫째로 주문 갈래를 나타내는 판별 유니언 Order입니다. 5단원 작은 주문표 만들기에서 썼던 모양 그대로 음료(kind: "drink")에는 추가 금액 extra가 있고, 음식(kind: "food")에는 price만 있습니다.

// 1. 타입 먼저
interface Drink {
  kind: "drink";
  price: number;
  extra: number;
}
interface Food {
  kind: "food";
  price: number;
}
type Order = Drink | Food;

둘째로 정산 결과를 담을 Summary 인터페이스를 이어 적고, 예시 데이터 하나를 넣어 타입이 잘 맞물리는지 확인합니다.

interface Summary {
  total: number;
  skipped: number;
}
const sample: Order = {
  kind: "drink",
  price: 4500,
  extra: 500,
};
const init: Summary = {
  total: sample.price,
  skipped: 0,
};
console.log(init.total);
실행 결과
4500

아직 계산 함수나 확인 함수는 한 줄도 쓰지 않았지만, 프로그램이 주고받을 데이터의 모양 네 개(Drink, Food, Order, Summary)가 이미 검사기에 등록되었습니다.

3만들 함수들의 머리(시그니처) 미리 맞추기

타입 네 개가 준비되었으니, 다음 레슨에서 만들 함수 네 개의 이름과 매개변수·반환 타입만 주석으로 미리 맞춰 봅니다.

// parseRaw(s: string): unknown
// isOrder(
//   v: unknown,
// ): v is Order
// cost(o: Order): number
// summarize(
//   lines: string[],
// ): Summary
  1. parseRaw는 문자열 s: string을 받아 JSON.parse 결과를 unknown으로 돌려줍니다(6단원 바깥 값 확인하기).
  2. isOrder는 v: unknown이 진짜 Order 모양인지 검사해 v is Order로 좁혀 줍니다.
  3. cost는 확인을 통과한 o: Order 하나를 받아 금액 number를 돌려줍니다.
  4. summarize는 문자열 배열 lines: string[]을 돌며 정산 결과 Summary를 돌려줍니다.

앞 함수의 반환 타입이 뒤 함수의 매개변수 타입(unknown → Order → number → Summary)과 아귀가 딱 맞아떨어지는 것을 눈으로 확인할 수 있습니다.

예제

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

2절에서 세운 Drink, Food, Order 타입에 맞춰 주문을 하나 적어 보는데, kind를 "snack"으로 잘못 적었습니다.

interface Drink {
  kind: "drink";
  price: number;
  extra: number;
}
interface Food {
  kind: "food";
  price: number;
}
type Order = Drink | Food;
const bad: Order = {
  kind: "snack",
  price: 2000,
};

검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 왜 막힐까요?

검사 결과 보기
검사 결과 오류 1개
main.ts(12,3): error TS2322: Type '"snack"' is not assignable to type '"drink" | "food"'.

타입을 먼저 적어 두면 함수를 아직 만들기 전이라도 예시 데이터가 규격에서 벗어났을 때 12번 줄에서 즉시 TS2322로 잡아 줍니다.

자주 틀리는 곳

틀린 생각: 함수 본문부터 먼저 쭉 짜 놓고 타입은 나중에 붙이는 것이 빠르다.

타입 없이 함수부터 여러 개 짜다 보면 매개변수에 TS7006(암묵적 any)이 줄줄이 뜨고, 한 함수가 돌려주는 객체 속성 이름(sum)과 다음 함수가 읽는 속성 이름(total)이 어긋나 나중에 한꺼번에 고쳐야 합니다.

맞는 규칙: 프로그램이 주고받을 데이터 모양(interface, type)을 맨 먼저 선언하고, 함수 머리(매개변수와 반환 타입)를 맞춘 뒤에 본문을 채웁니다.

틀린 생각: 바깥에서 온 JSON 글도 JSON.parse(s) as Order⁠로 바로 받아 계산 함수에 넘기면 된다.

설계 단계에서 확인 함수 상자를 빼먹고 as Order로 단언해 버리면, 파일에 깨진 줄이나 글자 금액("4500")이 섞여 들어왔을 때 검사기가 전혀 막지 못합니다(검사 규칙 6).

interface Food {
  kind: "food";
  price: number;
}
const s =
  '{"kind":"food",' +
  '"price":"2000"}';
const o = JSON.parse(s) as Food;
console.log(o.price + 500);
검사 결과 오류 없음
실행 결과
2000500

맞는 규칙: 바깥 글(string)에서 바로 Order로 넘어가지 않도록, 설계할 때부터 반드시 가운데에 unknown과 확인 함수(isOrder) 상자를 둡니다.

확인 문제

1. 다섯 상자(1. 바깥 글 · 2. 확인 함수 · 3. 갈래 타입 · 4. 계산 함수 · 5. 출력) 가운데 코드로 가장 먼저 작성해야 하는 상자는 무엇이고, 왜 먼저 적나요?

힌트

확인 함수와 계산 함수의 매개변수·반환 타입 자리에 어떤 이름이 쓰이는지 생각해 보세요.

정답

3. 갈래 타입(Drink · Food · Order · Summary)을 가장 먼저 적습니다. 데이터 모양을 나타내는 타입 이름이 먼저 선언되어 있어야 확인 함수(v is Order)와 계산 함수(o: Order, : Summary)의 타입 표기를 적을 수 있기 때문입니다.

2. 다음 네 함수의 머리에서 빈칸 (가) · (나) · (다)에 들어갈 타입 이름을 쓰세요.

  • parseRaw(s: string) → (가)
  • isOrder(v: unknown) → v is (나)
  • summarize(lines: string[]) → (다)
힌트

3절에서 앞 함수의 출력이 뒤 함수의 입력으로 어떻게 이어졌는지 떠올려 보세요.

정답

(가) unknown · (나) Order · (다) Summary입니다. parseRaw는 바깥 값을 안전하게 가두기 위해 unknown을 돌려주고, isOrder는 Order인지 확인하며, summarize는 정산 결과 모양인 Summary를 돌려줍니다.

3. 운동 기록 정산 프로그램을 설계하려고 합니다. 걷기(kind: "walk", steps: number)와 달리기(kind: "run", km: number) 두 갈래를 묶는 판별 유니언 Workout을 선언해 보세요.

힌트

2절의 Drink · Food · Order와 같은 꼴로 두 interface를 만들고 |로 묶습니다.

정답
interface Walk {
  kind: "walk";
  steps: number;
}
interface Run {
  kind: "run";
  km: number;
}
type Workout = Walk | Run;

갈래 속성 kind를 "walk"와 "run" 리터럴 타입으로 두면 나중에 switch (w.kind)로 안전하게 갈래를 나눌 수 있습니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 2절의 두 블록을 main.ts에 저장하고 npx tsc와 node main.ts로 돌려 4500이 찍히는지 확인해 보세요. 다음 레슨 함수 하나씩 검사하며 만들기에서는 이 타입들 위에 함수를 하나씩 쌓아 올립니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

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

타입스크립트 목차로