객체를 담은 배열은 주문 여러 건처럼 같은 모양의 객체를 차례로 모아 둔 값입니다. orders[1].qty처럼 바깥 배열부터 한 겹씩 꺼내고, 중간이 없을 수 있으면 ?. 기호로 멈추지 않고 undefined를 받습니다.
주문 한 건은 메뉴 이름 item과 수량 qty를 담은 객체 하나로 둘 수 있습니다(객체: 이름 붙은 칸 묶음). 주문이 여러 건이면 이 객체들을 배열 하나에 담습니다. 배열의 칸도 객체를 가리키므로, 객체에서 본 이름표 규칙(규칙 3)이 그대로 이어집니다(객체를 넘기고 복사하기).
이 레슨에서는 두 겹을 꺼내는 차례와 없는 자리에서 멈추지 않는 ?. 기호를 보고, 주문 배열을 펼쳐 복사했을 때 무엇이 같이 바뀌는지 먼저 맞혀 봅니다.
1바깥부터 한 겹씩 꺼낸다
그림의 코드입니다.
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
];
console.log(orders[1].qty);
console.log(orders[2]?.qty);
1
undefined1~4번 줄은 주문 객체 둘을 만들어 배열 하나에 담고 이름 orders를 붙입니다. 그림은 두 객체를 배열 칸 안에 넣지 않고 바깥에 따로 그린 뒤, 칸마다 화살표를 내어 가리킵니다. 배열의 칸도 이름처럼 객체를 가리킨다는 뜻입니다(규칙 3).
5번 줄 orders[1].qty는 왼쪽부터 한 겹씩 꺼냅니다. 먼저 orders[1]이 1번 칸이 가리키는 라테 객체를 꺼내고, 이어서 .qty가 그 객체의 수량 1을 꺼냅니다. 바깥 배열에서는 자리 번호로, 안쪽 객체에서는 키로 꺼냅니다.
두 걸음을 나눠 쓰면 중간에 꺼낸 것이 객체 하나라는 것이 보입니다.
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
];
const second = orders[1];
console.log(second);
console.log(second.qty);
{ item: '라테', qty: 1 }
15번 줄은 라테 객체에 이름 second를 하나 더 붙일 뿐, 객체를 복사하지 않습니다. orders[1].qty는 이 두 걸음을 한 줄에 이어 쓴 것입니다. 그림 코드 6번 줄의 orders[2]?.qty는 2절에서 봅니다.
2없을 수 있는 자리는 ?. 기호로 꺼낸다
그림 코드 6번 줄의 orders[2]는 없는 자리입니다. 배열에서 없는 자리를 꺼내면 오류 없이 undefined가 나왔습니다(배열과 이름 둘). 그런데 ?. 대신 .을 써서 거기서 한 겹 더 꺼내면 멈춥니다.
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
];
console.log(orders[1].qty);
console.log(orders[2].qty);
1main.js:6console.log(orders[2].qty); ^ TypeError: Cannot read properties of undefined (reading 'qty') at main.js:6:23undefined에서는 무엇을 꺼내도 TypeError로 멈춥니다(인덱스와 자르기). 오류 메시지의 reading 'qty'는 꺼내려던 키이고, undefined는 그 앞 orders[2]의 값입니다. 그래서 고칠 곳은 qty가 아니라 번호 2입니다.
?. 기호는 왼쪽 값이 undefined나 null이면 오른쪽을 꺼내지 않고 그 자리에서 undefined를 냅니다. 왼쪽에 값이 있으면 .과 똑같이 꺼냅니다. 그래서 그림 코드 6번 줄의 orders[2]?.qty는 멈추지 않고 undefined를 찍습니다. ?. 기호를 자료에 따라 optional chaining 연산자라고도 부릅니다.
없을 때 0으로 셀 거라면 orders[2]?.qty ?? 0처럼 뒤에 ?? 연산자를 붙입니다(참 같은 값과 ??). 왼쪽이 undefined라서 0이 나옵니다.
?. 기호는 없을 수 있는 자리에만 붙입니다. 늘 있어야 할 값이 없는 것은 실수라서, 오류로 멈춰야 고칠 곳을 바로 찾습니다(아래 자주 틀리는 곳).
3펼쳐 복사해도 안의 객체는 같다
주문 배열을 [...orders]로 펼쳐 복사한 배열에 이름 backup을 붙여 두고, 원래 배열의 첫 주문 수량을 3으로 바꿨습니다. backup으로 꺼낸 첫 주문 수량은 무엇일까요? 무엇을 찍을지 답을 정한 뒤 펼쳐 보세요.
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
];
const backup = [...orders];
orders[0].qty = 3;
console.log(backup[0].qty);
실행 결과 보기
3예상과 다르면 그곳이 배울 곳입니다. 5번 줄의 펼치기는 바깥 배열만 새로 만듭니다. 새 칸에 옮겨 담기는 것은 객체를 가리키는 화살표라서, backup의 0번 칸도 orders의 0번 칸과 같은 커피 객체를 가리킵니다. 6번 줄은 그 커피 객체의 qty를 바꾸므로 backup으로 꺼내도 3입니다.
그래서 backup === orders는 false이지만 backup[0] === orders[0]은 true입니다. 배열 안의 배열에서 본 '펼치기는 바깥 한 겹만'(배열 바꾸기와 복사)이 배열 안의 객체에서도 그대로입니다.
안쪽 객체까지 따로 두려면 5번 줄의 [...orders]를 structuredClone(orders)로 바꿉니다. 객체까지 새로 만든 사본이라 2가 찍힙니다.
예제
주문 합계 내기
오늘 주문 셋의 합계를 메뉴 가격으로 셈합니다. 먼저 메뉴 객체와 주문 배열을 만듭니다.
const menu = {
커피: 4000,
라테: 4500,
쿠키: 2000,
};
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
{ item: '쿠키', qty: 1 },
];
이어서 주문을 하나씩 꺼내 가격에 수량을 곱해 더합니다.
let total = 0;
for (const o of orders) {
const price = menu[o.item];
total = total + price * o.qty;
}
console.log(`${total}원`);
14500원12번 줄의 o는 바퀴마다 칸이 가리키는 주문 객체입니다(for...of로 배열 돌기). 그래서 13번 줄의 o.item은 '커피' 같은 메뉴 이름이고, menu[o.item]은 그 이름을 키로 메뉴의 가격을 찾습니다. 14번 줄은 가격에 수량 o.qty를 곱해 total에 더합니다.
바퀴마다 꺼낸 것이 무엇인지만 정하면, 안쪽은 1절처럼 키로 꺼내면 됩니다. 커피 8000원, 라테 4500원, 쿠키 2000원을 모아 14500원이 찍힙니다.
과목 끝의 할 일 앱에서도 할 일 목록은 할 일 객체(id · text · done)를 담은 배열 하나입니다.
자주 틀리는 곳
틀린 생각: 둘째 주문의 수량은 orders.qty[1]로 꺼낸다.
'주문들의 수량 가운데 1번'이라고 읽으면 이 차례가 자연스러워 보입니다.
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
];
console.log(orders.qty[1]);
main.js:5console.log(orders.qty[1]); ^ TypeError: Cannot read properties of undefined (reading '1') at main.js:5:23맞는 규칙: 꺼내는 식은 왼쪽부터 한 겹씩 읽습니다. orders는 배열이라 키 qty가 없어 orders.qty가 undefined이고, 거기서 1번을 꺼내려다 멈춥니다. 바깥 배열에서 번호로 객체를 먼저 꺼내고, 그 객체에서 키로 꺼냅니다(orders[1].qty).
틀린 생각: 꺼낼 때마다 ?. 기호를 붙여 두면 안전하다.
?. 기호를 쓰면 오류로 멈추지 않으니, 점을 모두 ?. 기호로 바꿔 두면 좋을 것 같습니다. 마지막 주문의 수량을 찍으려던 코드입니다.
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
];
const last = orders.length;
console.log(orders[last]?.qty);
undefined맞는 규칙: ?. 기호는 없을 수 있는 자리에만 붙입니다. 5번 줄의 last는 칸 수 2인데 자리 번호는 1까지라서, orders[last]는 없는 자리입니다(마지막 칸은 orders.at(-1)). 그런데 ?. 기호가 오류를 막아 undefined만 조용히 찍힙니다. ?. 대신 .이었다면 6번 줄에서 TypeError로 멈춰 고칠 곳을 알려 줬습니다. 늘 있어야 할 값은 .으로 꺼내, 없으면 바로 멈추게 둡니다.
틀린 생각: 주문 배열을 [...orders]로 펼쳐 복사하면 안의 주문 객체도 새로 생긴다.
새 배열을 만들었으니 칸에 든 주문도 새것일 것 같습니다.
맞는 규칙: 펼치기는 바깥 배열 한 겹만 새로 만들고, 새 칸은 같은 주문 객체를 가리킵니다(3절). 한쪽에서 주문의 수량을 바꾸면 다른 쪽에서도 보입니다. 안쪽 객체까지 따로 두려면 structuredClone을 씁니다.
확인 문제
1. 무엇이 찍힐까요? 세 줄을 글자 그대로 쓰세요.
const orders = [
{ item: '녹차', qty: 3 },
{ item: '쿠키', qty: 2 },
];
console.log(orders[1].item);
console.log(orders[0]?.qty);
console.log(orders[2]?.qty);
힌트
바깥 배열의 자리 번호를 먼저 적고, 줄마다 어느 칸의 객체를 꺼내는지 정합니다.
정답
쿠키
3
undefined자리 번호는 0과 1뿐입니다. 5번 줄의 orders[1]은 쿠키 객체라서 item은 쿠키입니다. 6번 줄의 orders[0]은 녹차 객체가 있어 ?. 기호가 .처럼 꺼내 3입니다. 7번 줄의 orders[2]는 없는 자리라서 ?. 기호가 멈추지 않고 undefined를 냅니다.
흔한 오답은 첫 줄을 녹차, 셋째 줄을 2로 쓴 것입니다. 자리 번호를 1부터 세어 orders[1]을 첫 주문, orders[2]를 둘째 주문으로 본 것입니다. 둘째 줄을 undefined로 썼다면 ?. 기호가 늘 undefined를 낸다고 본 것입니다. 왼쪽에 값이 있으면 .과 같습니다.
2. 주문의 잔 수를 모두 더해 5를 찍으려 합니다. 2번 줄의 커피 수량이 4면 7이 찍혀야 합니다. 빈칸을 채우세요.
const orders = [
{ item: '커피', qty: 2 },
{ item: '녹차', qty: 3 },
];
let cups = 0;
for (const o of orders) {
cups = cups + ________;
}
console.log(cups);
힌트
6번 줄의 o가 바퀴마다 무엇을 가리키는지 봅니다.
정답
o.qty입니다.
const orders = [
{ item: '커피', qty: 2 },
{ item: '녹차', qty: 3 },
];
let cups = 0;
for (const o of orders) {
cups = cups + o.qty;
}
console.log(cups);5바퀴마다 o는 칸이 가리키는 주문 객체라서, 키 qty로 수량을 꺼냅니다.
다르게 써도 되는 곳: o['qty']도 같습니다.
흔한 오답은 orders.qty입니다. 바깥 배열에는 키 qty가 없어 undefined를 더하므로 NaN이 찍힙니다. orders[o].qty로 썼다면 o를 자리 번호로 본 것입니다. o는 번호가 아니라 객체라서 orders[o]가 undefined가 되고, 7번 줄에서 TypeError로 멈춥니다.
3. 장바구니 배열을 펼쳐 복사해 두고 원래 배열을 고쳤습니다. 무엇이 찍힐까요? 두 줄을 글자 그대로 쓰세요.
const cart = [
{ name: '우유', count: 1 },
{ name: '빵', count: 2 },
];
const saved = [...cart];
cart[0].count = 3;
cart.pop();
console.log(saved.length);
console.log(saved[0].count);
힌트
6번 줄은 안쪽 객체를, 7번 줄은 바깥 배열을 바꿉니다. 각각 saved와 같이 쓰는 것인지 따로 봅니다.
정답
2
3펼치기는 바깥 배열을 새로 만들어서, 7번 줄이 cart에서 뺀 빵은 saved에 그대로 있습니다. 그래서 saved의 칸은 둘입니다. 안쪽 객체는 새로 생기지 않아 saved의 0번 칸도 cart와 같은 우유 객체를 가리키므로, 6번 줄에서 바꾼 3이 보입니다.
흔한 오답은 둘째 줄을 1로 쓴 것입니다. 펼치기가 안쪽 객체까지 새로 만든다고 본 것입니다(복사된다는 생각). 첫 줄을 1로 썼다면 saved가 cart와 같은 배열이라고 본 것입니다. 펼치기는 바깥 배열 하나는 새로 만듭니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
1절 그림 코드를 main.js에 저장해 돌린 뒤, 6번 줄의 ?. 기호를 .으로 바꿉니다. 무엇이 찍히고 어디서 멈출지 먼저 적고, 바꿔 돌려서 맞춰 봅니다. 1 다음에 TypeError로 멈추지 않으면 2절의 ?. 설명을 다시 봅니다.
저장하고 돌리는 법은 내 컴퓨터에서 돌려 보기에 있습니다.