JavaScript - 얕은 복사와 깊은 복사

uk·2022년 11월 7일

JavaScript

목록 보기
16/19

원시 자료형을 할당한 변수를 다른 변수에 할당하면 값 자체의 복사가 일어난다. 둘 중 하나의 값을 변경해도 다른 하나에는 영향을 미치지 않는다.

let a = 5;
let b = a;
console.log(a);  // 5
console.log(b);  // 5
console.log(a === b);  // true

b = 10;
console.log(a);  // 5
console.log(b);  // 10
console.log(a === b);  // false

참조 자료형은 임의의 저장공간에 값을 저장하고 그 저장공간을 참조하는 주소를 메모리에 저장하기 때문에 다른 변수에 할당할 경우 값 자체가 아닌 메모리에 저장되어 있는 주소가 복사된다.

let a = [1, 2, 3];
let b = a;

console.log(a); // [1, 2, 3]
console.log(b); // [1, 2, 3]
console.log(a === b) // true

둘 중 하나를 변경하면 해당 변수가 참조하고 있는 주소에 있는 값이 변경되기 때문에 다른 하나에도 영향을 미치게 된다.

배열을 할당한 변수 a를 변수 b에 할당한 후 b에 push() 메서드를 사용하여 배열의 요소를 추가하면 원본 배열인 a에도 동일하게 요소가 추가된다. a가 참조하고 있던 주소가 b로 복사되어 두 변수는 같은 주소를 참조하고 있기 때문이다.

b.push(4);

console.log(a); // [1, 2, 3, 4]
console.log(b); // [1, 2, 3, 4]
console.log(a === b) // true

두 변수는 같은 주소를 참조하고 있을뿐 값 자체가 복사되지 않았다.


배열 복사

배열을 복사하여 똑같은 요소를 가지지만 원본과 복사본이 서로 영향을 미치지 않는 방법은 slice()와 spread문법이 있다.

1. slice()

let a = [1, 2, 3];
let b = a.slice();
console.log(b);  // [1, 2, 3]
console.log(a === b);  // false

b.push(4);
console.log(a);  // [1, 2, 3]
console.log(b);  // [1, 2, 3, 4]

새롭게 생성된 b 배열은 원본 배열과 같은 요소를 가지지만 참조하고 있는 주소는 다르다.

또한 주소가 다르기 때문에 복사한 b 배열에 요소를 추가해도 원본 배열은 변하지 않는다.

2. spread syntax

let a = [1, 2, 3];

console.log(...a);  // 1 2 3

ES6에서 새롭게 추가된 문법으로 spread의 뜻처럼 배열을 펼칠 수 있다. 사용 방법은 배열이 할당된 변수명 앞에 ... 을 붙여준다.

let a = [1, 2, 3];
let b = [...a];
console.log(b);  // [1, 2, 3]
console.log(a === b);  // false

b.push(4);
console.log(b);  // [1, 2, 3, 4]
console.log(a);  // [1, 2, 3]

배열 b에 spread syntax로 펼쳐서 전달하면 slice() 메서드를 사용한 것과 동일하게 동작하며 b 배열은 원본 배열과 같은 요소를 가지지만 다른 주소를 참조한다.


객체 복사

1. Object.assign()

let a = { name: 'lee', age: 20 };
let b = Object.assign({}, a);

console.log(b);  // { name: 'lee', age: 20 }
console.log(a === b);  // false

배열의 slice() 메서드와 마찬가지로 객체를 복사할때는 Object.assign() 메서드를 사용한다.

2. spread syntax

let a = { name: 'lee', age: 20 };
let b = { ...a };

console.log(b);  // { name: 'lee', age: 20 }
console.log(a === b)  // false

spread syntax는 배열 뿐만 아니라 객체 복사도 가능하다.

하지만 참조 자료형 내부에 참조 자료형이 중첩되어 있는 구조는 slice(), Object.assign(), spread syntax를 사용해도 복사할 수 없다. 참조 자료형이 몇 단계로 중첩되어 있던 상관없이 한 단계까지만 복사할 수 있다.


얕은 복사(Shllow Copy)

참조 자료형을 복사할 때 원본 값과 복사된 값이 같은 주소를 참조하는 것을 얕은 복사라고 한다. 얕은 복사는 원본 객체에 영향을 끼친다.

배열과 객체를 복사하는 slice(), Object.assign() 메서드는 얕은 복사를 한다.

let a = [
	{
		name: "lee",
		age: 20,
		country: "KOR"
	},
	{
		name: "park",
		age: 30,
		country: "JAP"
	},
];

let b = a.slice();

console.log(a === b);  // false
console.log(a[0] === b[0]);  // true

b[1].name = 'kim';

console.log(a);  // [{ ... }, { name: "kim", age: 30, country: "JAP" }];
console.log(b);  // [{ ... }, { name: "kim", age: 30, country: "JAP" }];

객체 a와 b를 비교해보면 false가 반환된다. 하지만 a의 0번째 요소와 b의 0번째 요소를 비교하면 같은 주소를 참조하고 있기 때문에 true를 반환한다.

b 객체의 두번째 name 속성을 변경 시 a의 객체도 변경된다. 객체를 참조하는 주소값을 복사하기 때문이다.


깊은 복사(Deep Copy)

JSON.stringify(), JSON.parse()

참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것은 깊은 복사(deep copy)라고 한다. 배열 안에 배열이 존재할 경우 복사된 배열이 원본 배열과의 참조가 완전히 끊어진 것을 말한다.

JavaScript 내부적으로는 깊은 복사를 수행할 수 있는 방법이 없지만 JSON.stringify()와 JSON.parse() 메서드를 사용하면 깊은 복사와 같은 결과물을 만들어낼 수 있다.

let a = [1, 2, [3, 4]];
let b = JSON.parse(JSON.stringify(a));

console.log(a);  // [1, 2, [3, 4]]
console.log(b);  // [1, 2, [3, 4]]
console.log(a === b)  // false
console.log(a[2] === b[2])  // false

JSON.stringify()는 참조 자료형을 JSON 문자열 형태로 변환하는데 이 과정에서 원본 객체와의 참조가 모두 끊어진다. 이후 JSON.parse()는 문자열의 형태를 객체로 다시 변환하여 깊은 복사와 같은 결과물을 반환한다.

하지만 예외가 존재하는데 참조자료형 중 함수는 null로 처리한다.


외부 라이브러리 사용(lodash, ramda)

const lodash = require('lodash');

const a = [1, 2, [3, 4]];
const b = lodash.cloneDeep(a);

console.log(a);  // [1, 2, [3, 4]]
console.log(b);  // [1, 2, [3, 4]]
console.log(a === b)  // false
console.log(a[2] === b[2])  // false

완전한 깊은 복사를 해야 하는 경우 node.js 환경에서 외부 라이브러리인 lodash 또는 ramda를 사용한다.


결론

얕은 복사는 객체를 참조하는 주소 값을 복사하고
깊은 복사는 객체의 실제 값을 복사한다.

0개의 댓글