JavaScript에서 자료형(type)이란 값(value)의 종류입니다.
자료형은 크게 두 가지로 구분할 수 있는데, 바로 원시 자료형(primitive type)과 참조 자료형(reference type)입니다. JavaScript에서는 6개의 자료형(number, string, boolean, undefined, null, symbol)을 원시 자료형으로 구분합니다. 여러 데이터를 한 번에 다룰 수 있는 배열, 객체가 대표적인 참조 자료형입니다. 함수도 참조 자료형으로 분류합니다.
// 원시 자료형(primitive type): number, string, boolean, undefined, null
42, 'string', true, undefined, null
// 참조 자료형(reference type)
[0, 1, 2] // 배열
{name: 'kimcoding', age: 45} // 객체
function sum (x, y) { return x + y } // 함수
원시 자료형의 특징
원시 자료형을 변수에 할당하면 메모리 공간에 값 자체가 저장된다.
원시 값을 갖는 변수를 다른 변수에 할당하면 원시 값 자체가 복사되어 전달된다.
원시 자료형은 변경 불가능한 값(immutable value)이다. 즉, 한 번 생성된 원시 자료형은 읽기 전용(read only) 값이다.
참조 자료형의 특징
참조 자료형을 변수에 할당하면 메모리 공간에 주솟값이 저장된다.
참조 값을 갖는 변수를 다른 변수에 할당하면 주솟값이 복사되어 전달된다.
참조 자료형은 변경이 가능한 값(mutable value)이다.
원시 자료형을 할당한 변수를 다른 변수에 할당하면 값 자체의 복사가 일어 납니다. 값 자체가 복사된다는 것은 둘 중 하나의 값을 변경해도 다른 하나에는 영향을 미치지 않는다는 것을 의미합니다.
let num = 5;
let copiedNum = num;
console.log(num); // 5
console.log(copiedNum); // 5
console.log(num === copiedNum); // true
copiedNum = 6;
console.log(num); // 5
console.log(copiedNum); // 6
console.log(num === copiedNum); // false
반면, 참조 자료형은 임의의 저장공간에 값을 저장하고 그 저장공간을 참조하는 주소를 메모리에 저장하기 때문에 다른 변수에 할당할 경우 값 자체가 아닌 메모리에 저장되어 있는 주소가 복사됩니다.
let arr = [0, 1, 2, 3];
let copiedArr = arr;
console.log(arr); // [0, 1, 2, 3]
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr) // true
원시 자료형은 값을 저장하지만 참조 자료형은 주소를 저장합니다.
따라서 참조 자료형을 복사하면 주소값을 복사하기 때문에 아래의 코드 처럼 작성하면 값이 전부 변경됩니다.
let arr = [0, 1, 2, 3];
let copiedArr = arr;
console.log(arr); // [0, 1, 2, 3]
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr) // true
copiedArr.push(4);
console.log(arr); // [0, 1, 2, 3, 4]
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr === copiedArr) // true
이를 방지하기 위해서 참조 자료형을 복사하기 위해서는
let arr = [0, 1, 2, 3];
let copiedArr = arr.slice();
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false
//배열
let arr = [0, 1, 2, 3];
let copiedArr = [...arr];
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false
copiedArr.push(4);
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr); // [0, 1, 2, 3]
//객체
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = {...obj};
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = Object.assign({}, obj);
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
이러한 방식을 사용할 수 있습니다.
그러나 예외의 상황도 있습니다. 참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우, slice(), Object.assign(), spread syntax를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없습니다. 참조 자료형이 몇 단계로 중첩되어 있던지, 위에서 설명한 방법으로는 한 단계까지만 복사할 수 있습니다. 이것을 얕은 복사(shallow copy)라고 합니다.
반면, 참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것은 깊은 복사(deep copy)라고 합니다. 그러나 JavaScript 내부적으로는 깊은 복사를 수행할 수 있는 방법이 없습니다. 단, JavaScript의 다른 문법을 응용하면 깊은 복사와 같은 결과물을 만들어 낼 수 있습니다.
const arr = [1, 2, [3, 4]];
const copiedArr = JSON.parse(JSON.stringify(arr));
console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
완전한 깊은 복사를 반드시 해야 하는 경우라면, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 설치하면 됩니다. lodash와 ramda는 각각 방법으로 깊은 복사를 구현해 두었습니다. 다음은 lodash의 cloneDeep을 사용한 깊은 복사의 예시입니다.
const lodash = require('lodash');
const arr = [1, 2, [3, 4]];
const copiedArr = lodash.cloneDeep(arr);
console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false