DEV BOX

'26/10/15 UPDATE

WEB / JAVASCRIPT / FIELD NOTES

복사한 객체를 고쳤는데 원본은 왜 같이 바뀔까

{ ...user }로 만든 복사본의 주소를 고쳤더니 원본 주소까지 바뀌었습니다. 스프레드와 Object.assign은 바깥 객체만 새로 만들고 안쪽 객체와 배열은 원본과 함께 쓰기 때문입니다. Node.js v24.21.0에서 실제로 실행한 결과로, 바꾸는 경로만 새로 만드는 방법과 structuredClone·JSON 왕복이 각각 무엇을 남기고 잃는지 확인합니다.

설정 화면에서 사용자 정보를 고치기 전에 const draft = { ...user }로 복사본을 만들어 두었습니다. 저장 버튼을 누르기 전까지는 원본을 건드리지 않으려는 뜻이었는데, 취소를 눌러도 화면의 주소가 이미 바뀌어 있습니다. 복사가 실패한 것이 아니라, 스프레드 문법이 원래 '한 단계만' 복사하기 때문입니다. 이 글을 읽고 나면 어떤 값이 원본과 함께 바뀌는지 미리 짚을 수 있고, 필요한 만큼만 복사하는 방법과 통째로 복사하는 방법 가운데 무엇을 고를지 정할 수 있습니다.

예제는 2026-10-14에 macOS 27.0.1의 Node.js v24.21.0(V8 13.6.233.17)에서 실행했습니다. 경우마다 별도의 node 프로세스로 돌렸고, 이름·도시·주문 수량은 모두 가상의 값입니다. 동작 설명은 MDN 문서와 Node.js 공식 문서에 맞춰 확인했습니다. 브라우저에서는 실행하지 않았습니다.

스프레드는 바깥 객체만 새로 만든다

먼저 문제를 그대로 재현해 보았습니다.

const user = { name: 'Mina', address: { city: 'Seoul' }, tags: ['dev'] };
const copy = { ...user };
copy.name = 'Jun';
copy.address.city = 'Busan';
copy.tags.push('admin');

실행 뒤 원본 user를 찍어 보면 name은 그대로 Mina인데, city는 Busan으로, tags는 ["dev","admin"]으로 바뀌어 있었습니다. copy.address === user.address도 true였습니다. 두 변수가 서로 다른 바깥 객체를 갖고 있지만, 그 안의 address와 tags는 같은 객체를 함께 가리키고 있다는 뜻입니다.

MDN은 이런 복사를 얕은 복사라고 부르고, 객체 하나를 펼치면 원본의 얕은 복사본이 만들어진다고 설명합니다. 문자열이나 숫자처럼 값 자체가 들어 있는 속성은 복사본에서 바꿔도 원본과 상관이 없습니다. 하지만 속성의 값이 객체나 배열이면 복사되는 것은 '그 객체가 어디 있는지'를 가리키는 참조뿐입니다. 그래서 copy.address.city를 바꾸는 것은 원본과 함께 쓰는 하나의 address를 고치는 일이 됩니다.

Node.js v24.21.0 실행 결과. copy.mjs spread: user가 city Busan, tags dev·admin으로 바뀌었고 copy는 name만 Jun이며 same address?는 true. array: rows 첫 행 qty가 99, 길이는 2와 3, slice same row? true, assign same a? true. fix: user의 city는 Seoul, copy의 city는 Busan, same tags?는 true.
스프레드로 복사한 객체와 배열을 고친 실제 출력입니다. 안쪽 객체와 배열을 고치면 원본도 함께 바뀌었고, 바꾸는 경로를 새로 만들자 원본이 그대로 남았습니다.

배열도 마찬가지였습니다. [...rows]로 복사한 뒤 push로 행을 더하자 길이는 원본 2, 복사본 3으로 따로 갔습니다. 배열 자체는 새것이기 때문입니다. 그런데 copy[0].qty = 99로 첫 행의 수량을 바꾸자 원본의 첫 행도 99가 되었습니다. slice()로 만든 복사본의 첫 행도 원본과 같은 객체였고, Object.assign({}, src)로 복사한 객체의 안쪽 객체도 원본과 같았습니다. MDN은 스프레드가 배열을 복사할 때 사실상 한 단계만 들어가며, Object.assign()도 마찬가지라고 적고 있습니다.

개발 노트

{ ...user }가 새로 만드는 것

바깥 객체만 새것이고, 안쪽 객체는 원본과 같은 것을 가리킨다

  • 바깥 객체

    copy !== user

    • copy는 새 객체
    • copy.name을 바꿔도 원본은 그대로
  • 문자열·숫자 값

    name: 'Mina'

    • 값 자체가 복사됨
    • 서로 따로 바뀜
  • 안쪽 객체

    copy.address === user.address

    • 같은 객체를 가리키는 참조만 복사
    • copy.address.city를 바꾸면 원본도 바뀜
  • 안쪽 배열

    copy.tags.push('admin')

    • push도 같은 배열에 들어감
    • 원본 tags에도 'admin'이 생김

Node.js v24.21.0에서 2026-10-14에 실행한 결과입니다. Object.assign({}, user)와 배열의 [...rows], slice()도 같은 방식으로 한 단계만 복사했습니다.

스프레드 복사는 바깥 객체만 새로 만듭니다. 그 안의 객체와 배열은 원본과 같은 것을 함께 가리킵니다.

바꾸는 경로만 새로 만들면 원본이 남는다

모든 것을 깊이 복사해야 해결되는 것은 아닙니다. 원본이 바뀐 이유는 address를 새로 만들지 않고 고쳤기 때문이니, 바꾸려는 값까지 가는 길에 있는 객체만 새로 만들면 됩니다.

const copy = { ...user, address: { ...user.address, city: 'Busan' } };

이 한 줄은 바깥 객체를 새로 만들고, address도 펼쳐서 새 객체로 만든 다음 city만 새 값으로 덮어씁니다. 실행해 보니 원본의 city는 Seoul로 남고 복사본만 Busan이 되었습니다. React 공식 문서가 중첩된 상태를 바꾸는 방법으로 보여 주는 것도 이 모양입니다. 바뀐 경로만 새 객체가 되고 나머지는 원래 것을 그대로 쓰므로 복사하는 양도 적습니다.

대신 주의할 점이 하나 남습니다. 이 방법으로 만든 복사본에서도 copy.tags === user.tags는 true였습니다. 건드리지 않은 부분은 계속 공유하므로, 나중에 copy.tags.push(…)처럼 제자리에서 고치면 원본이 다시 바뀝니다. 배열을 바꿀 때는 tags: [...user.tags, 'admin']처럼 그 경로도 새로 만들어야 합니다.

개발 노트

바꾸는 경로만 새로 만들기

주소의 도시를 바꿀 때 새로 만들어야 하는 객체

  1. 바꿀 경로를 찾는다

    user → address → city

    • 바꿀 값은 user.address.city
    • 거쳐 가는 객체는 user와 address
  2. 경로의 객체를 각각 펼친다

    { ...user, address: { ...user.address, city } }

    • 바깥과 address를 모두 새로 만듦
    • city만 새 값으로 덮어씀
  3. 나머지는 공유한 채 둔다

    copy.tags === user.tags

    • 원본 city는 Seoul 그대로
    • 건드리지 않은 tags는 계속 공유

공유된 tags를 나중에 push로 고치면 다시 원본이 바뀝니다. 바꿀 때마다 그 경로를 새로 만드는 습관이 필요합니다.

바꾸려는 값까지 가는 경로의 객체만 새로 만들면, 원본은 그대로 두고 필요한 만큼만 복사할 수 있습니다.

통째로 독립된 복사본이 필요할 때: structuredClone

편집 화면의 임시 저장본처럼, 어디를 고칠지 미리 알 수 없고 원본과 완전히 떨어진 복사본이 필요할 때도 있습니다. 이때 쓸 수 있는 것이 structuredClone()입니다. MDN은 이 함수가 값을 깊이 복사한다고 설명하고, Node.js에서는 17.0.0부터 전역 함수로 들어 있습니다.

같은 사용자 객체에 가입일 Date, 권한을 담은 Map, 값이 undefined인 note 키를 더해 실행해 보았습니다. structuredClone으로 만든 복사본의 city를 바꿔도 원본은 Seoul이었고, Date는 Date로, Map은 Map으로 남았으며 note 키도 그대로 있었습니다.

예전부터 흔히 쓰던 JSON.parse(JSON.stringify(user))는 결과가 달랐습니다. 가입일은 "2026-10-15T00:00:00.000Z"라는 문자열이 되었고, Map은 빈 객체 {}가 되었으며, note 키는 사라졌습니다. MDN의 JSON.stringify() 설명대로, 객체 안의 undefined와 함수는 빠지고, Date는 ISO 문자열로 바뀌고, Map과 Set은 {}가 됩니다. 문자열과 숫자, 일반 객체와 배열만 있는 데이터라면 문제가 없지만, 그렇지 않다면 조용히 다른 값이 됩니다.

Node.js v24.21.0 실행 결과. copy.mjs clone: user.address는 city Seoul, Date kept? true, Map kept? true editor, note key kept? true, JSON joined는 string 2026-10-15T00:00:00.000Z, JSON roles는 {}, JSON note key? false. limits: class kept? false undefined, function -> DataCloneError.
structuredClone과 JSON 왕복을 같은 객체에 실행한 실제 출력입니다. structuredClone은 Date와 Map까지 유지했지만 클래스와 함수는 다루지 못했습니다.

structuredClone에도 못 하는 일이 있습니다. 메서드를 가진 클래스 Cart의 인스턴스를 복사하자 결과는 Cart가 아니었고 total 메서드도 없었습니다. MDN의 구조화된 복제 알고리즘 문서는 프로토타입 체인을 따라가거나 복사하지 않는다고 적고 있어, 클래스 인스턴스는 속성만 가진 일반 객체가 됩니다. 함수가 속성으로 들어 있는 객체는 아예 DataCloneError를 던졌습니다. 이 문서는 getter·setter 같은 속성 설명자와 클래스의 private 필드도 복사되지 않는다고 적고 있습니다. 이 두 가지는 이 글에서 실행하지 않았습니다.

개발 노트

통째로 복사할 때 무엇이 남나

Node.js v24.21.0에서 실행해 확인한 차이

  • 스프레드·Object.assign

    { ...user }

    • 한 단계만 새로 만듦
    • 안쪽은 원본과 공유
  • structuredClone

    structuredClone(user)

    • 안쪽까지 새로 만듦
    • Date·Map·undefined 값 유지
  • structuredClone이 못 하는 것

    instanceof Cart → false

    • 함수가 있으면 DataCloneError
    • 클래스 인스턴스는 일반 객체가 됨
  • JSON 왕복

    JSON.parse(JSON.stringify(user))

    • Date는 문자열, Map은 {}
    • undefined 값의 키는 사라짐

structuredClone은 Node.js 17.0.0부터 전역 함수로 쓸 수 있습니다. 결과는 2026-10-14 실행 기준입니다.

같은 '복사'라도 남는 것이 다릅니다. 안쪽까지 독립된 복사본이 필요하면 structuredClone이 가장 많이 보존하지만, 함수와 클래스는 따로 다뤄야 합니다.

복사본을 고치기 전에 확인할 것

복사본을 고쳤는데 원본이 바뀌었다면, 먼저 고친 값이 객체나 배열 안에 있는지 보십시오. 그렇다면 그 안쪽 객체는 아직 원본과 하나입니다. console.log(copy.address === user.address)처럼 비교해 보면 바로 확인할 수 있습니다.

고칠 곳이 정해져 있다면 그 경로만 { ...obj, key: { ...obj.key, … } }처럼 새로 만드는 쪽이 가볍고 의도가 잘 보입니다. 어디를 고칠지 모르는 편집용 사본이라면 structuredClone()을 쓰되, 클래스 인스턴스나 함수가 섞여 있지 않은지 먼저 확인하십시오. JSON.parse(JSON.stringify(…))는 날짜나 Map이 없다는 것이 확실할 때만 쓰는 편이 안전합니다.

끝 · END OF NOTE
RSS 구독목록으로
COMMENTS BOX

이 기록에 대화를 더해 주세요.

궁금한 점, 다른 접근, 직접 해 본 결과를 나눠 주세요.

최신순

로그인 상태 확인 중…

댓글을 불러오는 중…