본문 바로가기

[JS] 얕은 복사와 깊은 복사

[JS] 얕은 복사와 깊은 복사

summary

  • 배열의 경우 slice() 메서드 또는 spread syntax 등의 방법으로 복사할 수 있다.
  • 객체의 경우 Object.assign() 또는 spread syntax 등의 방법으로 복사할 수 있다.
  • 위 방법으로 참조 자료형을 복사할 경우, 중첩된 구조 중 한 단계까지만 복사된다. (얕은 복사)
  • JavaScript 내부적으로는 중첩된 구조 전체를 복사하는 깊은 복사를 구현할 수 없다. 단, 다른 문법을 응용하여 같은 결과물을 만들 수 있다.
  • 대표적인 JSON.stringify()와 JSON.parse()를 사용하는 방법이 있지만, 예외의 케이스가 존재한다. (참조 자료형 내부에 함수가 있는 경우)
  • 완전한 깊은 복사를 반드시 해야 하는 경우, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 사용하면 된다.

배열 복사하기

배열을 복사하는 방법은 크게 두 가지 방법이 있다. 배열 내장 메서드인 slice()를 사용하는 방법과 ES6에서 도입된 spread문법을 사용하는 방법이다.

slice()

배열 내장 메서드인 slice()를 사용하면 원본 배열을 복사할 수 있다.

let arr = [0, 1, 2, 3];
let copiedArr = arr.slice();
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false

새롭게 생성된 배열은 원본 배열과 같은 요소를 갖지만 참조하고 있는 주소는 다르다. 그렇기 때문에 복사한 배열에 요소를 추가해도 원본 배열에는 추가되지 않는다.

copiedArr.push(4);
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr); // [0, 1, 2, 3]

 

spread syntax

spread syntax는 ES6에서 새롭게 추가된 문법으로, spread라는 단어의 뜻처럼 배열을 펼칠 수 있다. 펼치는 방법은 배열이 할당된 변수명 앞에 ... 을 붙여주면 된다. 배열을 펼치면 배열의 각 요소를 확인할 수 있다.

let arr = [0, 1, 2, 3];

console.log(...arr); // 0 1 2 3

spread syntax로 배열을 복사하기 위해서 배열을 생성하는 방법을 이해해야 한다. 만약 같은 요소를 가진 배열을 두 개 만든 후 변수에 각각 할당하는 경우에는, 각각 다른 주소를 참조하게 된다.

let num = [1, 2, 3];
let int = [1, 2, 3];

console.log(num === int) // false

spread syntax를 이용해 새로운 배열 안에 원본 배열을 펼쳐서 전달하면 결과적으로 slice() 메서드를 사용한 것과 동일하게 동작한다.

let arr = [0, 1, 2, 3];
let copiedArr = [...arr];
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false

copiedArr.push(4);
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr); // [0, 1, 2, 3]

객체 복사하기

Object.assign()

객체를 복사하기 위해서는 Object.assign()을 사용한다.

let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = Object.assign({}, obj);

console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false

Object.assign()에 대한 MDN 예시

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
// Expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget === target);
// Expected output: true

 

spread syntax

spread syntax는 배열뿐만 아니라 객체를 복사할 때도 사용할 수 있다.

let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = {...obj};

console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false

예외의 상황 (얕은 복사의 상황) 

그러나 예외의 상황도 있다. 참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우, slice(), Object.assign(), spread syntax를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없다. 참조 자료형이 몇 단계로 중첩되어 있던지, 위에서 설명한 방법으로는 한 단계까지만 복사할 수 있다.

예를 들어, 유저의 정보를 담고 있는 객체를 요소로 가지고 있는 배열 users를 slice() 메서드를 사용하여 복사했다. users와 copiedUsers를 동치연산자(===)로 확인해 보면 false가 반환된다. 위에서 살펴본 바와 같이 각각 다른 주소를 참조하고 있기 때문이다.

let users = [
	{
		name: "kimcoding",
		age: 26,
		job: "student"
	},
	{
		name: "parkhacker",
		age: 29,
		job: "web designer"
	},
];

let copiedUsers = users.slice();

 

그러나 users와 copiedUsers의 0번째 요소를 각각 비교하면 true가 반환된다. users[0]과 copiedUsers[0]는 여전히 같은 주소값을 참조하고 있기 때문이다.

console.log(users[0] === copiedUsers[0]); // true

이처럼 slice(), Object.assign(), spread syntax 등의 방법으로 참조 자료형을 복사하면, 중첩된 구조 중 한 단계까지만 복사합니다. 이것을 얕은 복사(shallow copy)라고 한다.


깊은 복사

참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것은 깊은 복사(deep copy)라고 한다. 그러나 JavaScript 내부적으로는 깊은 복사를 수행할 수 있는 방법이 없다. 단, JavaScript의 다른 문법을 응용하면 깊은 복사와 같은 결과물을 만들어 낼 수 있다.

JSON.stringify()와 JSON.parse()

JSON.stringify()는 참조 자료형을 문자열 형태로 변환하여 반환하고, JSON.parse()는 문자열의 형태를 객체로 변환하여 반환한다. 먼저 중첩된 참조 자료형을 JSON.stringify()를 사용하여 문자열의 형태로 변환하고, 반환된 값에 다시 JSON.parse()를 사용하면, 깊은 복사와 같은 결과물을 반환한다.

const arr = [1, 2, [3, 4]];
const copiedArr = JSON.parse(JSON.stringify(arr));

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

깊은 복사의 예외 (함수가 포함된 경우)

간단하게 깊은 복사를 할 수 있는 것처럼 보이지만, 이 방법 또한 깊은 복사가 되지 않는 예외가 존재한다. 대표적인 예로 중첩된 참조 자료형 중에 함수가 포함되어 있을 경우 위 방법을 사용하면 함수가 null로 바뀌게 된다. 따라서 이 방법 또한 완전한 깊은 복사 방법이라고 보기 어렵다.

const arr = [1, 2, [3, function(){ console.log('hello world')}]];
const copiedArr = JSON.parse(JSON.stringify(arr));

console.log(arr); // [1, 2, [3, function(){ console.log('hello world')}]]
console.log(copiedArr); // [1, 2, [3, null]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false

외부 라이브러리 사용

완전한 깊은 복사를 반드시 해야 하는 경우라면, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 설치하면 된다. lodash와 ramda는 각각 방법으로 깊은 복사를 구현해 두었다. 다음은 lodash의 cloneDeep을 사용한 깊은 복사의 예이다.

const lodash = require('lodash');

const arr = [1, 2, [3, 4]];
const copiedArr = lodash.cloneDeep(arr);

console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
728x90
⬆︎