본문으로 건너뛰기

바깥 값 확인하기

타입스크립트 목차바깥에서 온 값 — any·unknown·as레슨 31 / 51

목차

바깥 값 확인하기는 JSON 글 목록을 읽어 깨진 글과 타입이 어긋난 글을 걸러 내고, 모양이 맞는 주문만 골라 합계를 내는 작은 완성품입니다. 이 단원에서 배운 unknown과 확인 함수(v is Order)를 하위 목표 네 단계로 조립합니다.

파일이나 네트워크에서 받은 JSON 글에는 정상 주문뿐 아니라 숫자 자리에 글자가 들어온 값("qty":"2")이나 깨진 글('{')이 섞여 있을 수 있습니다. 빈 화면에서 바로 쓰기는 누구에게나 어렵습니다. 그래서 할 일을 하위 목표 넷으로 나누고 블록 셋으로 조립합니다. 폰으로 보고 있다면 확인 문제의 순서 맞추기로 조립하고, 컴퓨터가 있다면 직접 쳐서 검사하고 돌려 봅니다.

7단계 중 1단계. 하위 목표 넷을 따라 바깥 값을 확인하는 프로그램을 읽습니다.

1무엇을 만드나 — 하위 목표 넷

만들 프로그램이 하는 일을 실행 모습으로 먼저 정합니다. 세 줄의 JSON 글('{"item":"라테","qty":2}', '{"item":"라테","qty":"2"}', '{')을 넣으면 바른 첫째 주문만 더해 이렇게 찍혀야 합니다.

합계 2잔

이 일을 하위 목표 넷으로 나눕니다.

하위 목표하는 일블록
1. unknown으로 받기try·catch로 JSON.parse를 감싸 깨진 글이면 null을, 아니면 : unknown으로 돌려준다첫째
2. 객체인지(null 아님)typeof v === 'object'⁠와 v !== null을 차례로 확인한다둘째
3. 속성마다 있는지와 타입'item' in v·'qty' in v와 각 값의 typeof를 확인해 v is Order를 돌려준다둘째
4. 확인한 뒤 쓰기isOrder(v)를 통과한 주문만 v.qty를 sum에 더하고 찍는다셋째

1은 unknown — 확인하고 쓰기에서 본 JSON.parse와 try·catch이고, 2·3은 직접 만드는 확인 함수의 isOrder이며, 4는 확인된 값만 모으는 합계입니다.

2블록 셋으로 조립하기

블록 셋을 차례로 씁니다. 둘째 · 셋째 블록은 앞 블록에 이어 쓰고, 셋을 차례로 한 파일(main.ts)에 두면 한 프로그램이 됩니다. 줄 번호도 셋을 이어서 셉니다.

첫째 블록은 주문 타입 Order와 하위 목표 1(parseRaw)입니다.

interface Order {
  item: string;
  qty: number;
}
// 1. unknown으로 받기
function parseRaw(
  s: string,
): unknown {
  try {
    return JSON.parse(s);
  } catch {
    return null;
  }
}
  • 1. unknown으로 받기: parseRaw의 반환 타입을 : unknown으로 적어, JSON.parse가 돌려주는 any가 바깥으로 새어 나가지 않게 막습니다. 깨진 JSON 글이 들어와 SyntaxError가 던져지면 catch가 잡아 null을 돌려줍니다(catch 뒤에 오류 변수를 쓰지 않으면 (e)를 생략할 수 있습니다).

앞 코드에 이어 둘째 블록, 확인 함수 isOrder를 씁니다. 검사 오류가 없습니다.

function isOrder(
  v: unknown,
): v is Order {
  return (
    // 2. 객체인지(null 아님)
    typeof v === 'object' &&
    v !== null &&
    // 3. 속성마다 있는지와 타입
    'item' in v &&
    'qty' in v &&
    typeof v.item === 'string'
    && typeof v.qty === 'number'
  );
}
검사 결과 오류 없음
  • 2. 객체인지(null 아님): 20~21번 줄이 원시 값과 null(parseRaw가 깨진 글에서 돌려준 null 포함)을 걸러 내어 v를 object로 좁힙니다.
  • 3. 속성마다 있는지와 타입: 23~24번 줄이 item과 qty 속성이 있는지 확인하고, 25~26번 줄이 두 속성의 값이 각각 string과 number인지 확인합니다.

앞 코드에 이어 셋째 블록, JSON 글 목록과 합계를 씁니다.

const lines = [
  '{"item":"라테","qty":2}',
  '{"item":"라테","qty":"2"}',
  '{',
];
// 4. 확인한 뒤 쓰기
let sum = 0;
for (const s of lines) {
  const v = parseRaw(s);
  if (isOrder(v)) {
    sum = sum + v.qty;
  }
}
console.log(`합계 ${sum}잔`);
실행 결과
합계 2잔

37번 줄에서 parseRaw(s)가 돌려준 v의 지금 타입은 unknown입니다. 38번 줄 if (isOrder(v))를 통과한 39번 줄에서 v의 지금 타입은 무엇일까요? 그리고 if 블록이 끝난 40번 줄에서는 무엇일까요?

지금 타입 보기

39번 줄에서는 Order, 40번 줄에서는 다시 unknown입니다. 예상과 다르면 그곳이 배울 곳입니다.

38번 줄의 isOrder(v)가 true인 갈래(39번 줄) 안에서는 반환 타입 v is Order에 따라 v가 Order로 좁혀져 v.qty(number)를 안전하게 더할 수 있습니다. if 블록 바깥(40번 줄)에서는 참 갈래와 거짓 갈래가 다시 만나 unknown입니다.

예제

손으로 셈해 맞춰 보기

2절 프로그램이 세 줄의 JSON 글을 하나씩 읽을 때 각 단계의 값을 손으로 따라가며 출력 합계 2잔과 맞춰 봅니다.

바퀴sparseRaw(s)isOrder(v)걸러진 곳sum
1'{"item":"라테","qty":2}'{ item: '라테', qty: 2 }true통과 (+ 2)2
2'{"item":"라테","qty":"2"}'{ item: '라테', qty: '2' }false26번 줄 typeof v.qty === 'number'2
3'{'null (catch)false21번 줄 v !== null2

마지막 sum 2가 출력 합계 2잔과 같습니다. 둘째 글은 속성이 모두 있지만 26번 줄의 타입 확인에서 걸러지고, 셋째 글은 parseRaw의 catch가 null로 바꾼 뒤 21번 줄에서 걸러집니다.

자주 틀리는 곳

틀린 생각: 'item' in v && 'qty' in v⁠로 속성이 있는지만 확인하면 충분하다.

속성 이름 두 개가 다 들어 있으니 그 안의 값도 맞을 것 같습니다. 25~26번 줄의 typeof 확인을 빼면 어떻게 되는지 봅니다.

interface Order {
  item: string;
  qty: number;
}
function badOrder(
  v: unknown,
): v is Order {
  return (
    typeof v === 'object' &&
    v !== null &&
    'item' in v &&
    'qty' in v
  );
}
const lines = [
  '{"item":"라테","qty":2}',
  '{"item":"라테","qty":"2"}',
];
let sum = 0;
for (const s of lines) {
  const v: unknown =
    JSON.parse(s);
  if (badOrder(v)) {
    sum = sum + v.qty;
  }
}
console.log(`합계 ${sum}잔`);
검사 결과 오류 없음
실행 결과
합계 22잔

검사기는 : v is Order라는 말만 믿어 오류 없이 통과시키지만, 실행하면 둘째 글의 "2"가 그대로 통과해 2 + '2'가 되어 합계 22잔이 찍힙니다.

맞는 규칙: 속성이 있는지(in)뿐 아니라 각 속성의 값 타입(typeof v.qty === 'number')⁠까지 모두 확인합니다(하위 목표 3 · 검사 규칙 6).

틀린 생각: interface Order를 선언해 두었으니 실행할 때도 타입스크립트가 모양을 맞춰 본다.

Order에 qty: number라고 적혀 있으니 실행할 때 알아서 맞춰 줄 것 같거나, v instanceof Order로 검사할 수 있을 것 같습니다.

맞는 규칙: interface Order는 실행 전에 지워져 남지 않으므로 instanceof Order도 쓸 수 없습니다(검사 규칙 1). 실행할 때 바깥 값을 확인하는 일은 isOrder 안의 자바스크립트 코드가 합니다.

틀린 생각: JSON.parse는 확인 함수가 알아서 막아 주니 try·catch가 필요 없다.

isOrder가 unknown을 받으니 isOrder(JSON.parse(s))⁠처럼 바로 넘기면 될 것 같습니다. 그러나 깨진 JSON 글('{')은 isOrder가 불리기도 전에 JSON.parse 안에서 SyntaxError를 던져 프로그램 전체를 멈춥니다.

맞는 규칙: 깨진 글이 올 수 있는 JSON.parse는 try·catch로 감싸 안전하게 : unknown으로 받습니다(하위 목표 1).

틀린 생각: JSON.parse(s) as Order 한 줄로 쓰면 코드가 짧고 깔끔하다.

as는 검사기에게 하는 말에서 보았듯, as Order는 실행 전에 빈칸으로 지워져 "qty":"2"를 조금도 걸러 내지 못합니다.

맞는 규칙: 바깥에서 온 값에는 as를 쓰지 않고 하위 목표 1~4를 거쳐 확인합니다(검사 규칙 6).

확인 문제

1. 다음 코드는 2절의 isOrder에서 typeof v.qty === 'number'⁠만 빠졌습니다. 검사를 지나갈까요? Node로 돌리면 무엇이 찍힐까요?

interface Order {
  item: string;
  qty: number;
}
function isOrder(
  v: unknown,
): v is Order {
  return (
    typeof v === 'object' &&
    v !== null &&
    'item' in v &&
    'qty' in v &&
    typeof v.item === 'string'
  );
}
const s =
  '{"item":"라테","qty":null}';
const v: unknown =
  JSON.parse(s);
if (isOrder(v)) {
  console.log(v.qty + 1);
}
힌트

qty 값이 null일 때 'qty' in v가 참인지 거짓인지, 그리고 자바스크립트에서 null + 1이 무엇이 되는지 따져 봅니다.

정답
검사 결과 오류 없음
실행 결과
1

검사는 오류 없이 지나가고, 실행하면 1이 찍힙니다. 객체에 "qty": null이 들어 있으면 qty라는 키 자체는 존재하므로 'qty' in v는 true입니다. typeof v.qty === 'number'⁠를 빼먹었기 때문에 isOrder(v)가 true가 되고, 실행할 때 null + 1이 계산되어 1이 찍힙니다. 흔한 오답은 '출력 없음(if 안으로 들어가지 않음)'입니다. 'qty' in v가 값이 null인 것도 걸러 준다고 본 것입니다.

2. 바깥 값을 확인하는 프로그램을 하위 목표 단위로 조립합니다.

(가) 하위 목표 넷의 차례가 섞였습니다. 처음부터 차례로 기호를 늘어놓으세요.

  • ㉠ 확인한 뒤 쓰기
  • ㉡ 객체인지(null 아님)
  • ㉢ unknown으로 받기
  • ㉣ 속성마다 있는지와 타입

(나) isOrder 안에서 확인하는 네 식의 차례가 섞였습니다. 차례로 기호를 늘어놓으세요.

㉠ typeof v.qty === 'number'
㉡ typeof v === 'object'
㉢ 'qty' in v
㉣ v !== null
힌트

(가)는 1절의 표를 떠올립니다. (나)는 앞 식이 통과해야만 뒤 식을 검사 오류 없이 쓸 수 있는 차례를 생각합니다.

놓은 뒤 물음

(다) (나)를 다 놓은 뒤에 고르세요. (나)에서 ㉢('qty' in v)이 ㉠(typeof v.qty === 'number')⁠보다 앞이어야 하는 까닭은 무엇일까요?

  • ① 'qty' in v로 속성이 있음을 먼저 좁혀야 검사기가 v.qty 읽기를 허용하기 때문(그렇지 않으면 TS2339)
  • ② 자바스크립트에서 in 연산자를 쓰지 않으면 v.qty가 늘 SyntaxError를 던지기 때문
  • ③ typeof 연산자는 변수 이름에만 쓸 수 있고 속성(v.qty)에는 쓸 수 없기 때문
정답

(가) ㉢ ㉡ ㉣ ㉠ — unknown으로 받기 · 객체인지(null 아님) · 속성마다 있는지와 타입 · 확인한 뒤 쓰기입니다.

(나) ㉡ ㉣ ㉢ ㉠입니다. ㉣ ㉡ ㉢ ㉠도 검사를 지납니다 — v !== null을 먼저 써도 검사기가 좁힙니다. 2절 isOrder는 ㉡ ㉣ 차례로 씁니다.

(다) ①입니다. v !== null까지만 확인한 v의 지금 타입은 object라서, 'qty' in v 없이 v.qty를 읽으면 TS2339(Property 'qty' does not exist)⁠로 막힙니다(직접 만드는 확인 함수 1절).

  • ② 자바스크립트에서 없는 속성을 읽으면 오류 대신 undefined가 나옵니다.
  • ③ typeof v.qty처럼 속성 식에도 typeof를 쓸 수 있습니다.

3. 도서관에서 책 정보 JSON 글 두 줄을 읽어 쪽수(page)를 찍는 코드입니다. 검사를 지나갈까요? Node로 돌리면 무엇이 찍힐까요?

interface Book {
  page: number;
}
function isBook(
  v: unknown,
): v is Book {
  return (
    typeof v === 'object' &&
    v !== null &&
    'page' in v &&
    typeof v.page === 'number'
  );
}
for (const s of [
  '{"page":"90"}',
  '{"page":120}',
]) {
  const v: unknown =
    JSON.parse(s);
  if (isBook(v)) {
    console.log(v.page);
  }
}
힌트

첫째 글의 "page":"90"과 둘째 글의 "page":120이 isBook의 11번 줄 typeof v.page === 'number'⁠에서 각각 어떻게 되는지 봅니다.

정답
검사 결과 오류 없음
실행 결과
120

검사는 오류 없이 지나가고, 실행하면 120만 찍힙니다. 첫째 글은 page가 글자 "90"이라 isBook에서 걸러지고, 둘째 글만 통과해 120을 찍습니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 2절의 블록 셋을 차례로 main.ts에 저장해 npx tsc로 검사하고 node main.ts로 돌려 합계 2잔이 찍히는지 확인합니다. 그다음 26번 줄의 && typeof v.qty === 'number'⁠를 지우고 다시 검사하고 돌려 합계 22잔으로 바뀌는지 직접 확인해 보세요. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

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

타입스크립트 목차로