본문으로 건너뛰기

객체를 넘기고 복사하기

자바스크립트 목차객체레슨 42 / 76

목차

객체를 다른 이름에 대입하거나 함수에 넘기면 사본이 생기지 않고, 같은 객체에 이름이 하나 더 붙습니다. 새 객체가 필요하면 펼치기 { ...a }로 만들고, 두 이름이 같은 객체를 가리키는지는 === 연산자로 견줍니다.

배열에서는 한 배열에 이름 둘이 붙는 것(배열과 이름 둘)과 펼치기로 사본을 만드는 것(배열 바꾸기와 복사)을 봤습니다. 객체도 같은 이름표 규칙(규칙 3)을 따릅니다. 이 레슨에서는 객체를 대입하고 넘기고 펼칠 때 이름이 어디에 붙는지 보고, 펼쳐 만든 객체 안의 배열을 바꾸면 원래 객체에서 무엇이 보이는지 그림에서 먼저 맞혀 봅니다.

7단계 중 1단계. 객체에 이름을 하나 더 붙이고, 펼쳐 복사한 뒤 안쪽 배열을 바꾸는 코드입니다.

1대입하고 넘기면 같은 객체

그림의 코드입니다.

const a = { items: ['커피'] };
const b = a;
const c = { ...a };
c.items.push('쿠키');
console.log(a.items);
실행 결과
[ '커피', '쿠키' ]

1번 줄은 객체 하나를 만들어 이름 a를 붙입니다. 이 객체의 키 items에 담긴 값은 배열입니다. 그림은 이 배열을 객체 칸 안에 넣지 않고 바깥에 따로 그린 뒤, items 줄의 값 칸에서 화살표를 내어 가리킵니다. 속성 값도 이름처럼 배열을 가리킨다는 뜻입니다(규칙 3). 2번 줄 const b = a;는 새 객체를 만들지 않고, a가 가리키는 객체에 이름 b를 하나 더 붙입니다. 배열에서와 같습니다.

함수에 객체를 넘길 때도 같습니다. 매개변수는 인자로 받은 객체에 붙는 이름 하나라서, 함수 안에서 속성을 바꾸면 밖의 이름으로도 보입니다. 손님이 자리를 옮겨 주문의 테이블 번호 table을 바꾸는 코드입니다.

function move(order) {
  order.table = 7;
}

const mine = { table: 3 };
move(mine);
console.log(mine);
실행 결과
{ table: 7 }

6번 줄이 move를 부르면 매개변수 order가 mine과 같은 객체에 붙습니다. 2번 줄은 그 객체의 table 값을 7로 바꾸므로, 함수가 끝난 뒤 mine으로 찍어도 7이 보입니다.

2펼치기는 바깥 한 겹만

배열의 [...a]처럼, 객체도 중괄호 안에 ...a를 쓰면 새 객체를 만들고 a의 속성을 같은 키와 값으로 옮겨 담습니다. 1절 코드 3번 줄 const c = { ...a };는 그래서 a와 다른 새 객체를 만들고 이름 c를 붙입니다. 그런데 옮겨 담는 것은 items 줄의 값, 곧 배열을 가리키는 화살표입니다. 그래서 c의 items 줄도 a의 items 줄이 가리키던 바로 그 배열을 가리키고, 배열은 새로 만들지 않습니다. 4번 줄은 c에서 출발해 그 배열을 찾아 '쿠키'를 더합니다. 5번 줄의 a.items도 같은 배열에 닿으므로 [ '커피', '쿠키' ]가 찍힙니다.

펼치기가 새로 만드는 것은 바깥 객체 한 겹뿐입니다. 그래서 c의 속성에 값을 대입하는 일은 a에 보이지 않습니다.

const a = { table: 3 };
const c = { ...a };
c.table = 5;
console.log(a.table);
console.log(c.table);
실행 결과
3
5

3번 줄은 새 객체 c의 table 줄만 5로 바꿉니다. a는 다른 객체라서 그대로 3입니다. 1절 코드 4번 줄이 a에서도 보였던 것은 바꾼 곳이 c의 줄이 아니라, 두 객체가 같이 가리키는 배열이었기 때문입니다.

안쪽 배열까지 새로 만들려면 structuredClone(a)를 씁니다. a를 안쪽까지 통째로 복사한 새 객체를 돌려줍니다.

const a = { items: ['커피'] };
const d = structuredClone(a);
d.items.push('녹차');
console.log(a.items);
console.log(d.items);
실행 결과
[ '커피' ]
[ '커피', '녹차' ]

바깥 한 겹만 새로 만드는 펼치기 복사를 얕은 복사(shallow copy), structuredClone처럼 안쪽까지 새로 만드는 복사를 깊은 복사(deep copy)라고 부릅니다.

3=== 연산자는 같은 객체인지 묻는다

배열끼리 === 연산자로 견주면 담긴 값이 아니라 같은 배열 하나인지를 물었습니다(같음과 비교). 객체도 같습니다.

const x = { name: '커피' };
const y = { name: '커피' };
const z = x;
console.log(x === y);
console.log(x === z);
실행 결과
false
true

x와 y는 담긴 키와 값이 같아도 1번 줄과 2번 줄이 따로 만든 두 객체라서 false입니다. z는 3번 줄에서 x가 가리키는 객체에 붙인 이름이라 true입니다. 내용이 같은지는 x.name === y.name처럼 속성 값끼리 견줍니다.

펼쳐 만든 객체와 원래 객체를 === 연산자로 견주면 어떻게 될까요? 무엇을 찍을지 답을 정한 뒤 펼쳐 보세요.

const a = { items: ['커피'] };
const c = { ...a };
console.log(a === c);
console.log(
  a.items === c.items,
);
실행 결과 보기
실행 결과
false
true

예상과 다르면 그곳이 배울 곳입니다. 2번 줄의 펼치기는 새 객체를 만들어서 a와 c는 다른 객체이고, 3번 줄은 false입니다. 그러나 c의 items 줄은 a의 items 줄과 같은 배열을 가리켜서 4~6번 줄은 true를 찍습니다. 2절의 '바깥 한 겹만'을 === 연산자로 확인한 것입니다.

예제

할인 가격으로 새 메뉴 만들기

메뉴 한 줄을 받아 500원 싼 메뉴를 새로 만들어 돌려주는 함수입니다. 원래 메뉴는 그대로 둡니다.

function sale(item) {
  const copy = { ...item };
  copy.price = item.price - 500;
  return copy;
}

const coffee = {
  name: '커피',
  price: 4000,
};
const cheap = sale(coffee);
console.log(coffee);
console.log(cheap);
실행 결과
{ name: '커피', price: 4000 }
{ name: '커피', price: 3500 }

11번 줄이 sale을 부르면 매개변수 item이 coffee와 같은 객체에 붙습니다(1절). 2번 줄이 펼치기로 새 객체를 만들어 copy를 붙이고, 3번 줄은 그 새 객체의 price만 바꿉니다(2절). 그래서 12번 줄의 coffee는 4000 그대로입니다. 2번 줄을 const copy = item;으로 쓰면 copy도 coffee와 같은 객체에 붙어, 두 줄 모두 price: 3500으로 찍힙니다. 이 메뉴 객체에는 안쪽 배열이 없어 펼치기 한 겹으로 충분합니다.

자주 틀리는 곳

틀린 생각: const b = a;는 객체 a를 복사해 b에 담는다.

대입하면 오른쪽 값이 왼쪽 이름에 옮겨 담긴다고 보면, 객체도 하나 더 생길 것 같습니다.

맞는 규칙: 대입은 새 객체를 만들지 않고, 오른쪽이 가리키는 객체에 이름을 하나 더 붙입니다(1절). 함수에 넘길 때도 같습니다. 따로 바꿀 객체가 필요하면 { ...a }로 새로 만듭니다.

틀린 생각: 키와 값이 같은 두 객체는 === 연산자로 견주면 true다.

'커피' === '커피'가 true이니 내용이 같은 객체끼리도 그럴 것 같습니다. 파이썬에서는 내용이 같은 두 딕셔너리를 == 연산자로 견주면 True입니다.

맞는 규칙: 객체끼리 === 연산자는 같은 객체 하나인지를 묻습니다(3절). 따로 만든 두 객체는 내용이 같아도 false입니다. 내용은 속성 값끼리 견줍니다.

틀린 생각: 펼쳐 만든 객체는 원래 객체와 아무것도 같이 쓰지 않는다.

새 객체를 만들었으니 그 안의 배열도 새것일 것 같습니다.

맞는 규칙: 펼치기는 바깥 객체 한 겹만 새로 만듭니다. 안쪽 배열은 두 객체가 같이 가리켜서, 한쪽에서 push하면 다른 쪽에서도 보입니다(2절). 안쪽까지 새로 만들려면 structuredClone을 씁니다.

틀린 생각: const로 만든 객체는 속성을 바꿀 수 없다.

const로 만든 이름은 다시 붙일 수 없으니, 그 객체 안의 값도 얼려 둘 것 같습니다.

const item = { price: 4000 };
item.price = 4500;
console.log(item);
item = { price: 3500 };
실행 결과
{ price: 4500 }main.js:4item = { price: 3500 };     ^ TypeError: Assignment to constant variable.    at main.js:4:6

맞는 규칙: const는 이름 item을 다른 객체에 다시 붙이는 4번 줄만 막습니다. 2번 줄처럼 그 이름이 가리키는 객체의 속성을 바꾸는 일은 됩니다. 배열에서 본 것(배열과 이름 둘)과 같습니다.

확인 문제

1. 무엇이 찍힐까요? 두 줄을 글자 그대로 쓰세요.

const cup = { size: 'L' };
const mine = cup;
const yours = { ...cup };
mine.size = 'M';
console.log(cup.size);
console.log(yours.size);
힌트

줄마다 이름이 어느 객체에 붙는지 그려 보고, 객체가 몇 개인지 셉니다.

정답
실행 결과
M
L

2번 줄의 mine은 cup과 같은 객체에 붙고, 3번 줄의 yours는 펼치기로 만든 새 객체에 붙습니다. 4번 줄은 mine이 가리키는 객체, 곧 cup의 객체를 바꾸므로 5번 줄은 M입니다. yours의 객체는 따로라서 6번 줄은 L 그대로입니다.

흔한 오답은 L · L입니다. 2번 줄이 사본을 만든다고 본 것입니다(대입은 복사라는 생각). M · M으로 썼다면 펼치기도 같은 객체에 이름을 붙인다고 본 것입니다. 펼치기는 새 객체를 만듭니다(2절).

2. 무엇이 찍힐까요? 두 줄을 글자 그대로 쓰세요.

const a = { items: ['라테'] };
const b = { ...a };
b.items.push('쿠키');
b.items = ['녹차'];
console.log(a.items);
console.log(b.items);
힌트

3번 줄과 4번 줄이 바꾸는 것이 배열인지, b의 줄인지 나눠 봅니다.

정답
실행 결과
[ '라테', '쿠키' ]
[ '녹차' ]

2번 줄의 b는 새 객체지만, b의 items 줄은 a의 items 줄과 같은 배열을 가리킵니다. 3번 줄은 그 배열에 '쿠키'를 더하므로 a에서도 보입니다. 4번 줄은 b의 items 줄에 새 배열을 대입해, b의 줄만 새 배열 ['녹차']를 가리키게 합니다. a의 items 줄은 '쿠키'를 더한 처음 배열을 그대로 가리킵니다.

흔한 오답은 첫 줄을 [ '라테' ]로 쓴 것입니다. 펼치기가 안쪽 배열까지 새로 만든다고 본 것입니다. 첫 줄을 [ '녹차' ]로 썼다면 4번 줄의 대입이 a 쪽 배열까지 바꾼다고 본 것입니다. 속성에 대입하면 그 줄이 가리키는 곳만 바뀝니다(규칙 3).

3. 장보기 목록에 우유를 더하는 함수입니다. 지금은 cart의 목록에도 우유가 들어가 아래처럼 찍힙니다.

function addMilk(list) {
  list.items.push('우유');
}

const cart = { items: ['빵'] };
const saved = cart;
addMilk(saved);
console.log(cart.items);
실행 결과
[ '빵', '우유' ]

6번 줄 오른쪽의 cart를 무엇으로 바꾸면 마지막 줄이 [ '빵' ]을 찍을까요? 보기에서 고르세요.

  • ① { ...cart }
  • ② { items: cart.items }
  • ③ structuredClone(cart)
힌트

보기마다 saved의 items 줄이 cart의 배열을 가리키는지 봅니다.

정답

③입니다. structuredClone은 안쪽 배열까지 새로 만든 사본을 돌려주므로, 7번 줄의 addMilk가 '우유'를 더하는 배열은 cart의 배열과 다른 배열입니다.

  • ① 펼치기는 바깥 객체 한 겹만 새로 만듭니다. saved의 items 줄이 cart의 배열을 그대로 가리켜 [ '빵', '우유' ]가 찍힙니다.
  • ② 객체는 새로 만들었지만 items 줄에 cart의 배열을 그대로 담았습니다. ①과 같은 꼴이라 [ '빵', '우유' ]가 찍힙니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 예제 코드를 main.js에 저장해 돌린 뒤, 2번 줄의 { ...item }을 item으로 바꿉니다. 첫 줄에 무엇이 찍힐지 먼저 적고, 바꿔 돌려서 맞춰 봅니다. { name: '커피', price: 3500 }⁠이 아니면 1절의 함수에 넘기기를 다시 봅니다. 저장하고 돌리는 법은 내 컴퓨터에서 돌려 보기에 있습니다.

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

자바스크립트 목차로