앞서 불변성(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
)는 여전히 원본obj1
과obj2
에서 공유됨.결과적으로,
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.details
와obj2.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" │ └──────────────────┘
└────────────────────┘
details
객체도 복사되어 새로운 메모리 주소(@8002)를 가리키기 때문에,obj2.details.city
= "Busan"
변경 시,obj1.details.city
에는 영향 없음.
📌 결론
✅ 객체는 가변적이므로 불변 객체를 만들기 위해 복사 개념을 알아야 한다.
✅ 얕은 복사(Shallow Copy)는 최상위 프로퍼티만 복사하고, 중첩 객체는 원본을 참조한다.
✅ 깊은 복사(Deep Copy)는 객체 전체를 새로운 공간에 복사하여 원본과 완전히 독립적인 객체를 만든다.
이제 불변 객체를 만들기 위해 얕은 복사와 깊은 복사를 활용하는 방법이 확실해졌죠? 🚀🔥