JavaScript에서 객체와 배열과 같은 참조 타입을 다룰 때 자주 마주치는 문제 중 하나는 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)의 차이다. 이 두 방식의 차이점을 이해하고 올바르게 사용하는 것은 중요하다.
먼저, JavaScript에서의 데이터 타입을 이해해야 한다. JavaScript의 데이터 타입은 크게 두 가지로 나뉜다.
원시 타입 (Primitive Type): 숫자(Number), 문자열(String), 불린(Boolean), null
, undefined
, Symbol
등이 여기에 속한다(총 6개). 이들 값은 메모리에 고정된 크기로 저장되며 값 자체를 변수에 저장하거나 다른 변수로 복사할 때 직접 그 값을 전달한다.
참조 타입 (Reference Type): 객체(Object), 배열(Array)와 함수(Function)가 여기에 속한다. 이들 값은 메모리의 한 위치에 저장되며, 변수에 저장하거나 다른 변수로 복사할 때 메모리 주소를 전달한다.
얕은 복사는 객체의 상위 레벨만 복사하는 방법이다. 복사된 객체의 참조 타입의 프로퍼티는 원본 객체의 메모리 주소를 그대로 가리키기 때문에, 복사된 객체에서 이 프로퍼티를 수정하면 원본 객체도 영향을 받는다.
예시:
let original = { a: 1, b: [2, 3], c: { d: 4 } };
let copied = { ...original };
copied.a = 10;
copied.b.push(4);
copied.c.d = 5;
console.log(original.a); // 1
console.log(original.b); // [2, 3, 4]
console.log(original.c.d); // 5
여기에서 copied
객체의 b
와 c
프로퍼티는 원본 original
객체의 메모리 주소를 참조하고 있기 때문에, copied
에서 이 프로퍼티를 수정하면 original
에도 영향을 미친다.
깊은 복사는 객체의 모든 레벨을 재귀적으로 복사하는 방법이다. 이 방법을 사용하면 복사된 객체와 원본 객체는 완전히 독립적이며, 한 객체에서의 변경이 다른 객체에 영향을 미치지 않는다.
예시:
let original = { a: 1, b: [2, 3], c: { d: 4 } };
let copied = JSON.parse(JSON.stringify(original));
copied.a = 10;
copied.b.push(4);
copied.c.d = 5;
console.log(original.a); // 1
console.log(original.b); // [2, 3]
console.log(original.c.d); // 4
이 예시에서는 JSON.parse()
와 JSON.stringify()
를 사용하여 깊은 복사를 수행한다. 이 방법은 객체 내부의 함수나 Symbol
타입의 데이터, undefined
등을 제대로 복사하지 못하는 제한 사항이 있지만, 일반적인 경우에는 잘 작동한다.
복사의 방법에 따라 원본 데이터에 미치는 영향이 달라진다. 얕은 복사를 사용하면 복사된 데이터에서의 변경이 원본 데이터에도 반영될 수 있다. 이는 의도하지 않은 사이드 이펙트(side effects)를 초래할 수 있다. 반면, 깊은 복사를 사용하면 원본 데이터와 복사된 데이터가 완전히 독립적이므로 이러한 문제를 피할 수 있다.
그러나 깊은 복사는 처리 시간이 길어질 수 있으며, 많은 메모리를 사용할 수 있다는 점도 고려해야 한다. 따라서 상황에 따라 적절한 복사 방법을 선택하는 것이 중요하다.
얕은 복사:
1. 스프레드 연산자 (Spread Operator): {...object}
또는 [...array]
2. Object.assign()
: Object.assign({}, object)
3. 배열의 slice()
메서드: array.slice()
깊은 복사:
1. JSON.parse()
와 JSON.stringify()
: JSON.parse(JSON.stringify(object))
2. 라이브러리를 사용하는 방법: 예를 들면 lodash의 _.cloneDeep()
함수
깊은 복사의 경우, 객체 내부에 함수나 특별한 데이터 타입이 포함된 경우에는 JSON.parse()
와 JSON.stringify()
방법이 제대로 작동하지 않을 수 있다. 이런 경우에는 lodash와 같은 라이브러리를 사용하는 것이 좋다.
결론적으로, JavaScript에서의 복사는 단순히 =
연산자를 사용하는 것만큼 간단하지 않다. 원하는 동작을 정확히 이해하고, 상황에 맞는 적절한 복사 방법을 선택해야 한다.
단순한 객체나 배열에는 JSON.parse()
와 JSON.stringify()
를 사용하는 방법이 편리하다. 그러나 복잡한 객체 구조나 특별한 데이터 타입, 함수 등을 포함한 객체를 복사하려면 직접 깊은 복사 함수를 구현하는 것이 좋다.
예시:
function deepCopy(obj, hash = new WeakMap()) {
if (Object(obj) !== obj) return obj; // 기본 타입은 그대로 반환
if (hash.has(obj)) return hash.get(obj); // 순환 참조 방지
const result = new obj.constructor();
hash.set(obj, result);
for (let key in obj) {
if (obj.hasOwnProperty(key))
result[key] = deepCopy(obj[key], hash);
}
return result;
}
let original = {
a: 1,
b: [2, 3],
c: { d: 4 },
e: function() { console.log('hello'); }
};
let copied = deepCopy(original);
copied.a = 10;
copied.b.push(4);
copied.c.d = 5;
console.log(original.a); // 1
console.log(original.b); // [2, 3]
console.log(original.c.d); // 4
copied.e(); // 'hello'
위의 deepCopy
함수는 재귀적으로 객체의 모든 프로퍼티를 복사한다. 또한, WeakMap
을 사용하여 객체의 순환 참조를 확인하고 방지한다. 이 방법은 JSON.parse()
와 JSON.stringify()
에 의한 깊은 복사의 제한을 해결한다.
복사 방법의 선택은 프로그램의 요구사항과 상황에 따라 다르다.
성능: 깊은 복사는 얕은 복사보다 처리 시간이 길다. 복잡한 객체 구조의 경우 깊은 복사가 상당한 시간을 소요할 수 있다. 반면, 얕은 복사는 빠르지만, 복사된 객체와 원본 객체 간의 연결성 때문에 의도하지 않은 사이드 이펙트를 초래할 위험이 있다.
메모리 사용량: 깊은 복사를 사용하면 원본 객체와 동일한 메모리를 또 다시 사용한다. 큰 객체의 경우 메모리 사용량이 두 배로 늘어날 수 있다. 얕은 복사는 추가적인 메모리 사용량이 거의 없다.
얕은 복사를 사용할 때: 객체나 배열의 구조가 간단하고, 복사 후에 원본과 복사본 사이의 연결이 문제가 되지 않을 때 얕은 복사를 사용한다. 또한, 성능이 중요한 경우에도 얕은 복사가 더 적합하다.
깊은 복사를 사용할 때: 객체나 배열의 구조가 복잡하거나, 원본과 복사본을 완전히 독립적으로 관리해야 할 때 깊은 복사를 사용한다.
복사를 수행할 때 주의해야 할 몇 가지 사항이 있다:
순환 참조: 객체가 자신을 참조하거나, 복잡한 참조 관계가 있는 경우에는 순환 참조가 발생할 수 있다. 이런 경우, 단순한 깊은 복사 방법은 무한 루프에 빠질 수 있다. 이를 방지하기 위해 위의 예시에서 사용한 WeakMap
과 같은 방법을 사용한다.
내장 객체와 함수: Date 객체나 RegExp 객체, 함수 등 특별한 객체 타입은 일반적인 복사 방법으로는 제대로 복사되지 않을 수 있다. 이 경우, 해당 타입에 맞는 복사 방법을 추가로 구현해야 한다.
JavaScript에서의 복사는 생각보다 복잡하다. 얕은 복사와 깊은 복사의 차이를 이해하고, 상황에 따라 적절한 방법을 선택하는 것이 중요하다. 복사의 목적과 요구사항을 분명히 정의하고, 이를 기반으로 얕은 복사와 깊은 복사 중 어느 것을 사용할지 결정해야 한다.