Search

[Javascript] shallow copy, deep copy | 원시값과 객체의 copy

subtitle
원시값과 객체의 차이로 보는 '얕은 복사'와 '깊은 복사'
Tags
개념
기초
Created
2021/03/18 14:43
얕은 복사는 한 단계까지만 복사하고, 깊은 복사는 객체에 중첩된 객체까지 모두 복사한다. 얕은 복사와 깊은 복사 모두 복사한 대상에 대해서 새로운 객체를 생성하여 기존 객체에는 영향을 주지 않는다. 하지만 얕은 복사와 깊은 복사는 어느 수준까지 복사하느냐의 차이를 가진다.
얕은 복사를 하면 한 단계만 복사하기 때문에 중첩된 객체에 대해서는 서로 영향을 주고, 깊은 복사는 중첩된 객체 역시 별개의 값으로서 서로 영향을 주지 않는다. 이번 포스팅에서는 원시값과 객체의 특징을 알아보고 객체의 얇은 복사와 깊은 복사가 왜 이런 결과가 나오는지 다룬다.

원시값의 불변성

원시값(primitive)은 변경 불가능한 불변의 값이다. 원시값에는 String, Number, undefined, Boolean, Symbol, BigInt 6종류가 존재한다.

변수 변경 ≠ 원시값의 변경

변수 값이 변경되는 것을 원시값이 변경된다고 생각하면 안된다. 이전 포스팅에서 변수는 값을 담고 있는 메모리 공간 자체 또는 그 메모리 공간의 주소를 가리키는 식별자라고 했다. 변수에 값이 할당되면 실제로는 메모리 공간에 값이 담기고 변수는 그 메모리 주소를 가리킨다.
아래 그림을 예로 들면, 원시값인 undefined나 100은 변경 불가능하므로 원시값을 재할당한다. 재할당은 새 메모리 공간을 확보하고 값을 저장한 후 변수가 참조하던 메모리 주소를 변경함으로써 이루어진다. 이렇게 해서 원시값을 불변성을 유지하고, 변수는 가리키는 메모리 주소를 바꿔가며 변수 값을 변경한다.

상수와의 차이

상수는 변수의 재할당이 금지된 것을 의미한다. 원시값이 불변성을 가지는 것과는 의미가 다르다.

원시값 copy, 값을 복사할 뿐 별개의 메모리 공간을 가진다

const original = 100; const copied = original; original = 2; console.log(copied); // 1
JavaScript
복사
원시값을 갖는 변수를 다른 변수에 할당하면 어떻게 될까? 원시값을 복사하면 값은 그대로 복사되지만, 같은 값을 가지는 별개의 메모리 공간을 가리킬 뿐이다. 따라서 각 변수에 어떤 짓을 하든 서로에게 영향을 주지 않는다.

객체

원시값과 달리 객체는 변경이 가능한 값이고, 프로퍼티의 집합이다. 클래스에 정의된 멤버대로 객체를 생성하는 자바같은 경우는 객체가 생성된 이후 멤버를 추가할 수 없다. 반면, 자바스크립트는 객체 생성 이후에도 프로퍼티를 추가하거나 삭제하는 등 변경할 수 있다.
데이터가 동적으로 추가되면, 선언 당시의 프로퍼티 데이터 타입이나 순서가 프로퍼티에 접근할 때와는 달라질 수 있기 때문에 프로퍼티 값을 읽을 때마다 프로퍼티를 찾아야 한다. v8에서는 객체의 동적 변경으로 인한 프로퍼티 접근 비용을 낮추기 위해 *히든 클래스를 사용한다.
*히든 클래스: 객체의 기존 프로퍼티로부터의 메모리 오프셋을 갖고 있어 변경된 프로퍼티를 찾을 수 있다. 이전 히든클래스는 변경사항이 적용되면 다음 클래스로 전환된다는 정보를 추가한다. 이는 같은 프로퍼티를 가지는 오브젝트들이 같은 히든 클래스를 공유함으로써 추가적인 히든클래스 생성을 막는다.

객체 값을 갖는 변수: 객체의 메모리 주소를 참조한다

객체 값을 갖는 변수가 가리키는 메모리 공간에는 객체가 아닌 객체의 메모리 주소 값이 들어간다. 따라서 객체가 변경되어도 변수의 객체 값에 대한 메모리 주소는 변경되지 않고 항상 객체를 참조할 수 있다.
객체는 원시값처럼 크기가 일정하지도 않고 프로퍼티 값이 객체인 경우 복사해서 생성하는 비용이 크고, 메모리 공간도 많이 차지하기 때문에 원시값처럼 할당할 때마다 생성하지 않고 참조해서 사용한다. 다만 이런 경우 여러 변수가 하나의 객체를 참조함으로써 서로 영향을 주는 문제가 있다.

얕은 복사와 깊은 복사

얕은 복사는 한 단계까지만 복사하고, 깊은 복사는 객체에 중첩된 객체까지 모두 복사한다. 얕은 복사의 경우 참조된 값, 즉 객체의 메모리 주소를 복사하여 같은 객체를 참조한다.
const object = { nested: { something: 'is good' } }; // 얕은 복사 const copied = { ...object } copied === object // false copied.nested === object.nested // true
JavaScript
복사
const copied = { ...object } : 객체 *리터럴을 사용하면 항상 *객체를 생성하기 때문에 객체 자체는 다른 것으로 평가되나 내부에 중첩된 객체는 같은 것으로 평가된다.
*리터럴: 사람이 이해할 수 있는 문자나 약속된 기호로 값을 표현하는 것.
만약 const copied = object 처럼 할당했다면 아예 같은 객체를 참조하게 되므로 copied === object 도 true로 평가된다.
반면 깊은 복사의 경우, 메모리를 새로 할당하여 원본 객체를 복사하므로 별개의 객체가 된다.
import cloneDeep from lodash/cloneDeep; const copied = original; copied === original // false copied.nested === original.nested // false
JavaScript
복사

references

모던 자바스크립트 딥 다이브 10장, 11장