이전 JS, 원시타입 & 객체 포스팅에서 언급했듯이, JS는 유독 다양한 여러 Object 를 지원하기도하고, 조금이라도 복잡한 데이터 구조가 형성 된다하면, 배열이나 객체에 데이터를 저장해놓는 작업이 많다.
복사라는 개념이 이런 관점에서 단순한 원시타입에 대해서는 일반적으로 생각할 수 있는 복사가 될 수 있지만, 객체수준부터는 복사에 대한 생각을 조금 깊게 해볼 필요가 있다.
그전에 확인 할 것
값 복사
참조(메모리 주소) 복사
{
/**
* 값 복사
* - 분리된 메모리 공간에, 데이터를 각각 저장(공유 X) 💡
*/
let num1 = 1;
let num2 = num1;
num2 = 2;
console.log(`num1 : ${num1}, num2 : ${num2}`);
// num1 : 1, num2 : 2
// 🔍 원본 데이터 값 보존
/**
* 객체 복사
* - 동일한 메모리 공간 내, 주소값 데이터에 대해, 동일한 주소값 데이터를 각각 저장(공유 O) 💡
*/
let person = {
name: "min",
job: "developer",
salary: 5000,
};
let clonePerson = person;
clonePerson.salary = 6000;
console.log(`Salary - Person : ${person.salary}, ClonePerson : ${clonePerson.salary}`);
// Salary - Person : 6000, ClonePerson : 6000
// 🔍 원본 데이터 값 변경됨
}
참조타입 복사는 크게 Shallow Copy(얕은 복사) 와 Deep Copy(깊은 복사)로 나뉘는데, 이 중 Shallow Copy 에 대해 먼저 알아본다.
위에 상황에서 원시타입이 아닌 일반 객체에 대한 단순 대입복사로 인한 문제를 해결하기 위해, 그리고 그 객체의 내부 데이터가 모두 원시타입 값으로 구성되어 있는 경우, Shallow Copy 로 충분히 해결 가능하다.
JS에서 Shallow Copy(얕은 복사)를 하는 방법은 몇 가지 방법들이 있지만, ES6+ 이후 기준에서 흔히, 그리고 비교적 간단 방법은 다음과 같다.
/**
* Shallow Copy - Object.assign() 사용
*/
let myCat = {
name: "Cash",
age: 5,
weight: "3kg",
};
let myCat2 = Object.assign({}, myCat);
myCat2.name = "Rolly";
myCat2.age = 2;
console.log(myCat); // { name: 'Cash', age: 5, weight: '3kg' }
console.log(myCat2); // { name: 'Rolly', age: 2, weight: '3kg' }
/**
* Shallow Copy - Spread Operator 사용
*/
let myCaptin = {
name: "Iron Man",
rank: 1,
gender: "M",
};
let myCaptin2 = { ...myCaptin };
myCaptin2.name = "Captin America";
myCaptin2.rank = 2;
console.log(myCaptin); // { name: 'Iron Man', rank: 1, gender: 'M' }
console.log(myCaptin2); // { name: 'Captin America', rank: 2, gender: 'M' }
💡 [참고]
객체가 아닌, 배열(Array)에 대해서도, Spread Operator 나 Array Object 내장 메소드인 Array.prototyp.slice( ) , Array.prototype.map( ) 같은 메소드들도, 기존 배열 데이터에 대해 조작후 새로운 배열자체를 반환하는 개념이라 객체에서 개념과 똑같은 Shallow Copy 가 가능하다.
두 방식을 보면, 맨 처음 객체를 대입복사 했을 경우와 비교했을 때, 값 복사처럼 각 변수에 대한 데이터가 보존되는 것처럼 보인다.
방금 말에서 ~되는 것처럼 보인다.라고 한 것에는 이유가 있다.
다음 상황을 보자.
/**
* Shallow Copy 에 함정
*/
let myCar = {
name: "부릉이",
price: 7000,
specialMode: {
mode: "Fly",
},
};
let myCar2 = { ...myCar };
myCar2.name = "따릉이";
myCar2.price = 4000;
myCar2.specialMode.mode = "Dive";
console.log(myCar); // { name: '부릉이', price: 7000, specialMode: { mode: 'Dive' } }
console.log(myCar2); // { name: '따릉이', price: 4000, specialMode: { mode: 'Dive' } } 🔍
// 두 car에 대해 프로퍼티 비교
console.log(myCar.name === myCar2.name); // false
console.log(myCar.specialMode === myCar2.specialMode); // true 🔍
console.log(myCar.specialMode.mode === myCar2.specialMode.mode); // true 🔍
다 좋은데, 마지막, specialMode(객체) 프로퍼티 내부에, mode 프로퍼티가 Shallow Copy 를 하고 myCar2 에 specialMode 를 변경했음에도 불구하고, myCar 와 myCar2 두가지 car 모두 mode 가 "Dive" 모드로 변경된 것을 볼 수 있다.
❓ 어떻게 된거지 ?
이게 바로 Shallow Copy 사용시 주의점이다.
MDN에 Spread Operator 문서를 보면 이런 설명이 있다.
이 문서는 Array 복사에서 Spread Operator 사용시 설명이라 그렇지, 아래에 보면, Object.assign( ) 도 동일한 개념이라고 말하고 있다.
중요한 건, 깊이(Depth) 가 1 이라는 말이 무엇이냐면, 쉽게 말해, 바로 위 예제에서, myCar 객체에 내부 프로퍼티에 대해, Depth 라는 표현을 빗대어 설명하자면
더 쉽게 이해하려면, 가장 바깥 껍데기만 복사되는 복사가 Shallow Copy(얕은 복사)라고 생각할만 하다.
이렇게 볼 수 있지만, 좀 더 상세하게 이해하자면
Shallow Copy는 새로운 객체에 원본 객체의 프로퍼티의 값을 정확히 복사한다.
단, 먄약 프로퍼티의 값이 원시타입이 아닌, 또 "객체(Object)"라면 객체의 주소를 복사한다. 🔍
즉, 복사된 객체는 원본 객체와 동일한 프로퍼티와 값들을 "새롭게" 가지지만, 주소가 복사된 프로퍼티는 새로운 형태가 아닌 "같은 것(객체의 메모리 주소)"을 공유하는 하게 된다..
그래서 결과적으로, 맨 처음 일반 객체의 대입복사처럼, 객체 내부에 또 다른 "객체나 함수 프로퍼티에 대해서는, 같은 데이터에 대해 메모리 주소를 공유하기 때문에, 완벽한(Deep 한) 복사가 이뤄지지 않는 것이다.
Deep Copy(깊은 복사)는 원본 객체를 완전히 복사하는 것이다.
방금 전에 살펴본 Shallow Copy에 치명적인 함정(Depth에 따른 완벽한 복사가 이뤄지지 않는 것)이 있는 것이 아닌, 말 그대로 새로운 메모리 공간을 확해 생성하게 되는 것이다.
내부적인 예로 들자면 방금 Shallow Copy 문제의 상황과 비교했을 때, Deep Copy 는 다음의 내부구조를 가진다고 예를 들 수 있다.
객체나 메소드처럼 프로퍼티에 추가적인 Depth 가 있다해도, 완벽히 독립된 새로운 메모리 공간 확보
Deep Copy(깊은 복사)를 구현하는 방법에는 크게 다음 방법들이 알려져 있다.
1️⃣. JSON.parse( ) 와 JSON.stringify( ) 함수 사용
/**
* Deep Copy(깊은 복사) - JSON 함수를 이용
*/
let myFruit = {
name: "Apple",
price: 1000,
characteristic: {
favor: "Sweet",
},
};
let myFruit2 = JSON.parse(JSON.stringify(myFruit));
myFruit2.characteristic.favor = "Very Sweet";
console.log(myFruit); // { name: 'Apple', price: 1000, characteristic: { favor: 'Sweet' } }
console.log(myFruit2); // { name: 'Apple', price: 1000, characteristic: { favor: 'Very Sweet' } } 🔍
마지막에 characteristic(객체 프로퍼티) 프로퍼티가 Depth 가 1이 아님에도 불구하고, myFruit 과 myFruit2 가 완벽히 분리되어 사용되는 것을 볼 수 있다.
위에 언급한 JSON 함수를 이용했을 때 문제점은 다음과 같다.
/**
* Deep Copy - JSON 함수 사용시 function 누락 현상
*/
let myObj = {
name: "obj",
sayHi: function () {
console.log("Hello Javascript");
},
};
let copyMyObj = JSON.parse(JSON.stringify(myObj));
console.log(myObj.sayHi); // [Function: sayHi]
console.log(copyMyObj.sayHi); // undefined 🔍
2️⃣. Lodash 의 cloneDeep 함수를 사용
/**
* Deep Copy(깊은 복사) - lodash 에 cloneDeep 함수를 이용
* - lodash 라이브러 import 필요
*/
const _ = require("lodash");
let myFruit = {
name: "Apple",
price: 1000,
characteristic: {
favor: "Sweet",
},
sayHi: function () {
console.log("과일이 인사를 하네요.");
},
};
let myFruit2 = _.cloneDeep(myFruit);
console.log(myFruit);
// {
// name: 'Apple',
// price: 1000,
// characteristic: { favor: 'Sweet' },
// sayHi: [Function: sayHi]
// }
console.log(myFruit2);
// {
// name: 'Apple',
// price: 1000,
// characteristic: { favor: 'Sweet' },
// sayHi: [Function: sayHi]
// }
3️⃣. 직접 구현(재귀)
/**
* Deep Copy - 재귀를 이용한 직접 구현
*/
function clone(source) {
var target = {};
for (let i in source) {
if (source[i] != null && typeof source[i] === "object") {
target[i] = clone(source[i]); // resursion
} else {
target[i] = source[i];
}
}
return target;
}
let myFruit = {
name: "Apple",
price: 1000,
characteristic: {
favor: "Sweet",
},
sayHi: function () {
console.log("과일이 인사를 하네요.");
},
};
const myFruit2 = clone(myFruit);
console.log(myFruit);
// {
// name: 'Apple',
// price: 1000,
// characteristic: { favor: 'Sweet' },
// sayHi: [Function: sayHi]
// }
console.log(myFruit2);
// {
// name: 'Apple',
// price: 1000,
// characteristic: { favor: 'Sweet' },
// sayHi: [Function: sayHi]
// }
[TMI]
결국, 지금까지 JS에서 "복사"에 대해 잘못 알고 있었던(나같이 😁) 대부분의 사람들이 Depth가 1인 데이터에 대해 Shallow Copy 만 해서, "음 ~ 이상없네"를 생각하고 개발을 해왔다면, 정작 중요한 건 Deep Copy(깊은 복사)이다.
그건 알아야 할 것 같다.
객체 복사를 함에 있어, 무조건 "Shallow Copy 나쁜거다" 는 잘못된 생각이다. 상황에 따라 Shallow Copy 정도만 진행해서 객체를 복사해도 충분한 상황이 있고, Deep Copy를 통해 주의깊게 데이터의 복사가 이뤄져야 하는 상황도 있을 것이다.
이를 잘 판단하며 사용하는 것이 중요하겠다.
이번 주제를 조사하다, 어떤 분의 포스팅에서 복사가 중요한 이유를 소신껏 적어주셨는데 다음과 같다.
예측할 수 없는 버그를 최소화하기 위해서
동의한다. JS 자체가 도입부에 말했던 동적 타이핑언어이고, 런타임 언어이기 떄문에, 도중에 내가 생각치도 못한 곳에서의 데이터가 조작이 일어날 수 있고, 이를 또 디버그 하기에도 프로그램이 커지면 커질 수록 발견하기도 힘들 것이니, 애초에 개발할 때 이번에 정리한 Shallow Copy 나 Deep Copy 를 잘 고려해서 개발을 하면 좋을 것 같다.
다음은, 이터러블(Iterable)객체에 대해 알아본다.