작은 프로그램을 처음부터 만들 때는 함수를 바로 짜기보다 프로그램이 다룰 데이터의 모양(타입)을 먼저 설계합니다. 문제를 입력부터 출력까지 다섯 상자로 나누고 가운데에 놓인 갈래 타입(Drink · Food · Order · Summary)을 먼저 고정해 두면, 이후 함수를 하나씩 만들 때마다 검사기가 빈틈을 즉시 알려 줍니다.
이번 11단원에서는 세 레슨에 걸쳐 하루 카페 주문 기록(JSON 글)을 읽어 확인하고, 갈래별 합계와 건너뛴 줄 수를 찍는 정산 프로그램을 처음부터 끝까지 완성합니다. 첫 걸음으로 문제를 다섯 단계로 쪼개고 네 가지 핵심 타입을 먼저 세웁니다.
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
parseRaw는 문자열s: string을 받아JSON.parse결과를unknown으로 돌려줍니다(6단원 바깥 값 확인하기).isOrder는v: unknown이 진짜Order모양인지 검사해v is Order로 좁혀 줍니다.cost는 확인을 통과한o: Order하나를 받아 금액number를 돌려줍니다.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,
};
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 왜 막힐까요?
검사 결과 보기
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이 찍히는지 확인해 보세요. 다음 레슨 함수 하나씩 검사하며 만들기에서는 이 타입들 위에 함수를 하나씩 쌓아 올립니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.