이전에 작성했던 '얕은 복사와 깊은 복사'에 이어서 깊은 복사 방법에 대해 알아 보려고 한다. 그 전에 자바스크립트에서 값이 저장되는 방법에 대해 알아본다.
원시 데이터 타입
const a = 1)const a = 'string')const a = true)const a = undefined)const a = null)원시 데이터 타입을 복사하는 경우, 변수들이 강하게 결합(tightly coupled)될 거라고 걱정하지 않아도 된다. 원시 데이터 타입의 복사는 항상 깊은 복사이기 때문이다.
const a = 5;
let b = a;
console.log(a, 'a');
console.log(b, 'b');
/*
5 'a'
5 'b'
*/
b = 10;
console.log(a, 'a');
console.log(b, 'b');
/*
5 'a'
10 'b'
*/
복합 데이터 타입
복합 데이터 타입을 생성하는 경우, 인스턴스화될 때 값들은 실제로 한번만 저장되고 변수에 할당하면 해당 값에 대한 참조만 생성하는 것이다.
const product = {
name: '키보드',
price: 100000
};
const copyOfProduct = product
console.log(product, 'product');
console.log(copyOfProduct, 'copyOfProduct');
/*
{ name: '키보드', price: 100000 } 'product'
{ name: '키보드', price: 100000 } 'copyOfProduct'
*/
copyOfProduct.price = 200000;
console.log(product, 'product');
console.log(copyOfProduct, 'copyOfProduct');
/*
{ name: '키보드', price: 200000 } 'product'
{ name: '키보드', price: 200000 } 'copyOfProduct'
*/
복합 데이터 타입을 복사하는 여러 방법이 존재한다.
배열 복사는 객체 복사와 비슷하다. 왜냐하면 배열도 객체이기 때문이다.
엄밀히 말하자면 스프레드 연산자는 배열에 대해 완전한 깊은 복사를 제공하지 않는다. 중첩 배열이나 2D, 3D 배열이 아닌 경우에만 깊은 복사를 제공한다. 중첩 배열일 경우, 스프레드 연산자는 값의 첫 번째 인스턴스에 대해서만 깊은 복사를 수행하고 중첩 배열에 대해서는 얕은 복사를 수행한다.
const c = [1, 2, [3, 4]];
const d = [...c]; // 1️⃣
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, 4 ] ] 'c'
[ 1, 2, [ 3, 4 ] ] 'd'
*/
d[0] = 0; // 2️⃣
d[2][1] = null; // 3️⃣
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, null ] ] 'c'
[ 0, 2, [ 3, null ] ] 'd'
*/
c 변수가 참조하는 배열을 복사해 d 변수가 참조하도록 하였다.d 변수가 참조하는 배열의 첫 번째 원소를 0으로 재할당하였다.d 변수가 참조하는 배열의 첫 번째 원소만 0으로 재할당되었다.d 변수가 참조하는 배열의 세 번째 원소의 두 번째 원소를 null로 재할당하였다.c 변수가 참조하는 배열의 세 번째 원소의 두 번째 원소 또한 null이 되었다.스프레드 연산자는 중첩 배열에 대해서는 깊은 복사를 제공하지 않기 때문이다.
배열 메서드 map, forEach, slice는 모두 새로운 배열을 반환하지만, 완전한 깊은 복사를 제공하지는 않는다. 스프레드 연산자와 유사하게 중첩 배열에 대해서는 깊은 복사를 수행하지 않는다.
const c = [1, 2, [3, 4]];
const d = c.map(el => el); // 1️⃣
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, 4 ] ] 'c'
[ 1, 2, [ 3, 4 ] ] 'd'
*/
d[0] = 0; // 2️⃣
d[2][1] = null; // 3️⃣
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, null ] ] 'c'
[ 0, 2, [ 3, null ] ] 'd'
*/
c 변수가 참조하는 배열의 원소를 갖는 배열을 새롭게 생성하고 d 변수가 참조하도록 하였다.d 변수가 참조하는 배열의 첫 번째 원소를 0으로 재할당하였다.d 변수가 참조하는 배열의 첫 번째 원소만 0으로 재할당되었다.d 변수가 참조하는 배열의 세 번째 원소의 두 번째 원소를 null로 재할당하였다.c 변수가 참조하는 배열의 세 번째 원소의 두 번째 원소 또한 null이 되었다.const c = [1, 2, [3, 4]];
const d = c.slice(0);
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, 4 ] ] 'c'
[ 1, 2, [ 3, 4 ] ] 'd'
*/
d[0] = 0;
d[2][1] = null;
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, null ] ] 'c'
[ 0, 2, [ 3, null ] ] 'd'
*/
const c = [1, 2, [3, 4]];
const d = [];
c.forEach(el => d.push(el));
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, 4 ] ] 'c'
[ 1, 2, [ 3, 4 ] ] 'd'
*/
d[0] = 0;
d[2][1] = null;
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, null ] ] 'c'
[ 0, 2, [ 3, null ] ] 'd'
*/
d 변수가 참조하는 배열에 c 변수가 참조하는 배열의 원소들을 push하였다.배열 메서드 map, slice, forEach 모두 중첩 배열에 대해서는 깊은 복사를 수행하지 않는다는 것을 알 수 있다.
객체를 복사하는 방법 중 하나로, 완전한 깊은 복사를 제공하는 JSON 메서드를 사용할 수 있다.
const c = [1, 2, [3, 4]];
const d = JSON.parse(JSON.stringify(c));
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, 4 ] ] 'c'
[ 1, 2, [ 3, 4 ] ] 'd'
*/
d[0] = 0;
d[2][1] = null;
console.log(c, 'c');
console.log(d, 'd');
/*
[ 1, 2, [ 3, 4 ] ] 'c'
[ 0, 2, [ 3, null ] ] 'd'
*/
parse와 stringify를 사용하여 c 변수가 참조하는 배열을 깊은 복사하고, d 변수가 참조하게 하였다.d 변수가 참조하는 배열의 세번째 원소(중첩된 배열)을 변경하여도 c 변수가 참조하는 배열의 세번째 원소가 변경되지 않는 것을 알 수 있다.JSON 메서드
parse와stringify를 사용해 배열에 대해 완전한 깊은 복사를 수행할 수 있다.
객체를 복사하는 다양한 방법이 있다.
assign메서드를 사용하는 경우, 객체가 두 번째 아규먼트를 복사하는지 확인해야 한다. 일반적으로 첫 번째 아규먼트로 빈 객체를 전달하게 되는데,assign메서드는 스프레드 연산자와 유사하게 완전한 깊은 복사를 제공하지 않는다.
const employee = {
name: 'cabbage',
salary: {
annual: '100K',
hourly: '$50'
}
};
const copyOfEmployee = Object.assign({}, employee); // 1️⃣
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee');
/*
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'employee'
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'copyOfEmployee'
*/
copyOfEmployee.name = '일론 머스크'; // 2️⃣
copyOfEmployee.salary.annual = '120K'; // 3️⃣
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee');
/*
{
name: 'cabbage',
salary: { annual: '120K', hourly: '$50' }
} 'employee'
{
name: '일론 머스크',
salary: { annual: '120K', hourly: '$50' }
} 'copyOfEmployee'
*/
Object.assign 메서드를 사용해 employee 변수가 참조하는 객체를 복사하여 copyOfEmployee 변수가 참조하도록 하였다.copyOfEmployee의 name 프로퍼티를 '일론 머스크'로 변경하였다.copyOfEmployee의 salary 프로퍼티(객체)의 annual 프로퍼티를 '120K'로 변경하였다.copyOfEmployee의 name 프로퍼티의 값만 변경되었다.copyOfEmployee가 참조하는 객체 뿐만 아니라 employee가 참조하는 객체의 salary 프로퍼티(객체)의 annual 프로퍼티의 값까지 변경되었다.Object.assign 메서드는 객체에 대해 완전한 깊은 복사를 수행하지 않는다는 것을 알 수 있다.
Object.create 메서드는 기존의 객체를 사용해 새로운 객체를 생성한다. 이 메서드는 기존의 객체를 새롭게 생성한 객체의 프로토타입으로 사용한다. 기존 객체의 모든 프로퍼티를 새로운 객체에서 사용할 수 있도록 기존 객체가 프로토타입이 된다. 하지만 복사의 경우, Object.create 메서드는 assign 메서드나 스프레드 연산자처럼 부분적인 깊은 복사만 제공한다.
const employee = {
name: 'cabbage',
salary: {
annual: '100K',
hourly: '$50'
}
};
const copyOfEmployee = Object.create(employee); // 1️⃣
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee'); // 2️⃣
/*
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'employee'
{
__proto__: {
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
}
} 'copyOfEmployee'
*/
copyOfEmployee.name = '일론 머스크'; // 3️⃣
copyOfEmployee.salary.annual = '120K'; // 4️⃣
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee');
/*
{
name: 'cabbage',
salary: { annual: '120K', hourly: '$50' }
} 'employee'
{
name: '일론 머스크',
__proto__: {
name: 'cabbage',
salary: { annual: '120K', hourly: '$50' }
}
} 'copyOfEmployee'
*/
Object.create 메서드를 사용해 employee가 참조하는 객체를 프로토타입으로 사용하는 새로운 객체를 생성해 copyOfEmployee 변수가 참조하게 하였다.copyOfEmployee 변수가 참조하는 객체를 확인하면 employee 변수가 참조하는 객체를 프로토타입으로 사용하고 있는 것을 알 수 있다.copyOfEmployee 변수가 참조하는 객체의 name 프로퍼티에 '일론 머스크'를 할당하였다.copyOfEmployee 변수가 참조하는 객체의 salary 프로퍼티(객체)의 annual 프로퍼티의 값을 수정하였다.copyOfEmployee 변수가 참조하는 객체 뿐만 아니라 employee 변수가 참조하는 객체의 salary 프로퍼티(객체)의 annual 프로퍼티의 값이 수정되었다.Object.create 메서드는 객체에 대해 완전한 깊은 복사를 수행하지 못한다는 것을 알 수 있다.
const employee = {
name: 'cabbage',
salary: {
annual: '100K',
hourly: '$50'
}
};
const copyOfEmployee = {...employee}
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee');
/*
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'employee'
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'copyOfEmployee'
*/
copyOfEmployee.name = '일론 머스크';
copyOfEmployee.salary.annual = '120K';
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee');
/*
{
name: 'cabbage',
salary: { annual: '120K', hourly: '$50' }
} 'employee'
{
name: '일론 머스크',
salary: { annual: '120K', hourly: '$50' }
} 'copyOfEmployee'
*/
스프레드 연산자를 사용해 객체를 복사하는 경우에도 Object.assign, Object.create 메서드처럼 완전한 깊은 복사를 수행하지 않는 것을 알 수 있다.
만약 중첩 객체의 깊이를 알고 있다면 깊은 복사를 수행하기 위해 중첩 스프레드 연산자를 사용할 수 있다. 하지만 중첩 객체의 깊이를 알 수 없는 경우라면 깊은 복사를 수행하기 위해 스프레드 연산자를 사용하지 않는 것이 좋다.
const employee = {
name: 'cabbage',
salary: {
annual: '100K',
hourly: '$50'
}
};
const copyOfEmployee = {...employee, salary: {...employee.salary}}; // 1️⃣
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee');
/*
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'employee'
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'copyOfEmployee'
*/
copyOfEmployee.name = '일론 머스크';
copyOfEmployee.salary.annual = '120K'; // 2️⃣
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee');
/*
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'employee'
{
name: '일론 머스크',
salary: { annual: '120K', hourly: '$50' }
} 'copyOfEmployee'
*/
employee 변수가 참조하는 객체의 salary 프로퍼티가 참조하는 객체를 깊은 복사하였다.salary 프로퍼티가 참조하는 객체의 annual 프로퍼티의 값을 수정하였다.employee 변수가 참조하는 객체가 copyOfEmployee 변수가 참조하는 객체가 서로 다르다는 것을 알 수 있다.객체를 복사하는 방법 중 하나로, 완전한 깊은 복사를 제공하는 JSON 메서드를 사용할 수 있다.
const employee = {
name: 'cabbage',
salary: {
annual: '100K',
hourly: '$50'
}
};
const copyOfEmployee = JSON.parse(JSON.stringify(employee));
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee');
/*
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'employee'
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'copyOfEmployee'
*/
copyOfEmployee.name = '일론 머스크';
copyOfEmployee.salary.annual = '120K';
console.log(employee, 'employee');
console.log(copyOfEmployee, 'copyOfEmployee');
/*
{
name: 'cabbage',
salary: { annual: '100K', hourly: '$50' }
} 'employee'
{
name: '일론 머스크',
salary: { annual: '120K', hourly: '$50' }
} 'copyOfEmployee'
*/
배열을 깊은 복사하는 경우, 객체를 깊은 복사하는 경우 모두 JSON의 parse, stringify 메서드를 사용할 수 있다는 것을 알 수 있다.