본 포스팅은 JavaScript에서 객체를 복사하는 방법을 소개합니다.


개요

때때로, 변수에 할당된 값을 다른 변수에 할당해야 하는 상황이 발생할 수 있다.

먼저, 문자열, 숫자, 불리언 등과 같은 원시 타입(Primitive Type)의 값을 다른 변수에 복사 후 값을 변경하면 서로 영향을 미치지 않는 것을 확인할 수 있다.

let s1 = 'Hello';
let s2 = s1;

s2 = 'Java';

console.log(s1); // Hello
console.log(s2); // Java

하지만, 다음 예제처럼 객체와 같은 참조 타입(Reference Type)을 사본에 복사 후 값을 변경하면 원본에 영향을 미치는 것을 확인할 수 있다.

let obj1 = {
  name: '홍길동'
};
let obj2 = obj1;

obj2.name = '마이콜'; // 사본 변경

console.log(obj1.name); // 마이콜
console.log(obj2.name); // 마이콜

반대의 경우도 마찬가지다. 원본을 변경하면 사본에 영향을 미친다.

let obj1 = {
  name: '홍길동'
};
let obj2 = obj1;

obj1.name = '마이콜'; // 원본 변경

console.log(obj1.name); // 마이콜
console.log(obj2.name); // 마이콜

이러한 현상을 얕은 복사(Shallow copy)라고 말하는데, 사본에 새로운 객체가 복사되는 것이 아닌 s1이 참조하고 있는 객체가 할당되기 때문이다. 즉, 원본 s1와 사본 s2는 동일한 객체를 참조하고 있다.

깊은 복사(Deep copy)는 사본에 새로운 객체가 복사되는 것을 말한다. 이 방법은 중첩된 객체까지 완전하게 복사할 수 있으며, 원본을 변경해도 사본이 변경되지 않는다. 반대로 사본을 변경해도 원본이 변경되지 않는다. 하지만, JavaScript에서 깊은 복사(Deep copy)를 지원하는 메서드가 없으므로 직접 구현하거나 라이브러리를 사용해야 한다는 문제가 있다.


방법 1. JSON.stringify() 및 JSON.parse()

JavaScript의 객체와 JSON은 완전히 동일하지는 않지만, 키와 값의 쌍이라는 이루어졌다는 점은 동일하다. 따라서, JSON.stringify() 메서드와 JSON.parse() 메서드를 사용하여 객체를 복사할 수 있다.

  • JSON.stringify(): JavaScript 객체를 JSON 문자열로 변환한다.
  • JSON.parse(): JSON 문자열을 JavaScript 객체로 변환한다.

다음 예제는 JSON.stringify()와 JSON.parse() 메서드로 객체 복사 후 값을 변경한다.

let obj1 = {
  name: '홍길동'
};
let obj2 = JSON.parse(JSON.stringify(obj1));

obj2.name = '마이콜';

console.log(obj1.name); // 홍길동
console.log(obj2.name); // 마이콜

중첩된 객체도 정상적으로 복사되는 것을 확인할 수 있다.

let obj1 = {
  name: {
    first: '홍',
    last: '길동'
  }
};
let obj2 = JSON.parse(JSON.stringify(obj1));

obj2.name.first = '강';
obj2.name.last = '감찬';

console.log(obj1.name); // {first: '홍', last: '길동'}
console.log(obj2.name); // {first: '강', last: '감찬'}

JSON.stringify() 메서드의 문제점은 JavaScript의 모든 데이터 타입을 문자열로 표현할 수 없다는 점이다.

다음 예제는 원본 객체의 프로퍼티에 undefined와 NaN을 할당 후 객체를 복사한다.

let obj1 = {
  name: undefined,
  age: NaN
};
let obj2 = JSON.parse(JSON.stringify(obj1));

console.log(obj1); // {name: undefined, age: NaN}
console.log(obj2); // {age: null}

undefined와 NaN 이외에도 함수(Function), 심볼(Symbol), 무한대(Infinity), 날짜(Date), 정규 표현식을 문자열로 제대로 표시하지 못한다. 따라서, JSON.stringify() 메서드로 객체를 문자열로 변환하기 전에 적절한 변환 또는 처리를 수행해야 한다.

방법 2. Object.assign()

Object.assign() 메서드는 객체의 열거 가능한 속성을 할당 후 객체를 반환한다. assign() 메서드가 “배정하다, 배치하다, 할당하다”라는 뜻을 가지므로 객체를 새로 정의하는 것이 아니라, 다음 예시처럼 기존 객체에 새로운 프로퍼티를 할당하는 용도로 사용한다.

let obj1 = {
  name: '홍길동'
};
let src1 = { age: 20 };

let obj2 = Object.assign(obj1, src1);

obj2.age = 100;

console.log(obj1); // {name: '홍길동', age: 100}
console.log(obj2); // {name: '홍길동', age: 100}

Object.assign() 메서드는 src1의 프로퍼티가 obj1에 할당된 obj1을 반환한다. 따라서, 사본 obj2는 원본 obj1와 동일한 객체를 참조하므로 얕은 복사(Shallow copy)가 발생한다.

Object.assign() 메서드를 사용하여 객체를 복사하려면 첫 번째 매개변수에 빈 객체를 전달하고 두 번째 매개변수에 원본 객체를 전달한다.

let obj1 = {
  name: '홍길동'
};
let obj2 = Object.assign({}, obj1);

obj2.name = '강감찬';

console.log(obj1); // {name: '홍길동'}
console.log(obj2); // {name: '홍길동'}

하지만, 다음 예시처럼 중첩된 객체는 참조가 복사되므로 중첩된 객체를 복사하는 경우 주의해서 사용한다.

let obj1 = {
  name: {
    first: '홍',
    last: '길동'
  }
};
let obj2 = Object.assign({}, obj1);

obj2.name.first = '강';
obj2.name.last = '감찬';

console.log(obj1.name); // {first: '강', last: '감찬'}
console.log(obj2.name); // {first: '강', last: '감찬'}

방법 3. Spread 연산자(… 연산자)

ES6에 도입된 펼치기 연산자라고 불리는 Spread 연산자를 사용하여 객체를 복사할 수 있다.

let obj1 = {
  name: '홍길동'
};
let obj2 = { ...obj1 };

obj2.name = '강감찬';

console.log(obj1); // {name: '홍길동'}
console.log(obj2); // {name: '강감찬'}

하지만, 다음 예시처럼 중첩된 객체는 참조가 복사된다.

let obj1 = {
  name: {
    first: '홍',
    last: '길동'
  }
};
let obj2 = {...obj1};

obj2.name.first = '강';
obj2.name.last = '감찬';

console.log(obj1.name); // {first: '강', last: '감찬'}
console.log(obj2.name); // {first: '강', last: '감찬'}

Spread 연산자를 사용하여 중첩된 객체를 복사하려면 각 객체를 따로 복사한다.

let obj1 = {
  name: {
    first: '홍',
    last: '길동'
  },
  age: 20
};
let obj2 = {...obj1, name: {...obj1.name}};

obj2.name.first = '강';
obj2.name.last = '감찬';

console.log(obj1); // {age: 20, name: {first: '홍', last: '길동'}}
console.log(obj2); // {age: 20, name: {first: '강', last: '감찬'}}

방법 4. Lodash 라이브러리의 clonedeep()

마지막 방법으로 Lodash 라이브러리를 사용하여 깊은 복사를 수행할 수 있다. clonedeep() 메서드를 사용하면 중첩된 객체와 그 하위 객체들까지 완전히 복사할 수 있다.

const _ = require('lodash');

const obj1 = {
  name: {
    first: '홍',
    last: '길동'
  },
  age: 20
};
const obj2 = _.cloneDeep(original);

Lodash 라이브러리는 오랜 기간 동안 널리 사용되고, 다양한 프로젝트에서 검증되었기에 높은 신뢰성과 안정성을 갖추고 있다. 그리고 라이브러리 번들링 도구와 압축 기술을 사용하면 Lodash의 용량을 충분히 최소화할 수 있으므로 웹 페이지 로딩 속도에 큰 영향은 없을 것이다.


Conclusion

JavaScript에서 = 연산자를 사용하여 객체를 복사하는 경우 참조가 복사되므로 값을 변경하면 원본과 사본에 서로 영향을 미친다. 이러한 현상을 얕은 복사(Shallow copy)라고 말하며, 참조가 아닌 새로운 객체가 복사되는 것을 깊은 복사(Deep copy)라고 말한다.

객체를 복사하기 위해 다음 네 가지 방법을 활용할 수 있으며, 각각의 방법마다 장단점이 있으므로 상황에 맞게 올바른 방법을 사용할 수 있어야한다.

JSON.stringify() 및 JSON.parse()

  • 장점: 중첩된 객체도 복사 가능
  • 단점: JavaScript의 특정 데이터 타입은 복사 불가능

Object.assign()

  • 단점: 중첩된 객체는 복사 불가능

Spread 연산자

  • 장점: 중첩된 객체도 복사 가능
  • 단점: 중첩된 객체는 각 객체를 따로 복사해야한다는 번거로움

Lodash 라이브러리의 cloneDeep()

  • 장점: 중첩된 객체도 복사 가능
  • 단점: 외부 라이브러리

참고

  1. 자바스크립트 객체 복제 방법
  2. 3 Ways to Copy Objects in JavaScript
  3. How to Copy object?

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다