JavaScript에서 객체 복사하기


부끄럽게도 얼마 전에야 자바스크립트에서 =을 사용한 객체 복사는 참조만 하는 것임을 알았다.
어찌보면 예상가능한 것임에도 불구하고 얕은/깊은 복사가 필요한 경우가 생각보다 많지 않았기 때문에 알지 못했던 것 같다.

어쨌든, 복사가 필요한 경우가 분명히 생기기 때문에 실제로 복사를 하는 방법에 대해 공부했다.

얕은 복사(Shallow Copy)

얕은 복사는 다양한 방법으로 가능하다. 일단 라이브러리를 사용하면 매우 쉽다.
아래는 underscore.js를 이용한 방법이다.

1
2
3
4
5
6
var foo = { key: 'value' };
var bar = _.clone(foo);
foo.key = 'other value';

console.log(foo);
console.log(bar);

다음은 jQuery extend()를 이용한 방법이다. extend()는 원래 객체를 합병(Merge)하기 위한 메소드이지만 복사를 위해 사용할 수도 있다. (사실 그런 용도로 사용하는 경우가 더 많은 것 같다.)

1
2
3
4
5
6
var foo = { key: 'value' };
var bar = $.extend({}, foo);
foo.key = 'other value';

console.log(foo);
console.log(bar);

순수한 자바스크립트로 직접 구현하고자 한다면 다음과 같은 방식으로 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function clone(obj) {
if (obj === null || typeof(obj) !== 'object')
return obj;

var copy = obj.constructor();

for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = obj[attr];
}
}

return copy;
}

var foo = { key: 'value' };
var bar = clone(foo);
foo.key = 'other value';

console.log(foo);
console.log(bar);

constructor() 메소드는 해당 객체와 똑같은 타입의 객체를 하나 더 생성하는 메소드이다.
hasOwnProperty() 메소드는 해당 객체가 파라미터로 넘긴 프로퍼티를 가지고 있는지 확인하는 메소드이다.

깊은 복사(Deep Copy)

깊은 복사는 더 많은 방법이 존재한다. 당연하지만 깊은 복사는 보통 얕은 복사보다는 느리다.
먼저 jQuery는 얕은 복사에서 사용했던 extend() 메소드에서 Deep Copy를 위한 Flag를 true로 주면 쉽게 사용가능하다.

1
2
3
4
5
6
7
8
9
10
11
var foo = {
deep: {
key: 'value'
},
shallow: false
};
var bar = $.extend(true, {}, foo);
foo.deep.key = 'other value';

console.log(foo);
console.log(bar);

Angular.js를 사용한다면 Angular.js에서 제공하는 angular.copy() 메소드를 사용할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
var foo = {
deep: {
key: 'value'
},
shallow: false
};
var bar = {};
angular.copy(foo, bar);
foo.deep.key = 'other value';

console.log(foo);
console.log(bar);

그리고 깊은 복사는 순수 자바스크립트만 이용한 방법이 여러가지 있다.
먼저 JSON.stringify()JSON.parse()를 이용한 방법이다.

1
2
3
4
5
6
7
8
9
10
11
var foo = {
deep: {
key: 'value'
},
shallow: false
};
var bar = JSON.parse(JSON.stringify(foo));
foo.deep.key = 'other value';

console.log(foo);
console.log(bar);

엥? 이거 완전 느린거 아니냐? 하는 생각이 드는데, 일단 성능에 관해서는 후술.

위와 비슷한 방식으로 eval()JSON.stringify()를 활용하는 방법이 있다. eval()은 나중에 따로 포스팅할 예정.

1
2
3
4
5
6
7
8
9
10
11
var foo = {
deep: {
key: 'value'
},
shallow: false
};
var bar = eval('(' + JSON.stringify(foo) + ')');
foo.deep.key = 'other value';

console.log(foo);
console.log(bar);

마지막으로 순수 자바스크립트로 직접 구현하는 방법. Shallow Copy에서 함수를 재귀적으로 바꿔주기만 하면 구현이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function clone(obj) {
if (obj === null || typeof(obj) !== 'object')
return obj;

var copy = obj.constructor();

for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = clone(obj[attr]);
}
}

return copy;
}

var foo = {
deep: {
key: 'value'
},
shallow: false
};
var bar = clone(foo);
foo.deep.key = 'other value';

console.log(foo);
console.log(bar);

성능

성능은 jsperf 링크를 참고. 계속 리비전이 올라오고 있다.
위에서 언급했듯이 얕은 복사는 깊은 복사에 비해 압도적으로 빠르다.
어떤 형태의 객체를 복사하는가, 브라우저 종류가 무엇이냐에 따라 차이가 있지만 얕은 복사도 그렇고 깊은 복사도 대체로 jQuery.extend()는 다른 방식에 비해 느린 편이다. (원래 복사를 위해 만들어진 녀석이 아니니 당연한 것일지도..)
깊은 복사는 angular.copy() 그리고 eval()을 이용한 방식이 빠른 편이지만 유의미한 차이는 거의 없는 것 같다. 사실 웹 개발하면서 성능이 중요할 정도로 복사를 자주할 일도 없으므로 입맛에 맞게 골라서 쓰면 좋을 것 같다.

참고링크