[JavaScript]얕은 복사, 깊은 복사 이해하기
서론
코딩을 함에 있어서 꼭 알아야 될 개념이 얕은 복사(shallow copy)와 깊은 복사(deep copy)입니다. 문자열(string), 숫자형(number) 등의 자료형과 array 또는 object 같은 객체형 자료가 복사될 때의 차이점을 아시나요?
이 개념을 모르고 코딩을 한다면, 의도치 않게 데이터값들을 무자비(?)하게 훼손하는 코딩을 하고 있을 확률이 높고 언젠가는 미궁에 빠질지도 모릅니다. 여태까지 무탈했다면 그냥 운이 좋았던 겁니다.
목차
- 원시타입 자료형 vs 참조타입 자료형
- 콜스택과 힙메모리
- 얕은 복사
- 깊은 복사
원시타입 자료형
서론에서 말씀드렸던 string, number, boolean 과 같은 자료형들은 크기가 유한합니다. 예를 들어, 이름, 이메일 같은 문자열이나, 숫자같은 자료를 생각하시면 이 값들이 고무줄처럼 늘어나는 값들은 아니죠?
이러한 자료들을 유한(infinite)하고 정적(static)하다고 하고, 원시타입 자료형(primitive type)이라고 합니다.
참조 자료형
반대로, array 배열객체를 생각해보면, 해당 배열에 push 또는 remove같은 메소드를 사용하여 얼마든지 해당자료형의 크기가 고무줄 처럼 변할 수가 있습니다. 말 그대로 동적인 성질의 자료형입니다. object 객체도 동일한 원리로 참조자료형으로 분류됩니다.
콜스택과 힙메모리
이렇게, 원시형과 참조형이 다른 성질을 나타내기 때문에, js에서는 원시형은 콜 스택(call stack)이라는 정적 메모리에 저장을 하고, 참조형은 힙 메모리(heap)에 저장을 합니다. 여기서 큰 차이는, 원시형은 call stack에 바로 저장이 되어있지만, 참조형은 효율성 차원에서 힙메모리에 바로 접근하지 않고, call stack에 그 참조형이 저장된 힙의 주소를 저장해놓습니다. 그리고 그 주소를 참조하여 heap 메모리를 찾아갑니다.
이러한 구조에서 어느정도 눈치를 챌 수 있듯이, 단순하게 변수에 할당연산자로 할당시켜서 복사를 하게 되면, 원시형 자료형은 값 자체가 새로운 주소와 함께 할당이 되기 때문에 문제가 안되지만, 참조형 자료형의 경우 그냥 js는 주소값만 할당을 해줍니다. 이 주소값만 전달해주는 행동으로 인해 얕은 복사와 깊은 복사라는 개념이 생기게 됩니다.
얉은 복사
그럼 다음 위에서 얘기한 얕은 복사가 무엇인지 보겠습니다.
1. 할당연산자로 할당한 경우
// 얉은 복사
// 1. 단순하게 할당연산자로 변수에 할당한 경우
let obj = { a: 1, b: { c: 2, d: 3 } };
let copiedObj = obj; // 참조 주소값만 넘겨받음
copiedObj.a = 5;
console.log(obj.a) // 5
console.log(copiedObj.a) //5
위에서 보면 copiedObj의 a키값만 변경했으나, 원본 객체인 obj.a값도 같이 5로 변경된 걸 볼 수 있습니다. copiedObj에 할당된 값은 원본 obj와 같은 참조 주소값입니다. 그렇기 때문에 copiedObj에서 변경한 값은 원본 obj도 변경시킵니다.
2. spread를 사용한 경우 (1depth 까지는 깊은 복사)
여기서 depth는 객체안에 객체가 있는 경우를 명시하기 위해 사용합니다. 객체 안에있는 값들이 원시형인 경우 1depth, 객체안에 객체가 또 있는 경우 2depth입니다.
spread 연산자를 사용한 경우 1depth까지는 깊은 복사가 됩니다. 하지만 2depth 이후부턴 다시 얕은 복사로 바뀌죠. 아래 예시를 보겠습니다.
// 2. spread 구문 이용하기
let obj = { a: 1, b: { c: 2, d: 3 } };
let copiedObj = { ...obj }; // Spread로 사용
// 1depth까지는 깊은 복사
copiedObj.a = 5;
console.log(obj.a); // 1
console.log(copiedObj.a); // 5
// 2depth부터는 얕은 복사
copiedObj.b.c = 5;
console.log(obj.b.c); //5
console.log(copiedObj.b.c); //5
위에서 보면 spread로 할당한 경우, 1depth까지는 주소가 따로 구분되어 관리되지만, 2depth로 넘어가자 다시 원본 데이터를 변경시킵니다. 이를 통해 spread를 활용한 방식은, 객체안의 객체값인 2depth 부터는 다시 같은 주소를 참조한다는 것을 알 수 있습니다.
3. object.assign() 을 이용한 경우(1depth까지는 깊은 복사)
object.assign()메소드 또한 spread와 같이 1depth까지만 깊은 복사가 됩니다.
// 3. object.assign() 이용하기
let obj = { a: 1, b: { c: 2, d: 3 } };
let copiedObj = Object.assign({}, obj);
// 1depth까지는 깊은 복사
copiedObj.a = 5;
console.log(obj.a); // 1
console.log(copiedObj.a); // 5
// 2depth부터는 얕은 복사
copiedObj.b.c = 5;
console.log(obj.b.c); //5
console.log(copiedObj.b.c); //5
깊은 복사
그러면 객체안의 객체값도 다른 주소를 참조하는 깊은 복사를 해보겠습니다. 방법으로는 (1)재귀함수를 이용한 방법, (2)Json메소드를 이용한 방법이 있습니다.
1. 재귀함수 방법
말그대로 재귀함수를 만드는 것입니다. 객체안의 요소값이 객체인가를 묻게하는 조건문 if문을 만들고 그게 참이라면 함수를 한번 더 실행시키는 로직으로 요소값이 원시값이 나올때까지 반복하는 로직을 갖고 있습니다.
// 깊은 복사
// 1. 재귀함수를 이용
let obj = { a: 1, b: { c: 2, d: 3 } };
function copyObj(obj) {
const result = {};
// 해당 로직은 키값이 원시값이 나올 때 까지 계속 반복되는 재귀함수를 이용
for (let key in obj) {
if (typeof obj[key] === 'object') {
result[key] = copyObj(obj[key]);
} else {
result[key] = obj[key];
}
}
return result;
}
let copiedObj = copyObj(obj);
// 1 depth 복사
copiedObj.a = 3;
console.log(obj.a); //1
console.log(copiedObj.a); //3
// 2 depth 복사
copiedObj.b.c = 5;
console.log(obj.b.c); //2
console.log(copiedObj.b.c); //5
재귀함수를 씀으로써 원시값이 나올때 까지 끝까지 반복되며, 이로인해 깊은 복사를 할 수 있습니다. 위 코드를 보면 2depth도 문제가 없는 걸 볼 수 있습니다.
2. JSON.stringify, JSON.parse 메소드 이용하기
json 메소드인 stringify는 객체를 문자열로 바꿉니다. 문자열로 바꿀 때 기존에 있던 참조 주소가 사라지고 parse메소드로 문자열을 객체로 재변환할 때 새로운 주소를 받음으로서 깊은 복사를 할 수 있습니다.
// 2. JSON.stringify, JSON.parse 이용하기
let obj = { a: 1, b: { c: 2, d: 3 } };
// 객체 -> 문자열 -> 객체 변환과정에서 기존 참조주소가 사라짐
let copiedObj = JSON.parse(JSON.stringify(obj));
// 1 depth 복사
copiedObj.a = 3;
console.log(obj.a); //1
console.log(copiedObj.a); //3
// 2 depth 복사
copiedObj.b.c = 5;
console.log(obj.b.c); //2
console.log(copiedObj.b.c); //5
하지만, 위 방법은 처리 과정이 다소 무거운 편이기 때문에 속도저하를 가져올 수가 있습니다.
결론
원시 자료형과 참조 자료형에 대해 알아보았고, 이로 인해 파생되는 개념인 얕은 복사와 깊은 복사에 대해 알아보았습니다. 프로그래밍 개발업무를 위해 필수로 알아되는 개념이라고 생각합니다. 혹여 의도치 않게 기존 데이터가 변경되고 있고 그 이유를 모르겠다면, 오늘 공부한 얕은 복사 문제가 아닌 지 확인해 볼 필요가 있겠습니다.