얕은 복사와 깊은 복사에 대해 본격적으로 알아보기 전에
1. 데이터 타입의 종류
2. 변수 선언과 데이터 할당(기본형, 참조형)
3. 기본형 vs 참조형 변수 복사 비교
4. 불변 객체에 대해 먼저 알아보려 한다.

기본형으로는 Number, String, Boolean, null, undefined, Symbol(ES6)이 있고,
참조형으로는 Object가 있다. 따라서 Object가 아닌 경우 기본형이라고 생각하면 될 것 같다.
기본형과 참조형의 구분 기준은 값의 저장 방식(복제)과 불변성 여부이다.
[기본형과 참조형의 구분 기준]
1. 복제의 방식
- 기본형: 값이 담긴 주소값을 바로 복제
- 참조형: 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제
2. 불변성의 여부
- 기본형: 불변성을 띔
- 참조형: 불변성을 띄지 않음
[할당 예시]
// 선언과 할당을 풀어 쓴 방식
var str;
str = 'test!';
// 선언과 할당을 붙여 쓴 방식
var str = 'test!';

위와 같은 경우 변수 영역의 주소 중 빈 공간(1002)에 변수의 이름이 선언되고, 데이터 값은 데이터 영역 중 빈 공간인 (5002)에 할당된다. 데이터 값이 변수 영역에 바로 대입되는 것이 아니라 주소를 통해 데이터 값을 가져오는 방식이다.
자유로운 데이터 변환, 메모리의 효율적 관리를 위해 위와 같이 동작한다.
[기본형 데이터의 할당(불변값과 불변성)]
// a라는 변수가 abc에서 abcdef가 되는 과정을 통해 불변성을 유추
// 'abc'라는 값이 데이터영역의 @5002라는 주소에 들어갔다고 가정
var a = 'abc';
// 'def'라는 값이 @5002라는 주소에 추가되는 것 x
// @5003에 별도로 'abcdef'라는 값이 생기고 a라는 변수는 @5002 -> @5003
// 즉, "변수 a는 불변하다."
// 이 때, @5002는 더 이상 사용되지 않기 때문에 가비지컬렉터의 수거 대상
a = a + 'def';

var a = 'abc';일 때는 위의 사진과 같으나, a = a + 'def'; 일 때는

이렇게 바뀌게 되는 것이다. 기존의 abc의 값에 def가 추가되는 것이 아니다. 기존의 abc는 변하지 않고, 새로운 주소에 abcdef라는 새로운 값이 생기는 것이다.
즉, 데이터 영역의 메모리는 변경될 수 없다. 이것이 불변성이다.
[참조형 데이터의 할당(가변값과 가변성)]
// 참조형 데이터는 별도 저장공간(obj을 위한 별도 공간)이 필요
var obj = {
a: 1,
b: 'bbb',
};

참조형 데이터의 경우, 위와 같이 객체를 위한 별도의 공간(7000~)이 생성되게 된다. 따라서 데이터를 변경할 때,
var obj1 = {
a: 1,
b: 'bbb',
};
// 데이터 변경
obj.a = 2;

위의 사진을 보면 알 수 있듯이, 데이터 영역에 저장된 값은 여전히 계속 불변값이지만, obj1을 위한 별도 영역은 얼마든지 변경이 가능하다. 이것 때문에 참조형 데이터를 흔히, ‘불변하지 않다(=가변하다)’라고 한다.
기본형과 참조형 변수를 각각 선언한 뒤 복사를 진행해보겠다.
// STEP01. 선언
var a = 10; //기본형
var obj1 = { c: 10, d: 'ddd' }; //참조형
// STEP02. 복사
var b = a; //기본형
var obj2 = obj1; //참조형
step01. 선언 후 상태

step02. 복사 후 상태

아직까지는 두드러지는 차이점이 없다는 것을 알 수 있다.
기본형과 참조형의 차이는 복사한 후의 값 변경에서 일어나는데, 값 변경을 진행해보겠다.
// STEP01. 선언
var a = 10; //기본형
var obj1 = { c: 10, d: 'ddd' }; //참조형
// STEP02. 복사
var b = a; //기본형
var obj2 = obj1; //참조형
b = 15;
obj2.c = 20;
코드 맨 아래에 값 변경 부분이 추가되었다. 변경 후의 상태를 살펴보면,

기본형과 참조형의 변수 복사 시 주요한 절차의 차이점은 다음과 같다.
영향 없음obj1까지 변경이 됨따라서 원치 않는 결과가 발생하는 것을 막기 위해
복사 이후 객체 자체 변경을 진행하려 한다.
객체의 프로퍼티(=속성)에 접근해서 값을 변경하는 것이 아니라 객체 자체를 변경하는 방식으로 값을 바꾸면 이를 해결할 수 있을 것이다.
//기본형 데이터
var a = 10;
var b = a;
//참조형 데이터
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
b = 15;
obj2 = { c: 20, d: 'ddd'};
이와 같이 객체 자체를 아예 갈아 끼울 경우 메모리에 객체를 위한 별도의 공간이 새로 생성된다.

따라서 의도치 않은 값 변경을 막을 수 있게 된다.
앞선 과정에서, 가변하다와 불변하다의 개념을 배울 수 있었다.
다시 정리해서 객체를 예로 들면, 객체의 속성에 접근해서 값을 변경하면 가변이 성립했다. 반면, 객체 데이터 자체를 변경(새로운 데이터를 할당)하고자 한다면 기존 데이터는 변경되지 않는다. 즉, 불변하다라고 볼 수 있다.
예시를 통해 불변 객체의 필요성에 대해 알아보도록 하겠다.
다음 예시는 객체의 가변성에 따른 문제점을 보여주고 있다.
// user 객체를 생성
var user = {
name: 'wonjang',
gender: 'male',
};
// 이름을 변경하는 함수, 'changeName'을 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티(속성)에 접근해서 이름을 변경 -> 가변
var changeName = function (user, newName) {
var newUser = user;
newUser.name = newName;
return newUser;
};
// 변경한 user정보를 user2 변수에 할당
// 가변이기 때문에 user1도 영향을 받음
var user2 = changeName(user, 'twojang');
// 결국 아래 로직은 skip하게 됨
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // twojang twojang
console.log(user === user2); // true
객체에 속성에 접근해서 값을 변경했기에 가변이 성립하여 원치 않았던 user1의 정보까지 변경된 것을 알 수 있다.
위의 예시는 다음과 같이 개선할 수 있다.
// user 객체를 생성
var user = {
name: 'wonjang',
gender: 'male',
};
// 이름을 변경하는 함수 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티에 접근하는 것이 아니라, 아예 새로운 객체를 반환 -> 불변
var changeName = function (user, newName) {
return {
name: newName,
gender: user.gender,
};
};
// 변경한 user정보를 user2 변수에 할당
// 불변이기 때문에 user1은 영향이 없음
var user2 = changeName(user, 'twojang');
// 결국 아래 로직이 수행 됨
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // wonjang twojang
console.log(user === user2); // false 👍
객체의 속성에 접근하는 것이 아닌, 아예 새로운 객체를 반환함으로써 user1의 정보에 영향을 주지 않고 원하는 user2의 정보만을 변경한 것을 알 수 있다.
그러나 위의 방법은 최선이 아니다. 다음과 같은 문제점이 있기 때문이다.
얕은 복사를 사용하게 되는 것이다!!!얕은 복사를 먼저 살펴보겠다.
// 복사 패턴
var copyObject = function (target) {
var result = {};
// for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있음
// 하드코딩 필요 x
// copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경
for (var prop in target) {
result[prop] = target[prop];
}
return result;
}
//패턴 적용
var user = {
name: 'wonjang',
gender: 'male',
};
var user2 = copyObject(user);
user2.name = 'twojang';
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name);
console.log(user === user2);
굳이 하드코딩을 하지 않아도 for ~ in 구문을 통해 내부의 객체들을 전부 각각 복사할 수 있게 된다.
그 과정을 통해 user2는 user1과 항상 별도의 객체인게 보장이 될 것이다.
그러나 이 패턴에도 문제점이 존재한다.
얕은 복사의 경우 바로 아래 단계의 값만 복사하게 된다.(위의 예제)
따라서 중첩된 객체의 경우 참조형 데이터가 저장된 프로퍼티를 복사할 때, 주소값만 복사하여 완벽한 복사를 할 수 없다는 것이다.
이를 해결하기 위해 내부의 모든 값을 하나하나 다 찾아서 복사하는 깊은 복사를 사용하는 것이다.
우선 중첩된 객체에 대한 얕은 복사를 먼저 살펴보겠다.
var user = {
name: 'wonjang',
urls: {
portfolio: 'http://github.com/abc',
blog: 'http://blog.com',
facebook: 'http://facebook.com/abc',
}
};
var user2 = copyObject(user);
user2.name = 'twojang';
// 바로 아래 단계에 대해서는 불변성을 유지하기 때문에 값이 달라짐
console.log(user.name === user2.name); // false
// 그러나!!! 더 깊은 단계에 대해서는 불변성을 유지하지 못하기 때문에 값이 같음
user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); // true
// 블로그 주소도 마찬가지
user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); // true
결국, 이를 해결하기 위해 user.urls 프로퍼티도 불변 객체로 만들어야 한다.
따라서 재귀적 수행을 사용하여 깊은 복사를 진행한다.
재귀적 수행은 함수나 알고리즘이 자기 자신을 호출하여 반복적으로 실행되는 것을 뜻하며, 이를 통해 중첩된 횟수와 상관 없이 객체 내부의 모든 값을 하나하나 다 찾아 복사할 수 있게 되는 것이다.
중첩된 객체에 대한 깊은 복사 예시를 살펴보겠다.
var copyObjectDeep = function(target) {
var result = {};
if (typeof target === 'object' && target !== null) {
for (var prop in target) {
result[prop] = copyObjectDeep(target[prop]); // 재귀
}
} else {
result = target;
}
return result;
}
이를 통해 객체의 모든 요소 하나하나가 불변성을 유지할 수 있게 된다!
마지막 방법으로 JSON(=JavaScript Object Notation)을 이용하는 방법도 존재한다. 하지만 완벽한 방법은 아니다. 간략히 장/단점을 정리하자면 다음과 같다.
장점:
단점:
JSON을 이용한 깊은 복사는 원본 객체가 가지고 있는 모든 정보를 복사하지 않는다. 예를 들어, 함수나 undefined와 같은 속성 값은 복사되지 않는다.
JSON.stringify() 함수는 순환 참조(Recursive Reference)를 지원하지 않는다. 따라서 객체 안에 객체가 중첩되어 있는 경우, 이 방법으로는 복사할 수 없다.
따라서 JSON을 이용한 깊은 복사는 객체의 구조가 간단하고, 함수나 undefined와 같은 속성 값이 없는 경우에 적합한 방법이다. 만약 객체의 구조가 복잡하거나 순환 참조가 있는 경우에는 다른 깊은 복사 방법을 고려해야 한다.