오늘은 원시 자료형과 참조자료형에 대해 공부하고 얕은 복사와 깊은 복사에 대해 공부했다.
예전 전공 수업 중 프로그래밍언어론 이라는 과목에서 언어들의 원리와 동작에 대해 배웠었다. 그 과목을 수강할 시절에는 정말 어려워서 공부하기 힘들었는데 그 덕분에 오늘 학습한 부분은 수월하게 이해하고 넘어갈 수 있었던 거 같다.
그럼 오늘 공부한 원시 자료형과 참조 자료형, 얕은 복사 깊은 복사에 대해 정리해보러 가보자 !
원시 자료형과 참조 자료형
원시 자료형은 대표적으로 ( number , string , boolean , null , undefined , symbol ) 총 6가지가 있으며 고정된 저장 공간을 차지하는 자료형 입니다.
참조 자료형은 이를 제외한 ( object , function ) 등 원시 자료형이 아닌 모든 자료형을 칭합니다.
특징
원시 자료형 | 참조 자료형 | |
변수 할당 | 변수에 할당 시 메모리 공간에 값 자체가 저장됨 | 변수에 할당시 메모리 공간에 주소값이 저장됨 |
다른 변수에 할당 | 원시값 자체가 복사되어 전달됨 | 주소값이 복사되어 전달됨 |
변경 | 변경 불가능한 값, 읽기 전용(read only)값 | 변경 가능한 값 |
1. 값 자체를 저장 vs 주소값을 저장
원시 자료형은 변수에 할당할 때 값 자체가 할당됩니다. 예를 들어 변수 num을 선언하고 숫자 10을 할당했을 때 컴퓨터는 num이라는 이름의 공간을 확보하고 10이라는 원시 값을 그 공간에 저장합니다.
반면 참조 자료형은 변수에 할당할때 주소값을 할당합니다. 예를 들어 변수 arr 에 배열을 할당했을 때 (arr = [ 1, 2, 3 ]) 배열의 요소 각각이 하나의 값이므로 하나의 공간에 배열 자체를 저장하는 것은 불가능합니다. 따라서 특별한 저장공간 heap 에 참조자료형을 저장한 후 그 저장공간을 참조할 수 있는 주소값을 변수에 저장합니다. 즉 기존에 변수가 저장되는 공간에는 heap영역에 저장된 배열의 주소값을 arr에 저장한다고 생각하면됩니다.
2.원시 값 자체를 복사 vs 주소값을 복사
원시 자료형은 다른 변수에 할당 할 시 값 자체가 복사됩니다.
let num = 10 ;
let copiedNum = num ;
// copiedNum 에는 num의 값인 10이 할당 됨
참조 자료형은 다른 변수에 할당 시 두 변수가 같은 주소를 가리킵니다.
let arr = [ 1, 2, 3 ] ;
let copiedArr = arr ;
// arr의 주소값이 15일 때 copiedArr도 같은 주소인 15를 가리키게 됩니다.
만약 여기서 원본을 수정하게 된다면 ?
원시 자료형의 원본에 다른 값을 재할당해도 복사본에는 영향을 미치지 않습니다. 하지만 참조 자료형은 원본을 변경하게 되면 복사본도 영향을 미칩니다. 왜냐하면 같은 주소를 참조하고 있기 때문입니다 . 따라서 원본을 바꾸던 복사본을 바꾸던 두개가 같이 변경됩니다.
3. 변경 불가능한 값 vs 변경이 가능한 값
한번 생성된 원시값은 변경할 수 없습니다.
let num = 10 ;
num = 20;
// num 값이 변경 ?
위의 코드를 살펴보면 num을 10으로 할당하고 다시 num을 20으로 재할당했습니다. 눈으로 보기엔 원시자료형인 숫자 타입의 값이 변경된 것처럼 보이지만 메모리 내부에서는 20이라는 원시값을 저장하기 위한 새로운 공간을 확보한 후 그 뒤 num이라는 이름을 붙이고 20을 저장합니다.
이처럼 변수에 다른 값을 재할당해도 원시값 자체가 변경된 것이 아니라 새로운 원시값을 생성하고 변수가 다른 메모리 공간을 참조합니다.
따라서 원시자료형은 읽기 전용 데이터라고 할 수 있고 신뢰성이 높다고 말할 수 있습니다. 그럼 남아있는 값은 어떻게 처리할지 궁금증이 생기실겁니다. 이는 자바스크립트 엔진 가비지콜렉터에서 자동으로 메모리에서 삭제합니다.
let str = 'code';
str = 'states';
str[5] = 'z';
// str은 원시자료형이기 때문에 변경되지 않습니다.
크기가 일정하지 않은 참조 자료형의 경우 매번 값을 복사하게 된다면 효율성이 떨어질 것입니다. 따라서 참조자료형은 변경 가능하도록 설계되었습니다.
얕은 복사와 깊은 복사
원시 자료형은 할당한 변수를 다른 변수에 할당하면 값 자체의 복사가 일어납니다. 즉 둘 중의 하나의 값을 변경해도 다른 하나에는 영향을 미치지 않습니다.
let num = 5;
let copiedNum = num;
console.log(num); // 5
console.log(copiedNum); // 5
console.log(num === copiedNum); // true
copiedNum = 6; // num 값에는 영향을 미치지 않는다.
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
copiedArr.push(4); // 원본 배열에도 4가 추가됩니다.
console.log(arr); // [0, 1, 2, 3, 4]
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr === copiedArr) // true
참조 자료형이 저장된 변수를 다른 변수에 할당할 경우 , 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사되었다고 볼 순 없습니다.
그럼 이처럼 참조자료형은 복사본을 변경하면 원본도 변경되는데 어떻게 원본은 유지하면서 변경할 수 있을까요 ?
참조자료형인 배열과 객체를 통해 알아보겠습니다.
배열의 복사
slice()
let arr = [0, 1, 2, 3];
let copiedArr = arr.slice();
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false
// slice()를 사용하면 새롭게 생성된 배열은 원본 배열과 같은 요소를 갖지만 참조하는 주소는 다릅니다.
새로운 주소에 원본 배열 값을 저장합니다. 따라서 복사한 배열을 수정해도 원본 배열은 수정되지 않습니다.
spread문법
ES6에서 새롭게 추가된 문법으로 배열을 펼칠 수 있습니다. 펼치는 방법은 ... 을 붙여주면 됩니다.
let arr = [0, 1, 2, 3];
console.log(...arr); // 0 1 2 3
spread syntax로 배열을 복사하기 위해서 배열을 생성하는 방법을 이해해야 합니다. 만약 같은 요소를 가진 배열을 두 개 만든 후 변수에 각각 할당한다면, 두 변수는 같은 주소를 참조할까요? 참조 자료형이기 때문에 각각 다른 주소를 참조합니다.
let num = [1, 2, 3];
let int = [1, 2, 3];
console.log(num === int) // false
//다른 변수에 각각 할당했기 때문에 값은 같아도 주소값이 다르므로 false입니다.
그렇다면 새로운 배열 안에 원본 배열을 펼쳐서 전달하면 어떻게 될까요? 원본 배열과 같은 요소를 가지고 있지만 각각 다른 주소를 참조하게 됩니다. 결과적으로 slice() 메서드를 사용한 것과 동일하게 동작합니다.
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()
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = Object.assign({}, obj);
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = {...obj};
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
spread syntax는 배열 뿐만 아니라 객체를 복사할 때도 사용 가능합니다.
그러나 예외의 상황도 있습니다. 참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우, 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
console.log(users[0] === copiedUsers[0]); // true
만약 객체를 요소로 가지고 있는 배열 user를 slice() 메서드를 사용하여 복사했으므로 동치연산자로 확인해보면 false 가 반환됩니다. 그 이유는 각각 다른 주소 참조하고 있기 때문입니다. 하지만 0번째 요소를 비교하면 true가 반환됩니다.
이처럼 slice() , Object.assign() , spread syntax 등의 방법으로 참조 자료형을 복사하면 중첩된 구조 중 한단계까지만 복사하는데 이를 얕은 복사 라고 합니다.
반면 참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것을 깊은 복사 라고 합니다.
javascipt 내부적으로는 깊은 복사를 수행할 순 없지만 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
오늘은 이렇게 참조자료형과 원시자료형의 특징과 얕은복사와 깊은복사에 대해 공부해보았습니다. 내용이 쉽지가 않으니 한번 더 복습하고 공부해야 할 거같습니다 !!
오늘도 열공!
'HTML-CSS-JavaScript > JavaScript' 카테고리의 다른 글
[javascript] 문자열 - 숫자는 ? "10" - 1 = ? (0) | 2023.03.04 |
---|---|
[javascript] Scope에 대해 (2) | 2023.03.02 |
[javascript] 배열 join() 사용하기 (0) | 2023.02.28 |
[javascript] 배열 slice() 사용하기 (0) | 2023.02.27 |
[javascript] for of 문이란 ? (0) | 2023.02.27 |