코로 넘어져도 헤딩만 하면 그만
원시, 참조 자료형 및 얕은 복사와 깊은 복사(JS) 본문
JS 핵심 개념과 주요 문법, 그 중에서도
늘 헷갈리던 원시 자료 / 참조 자료 및 얕은 복사와 깊은 복사에 대해 면밀하게 알아보았다.
원시 자료형과 참조 자료형
원시 자료형(primitive data type)
- 6가지 타입(string, number, undefined, boolean, null, symbol)이 여기에 속한다.
- 저장 공간이 고정되어 있다. 또한 불변하는 값이다.
- 원시값을 갖는 변수가 다른 변수에 할당되면, 값 자체가 복사되어 전달된다.
참조 자료형(reference data type)
- 객체object, 배열array, 함수function 이 여기에 속한다. 원시 자료형이 아니면 전부 참조 자료형.
- 원시 자료형과 달리 저장 공간이 늘어날 수 있고, 값이 변할 수 있다.
- 바로 이 특성 때문에 '주솟값'을 부여하고 주소를 '참조'하는 식으로 불러온다.
- 만약 참조값을 갖는 변수가 다른 변수에 할당되면, 값이 아니라 주솟값이 복사되어 전달된다.
그렇다면, 왜 이 차이가 중요할까?
원시 자료형은 다른 변수로 할당해도 값 자체가 할당되기 때문에, 원본이 변하는 것은 아니다. 가령
let num = 10;
let copy = num;
let pay = 20;
pay = 100;
- 위와 같은 경우, 변수 copy로 변수 num에 담긴 값이 그대로 복사되었다. 즉 copy의 값도 10이 된다.
- 아래와 같은 경우, 변수 pay의 값이 20이었으나 이후 값을 100으로 변경해주었다. 단! 여기서 흔히 생각하듯 원본의 값이 100으로 변경된 것이 아니다. 메모리 내부에서 100을 저장하기 위한 새로운 공간을 확보한 뒤에, pay이라는 이름을 떼어서 거기에 붙여주는 것뿐이다. 이제 pay이 떨어지고 20의 값만 가진 원본은 JS 내부의 garbage collector 가비지 컬렉터(GC)가 자동으로 삭제해준다. 최종적으로는 pay이라는 이름을 가지고 100의 값을 가진 변수만 남게 되었다.
즉 원시 자료형의 원본은 절대로 변하지 않는다.
하지만 참조 자료형의 경우는 다르다. 참조 자료형의 가장 큰 특징으로는 '변경 가능'이 있다. 배열, 객체 같은 것들은 크기도 일정하지 않고 데이터가 추가되고 삭제되며 자주 변경되기 때문에 원시 자료형처럼 저장하는 것은 몹시 비효율적이다. 따라서 독자적인 저장공간(heap)에 데이터를 저장한 뒤 그 데이터가 든 주솟값을 변수가 참조하여 불러오는 방식으로 이루어진다.
let array = [0, 1, 2, 3, 4, 5];
let copiedArray = array;
console.log(array); // [0, 1, 2, 3, 4, 5]
console.log(copiedArray); // [0, 1, 2, 3, 4, 5]
console.log(array === copiedArray) // true
위와 같이 array를 copiedArray로 복사한다고 가정한다. 겉보기에는 값인 [0, 1, 2, 3, 4, 5]가 복사된 것처럼 보이지만, 사실은 array가 가진 '주솟값'을 그대로 복사해가서 copiedArray도 동일한 주솟값에 담긴 동일한 값들을 가리키고 있다.
아래 예시를 보면 더 자세히 이해가 될 것이다.
let array = [0, 1, 2, 3, 4, 5];
let copiedArray = array;
copiedArray.push(7);
console.log(array); // [0, 1, 2, 3, 4, 5, 7]
console.log(copiedArray); // [0, 1, 2, 3, 4, 5, 7]
console.log(array === copiedArray) // true
분명히 copiedArray에만 7을 밀어넣었는데, 어쩐지 array에도 7이 나오고 있다. 두 변수가 동일한 주소의 값을 참조하기 때문에 해당 주소에 저장된 값에 7을 넣는다면 둘 모두 [0, 1, 2, 3, 4, 5, 7]을 보여준다.
즉, 참조 자료형에 push나 pop, shift같은 방식으로 데이터를 추가, 삭제할 경우, 원시 자료형처럼 값을 싹 복사해서 바꿔주는 것이 아니라 처음 지정된 주솟값에 저장되어 있던 '원본 데이터'를 건드려 변형시키는 문제가 발생한다.
만약 원본은 건드리지 않아야 하는 상황이라면? 상당히 치명적인 오류가 발생할 수 있다.
그렇다면 원본을 건드리지 않고 참조 자료형을 복사하는 방법이 없을까?
우선 배열array을 복사하는 방법에는 흔히 두 가지가 쓰인다.
1. slice()
let array = [0, 1, 2, 3, 4, 5];
let copiedArray = array.slice();
console.log(copiedArray); // [0, 1, 2, 3, 4, 5]
console.log(array === copiedArray); // false
slice()메서드를 사용하여 원본 배열을 복사하는 방식이다. copiedArray 배열은 원본 배열과 같은 요소들을 갖지만, 참조하는 주소가 다르게 생성된다. 즉 이제 copiedArray에 요소를 추가하거나 빼도 array의 요소는 변화하지 않는다.
2. spread syntax ...
유사한 복사 방식으로 'spread syntax'가 존재한다. 배열을 펼친다는 의미.
let array = [0, 1, 2, 3, 4];
let copiedArray = [...array];
console.log(copiedArray); // [0, 1, 2, 3, 4]
console.log(array === copiedArray); // false
펼쳐서 전달된 array는 같은 요소를 가지되 다른 주소를 참조한다. slice()와 결과는 동일하다. 다른 주소를 참조하기 때문에 copiedArray에 요소를 추가해도 array는 변하지 않는다.
object객체를 복사하는 방법으론 Object.assign(), 또는 위에서 설명한 spread syntax이 사용된다.
let obj = { age: "12", Name: "Kim" };
let copiedObj = Object.assign({}, obj);
console.log(copiedObj) // { age: "12", Name: "Kim" }
console.log(obj === copiedObj) // false
원리는 배열 복사의 설명과 같다.
하지만 위에 설명한 복사 방법들로는 한 단계의 깊이만 복사하는 '얕은 복사'만 이루어진다.
가령 참조 자료형 내부에 다른 참조 자료형들이 중첩된 경우, 얕은 복사로는 그 중첩된 내부 자료들까지는 복사가 불가능하다. 즉, 내부에서 중첩된 참조 자료형들은 여전히 원본과 동일한 주솟값에서 참조하게 되는 것.
일단 확실히 하자면 모든 원시 자료형의 복사는 '깊은 복사'에 해당된다. 깊은 복사란 원본 참조를 더이상 하지 않고 완전히 영향을 주고받지 않는다고 보면 된다. 하지만 위와 같이 참조 자료형이 참조 자료형 내부에 중첩되어 있다면, 여전히 안쪽의 참조 자료형은 원본과 서로 영향을 주고받기 때문에 깊은 복사라고 보기 어렵다.
사실 JavaScript 내부적으로는 참조 자료형을 깊은 복사하는 방법이 없다. 다만 다른 문법들을 응용해서 비슷한 결과물을 만들 수 있다.
- JSON.stringfy()와 JSON.parse()
위 두 문법을 섞어 사용하면 깊은 복사와 유사한 결과를 만든다. JSON.stringfy()는 참조 자료형을 문자열로 변환시킨다. JSON.parse()는 문자열의 형태를 객체로 변환시킨다.
중첩된 참조 자료형을 JSON.stringfy()로 문자열로 변환시킨 뒤 이 값을 JSON.parse()를 사용해 객체로 변환시키면 깊은 복사와 같은 결과를 반환할 수 있다.
const array = [1, 2, [3, 4, 5]];
const copiedArray = JSON.parse(JSON.stringify(array));
console.log(array); // [1, 2, [3, 4, 5]]
console.log(copiedArray); // [1, 2, [3, 4, 5]]
console.log(array === copiedArray) // false
console.log(array[2] === copiedArray[2]) // false
참조 자료형의 내부 참조 자료형도 결과가 false, 즉 다른 주솟값을 갖는 것을 볼 수 있다.
단! 이 방법도 중첩된 참조 자료형에 함수가 있을 경우 함수가 null로 바뀌어버리는 아쉬움이 있다. 따라서 더 확실한 깊은 복사를 하기 위해서는 node.js환경의 외부 라이브러리인 'lodash'나 'ramda'를 설치하면 된다. 따로 설치해야 하고, 코딩 테스트 같은 경우에는 사용할 수 없다는 단점이 있으나 그럼에도 깊은 복사를 효과적으로 할 수 있다.
'CODE STATES 44' 카테고리의 다른 글
16-20일차 스터디 정리본 (0) | 2023.03.08 |
---|---|
11-15일차 스터디 정리본 (0) | 2023.03.07 |
10일차. 우분투 설치 및 리눅스와 깃 (0) | 2023.02.24 |
9일차. 계산기 구현 (0) | 2023.02.23 |
8일차. 코플릿 조건문 반복문 (0) | 2023.02.22 |