객체를 복사했는데 원본도 바뀌나요? JavaScript 전개 구문의 경계

객체 전개 구문 { ...original }은 바깥 객체를 새로 만들지만, 안에 든 객체까지 새로 만들지는 않습니다. 복사본 안의 객체를 수정했다면 원본에서도 같은 변경이 보일 수 있습니다.

AI와 함께 웹앱을 만들다가 복사 전 데이터까지 바뀌었다면, 복사한 줄보다 실제로 값을 바꾼 경로를 보세요. copy.status를 바꾸는 일과 copy.shipping.city를 바꾸는 일은 다릅니다.

바깥 객체와 배송지 객체를 따로 비교해 보세요

다음은 실제 주문이 아닌 합성 데이터입니다. 바깥 객체와 배송지 객체가 각각 같은지 먼저 비교한 뒤, 두 위치의 값을 바꿉니다.

const original = {
  status: "draft",
  shipping: { city: "Seoul" }
};
const copy = { ...original };

console.log(copy === original); // false
console.log(copy.shipping === original.shipping); // true

copy.status = "done";
copy.shipping.city = "Busan";
console.log(original.status); // "draft"
console.log(original.shipping.city); // "Busan"

크림색 배경에 두 남색 종이 겉통이 하나의 코랄색 안쪽 물체로 연결된 객체 복사 설명 그림
바깥 객체를 새로 만들어도 안쪽 객체는 공유할 수 있습니다. 설명용 생성 일러스트입니다.

copy === original은 false입니다. 서로 다른 바깥 객체라는 뜻입니다. 그런데 shipping끼리 비교하면 true입니다. 두 바깥 객체가 같은 배송지 객체를 가리킵니다.

그래서 copy.status를 바꿔도 원본의 상태는 draft로 남지만, copy.shipping.city를 바꾸면 원본에서 읽은 도시도 Busan입니다. MDN의 객체 전개 구문 설명은 이처럼 한 번의 전개가 얕은 복사를 만든다고 설명합니다.

배송지만 바꾸려면, 그 경로를 새로 만듭니다

아래 함수는 기존 속성들을 옮긴 뒤 shipping도 새 객체로 만듭니다. 마지막에 놓은 city가 새 도시 이름을 넣습니다. 여기서 쓰는 입력은 보통의 객체 리터럴이며, 복잡한 클래스나 접근자 속성을 다루는 복제 함수가 아닙니다.

function updateShippingCity(source, city) {
  return {
    ...source,
    shipping: { ...source.shipping, city }
  };
}

const source = {
  id: 7,
  shipping: { city: "Seoul", zip: "00000" }
};
const next = updateShippingCity(source, "Busan");

console.log(source.shipping.city); // "Seoul"
console.log(next.shipping.city); // "Busan"
console.log(next.shipping === source.shipping); // false
console.log(next.id, next.shipping.zip); // 7 "00000"

분리된 남색 사각 테두리 두 개가 같은 코랄색 원 하나로 이어지고 각각 청록색 사각형을 담은 종이 조형
서로 다른 바깥 객체와 공유되는 안쪽 객체를 구분한 설명용 생성 그림입니다. 실제 메모리 주소를 그린 것은 아닙니다.

원본 도시는 Seoul, 새 도시는 Busan으로 갈라집니다. 새 배송지는 원본 배송지와 다른 객체이고, 예제의 id와 zip은 남습니다. shipping 안의 다른 값도 보존하려고 ...source.shipping을 함께 넣었습니다.

중요한 차이는 중첩 속성을 직접 바꾸느냐, 바깥 객체의 속성에 새 객체를 넣느냐입니다. copy.shipping.city = "Busan"은 공유 중인 배송지를 바꿉니다. 반면 copy.shipping = { city: "Busan" }은 복사본의 shipping이 가리키는 대상을 새 객체로 교체합니다. 뒤의 한 줄은 기존 배송지의 다른 필드를 옮기지 않으므로 그대로 교체해도 되는지 따로 판단해야 합니다.

ECMAScript의 객체 속성 정의 평가에서도 객체 전개는 CopyDataProperties를 수행하는 것으로 정의됩니다. 이 예제를 객체 전체를 재귀적으로 복제하는 동작으로 넓혀 읽으면 안 됩니다.

더 깊은 연락처와 품목은 여전히 공유합니다

같은 함수에 더 깊은 객체와 다른 가지를 넣으면 한계가 보입니다. 아래 코드는 앞의 updateShippingCity 함수에 이어 실행하는 예입니다.

const sourceWithMore = {
  shipping: {
    city: "Seoul",
    contact: { name: "sample" }
  },
  items: [{ quantity: 1 }]
};
const nextWithMore = updateShippingCity(sourceWithMore, "Busan");

console.log(
  nextWithMore.shipping.contact === sourceWithMore.shipping.contact
); // true
console.log(nextWithMore.items === sourceWithMore.items); // true

남색 시작점에서 코랄색 새 경로와 청록색 공유 곁가지가 갈라지는 종이 나뭇가지 조형
수정하는 경로를 새로 만들더라도 다른 가지는 공유될 수 있다는 개념도입니다. 코드의 전체 구조를 그대로 옮긴 그림이 아닌 설명용 생성 일러스트입니다.

두 비교 결과는 모두 true입니다. 새로 만든 것은 바깥 객체와 shipping 객체이고, 그 안의 contact와 다른 가지인 items까지 새로 만든 것은 아닙니다. 연락처나 품목을 나중에 직접 수정하면 공유된 객체에 영향을 줄 수 있습니다.

MDN의 얕은 복사 설명처럼 최상위 속성의 교체와 중첩 객체 내부의 변경을 나눠 보세요. 바꿀 경로만 새로 만드는 코드는 전체 깊은 복사와 다릅니다.

AI에 수정을 맡길 때는 변경 경로를 적으세요

“원본은 바꾸지 말고 배송 도시만 바꾼 새 객체를 반환해. 연락처와 품목까지 복제했다고 설명하지 말고, 원본 값과 객체 동일성 비교로 결과를 보여줘.”처럼 요청할 수 있습니다. 이는 위 합성 예제에 맞춘 요청 예시이며, 임의의 데이터 구조까지 해결하는 보증은 아닙니다.

입력값이 아예 잘못됐는지 확인하려는 경우에는 JSON 파싱 성공과 값 검사를 구분한 글이 다른 질문에 답합니다. 이 글의 핵심은 값의 정답 검사가 아니라, 어느 객체를 함께 가리키고 있는지입니다.

2026년 10월 24일 Windows x64의 Aside 브라우저에서 본문 코드 3블록과 객체 속성 교체 조건 1개를 합성 데이터로 확인했습니다. 브라우저가 보고한 User-Agent의 Chrome 버전은 154.0.0.0입니다. 이 결과는 위 예제의 동작에 한정하며 실제 서비스·사용자 데이터나 다른 실행 환경 전체를 검증한 것은 아닙니다.

출처 및 확인일