오늘은 지난번에 언급되던 얕은 복사(shallow copy)와 깊은 복사(deep copy)에 대해 물어보았다.
// 원시 타입(primitive type) 과 참조 타입(reference type)에 대해 알고 있으면 편하다..
// 이건 다음시간에 더 살펴보기로 하고..
"얕은 복사"와 "깊은 복사"는 객체를 복사할 때 사용되는 용어입니다.
이들은 복사된 객체와 원본 객체가 서로 어떻게 연관되어 있는지를 설명합니다.
// 어떤 객체(배열도 포함)를 복사하고 싶은 경우에..
let origin = { name: 'kim', age : 20 };
let copy = origin;
// 이렇게 하면 망한다
// 이렇게 하면 망한다. 왜냐하면 copy 는 그냥 origin을 '가리키고' (참조하고) 있을 뿐이라서 origin 바뀌면 copy도 바뀜 ㅅㄱ
// 그래서 아래와 같은 방법들을 알아보자..
- 얕은 복사(Shallow Copy) : 얕은 복사는 객체의 가장 상위 수준만 복사합니다. 만약 복사된 객체 내부에 또 다른 객체가 있을 경우, 그 내부 객체는 참조로 연결됩니다. 즉, 복사된 객체의 내부 객체를 변경하면 원본 객체의 내부 객체도 변경됩니다.
예를 들어:
let original = {
name: 'John',
details: {
age: 25,
address: '123 Street'
}
};
let shallowCopy = Object.assign({}, original);
shallowCopy.details.age = 30;
console.log(original.details.age); // 출력값: 30
console.log(shallowCopy.details.age); // 출력값: 30
여기서 복사본의 details 객체를 변경했음에도 불구하고, 원본 객체의 details 객체도 변경되었습니다. 이것이 얕은 복사의 한계입니다.
- 깊은 복사(Deep Copy) : 깊은 복사는 객체의 모든 수준을 복사합니다. 이는 복사된 객체와 원본 객체가 완전히 독립적임을 의미합니다. 복사된 객체를 변경하더라도 원본 객체에는 영향을 주지 않습니다.
예를 들어:
let original = {
name: 'John',
details: {
age: 25,
address: '123 Street'
}
};
let deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.details.age = 30;
console.log(original.details.age); // 출력값: 25
console.log(deepCopy.details.age); // 출력값: 30
이 경우에는 복사본의 details 객체를 변경해도 원본 객체의 details 객체는 변경되지 않았습니다.
이렇게 깊은 복사를 통해 원본 객체를 안전하게 보호할 수 있습니다.
하지만 이 방법도 주의해야 할 점이 있는데, JSON.stringify와 JSON.parse 메소드를 사용하는 것은 함수나 Symbol, undefined, BigInt와 같은 JSON에서 지원하지 않는 데이터 타입이 있는 경우에는 사용할 수 없습니다.
이런 경우에는 lodash 라이브러리의 _.cloneDeep() 함수 등을 사용하면 깊은 복사를 할 수 있습니다.
// lodash 는 잠시 넘어가고,
// 그러면, 라이브러리를 사용하지 않고 '재귀함수'를 만들어 쓰면 어떨까?
let origin = {
name: 'kim',
appearance: {
height : 170
}
};
function deepCopy(origin) {
let result = {};
for (let key in origin) {
if (typeof origin[key] === 'object') {
result[key] = deepCopy(origin[key]);
} else {
result[key] = origin[key];
}
}
return result;
}
let copy = deepCopy(origin);
copy.appearance.height = 180;
console.log(origin.appearance.height === copy.appearance.height); // false
먼저 deepCopy 함수는 객체의 깊은 복사를 수행하는 재귀 함수입니다.
재귀 함수란 함수가 자신을 다시 호출하는 함수를 말합니다.
이 함수는 origin 객체의 각 속성을 순회하면서 다음 두 가지 조건 중 하나에 따라 처리를 합니다:
- 속성의 값이 객체인 경우 (즉, typeof origin[key] === 'object'이 true인 경우): 이 경우 deepCopy 함수를 다시 호출하여 해당 객체를 깊은 복사하고, 결과를 새 객체의 해당 키에 할당합니다. 이것이 재귀의 부분입니다. 이 과정은 중첩된 객체 구조를 모두 순회할 때까지 계속되며, 각 단계에서 새로운 복사본 객체가 생성됩니다.
- 속성의 값이 객체가 아닌 경우 (즉, typeof origin[key] === 'object'이 false인 경우): 이 경우 원래 값이 원시 값(문자열, 숫자, 불리언 등)이므로 그대로 복사하고 새 객체의 해당 키에 할당합니다.
함수가 모든 속성을 처리한 후에는 새로 만든 객체를 반환합니다.
이렇게 하면, 각 중첩된 객체에 대해 새로운 복사본 객체가 생성되므로, 원본 객체를 수정하더라도 복사본 객체에는 영향을 미치지 않습니다. 이것이 깊은 복사를 수행하는 이유입니다.
마지막에 console.log(origin.appearance.height === copy.appearance.height)를 통해 확인하면, copy.appearance.height 값을 변경했지만 origin.appearance.height는 변경되지 않았으므로 false가 출력됩니다. 이것은 복사본과 원본이 서로 독립적임을 보여줍니다.
// 재귀함수 사용시 주의점이 있다면?!
객체의 모든 속성들을 순회하면서 해당 속성의 값이 또 다른 객체인 경우 재귀적으로 깊은 복사를 수행하는 방식의 재귀 함수에는 그러나 몇 가지 주의할 점이 있습니다:
- 원본 객체가 순환 참조를 가지고 있는 경우, 즉 객체 내부의 어떤 속성이 자기 자신을 참조하는 경우에는 무한 재귀에 빠질 수 있습니다. 이를 해결하기 위해서는 이미 복사된 객체를 추적해야 합니다.
- 특별한 처리 없이는 함수도 단순히 복사되므로, 원본 객체에서 함수가 가지고 있던 클로저 등의 컨텍스트를 유지하지 못합니다.
따라서, 이러한 이유로 많은 개발자들은 깊은 복사를 위해 lodash와 같은 라이브러리를 사용하는 것을 선호합니다. 이러한 라이브러리들은 이런 상황들을 처리하기 위한 로직이 이미 구현되어 있습니다.
// 1번 순환 참조의 예:
let obj = {};
obj.self = obj;
여기서, obj.self는 obj 자신을 참조하고 있습니다. 이것이 바로 순환 참조입니다.
이런 객체를 재귀적으로 복사하려고 하면, 복사 함수는 obj.self, obj.self.self, obj.self.self.self...와 같이 무한히 깊게 복사하려고 시도하게 됩니다. 이는 무한 재귀를 일으키고, 결국에는 스택 오버플로우 에러를 발생시킵니다.
따라서, 깊은 복사를 수행할 때는 순환 참조를 주의해야 합니다. 이미 복사된 객체를 추적하여 순환 참조를 탐지하고, 적절하게 처리할 수 있는 로직이 필요합니다.
다만, 이런 상황은 일반적인 경우에는 잘 발생하지 않으며, 순환 참조 자체가 코드의 가독성과 유지보수성을 떨어뜨리는 요인이므로, 가능하면 피하는 것이 좋습니다.
// 2번 '클로저 등의 컨텍스트를 유지하지 못합니다' 에 대해
클로저란, 함수와 함수가 선언된 어휘적 환경의 조합입니다.
이는 함수가 생성된 곳의 범위(scope)에서 변수를 기억하고 접근할 수 있게 해줍니다.
클로저를 이해하기 위해서는 먼저 JavaScript에서의 스코프(scope)와 자유 변수(free variable)에 대해 이해해야 합니다.
스코프는 변수의 생존 범위를 나타내며, 자유 변수는 함수 바디 안에서 사용되지만 그 함수의 파라미터나 로컬 변수로 정의되지 않은 변수를 말합니다.
클로저는 이런 자유 변수에 대해 어휘적 환경을 '기억'합니다. 즉, 함수가 선언된 위치에 따라 자신이 접근할 수 있는 변수가 결정됩니다.
그런데, 단순히 함수를 복사하게 되면, 이렇게 클로저에 의해 '기억'되었던 어휘적 환경이 복사되지 않습니다. 즉, 원본 함수가 가지고 있던 클로저의 컨텍스트를 복사본 함수는 유지하지 못하는 것입니다.
예를 들어, 아래와 같은 코드를 생각해봅시다:
function outer() {
let outerVar = 'I am from outer function';
function inner() {
console.log(outerVar);
}
return inner;
}
let myInner = outer();
myInner(); // "I am from outer function"
여기서 inner 함수는 outerVar라는 자유 변수를 가지고 있습니다.
outer 함수를 실행하여 반환된 myInner 함수를 실행하면, 여전히 outerVar에 접근할 수 있습니다.
이는 inner 함수가 클로저이기 때문입니다. 이런 클로저의 특성을 단순한 복사로는 복제할 수 없습니다.
// 이건 어려우니까 다~~음에..
// 정리하자면 아래 방법들이 있다..!
// 얕은 복사 1 : Object.assign()
let copy = Object.assign({}, 복사대상객체);
// 얕은 복사 2 : 전개 연사자 활용
let copyArr = [...복사대상객체]
let copyObj = {...복사대상객체}
// 깊은 복사 1 : JSON 메소드 활용, 단 함수 복사 안됨
let copy = JSON.parse(JSON.stringify(복사대상객체));
// 깊은 복사 2 : 재귀 함수 만들어서 쓰기, 클로저등 컨텍스트 유지 안됨? 그렇다고 한다
function deepCopy(origin) {
let result = {};
for (let key in origin) {
if (typeof origin[key] === 'object') {
result[key] = deepCopy(origin[key]);
} else {
result[key] = origin[key];
}
}
return result;
}
let copy = deepCopy(복사할객체);
// 대충 이런식으로....
// 깊은 복사 3 : lodash 라이브러리 활용
import _ from 'lodash';
let copy = _.cloneDeep(복사할객체);
// 이건 라이브러리 사용이니 넘어가자..
'javascript' 카테고리의 다른 글
[자바스크립트 23] DOM 조작 2(추가, 수정, 삭제) (0) | 2023.05.13 |
---|---|
[자바스크립트 22] DOM 조작 1(선택) (0) | 2023.05.13 |
[자바스크립트 20] 배열 메소드 (0) | 2023.05.12 |
[자바스크립트 19] 메소드와 함수의 차이 (0) | 2023.05.12 |
[자바스크립트 18] 객체 메소드 (0) | 2023.05.12 |