[js] 불변 객체(얕은 복사, 깊은 복사)

[js] 불변 객체(얕은 복사, 깊은 복사)

·

7 min read

앞서 불변성(immutability)의 개념과 중요성에 대해 알아보았습니다.

(아직 읽지 않았다면 아래 포스팅을 먼저 확인해주세요!)

https://ddoit.hashnode.dev/js-immutability

그렇다면 이제 불변 객체를 만들려면 어떻게 해야할까요?

📌 불변 객체를 만드는 방법

기존 객체의 원본을 직접 수정하지 않으려면 새로운 객체를 생성하는 방식이 필요하다.
이를 위해 "얕은 복사(Shallow Copy)" 또는 "깊은 복사(Deep Copy)" 기법을 활용할 수 있다.

✅ 1. 얕은 복사(Shallow Copy)

  • 객체의 최상위 프로퍼티만 복사하고, 중첩된 객체는 원본을 참조하는 방식.

  • 즉, 중첩된 객체 내부 값은 여전히 원본과 연결되어 있음!

📌 Object.assign()을 사용한 얕은 복사

const obj1 = { name: "Alice", details: { age: 25, city: "Seoul" } };
const obj2 = Object.assign({}, obj1);

obj2.name = "Bob"; // obj1에는 영향 없음
obj2.details.city = "Busan"; // obj1에도 영향 있음

console.log(obj1.name);      // "Alice" (원본 유지)
console.log(obj2.name);      // "Bob" (복사본 변경)
console.log(obj1.details.city); // "Busan" 
console.log(obj2.details.city); // "Busan" (원본도 변경됨)

✔️ name은 복사되었지만, details는 원본과 같은 참조값을 공유하므로 원본도 변경됨.


📌 Spread Operator (...)을 사용한 얕은 복사

const obj1 = { name: "Alice", details: { age: 25, city: "Seoul" } };
const obj2 = { ...obj1 };

obj2.name = "Bob"; // obj1에는 영향 없음
obj2.details.city = "Busan"; // obj1에도 영향 있음

console.log(obj1.name);      // "Alice" (원본 유지)
console.log(obj2.name);      // "Bob" (복사본 변경)
console.log(obj1.details.city); // "Busan" (원본도 변경됨)
console.log(obj2.details.city); // "Busan"

✔️ Spread 연산자도 최상위 프로퍼티만 복사하므로, 중첩된 객체(details)는 여전히 원본을 참조한다.

✔️ 즉, 얕은 복사는 중첩 객체를 복사할 때 불변성을 유지하지 못한다.

📌 메모리 구조 (얕은 복사)

1️⃣ const obj1 = { name: "Alice", details: { age: 25, city: "Seoul" } }; (객체 생성)

📌 변수 영역:            📌 데이터 영역:             📌 객체의 변수(프로퍼티) 영역:
┌─────1003─────┐        ┌────────5001────────┐       ┌───────7001───────┐
│ obj1: @5001  │  →     │ @7001, @7002       │  →    │ name: @6001      │
└──────────────┘        └────────────────────┘       └──────────────────┘
                        ┌────────6001────────┐       ┌───────7002───────┐
                        │ "Alice"            │       │ details: @8001   │
                        └────────────────────┘       └──────────────────┘
                        ┌────────6002────────┐       ┌───────8001───────┐
                        │   25               │       │ age: @6002       │
                        └────────────────────┘       │ city: @6003      │
                        ┌────────6003────────┐       └──────────────────┘
                        │ "Seoul"            │
                        └────────────────────┘

2️⃣ let obj2 = { ...obj1 }; (얕은 복사 수행)

📌 변수 영역:            📌 데이터 영역:             📌 객체의 변수(프로퍼티) 영역:
┌─────1003─────┐        ┌────────5001────────┐       ┌───────7001───────┐
│ obj1: @5001  │  →     │ @7001, @7002       │  →    │ name: @6001      │
└──────────────┘        └────────────────────┘       └──────────────────┘
┌─────1002─────┐        ┌────────5002────────┐       ┌───────7002───────┐
│ obj2: @5002  │        │ @7003, @7004       │       │ details: @6002   │
└──────────────┘        └────────────────────┘       └──────────────────┘
                        ┌────────6001────────┐       ┌───────7003───────┐
                        │ "Alice"            │       │ name: @6001      │
                        └────────────────────┘       └──────────────────┘
                        ┌────────6002────────┐       ┌───────7004───────┐
                        │   25               │       │ details: @6002   │
                        └────────────────────┘       └──────────────────┘
                        ┌────────6003────────┐       ┌───────8001───────┐
                        │ "Seoul"            │       │ age: @6002       │
                        └────────────────────┘       │ city: @6003      │
                                                     └──────────────────┘
  • name은 개별적으로 복사되었지만, 중첩된 details 객체는 공유된 상태.

3️⃣ obj2.name = "Bob"; (기본 속성 변경)

📌 변수 영역:            📌 데이터 영역:             📌 객체의 변수(프로퍼티) 영역:
┌─────1003─────┐        ┌────────5001───────┐       ┌───────7001───────┐
│ obj1: @5001  │  →     │ @7001, @7002      │  →    │ name: @6001      │
└──────────────┘        └───────────────────┘       └──────────────────┘
┌─────1002─────┐        ┌────────5002───────┐       ┌───────7002───────┐
│ obj2: @5002  │        │ @7003, @7004      │       │ details: @8001   │
└──────────────┘        └───────────────────┘       └──────────────────┘
                        ┌────────6001───────┐       ┌───────7003───────┐
                        │ "Alice"           │       │ name: @6004      │(변경)
                        └───────────────────┘       └──────────────────┘
                        ┌────────6002───────┐       ┌───────7004───────┐
                        │   25              │       │ name: @8001      │
                        └───────────────────┘       └──────────────────┘
                        ┌────────6003───────┐       ┌───────8001───────┐
                        │ "Seoul"           │       │ age: @6002       │
                        └───────────────────┘       │ city: @6003      │
                        ┌────────6004───────┐       └──────────────────┘ 
                        │ "Bob"             │
                        └───────────────────┘
  • 최상위 프로퍼티는 독립적으로 복사되므로, 원본에는 영향 없음.

4️⃣ obj2.details.city = "Busan" (내부 객체 변경)

📌 변수 영역:            📌 데이터 영역:             📌 객체의 변수(프로퍼티) 영역:
┌─────1003─────┐        ┌────────5001───────┐       ┌───────7001───────┐
│ obj1: @5001  │  →     │ @7001, @7002      │  →    │ name: @6001      │
└──────────────┘        └───────────────────┘       └──────────────────┘
┌─────1002─────┐        ┌────────5002───────┐       ┌───────7002───────┐
│ obj2: @5002  │        │ @7003, @7004      │       │ details: @8001   │
└──────────────┘        └───────────────────┘       └──────────────────┘
                        ┌────────6001───────┐       ┌───────7003───────┐
                        │ "Alice"           │       │ name: @6004      │
                        └───────────────────┘       └──────────────────┘
                        ┌────────6002───────┐       ┌───────7004───────┐
                        │   25              │       │ name: @8001      │
                        └───────────────────┘       └──────────────────┘
                        ┌────────6003───────┐       ┌───────8001───────┐
                        │ "Seoul"           │       │ age: @6002       │
                        └───────────────────┘       │ city: @6005      │(변경)
                        ┌────────6004───────┐       └──────────────────┘ 
                        │ "Bob"             │
                        └───────────────────┘
                        ┌────────6005───────┐        
                        │ "Busan"           │
                        └───────────────────┘
  • details 객체(@8001)는 여전히 원본 obj1obj2에서 공유됨.

  • 결과적으로, obj1.details.city"Busan"으로 변경되어 영향을 받음!


✅ 2. 깊은 복사(Deep Copy)

  • 객체 내부의 모든 속성(중첩된 객체 포함)을 완전히 새로운 메모리 공간에 복사하여 원본과 독립적인 객체를 만드는 방식.

  • 즉, 원본 객체와 복사본이 완전히 분리되므로, 한쪽을 수정해도 다른 쪽에 영향을 주지 않는다.

📌 JSON.parse(JSON.stringify())을 이용한 깊은 복사

const obj1 = { name: "Alice", details: { age: 25, city: "Seoul" } };
const obj2 = JSON.parse(JSON.stringify(obj1));

obj2.name = "Bob";
obj2.details.city = "Busan";

console.log(obj1.name);        // "Alice" (원본 유지)
console.log(obj2.name);        // "Bob" (복사본 변경)
console.log(obj1.details.city); // "Seoul" (원본 유지)
console.log(obj2.details.city); // "Busan" (복사본 변경)

✔️ 객체를 JSON 문자열로 변환했다가 다시 객체로 변환하면, 중첩된 객체까지 완전히 새로운 메모리에 복사되어 원본이 유지됨

하지만 undefined, Symbol, function 같은 값은 복사되지 않음.

📌 Lodash _.cloneDeep()을 사용한 깊은 복사

const _ = require('lodash');

const obj1 = { name: "Alice", details: { age: 25, city: "Seoul" }, func: () => "Hello" };
const obj2 = _.cloneDeep(obj1);

obj2.name = "Bob";
obj2.details.city = "Busan";

console.log(obj1.name);        // "Alice" (원본 유지)
console.log(obj2.name);        // "Bob" (복사본 변경)
console.log(obj1.details.city); // "Seoul" (원본 유지)
console.log(obj2.details.city); // "Busan" (복사본 변경)
console.log(obj1.func === obj2.func); // false (함수도 복사됨)

✔️ Lodash _.cloneDeep()은 모든 환경에서 동작하며, 함수(Function)도 복사 가능.
✔️ 브라우저 버전과 상관없이 사용 가능.
외부 라이브러리(lodash)를 설치해야 함.

📌 structuredClone()을 사용한 깊은 복사

const obj1 = { name: "Alice", details: { age: 25, city: "Seoul" } };
const obj2 = structuredClone(obj1);

obj2.name = "Bob";
obj2.details.city = "Busan";

console.log(obj1.name);        // "Alice" (원본 유지)
console.log(obj2.name);        // "Bob" (복사본 변경)
console.log(obj1.details.city); // "Seoul" (원본 유지)
console.log(obj2.details.city); // "Busan" (복사본 변경)

✔️ structuredClone()JSON.parse(JSON.stringify())보다 더 강력한 깊은 복사를 제공.

✔️ undefined, Symbol, function도 복사 가능!

하지만 최신 브라우저(Chrome 98+, Node.js 17+)에서만 지원됨.

📌 메모리 구조 (깊은 복사)

1️⃣ const obj1 = { name: "Alice", details: { age: 25, city: "Seoul" } }; (객체 생성)

📌 변수 영역:            📌 데이터 영역:             📌 객체의 변수(프로퍼티) 영역:
┌─────1003─────┐        ┌────────5001────────┐       ┌───────7001───────┐
│ obj1: @5001  │  →     │ @7001, @7002       │  →    │ name: @6001      │
└──────────────┘        └────────────────────┘       └──────────────────┘
                        ┌────────6001────────┐       ┌───────7002───────┐
                        │ "Alice"            │       │ details: @8001   │
                        └────────────────────┘       └──────────────────┘
                        ┌────────6002────────┐       ┌───────8001───────┐
                        │   25               │       │ age: @6002       │
                        └────────────────────┘       │ city: @6003      │
                        ┌────────6003────────┐       └──────────────────┘
                        │ "Seoul"            │
                        └────────────────────┘

2️⃣ const obj2 = structuredClone(obj1); (깊은 복사 수행)

📌 변수 영역:            📌 데이터 영역:             📌 객체의 변수(프로퍼티) 영역:
┌─────1003─────┐        ┌────────5001────────┐       ┌───────7001───────┐
│ obj1: @5001  │  →     │ @7001, @7002       │  →    │ name: @6001      │
└──────────────┘        └────────────────────┘       └──────────────────┘
┌─────1002─────┐        ┌────────5002────────┐       ┌───────7002───────┐
│ obj2: @5002  │        │ @7003, @7004       │       │ details: @8001   │
└──────────────┘        └────────────────────┘       └──────────────────┘
                        ┌────────6001────────┐       ┌───────7003───────┐
                        │ "Alice"            │       │ name: @6001      │
                        └────────────────────┘       └──────────────────┘
                        ┌────────6002────────┐       ┌───────7004───────┐
                        │   25               │       │ details: @8002   │
                        └────────────────────┘       └──────────────────┘
                        ┌────────6003────────┐       ┌───────8001───────┐
                        │ "Seoul"            │       │ age: @6002       │
                        └────────────────────┘       │ city: @6003      │
                                                     └──────────────────┘
                                                     ┌───────8002───────┐
                                                     │ age: @6002       │
                                                     │ city: @6003      │
                                                     └──────────────────┘
  • obj1.detailsobj2.details는 이제 완전히 독립적인 메모리 공간을 차지함.

3️⃣ obj2.name = "Bob"; (기본 속성 변경)

📌 변수 영역:            📌 데이터 영역:             📌 객체의 변수(프로퍼티) 영역:
┌─────1003─────┐        ┌────────5001────────┐       ┌───────7001───────┐
│ obj1: @5001  │  →     │ @7001, @7002       │  →    │ name: @6001      │
└──────────────┘        └────────────────────┘       └──────────────────┘
┌─────1002─────┐        ┌────────5002────────┐       ┌───────7002───────┐
│ obj2: @5002  │        │ @7003, @7004       │       │ details: @8001   │
└──────────────┘        └────────────────────┘       └──────────────────┘
                        ┌────────6001────────┐       ┌───────7003───────┐
                        │ "Alice"            │       │ name: @6004      │(변경)
                        └────────────────────┘       └──────────────────┘
                        ┌────────6002────────┐       ┌───────7004───────┐
                        │   25               │       │ details: @8002   │
                        └────────────────────┘       └──────────────────┘
                        ┌────────6003────────┐       ┌───────8001───────┐
                        │ "Seoul"            │       │ age: @6002       │
                        └────────────────────┘       │ city: @6003      │
                        ┌────────6004────────┐       └──────────────────┘
                        │  "Bob"             │       ┌───────8002───────┐
                        └────────────────────┘       │ age: @6002       │
                                                     │ city: @6003      │
                                                     └──────────────────┘

4️⃣ obj2.details.city = "Busan" (내부 객체 변경)

📌 변수 영역:            📌 데이터 영역:             📌 객체의 변수(프로퍼티) 영역:
┌─────1003─────┐        ┌────────5001────────┐       ┌───────7001───────┐
│ obj1: @5001  │  →     │ @7001, @7002       │  →    │ name: @6001      │
└──────────────┘        └────────────────────┘       └──────────────────┘
┌─────1002─────┐        ┌────────5002────────┐       ┌───────7002───────┐
│ obj2: @5002  │        │ @7003, @7004       │       │ details: @8001   │
└──────────────┘        └────────────────────┘       └──────────────────┘
                        ┌────────6001────────┐       ┌───────7003───────┐
                        │ "Alice"            │       │ name: @6004      │
                        └────────────────────┘       └──────────────────┘
                        ┌────────6002────────┐       ┌───────7004───────┐
                        │   25               │       │ details: @8002   │
                        └────────────────────┘       └──────────────────┘
                        ┌────────6003────────┐       ┌───────8001───────┐
                        │ "Seoul"            │       │ age: @6002       │
                        └────────────────────┘       │ city: @6003      │
                        ┌────────6004────────┐       └──────────────────┘
                        │  "Bob"             │       ┌───────8002───────┐
                        └────────────────────┘       │ age: @6002       │
                        ┌────────6005────────┐       │ city: @6005      │(변경)
                        │ "Busan"            │       └──────────────────┘
                        └────────────────────┘

📌 결론

객체는 가변적이므로 불변 객체를 만들기 위해 복사 개념을 알아야 한다.

얕은 복사(Shallow Copy)는 최상위 프로퍼티만 복사하고, 중첩 객체는 원본을 참조한다.

깊은 복사(Deep Copy)는 객체 전체를 새로운 공간에 복사하여 원본과 완전히 독립적인 객체를 만든다.

이제 불변 객체를 만들기 위해 얕은 복사와 깊은 복사를 활용하는 방법이 확실해졌죠? 🚀🔥