3단원 '객체의 모양'의 여섯 레슨에서 아홉 문제를 내고, 2단원 '기본 타입과 함수'에서 두 문제, 1단원 '타입 검사기'에서 한 문제를 섞었습니다. 앞 단원 문제가 어느 레슨인지는 정답을 펼치면 보입니다. 검사 결과는 지나가는지와 막히는 줄을, 실행 결과는 찍히는 글자를 종이에 끝까지 쓴 뒤 정답을 펼치세요. 실행이 오류로 멈추면 오류 이름이 든 줄까지만 써도 됩니다. 막히면 정답 앞의 힌트를 먼저 펼칩니다. 점수는 매기지 않습니다.
한 번에 다 풀 필요는 없습니다. 문제 1~10은 폰으로도 풀 수 있고(약 30~40분), 끝의 만들어 보기 둘은 컴퓨터에서 tsc와 Node로 직접 검사하고 돌려 봅니다. 이 단원을 건너뛸지 보려면 진단 문항인 문제 1 · 3 · 5 · 6 · 8 다섯을 먼저 풀어 보세요(약 15분). 진단 문항은 힌트를 펴지 않고 풉니다. 힌트를 펴고 맞힌 문제나 짐작으로 맞힌 문제는 틀린 것으로 칩니다. 모두 맞으면 이 단원 레슨은 건너뛰고, 틀린 문제가 있으면 그 정답 끝의 레슨만 봅니다. 끝의 만들어 보기 둘은 골라서 풉니다.
끝의 두 문제는 자바스크립트 코드에 타입을 붙여 봅니다. 컴퓨터가 있으면 main.ts로 저장해 npx tsc로 검사 오류가 0인지 본 뒤 node main.ts로 돌려 맞춰 봅니다. 플레이그라운드에서는 맨 위에 export {}; 한 줄을 더합니다. 폰으로만 본다면 문제에 적힌 값으로 내 코드를 한 줄씩 검사하고 따라가 나와야 할 화면과 맞춰 봅니다. 검사 결과는 tsc 7.0.2로, 실행 결과는 Node 26으로 돌린 것입니다.
문제 1
장보기 목록의 물건 하나를 Item 타입 자리에 넣었더니 검사에서 막혔습니다. 12번 줄이 막힌 까닭으로 알맞은 것을 고르세요.
interface Item {
name: string;
count: number;
fresh: boolean;
}
const milk = {
name: '우유',
count: 2,
fresh: 'yes',
};
const it: Item = milk;
main.ts(12,7): error TS2322: Type '{ name: string; count: number; fresh: string; }' is not assignable to type 'Item'. Types of property 'fresh' are incompatible. Type 'string' is not assignable to type 'boolean'.- ① milk에 Item에 없는 속성이 있어서
- ② milk의 fresh 속성이 boolean 자리에 글자 값을 가져서
- ③ milk에 Item의 속성 하나가 빠져서
힌트
오류 문장에서 milk의 속성 이름이 나오는 곳을 모두 찾아, Item의 그 속성 줄과 견줘 봅니다.
힌트 둘
첫 줄 아래 들여 쓴 두 줄을 차례로 읽습니다.
정답
② — 첫 줄은 milk의 모양이 Item 자리에 들지 않는다는 것만 말합니다. 까닭은 들여 쓴 줄에 있습니다. 들여 쓴 첫 줄은 'fresh 속성의 타입이 서로 맞지 않는다', 그 아래 줄은 'string 타입은 boolean 타입에 넣을 수 없다'입니다. milk의 fresh가 글자 'yes'라 boolean 자리에 들지 않습니다.
- ① 남는 속성 때문이라고 봤습니다. milk의 속성 셋은 모두 Item에 있습니다. 남는 속성이라면 들여 쓴 줄 대신 다른 오류(TS2353)가 나옵니다.
- ③ 빠진 속성 때문이라고 봤습니다. 빠진 속성이라면 'is missing'이 든 TS2741이 나옵니다.
두 오답 모두 첫 줄만 읽고 들여 쓴 줄을 건너뛴 것입니다. 들여 쓴 줄은 어느 속성이 왜 안 맞는지 알려 줍니다.
다시 볼 레슨: 객체 타입과 interface · 새로 쓴 객체의 남는 속성
문제 2
4000원보다 비싼 메뉴의 이름을 모으는 코드입니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.
const menu = [
{ name: '라테', won: 4500 },
{ name: '녹차', won: 3500 },
];
const big = menu
.filter((m) => m.won > 4000);
function title(m) {
return m.name;
}
console.log(big.map(title));
힌트
6번 줄의 m과 7번 줄의 m을 견줍니다. 검사기가 두 m의 타입을 각각 어디서 알 수 있을지 봅니다.
힌트 둘
하나는 넘기는 자리에서 바로 쓴 콜백이고, 하나는 따로 만든 함수입니다.
정답
main.ts(7,16): error TS7006: Parameter 'm' implicitly has an 'any' type.7번 줄에서만 막힙니다. 6번 줄의 콜백은 filter에 넘기는 자리에서 바로 쓴 것이라, m의 타입이 배열의 원소 타입으로 정해집니다(문맥에서 추론 — 검사 규칙 3). 7번 줄의 title은 따로 만든 함수라 그 자리에서는 m이 무엇을 받을지 모릅니다. 10번 줄에서 map에 넘기더라도 매개변수 타입은 정해지지 않습니다.
흔한 오답은 '지나감'입니다. map에 넘길 함수라 m도 추론된다고 본 것입니다(매개변수도 추론된다는 생각). 6번 줄까지 막힌다고 쓰면 거꾸로, 넘기는 자리에서 쓴 콜백도 매개변수 타입을 다 적어야 한다고 본 것입니다.
다시 볼 레슨: 함수 타입과 콜백 · 함수의 입력과 출력
문제 3
영수증 번호는 바꾸지 못하게 readonly로 적었습니다. 검사 결과와 실행 결과를 각각 쓰세요.
interface Receipt {
readonly no: number;
total: number;
}
const r: Receipt = {
no: 1,
total: 4500,
};
r.total = 9000;
r.no = 2;
console.log(r.no, r.total);
힌트
2번 줄에서 readonly가 붙은 속성과, 10 · 11번 줄이 다시 대입하는 속성을 맞춰 봅니다.
힌트 둘
readonly를 검사기가 보는지, Node가 보는지 가릅니다.
정답
main.ts(11,3): error TS2540: Cannot assign to 'no' because it is a read-only property.2 9000검사는 11번 줄에서 막힙니다. TS2540은 'no는 readonly 속성이라 다시 대입할 수 없다'는 뜻입니다. 10번 줄의 total은 readonly가 아니라 지나갑니다.
Node는 타입 글자와 함께 readonly도 지우고 돌립니다. 그래서 11번 줄도 실행되어 2 9000을 찍습니다.
흔한 오답은 실행 결과를 1 9000으로 쓴 것입니다. readonly가 실행할 때도 바꾸기를 막는다고 본 것입니다(타입이 실행 때 남는다는 생각). 아무것도 찍지 않는다고 쓰면 검사 오류가 있으면 Node가 돌지 않는다고 본 것입니다(검사 오류가 실행을 막는다는 생각). readonly는 검사기만 보는 표시이고 실행 때는 남지 않습니다(검사 규칙 1).
다시 볼 레슨: 선택 속성과 readonly · 타입은 실행 전에 지워진다
문제 4
메모가 있을 수도 있는 주문과, 크기를 비워도 되는 함수입니다. 9번 줄에서 o.memo와 size의 지금 타입은 무엇일까요? Node로 돌리면 무엇이 찍힐까요?
interface Order {
item: string;
memo?: string;
}
function pour(
o: Order,
size?: string,
) {
console.log(o.memo, size);
}
pour({ item: '라테' });
힌트
2 · 3번 줄과 7번 줄의 물음표가 각각 무엇에 붙었는지 보고, 11번 줄에서 무엇을 비웠는지 봅니다.
정답
o.memo와 size의 지금 타입은 둘 다 string | undefined입니다.
undefined undefined- o.memo: 선택 속성이라 속성의 타입 string에 undefined가 더해집니다. 11번 줄의 주문에는 memo가 없습니다.
- size: 선택 매개변수라 선언 타입 string에 undefined가 더해집니다. 11번 줄은 size를 비웠습니다.
- Node는 없는 memo와 비운 size를 모두 undefined로 찍습니다.
같은 답으로 치는 꼴: undefined | string처럼 차례만 바꿔 쓴 것.
흔한 오답은 둘 중 하나나 둘 다를 string으로 쓴 것입니다. 물음표를 붙인 속성 · 매개변수도 꺼내면 늘 값이 있다고 본 것입니다(꺼낸 값은 늘 있다는 생각). 물음표는 속성에 붙든 매개변수에 붙든, 없을 수 있으니 undefined를 더해 두라는 표시입니다(검사 규칙 2).
다시 볼 레슨: 선택 속성과 readonly · 선택·기본값 매개변수
문제 5
반려동물과 화분을 기록하는 코드입니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.
interface Pet {
name: string;
age: number;
}
interface Plant {
name: string;
}
const dog: Pet = {
name: '콩이',
age: 3,
};
const p: Plant = dog;
const q: Pet = p;
힌트
13번 줄과 14번 줄마다 받는 자리의 타입과 넣는 값을 찾아 둡니다.
힌트 둘
13 · 14번 줄마다 받는 자리의 타입이 어떤 속성 줄을 요구하는지, 넣는 값이 그 줄을 다 가졌는지 봅니다.
정답
main.ts(14,7): error TS2741: Property 'age' is missing in type 'Plant' but required in type 'Pet'.14번 줄에서만 막힙니다.
- 13번 줄은 지나갑니다. Plant 자리가 요구하는 줄은 name 하나이고 dog는 그 줄을 가졌습니다. age 줄이 더 있어도 이름을 거친 값이라 오류가 아닙니다. 검사기는 타입 이름(Pet · Plant)이 아니라 모양으로 맞춥니다(검사 규칙 4).
- 14번 줄은 막힙니다. p의 선언 타입 Plant에는 age 줄이 없어, Pet 자리가 요구하는 age를 가졌다고 볼 수 없습니다. 실행 때 p가 붙은 값에 age가 들어 있어도 검사기는 선언 타입으로 봅니다.
흔한 오답은 13번 줄도 막힌다고 쓴 것입니다. 타입 이름이 같아야 맞는다고 본 것입니다(이름이 같아야 맞는다는 생각). 14번 줄이 지나간다고 쓰면 p가 붙은 값이 dog라는 것을 검사기가 따라간다고 본 것입니다.
다시 볼 레슨: 모양으로 맞추기
문제 6
음료를 만드는 함수에 얼음 여부까지 든 객체를 넘깁니다. 검사 결과와 실행 결과를 각각 쓰세요.
interface Drink {
name: string;
}
function brew(d: Drink) {
console.log(d);
}
const iced = {
name: '라테',
ice: true,
};
brew(iced);
brew({
name: '녹차',
ice: false,
});
힌트
11번 줄과 12~15번 줄에서 brew에 넘기는 것을 하나씩 봅니다.
힌트 둘
11번 줄과 12번 줄은 넘기는 객체를 어디서 만들었는지가 다릅니다.
정답
main.ts(14,3): error TS2353: Object literal may only specify known properties, and 'ice' does not exist in type 'Drink'.{ name: '라테', ice: true }
{ name: '녹차', ice: false }검사는 14번 줄에서만 막힙니다. 12번 줄은 Drink 자리에 객체를 바로 새로 써서 넘겼고, 검사기는 새로 쓴 객체의 남는 속성 ice(14번 줄)를 오타일 수 있다고 잡습니다. 11번 줄은 이름 iced를 거친 객체라 name 줄만 맞으면 지나갑니다(검사 규칙 4). Node는 남는 속성을 지우지 않습니다. 두 객체를 받은 그대로 찍습니다.
흔한 오답 하나는 11번 줄도 막힌다고 쓴 것입니다. 남는 속성이 있으면 어디서나 오류라고 본 것입니다(남는 속성은 늘 오류라는 생각). 다른 흔한 오답은 실행 결과에서 ice를 뺀 것입니다. 남는 속성 검사가 실행 때 그 속성을 지운다고 본 것입니다(타입이 실행 때 남는다는 생각).
다시 볼 레슨: 새로 쓴 객체의 남는 속성 · 모양으로 맞추기
문제 7
대회에서 받은 메달의 색을 적습니다. 3번 줄에서 best와 medal.color의 지금 타입을 쓰세요.
const best = 'gold';
const medal = { color: best };
console.log(best, medal.color);
힌트
best와 medal이 각각 몇 번 줄에서 무엇으로 만들어지는지 봅니다.
힌트 둘
best는 const로 만든 글자 하나이고, medal.color는 객체의 속성입니다.
정답
검사 오류 없이 지나가고, 3번 줄의 지금 타입은 best가 "gold", medal.color가 string입니다.
- best는 const로 만든 글자 하나라 그 값 하나짜리 타입
"gold"입니다. - medal.color는
"gold"인 best로 만들었어도 객체의 속성이라 string으로 넓혀집니다. 객체의 속성은 나중에 다른 값에 다시 붙을 수 있기 때문입니다(검사 규칙 3).
같은 답으로 치는 꼴: best를 'gold'처럼 작은따옴표로 쓴 것.
흔한 오답은 medal.color를 "gold"로 쓴 것입니다. "gold" 타입인 값을 넣었으니 속성도 그 값 하나라고 본 것입니다(넓어지지 않는다는 생각). best를 string으로 쓰면 const로 만든 글자도 넓힌다고 본 것입니다.
다시 볼 레슨: 적지 않아도 붙는 타입 — 추론
문제 8
메뉴마다 좋아요 수를 모읍니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요. 8번 줄에서 likes['커피']의 지금 타입은 무엇이고, Node로 돌리면 무엇이 찍힐까요?
interface Likes {
[menu: string]: number;
}
const likes: Likes = {};
likes['라테'] = 3;
likes['녹차'] = '두 표';
console.log(likes['커피'] + 1);
힌트
2번 줄의 한 줄이 칸 이름과 칸의 값에 대해 각각 무엇을 말하는지 봅니다.
정답
main.ts(7,1): error TS2322: Type 'string' is not assignable to type 'number'.NaN검사는 7번 줄에서만 막히고, likes['커피']의 지금 타입은 number입니다.
- 2번 줄은 '어떤 이름의 칸이든 값은 number'라는 뜻입니다. 칸 이름은 무엇이든 되지만 값은 수여야 해서, 7번 줄의 글자 '두 표'는 넣을 수 없습니다.
- 8번 줄은 지나갑니다. 검사기는 '커피' 칸이 정말 있는지 모르고, 어떤 칸에서 꺼내든 number로 봅니다.
- Node는 7번 줄도 실행하고, 8번 줄에서 없는 칸 '커피'를 꺼내 undefined에 1을 더해 NaN을 찍습니다.
흔한 오답은 8번 줄도 막힌다고 쓰거나 실행 결과를 1로 쓴 것입니다. 인덱스 시그니처로 꺼낸 값은 늘 들어 있다고, 또는 없는 칸은 검사기가 막아 준다고 본 것입니다(꺼낸 값은 늘 있다는 생각). 없을 수 있는 칸은 ?? 0처럼 실행할 때 걸러 씁니다(검사 규칙 2).
다시 볼 레슨: 키가 정해지지 않은 객체 — 인덱스 시그니처
문제 9
하고 싶은 일마다 쓸 기호나 낱말, 한 줄을 쓰세요.
- (가) 있어도 없어도 되는 속성의 이름 뒤에 붙이는 기호
- (나) 검사기가 다시 대입하기를 막게 속성 이름 앞에 붙이는 낱말
- (다) 칸 이름이 정해지지 않은 객체에서, 어떤 이름의 칸이든 값은 수라는 것을 interface 안에 적는 한 줄
- (라) 객체의 모양에 이름을 붙여 적을 때 맨 앞에 쓰는 낱말
힌트
(다)는 대괄호로 시작합니다.
정답
받는 답: (가) ? · (나) readonly · (다) [item: string]: number; · (라) interface
같은 답으로 치는 꼴:
- (다) 대괄호 안 이름은 아무 낱말이나 됩니다(
[key: string]: number). 줄 끝;을 빼도 됩니다. - (라)
type도 객체 모양에 이름을 붙일 수 있습니다. 이 과목은 객체 모양을 interface로 적습니다.
틀리는 꼴:
- (가)
?.— 속성을 꺼낼 때 쓰는 자바스크립트 연산자라 타입을 적는 자리에는 쓰지 않습니다. - (나)
const— 이름을 다른 값에 다시 붙이지 못하게 할 뿐, 그 객체의 속성은 막지 않습니다. - (다)
{}같은 빈 객체 타입 — 어떤 칸도 없는 모양이라, 칸을 더하면 TS7053으로 막힙니다. - (라)
const—const Menu = { name: string };처럼 쓰면 모양이 아니라 객체 값을 만듭니다. 값 자리에 쓴 string이 TS2693으로 막힙니다(타입과 값을 섞는 생각). 타입 이름은 실행할 때 없어 값 자리에 쓸 수 없습니다(검사 규칙 1).
다시 볼 레슨: 선택 속성과 readonly · 키가 정해지지 않은 객체 — 인덱스 시그니처 · 객체 타입과 interface
문제 10
카페 자리 번호 C12를 찍으려 합니다. 검사에서 이렇게 막혔습니다. 몇 번 줄의 무엇을 고쳐야 할까요? 고친 줄을 쓰세요.
interface Seat {
row: string;
num: number;
}
const mine = {
row: 'C',
nmu: 12,
};
const s: Seat = mine;
console.log(s.row + s.num);
main.ts(10,7): error TS2741: Property 'num' is missing in type '{ row: string; nmu: number; }' but required in type 'Seat'.힌트
오류 문장이 보인 mine의 모양에서 속성 이름을 하나씩 Seat와 견줘 봅니다.
정답
8번 줄을 num: 12,로 고칩니다.
interface Seat {
row: string;
num: number;
}
const mine = {
row: 'C',
num: 12,
};
const s: Seat = mine;
console.log(s.row + s.num);C12오류는 10번 줄에서 났지만 까닭은 8번 줄의 오타 nmu입니다. mine은 이름을 거친 객체라, 8번 줄에서는 남는 속성 nmu를 잡지 않습니다(남는 속성 검사는 새로 쓴 객체에만 — 검사 규칙 4). 그래서 Seat 자리에 넣는 10번 줄에서야 num이 빠졌다고 나옵니다.
흔한 오답은 오류가 난 10번 줄이나 Seat를 고치는 것입니다.
- 10번 줄의
: Seat를 지우면 오류가 11번 줄로 옮겨 갑니다(Property 'num' does not exist— TS2339). - Seat의 num을
num?: number;로 바꾸면 검사 오류는 0이지만 실행 결과가Cundefined입니다.
interface Seat {
row: string;
num?: number;
}
const mine = {
row: 'C',
nmu: 12,
};
const s: Seat = mine;
console.log(s.row + s.num);Cundefined오류가 난 줄을 고칠 줄로 본 것입니다(오류 줄이 고칠 줄이라는 생각). 타입을 틀린 값에 맞추면 검사는 지나도 결과는 틀립니다(검사를 지나면 맞다는 생각).
다시 볼 레슨: 새로 쓴 객체의 남는 속성 · 모양으로 맞추기 · 타입 오류 읽기
문제 11 (만들어 보기)
자바스크립트로 쓴 책 목록 코드에 타입을 붙여 봅니다. 아래 뼈대는 그 코드에 하위 목표 주석 둘을 넣은 것입니다. 타입을 붙여 돌리면 이렇게 나와야 합니다.
숲 200쪽
주석 '1. 모양을 interface로' 아래를 채우고, '2. 매개변수에 타입' 아래 함수 머리에 타입을 붙이세요. 검사 오류는 0이어야 합니다.
// 1. 모양을 interface로
const books = [
{ name: '봄', pages: 120 },
{ name: '숲', pages: 200 },
];
// 2. 매개변수에 타입
function label(b) {
const p = `${b.pages}쪽`;
return `${b.name} ${p}`;
}
console.log(label(books[1]));
다 쓰면 마지막 줄의 books[1]을 books[0]으로 바꿔 따라가 봅니다. 봄 120쪽이 나와야 합니다.
힌트
books의 원소 하나가 가진 속성 이름과 값의 종류를 보고 interface의 줄을 씁니다. 그 interface 이름을 매개변수 b의 타입으로 적습니다.
정답
모범 답입니다.
// 1. 모양을 interface로
interface Book {
name: string;
pages: number;
}
const books = [
{ name: '봄', pages: 120 },
{ name: '숲', pages: 200 },
];
// 2. 매개변수에 타입
function label(b: Book) {
const p = `${b.pages}쪽`;
return `${b.name} ${p}`;
}
console.log(label(books[1]));숲 200쪽books[0]으로 바꾸면 b가 첫 원소에 붙어 봄 120쪽입니다.
- 다르게 써도 되는 곳: interface 이름은 달라도 됩니다. 함수 머리를 매개변수마다 한 줄로 나눠도 됩니다.
type Book = { … };로 적어도 검사는 지나지만, 이 과목은 객체 모양을 interface로 적습니다. - 검사 오류가 0이고, 찍은 화면이 위와 같고,
books[0]도 맞으면 맞습니다. - 흔한 오답 하나는 interface만 쓰고 b에 타입을 붙이지 않은 것입니다. 넘기는 값이 Book 모양이니 b도 추론된다고 본 것입니다(매개변수도 추론된다는 생각). 실행 결과는 같아도 검사가 TS7006으로 막힙니다.
- 다른 흔한 오답은 매개변수를 구조 분해로 받으며 중괄호 안에 타입을 쓴 것입니다(
{ name: string }꼴 — 구조 분해에 타입을 붙이는 생각). TS7031 · TS2304로 막히고 실행도ReferenceError로 멈춥니다. 구조 분해로 받으려면 타입은 중괄호 뒤에}: Book으로 적습니다.
다시 볼 레슨: JS 코드에 타입 붙이기 · 객체 타입과 interface
문제 12 (만들어 보기)
이번에는 빈 데서 씁니다. 오늘 팔린 컵을 크기별로 세는 프로그램을 쓰세요. 팔린 컵의 크기 목록은 ['S', 'L', 'S']이고, 크기마다 몇 잔인지 빈 객체에 모아 찍습니다. 검사 오류는 0이어야 하고, 이렇게 나와야 합니다.
{ S: 2, L: 1 }
다 쓰면 목록 끝에 'L'을 하나 더해 따라가 봅니다. { S: 2, L: 2 }가 나와야 합니다.
힌트
하위 목표 셋의 주석(1. 빈 객체와 빈 배열에 타입 · 2. 하나씩 꺼내 칸 올리기 · 3. 끝나고 쓰기)을 먼저 쓰고, 그 아래를 채웁니다.
정답
모범 답입니다.
// 1. 빈 객체와 빈 배열에 타입
interface Count {
[size: string]: number;
}
const cups = ['S', 'L', 'S'];
const count: Count = {};
// 2. 하나씩 꺼내 칸 올리기
for (const c of cups) {
const n = count[c] ?? 0;
count[c] = n + 1;
}
// 3. 끝나고 쓰기
console.log(count);{ S: 2, L: 1 }'L'을 하나 더하면 넷째 바퀴에서 L 칸이 1에서 2로 올라 { S: 2, L: 2 }입니다.
- 다르게 써도 되는 곳: interface와 칸 이름(size · c)은 달라도 됩니다. 9 · 10번 줄을 한 줄로 합쳐
(count[c] ?? 0) + 1을 넣어도 같습니다. - 검사 오류가 0이고, 찍은 화면이 위와 같고, 'L'을 더한 결과도 맞으면 맞습니다.
- 흔한 오답 하나는
const count = {};처럼 빈 객체에 타입을 붙이지 않은 것입니다. 자바스크립트 그대로라 9 · 10번 줄이 TS7053으로 막힙니다.{}에는 칸이 없다고 보기 때문입니다. - 다른 흔한 오답은
?? 0없이count[c] = count[c] + 1;로 쓴 것입니다. 인덱스 시그니처 덕에 검사는 지나지만, 처음 보는 크기의 칸은 undefined라 NaN이 찍힙니다.
interface Count {
[size: string]: number;
}
const cups = ['S', 'L', 'S'];
const count: Count = {};
for (const c of cups) {
count[c] = count[c] + 1;
}
console.log(count);{ S: NaN, L: NaN }인덱스 시그니처로 꺼낸 값은 늘 들어 있다고 본 것입니다(꺼낸 값은 늘 있다는 생각). 검사를 지났어도 결과는 틀립니다(검사를 지나면 맞다는 생각).
JS 다시 보기: 세기 틀은 빈 객체에서 시작해 하나씩 꺼낼 때마다 그 칸을 1 올립니다(JS '세기: 메뉴별로 모으기').
다시 볼 레슨: 키가 정해지지 않은 객체 — 인덱스 시그니처 · JS 코드에 타입 붙이기