세기는 배열의 값마다 몇 번 나왔는지를 객체에 모으는 틀입니다. 값을 키로 삼아, 그 값을 꺼낼 때마다 그 키의 칸을 1 올리고, 처음 보는 값은 ?? 0으로 0에서 시작합니다.
앞에서 배운 세기(합 모으기와 세기)는 조건에 맞는 수의 개수를 이름 하나에 모았습니다. 메뉴마다 따로 세려면 메뉴마다 이름이 있어야 하는데, 어떤 메뉴가 들어올지는 코드를 쓸 때 알 수 없습니다. 객체는 처음 보는 메뉴의 칸을 그때 새로 더할 수 있습니다. 이 레슨은 메뉴별 판매 수를 세는 코드를 직접 쓰는 틀 레슨입니다. 풀이 예제를 하위 목표 셋으로 나눠 읽고, 빈칸을 채우며 쓰기로 넘어갑니다.
1세기의 세 하위 목표
오늘 주문 넷을 메뉴별로 세는 풀이 예제입니다. 그림은 주문 셋만 쓰는 여덟 줄이라 줄 번호가 이 코드와 다릅니다.
const today = [
'커피',
'라테',
'커피',
'쿠키',
];
// 1. 빈 객체
const sold = {};
// 2. 하나씩 꺼내 칸 올리기
for (const item of today) {
const n = sold[item] ?? 0;
sold[item] = n + 1;
}
// 3. 끝나고 쓰기
console.log(sold);
{ '커피': 2, '라테': 1, '쿠키': 1 }- 1. 빈 객체: 아직 센 메뉴가 없으니 칸도 없습니다. 8번 줄이
sold를 새 빈 객체에 붙입니다. 칸을 더하기만 하고 이름을 다시 붙이지 않으니const로 만듭니다. - 2. 하나씩 꺼내 칸 올리기: 10번 줄의
for...of가 주문을 하나씩 꺼내item을 붙입니다. 11번 줄이 그 메뉴 칸의 지금 값에 이름n을 붙이고, 12번 줄이n + 1을 그 칸에 담습니다. 읽고 나서 담는 두 박자입니다. - 3. 끝나고 쓰기: 반복이 다 끝난 뒤 15번 줄이 한 번 찍습니다.
11번 줄의 ?? 0이 처음 보는 메뉴를 맡습니다. 칸이 없는 키로 꺼내면 오류 없이 undefined가 나오고(객체: 이름 붙은 칸 묶음), ?? 연산자는 왼쪽이 null이나 undefined일 때만 오른쪽 0을 고릅니다(참 같은 값과 ??). 그래서 처음 보는 메뉴는 0에서 시작하고, 칸이 있는 메뉴는 그 칸의 값에서 이어 셉니다.
11·12번 줄이 점이 아니라 대괄호를 쓰는 것은 item이 가리키는 메뉴 이름을 키로 쓰기 위해서입니다.
바퀴마다 n과 그 칸을 적으면 칸이 언제 생기는지 보입니다.
| 바퀴 | item | 11번 줄의 n | 12번 줄 뒤 그 칸 |
|---|---|---|---|
| 첫째 | '커피' | 0(칸 없음) | 커피 1 — 새 칸 |
| 둘째 | '라테' | 0(칸 없음) | 라테 1 — 새 칸 |
| 셋째 | '커피' | 1 | 커피 2 |
| 넷째 | '쿠키' | 0(칸 없음) | 쿠키 1 — 새 칸 |
칸은 그 메뉴를 처음 본 바퀴에 생겨서, 찍을 때도 커피, 라테, 쿠키 차례입니다.
sold처럼 센 값을 모아 담는 이름을 담는 그릇이라고 부릅니다. 거르기(거르기와 바꿔 담기)의 담는 그릇은 배열이었고, 세기의 담는 그릇은 객체입니다. item은 주문을 차례로 받는 걸음꾼입니다.
2빈 객체는 반복 앞에 한 번
1절 풀이 예제에서 빈 객체를 만드는 줄을 반복 안으로 옮기고, 바퀴마다 sold를 찍게 했습니다. 마지막 줄까지 무엇이 찍힐지 정한 뒤 펼쳐 보세요.
const today = [
'커피',
'라테',
'커피',
'쿠키',
];
for (const item of today) {
const sold = {};
const n = sold[item] ?? 0;
sold[item] = n + 1;
console.log(sold);
}
실행 결과 보기
{ '커피': 1 }
{ '라테': 1 }
{ '커피': 1 }
{ '쿠키': 1 }예상과 다르면 그곳이 배울 곳입니다. 8번 줄은 중괄호 안에 있어 바퀴마다 실행되고, 그때마다 sold를 새 빈 객체에 붙입니다. 그래서 셋째 바퀴의 '커피'도 칸이 없는 객체에서 0으로 시작해 1이 됩니다.
sold는 바퀴마다 새로 만들어지는 이름이라 const로 만들어도 오류는 아닙니다. 센 값을 모으려면 거르기의 빈 배열처럼 담는 그릇은 반복 앞에서 한 번만 만듭니다.
예제
메뉴별 매출 모으기
세기 틀은 개수만 세지 않습니다. 2번 하위 목표에서 1 대신 가격을 더하면 메뉴별 매출이 모입니다.
const menu = {
커피: 4000,
라테: 4500,
};
const today = [
'커피',
'라테',
'커피',
];
const sales = {};
for (const item of today) {
const n = sales[item] ?? 0;
sales[item] = n + menu[item];
}
console.log(sales);
{ '커피': 8000, '라테': 4500 }13번 줄의 menu[item]은 꺼낸 메뉴 이름을 키로 가격을 찾습니다. 그 가격을 칸의 지금 값 n에 더하니, 커피 칸에는 4000이 두 번 더해져 8000이 됩니다.
자주 틀리는 곳
틀린 생각: 칸 올리기는 sold[item] = sold[item] + 1; 한 줄이면 된다.
n 없이 칸을 바로 읽어 1을 더하면 줄이 줄 것 같습니다. 그런데 처음 보는 메뉴는 칸이 없어 sold[item]이 undefined이고, undefined에 1을 더하면 오류 없이 NaN이 됩니다(매개변수와 인자).
const today = ['커피', '커피'];
const sold = {};
for (const item of today) {
sold[item] = sold[item] + 1;
}
console.log(sold);
{ '커피': NaN }첫 바퀴에 칸 '커피'가 NaN으로 생기고, 둘째 바퀴는 NaN에 1을 더해 다시 NaN입니다. 한번 생긴 NaN은 계산을 따라 퍼져서(글자를 수로: Number와 NaN), 오류 없이 틀린 값이 찍힙니다.
맞는 규칙: 처음 보는 키는 칸이 없어 undefined가 나옵니다. 칸 올리기는 const n = sold[item] ?? 0;으로 지금 값을 받아 두고, 거기에 1을 더해 담습니다.
틀린 생각: 빈 객체를 반복 안에서 만들어도 센 값은 쌓인다.
중괄호 안의 줄은 바퀴마다 실행되어, 2절에서 본 것처럼 바퀴마다 새 빈 객체가 생깁니다. 찍는 줄을 반복 뒤로 옮기면 sold가 이미 사라져 sold is not defined로 멈춥니다.
맞는 규칙: 담는 그릇은 반복 앞에 한 번 만듭니다. 반복 안에서 만든 이름은 그 중괄호 안에서만 삽니다(규칙 4).
틀린 생각: 11·12번 줄의 sold[item]을 sold.item으로 써도 같다.
item이 메뉴 이름을 가리키니 점으로 써도 될 것 같습니다. 그러나 점 뒤의 글자는 키 글자 그대로라서, sold.item은 메뉴와 상관없이 키 'item'의 칸입니다. 1절 풀이 예제의 11·12번 줄을 이렇게 바꾸면 주문 넷이 모두 한 칸에 쌓여 { item: 4 }가 찍힙니다. 앞 레슨의 menu.name과 같은 실수입니다(객체 돌기).
맞는 규칙: 이름이 가리키는 값을 키로 쓸 때는 대괄호를 씁니다. sold[item]은 item이 가리키는 메뉴 이름을 키로, sold.item은 글자 'item'을 키로 씁니다.
틀린 생각: 11번 줄의 ?? 0과 || 0은 언제나 같은 일을 한다.
1절 풀이 예제의 11번 줄을 || 0으로 바꿔 돌려도 같은 값이 찍힙니다. 칸이 없을 때의 undefined는 둘 다 0으로 바꾸고, 칸이 있으면 1 이상이라 둘 다 그 값을 쓰기 때문입니다. 그러나 || 연산자는 0이나 빈 문자열처럼 제 값일 수 있는 거짓 같은 값도 '없음'으로 칩니다. 재고 0이 기본값에 지워진 장면이 그것입니다(참 같은 값과 ??).
맞는 규칙: ?? 연산자는 null·undefined일 때만 기본값을 고릅니다. 세기 틀이 뜻하는 것은 '칸이 없을 때만 0'이니 ?? 연산자를 씁니다.
확인 문제
1. 1절 풀이 예제의 주문만 바꿨습니다. 무엇이 찍힐까요?
const today = [
'쿠키',
'녹차',
'쿠키',
'쿠키',
];
// 1. 빈 객체
const sold = {};
// 2. 하나씩 꺼내 칸 올리기
for (const item of today) {
const n = sold[item] ?? 0;
sold[item] = n + 1;
}
// 3. 끝나고 쓰기
console.log(sold);
힌트
바퀴마다 11번 줄의 n을 적고, 칸이 어느 바퀴에 생기는지 봅니다.
정답
{ '쿠키': 3, '녹차': 1 }'쿠키'는 첫 바퀴에 칸이 1로 생기고, 셋째와 넷째 바퀴에서 n이 1과 2에 붙어 칸이 3이 됩니다. '녹차'는 둘째 바퀴에 칸이 생겨 1입니다. 칸은 처음 본 차례대로 생기니 쿠키, 녹차 차례로 찍힙니다.
흔한 오답은 { '녹차': 1, '쿠키': 3 }입니다. 키가 가나다 차례로 찍힌다고 본 것입니다. 키는 객체에 담은 차례대로 나옵니다.
2. 빈칸을 채워 코드를 완성하세요.
(가) 컵 크기별 판매 수를 셉니다. { S: 2, L: 1 }이 찍히도록 7번 줄의 빈칸을 채우세요.
const cups = ['S', 'L', 'S'];
// 1. 빈 객체
const sold = {};
// 2. 하나씩 꺼내 칸 올리기
for (const cup of cups) {
const n = sold[cup] ?? 0;
sold[cup] = ________
}
// 3. 끝나고 쓰기
console.log(sold);
힌트
2번 하위 목표 '하나씩 꺼내 칸 올리기'의 둘째 박자입니다. 6번 줄의 n이 무엇을 받아 두었는지 봅니다.
정답
n + 1;입니다.
const cups = ['S', 'L', 'S'];
// 1. 빈 객체
const sold = {};
// 2. 하나씩 꺼내 칸 올리기
for (const cup of cups) {
const n = sold[cup] ?? 0;
sold[cup] = n + 1;
}
// 3. 끝나고 쓰기
console.log(sold);{ S: 2, L: 1 }S와 L은 영어 글자라 Node가 따옴표 없이 찍습니다.
흔한 오답은 1만 쓰는 것입니다. 지금 값에 더하지 않고 칸마다 1을 다시 담아 { S: 1, L: 1 }이 찍힙니다.
(나) 장바구니에 담은 것을 셉니다. 2번 하위 목표를 통째로 채우세요. 다 채운 코드는 { '빵': 2, '잼': 1 }을 찍어야 하고, 1~5번 줄을 const bag = ['우유']; 한 줄로 바꾸면 { '우유': 1 }을 찍어야 합니다.
const bag = [
'빵',
'잼',
'빵',
];
// 1. 빈 객체
const count = {};
// 2. 하나씩 꺼내 칸 올리기
// 3. 끝나고 쓰기
console.log(count);
힌트
1절 풀이 예제의 2번 하위 목표와 같은 네 줄입니다. 걸음꾼 이름만 이 코드에 맞춥니다.
정답
const bag = [
'빵',
'잼',
'빵',
];
// 1. 빈 객체
const count = {};
// 2. 하나씩 꺼내 칸 올리기
for (const food of bag) {
const n = count[food] ?? 0;
count[food] = n + 1;
}
// 3. 끝나고 쓰기
console.log(count);{ '빵': 2, '잼': 1 }다르게 써도 되는 곳: 걸음꾼 이름은 food 말고 다른 낱말이어도 됩니다. n + 1은 1 + n으로 써도 같습니다. 10번 줄의 ?? 0을 || 0으로 써도 여기서는 같게 찍히지만, 세는 틀에는 ?? 연산자를 씁니다(자주 틀리는 곳 넷째).
흔한 오답은 10·11번 줄 대신 count[food] += 1; 한 줄을 쓰는 것입니다. 처음 보는 '빵'에서 칸이 없어 undefined에 1을 더하니, 오류 없이 { '빵': NaN, '잼': NaN }이 찍힙니다.
3. 낱말에 든 글자를 셉니다. 무엇이 찍힐까요?
const count = {};
for (const ch of '바나나') {
const n = count[ch] ?? 0;
count[ch] = n + 1;
}
console.log(count);
힌트
문자열을 for...of로 돌면 한 글자씩 꺼냅니다(for...of로 배열 돌기). 바퀴마다 3번 줄의 n을 적어 봅니다.
정답
{ '바': 1, '나': 2 }'바'와 첫 '나'는 처음 보는 키라 칸이 1로 생기고, 셋째 글자 '나'는 n이 1에 붙어 칸이 2가 됩니다.
흔한 오답은 { '바': 1, '나': 1 }입니다. 셋째 글자 '나'에서도 n이 0에 붙는다고 본 것입니다. 칸 '나'는 둘째 바퀴에 이미 생겨, 셋째 바퀴의 count[ch] ?? 0은 1입니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
1절 코드(열다섯 줄)를 main.js에 저장해 돌린 뒤, 11번 줄의 ?? 0을 지웁니다. 무엇이 찍힐지 먼저 적고, 지워 돌려서 맞춰 봅니다. 세 칸이 모두 NaN이 아니면 '자주 틀리는 곳' 첫째를 다시 봅니다.
저장하고 돌리는 법은 내 컴퓨터에서 돌려 보기에 있습니다.