원시 자료형을 할당한 변수를 다른 변수에 할당하면 값 자체의 복사가 일어 납니다. 값 자체가 복사된다는 것은 둘 중 하나의 값을 변경해도 다른 하나에는 영향을 미치지 않는다는 것을 의미합니다.
반면, 참조 자료형은 임의의 저장공간에 값을 저장하고 그 저장공간을 참조하는 주소를 메모리에 저장하기 때문에 다른 변수에 할당할 경우 값 자체가 아닌 메모리에 저장되어 있는 주소가 복사됩니다.
copiedArr.push(4);
console.log(arr); // [0, 1, 2, 3, 4]
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr === copiedArr) // true
다시 말해, 참조 자료형이 저장된 변수를 다른 변수에 할당할 경우, 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사 되었다고 볼 수 없습니다.
그렇다면 배열과 객체 같은 참조 자료형을 복사하여,똑같은 요소와 프로퍼티를 가지지만 원본과 복사본이 서로 영향을 미치지 않도록 할 수는 없을까요?
summary
- 원시 자료형이 할당된 변수를 다른 변수에 할당하면 값 자체의 복사가 일어난다. 따라서 원본과 복사본 중 하나를 변경해도 다른 하나에 영향을 미치지 않는다.
- 참조 자료형이 할당된 변수를 다른 변수에 할당하면 주소가 복사되어 원본과 복사본이 같은 주소를 참조한다.
- 참조 자료형의 주소값을 복사한 변수에 요소를 추가하면 같은 주소를 참조하고 있는 원본에도 영향을 미친다.
- 참조 자료형이 저장된 변수를 다른 변수에 할당할 경우, 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사 되었다고 볼 수 없다.
배열 복사하기
배열을 복사하는 방법은 크게 두 가지 방법이 있습니다. 배열 내장 메서드인 slice()를 사용하는 방법과 ES6에서 도입된 spread문법을 사용하는 방법입니다.
slice()
배열 내장 메서드인 slice()를 사용하면 원본 배열을 복사할 수 있습니다.
let arr = [0, 1, 2, 3];
let copiedArr = arr.slice();
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]
spread syntax
spread syntax는 ES6에서 새롭게 추가된 문법으로, spread라는 단어의 뜻처럼 배열을 펼칠 수 있습니다. 펼치는 방법은 배열이 할당된 변수명 앞에 ... 을 붙여주면 됩니다. 배열을 펼치면 배열의 각 요소를 확인할 수 있습니다.
let arr = [0, 1, 2, 3];
console.log(...arr); // 0 1 2 3
let num = [1, 2, 3];
let int = [1, 2, 3];
console.log(num === int) // 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]
객체 복사하기
Object.assign()
객체를 복사하기 위해서는 Object.assign()을 사용합니다.
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = Object.assign({}, obj);
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
spread syntax
spread syntax는 배열뿐만 아니라 객체를 복사할 때도 사용할 수 있습니다.
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = {...obj};
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
얕은 복사
그러나 예외의 상황도 있습니다. 참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우, slice(), Object.assign(), spread syntax를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없습니다. 참조 자료형이 몇 단계로 중첩되어 있던지, 위에서 설명한 방법으로는 한 단계까지만 복사할 수 있습니다.
let users = [
{
name: "kimcoding",
age: 26,
job: "student"
},
{
name: "parkhacker",
age: 29,
job: "web designer"
},
];
let copiedUsers = users.slice();
console.log(users === copiedUsers); // false
그러나 users와 copiedUsers의 0번째 요소를 각각 비교하면 true가 반환됩니다. users[0]과 copiedUsers[0]는 여전히 같은 주소값을 참조하고 있기 때문입니다.
console.log(users[0] === copiedUsers[0]); // true
이처럼 slice(), Object.assign(), spread syntax 등의 방법으로 참조 자료형을 복사하면, 중첩된 구조 중 한 단계까지만 복사합니다. 이것을 얕은 복사(shallow copy)라고 합니다.
깊은 복사
반면, 참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것은 깊은 복사(deep copy)라고 합니다. 그러나 JavaScript 내부적으로는 깊은 복사를 수행할 수 있는 방법이 없습니다. 단, JavaScript의 다른 문법을 응용하면 깊은 복사와 같은 결과물을 만들어 낼 수 있습니다.
JSON.stringify()와 JSON.parse()
JSON.stringify()는 참조 자료형을 문자열 형태로 변환하여 반환하고, JSON.parse()는 문자열의 형태를 객체로 변환하여 반환합니다. 먼저 중첩된 참조 자료형을 JSON.stringify()를 사용하여 문자열의 형태로 변환하고, 반환된 값에 다시 JSON.parse()를 사용하면, 깊은 복사와 같은 결과물을 반환합니다.
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
깊은 복사가 되지 않는 예외가 존재합니다. 대표적인 예로 중첩된 참조 자료형 중에 함수가 포함되어 있을 경우 위 방법을 사용하면 함수가 null로 바뀌게 됩니다. 따라서 이 방법 또한 완전한 깊은 복사 방법이라고 보기 어렵습니다.
const arr = [1, 2, [3, function(){ console.log('hello world')}]];
const copiedArr = JSON.parse(JSON.stringify(arr));
console.log(arr); // [1, 2, [3, function(){ console.log('hello world')}]]
console.log(copiedArr); // [1, 2, [3, null]]
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
summary
- 배열의 경우 slice() 메서드 또는 spread syntax(...arr) 등의 방법으로 복사할 수 있다.
- 객체의 경우 Object.assign() 또는 spread syntax(...obj) 등의 방법으로 복사할 수 있다.
- 위 방법으로 참조 자료형을 복사할 경우, 중첩된 구조 중 한 단계까지만 복사된다. (얕은 복사)
- JavaScript 내부적으로는 중첩된 구조 전체를 복사하는 깊은 복사를 구현할 수 없다. 단, 다른 문법을 응용하여 같은 결과물을 만들 수 있다.
- 대표적인 JSON.stringify()와 JSON.parse()를 사용하는 방법이 있지만, 예외의 케이스가 존재한다. (참조 자료형 내부에 함수가 있는 경우, null)
- 완전한 깊은 복사를 반드시 해야 하는 경우, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 사용하면 된다.
'Javascript' 카테고리의 다른 글
DOM 다루기 (CRUD + append) (0) | 2023.03.07 |
---|---|
자바스크립트 입문편: 디지털 시계만들기 (2) | 2023.03.05 |
현재 시간 브라우저에 나타내기 (0) | 2023.03.04 |
생성자 함수로 여러가지 객체 만들기 (0) | 2023.03.04 |
원시자료형과 참조자료형 (0) | 2023.03.02 |