본문 바로가기
빌드노트 검색

빌드노트

Object.assign으로 복사했는데 원본의 중첩 배열도 바뀐다

Object.assign과 spread로 최상위 객체를 새로 만들어도 중첩 배열은 원본과 공유될 수 있다. 실제 수정 경로의 참조를 비교해 배열과 중간 객체를 함께 복사하고, 전체 분리가 필요할 때 structuredClone의 지원 타입을 확인하는 방법을 설명한다.

Object.assign({}, original)이나 { ...original }로 설정을 복사한 뒤 배열에 항목을 추가했는데 원본에도 같은 항목이 생길 수 있다. 최상위 객체만 새로 만들어지고, 그 안의 객체와 배열은 원본과 공유되기 때문이다. 원본을 보존하려면 바꿀 배열뿐 아니라 그 배열까지 이어지는 객체도 새로 만들어야 한다.

복사 직후, 실제로 바꿀 경로를 비교한다

기존 코드에서 배열을 수정하기 전에 copy === original, copy.panel === original.panel, copy.panel.columns === original.panel.columns를 확인한다. 변수 이름과 panel.columns는 자기 코드의 수정 경로에 맞춘다. 객체와 배열의 ===는 내용이 같은지가 아니라 같은 객체를 가리키는지 검사한다.

다음은 최상위 복사와 중첩 공유를 구별하는 작은 재현이다. Node.js가 설치된 환경에서 빈 copy-demo.mjs에 넣고 그 파일이 있는 폴더에서 node copy-demo.mjs를 실행한다. Windows의 Node.js 22.23.2에서 아래 코드를 실행했다.

const original = {
  panel: { columns: ["title", "date"] },
  appearance: { theme: "light" },
};
const copy = Object.assign({}, original);
console.log(copy === original, copy.panel === original.panel,
  copy.panel.columns === original.panel.columns);
copy.panel.columns.push("author");
console.log(JSON.stringify(original.panel.columns));

첫 줄은 false true true, 다음 줄은 ["title","date","author"]였다. 최상위 객체는 달라도 panel과 columns는 같았다. push가 공유 배열을 바꿨으므로 원본을 통해 읽어도 author가 보였다. 배열 내용은 문자열로 출력해 그 시점의 값을 확인했다.

MDN의 Object.assign 설명 (새 탭에서 열림)은 속성값이 객체 참조이면 그 참조를 복사한다고 명시한다. 이것이 얕은 복사다. 복사 줄만 const copy = { ...original };로 바꿔 새로 실행해도 두 출력은 같았다. 객체 spread도 얕은 복사 (새 탭에서 열림)이므로 문법을 바꾸는 것으로는 이 공유를 해결할 수 없다.

자기 코드에서 중첩 비교까지 false라면 검사한 경로는 분리돼 있다. 그런데도 원본이 바뀐다면 더 깊은 객체나 다른 공유 경로, 원본에 직접 대입하는 코드를 추적한다. Object.assign(original, 변경값)처럼 원본을 첫 인자로 넘겼다면 최상위부터 원본을 바꾸는 코드이므로 새 대상을 만드는 수정이 먼저다.

columns와 panel을 함께 새로 만든다

이 설정에서 바꿀 곳이 panel.columns뿐이라면 해당 배열과 panel, 최상위 객체를 새로 만든다. 위 파일의 const copy = Object.assign({}, original); 한 줄을 다음 식으로 교체하고, copy.panel.columns.push("author"); 줄은 지운 뒤 같은 명령으로 다시 실행한다. 새 실행은 변경 전의 original에서 시작하며 항목 추가는 새 배열을 만드는 식에서 끝낸다.

const copy = {
  ...original,
  panel: {
    ...original.panel,
    columns: [...original.panel.columns, "author"],
  },
};

이번 출력은 false false false와 ["title","date"]였다. 별도로 복사본을 읽으면 columns는 ["title","date","author"]였다. 수정 경로의 참조가 모두 달라져 원본 배열은 유지됐다. 바꾸지 않은 appearance는 공유하며, copy.appearance === original.appearance는 true였다. 나중에 테마도 바꾸려면 그때는 appearance도 새로 만들어야 한다.

중간 객체를 빼먹으면 다시 원본이 바뀐다. panel만 복사하고 기존 columns에 push하면 배열은 여전히 공유된다. 반대로 얕은 복사 뒤 copy.panel.columns = [...copy.panel.columns, "author"]만 실행해도 안전하지 않다. 새 배열을 공유 중인 panel의 속성에 대입하므로 원본의 columns도 교체된다. 두 조건을 각각 새 데이터로 실행해 원본에 author가 나타나는 것을 확인했다. 두 번째 조건에서는 이전 배열 자체는 ["title","date"]로 남았지만 원본이 가리키는 배열이 바뀌었다.

이 예제의 배열 요소는 문자열이다. 요소가 객체이고 그 속성까지 수정한다면 배열을 펼치는 것만으로는 부족하다. 배열 spread는 요소의 동일성을 유지 (새 탭에서 열림)하므로 실제로 바꿀 요소 객체까지 새로 만들어야 한다.

설정 전체를 분리해야 한다면

수정 경로를 정해 공유할 가지를 남길 수 있으면 위처럼 복사한다. 일반 객체·배열·문자열로 된 설정 전체를 분리해야 한다면 structuredClone을 검토한다. 첫 예제의 복사 줄만 const copy = structuredClone(original);로 바꾸고 push 줄은 그대로 두어 새로 실행했다. 출력은 false false false와 ["title","date"]였고, 복사본 배열에만 author가 추가됐다. 경로별 복사와 달리 appearance 참조도 달랐으며, 별도 비교에서 복사본 테마를 dark로 바꿔도 원본은 light로 남았다.

다만 structuredClone (새 탭에서 열림)은 지원 가능한 값을 복사하는 API다. 구조화 복사 알고리즘의 제한 (새 탭에서 열림)에 따르면 함수와 DOM 노드는 복사할 수 없고 사용자 정의 프로토타입 체인도 복제하지 않는다. 콜백이 든 설정이나 클래스 인스턴스의 동작까지 보존하는 범용 복사법으로 쓰면 안 된다.

실행 환경에서 typeof structuredClone이 "function"인지 확인한다. Node.js 22.23.2 공식 문서 (새 탭에서 열림)에는 v17.0.0부터 제공되는 전역 함수로 나와 있다. 함수가 없으면 런타임 지원을, DataCloneError가 나면 복사하려는 데이터의 타입을 확인한다. 오류를 없애려고 얕은 복사로 바꾸면 중첩 공유는 그대로 남을 수 있다.

이번 실행은 작은 합성 일반 객체·배열·문자열의 참조와 원본 내용만 비교했다. 함수·DOM·클래스 인스턴스의 제한은 문서에서 확인했으며 직접 실행하지 않았다. 브라우저별 동작이나 React 같은 프레임워크의 화면 갱신도 검증하지 않았다.

← 전체 글 보기