[모던 자바스크립트 Deep Dive] 11장

Gyuwon Lee·2022년 7월 22일
0
post-thumbnail

42 서울의 모던 자바스크립트 Deep Dive 스터디 학습의 일환으로, 새롭게 알게 된 내용 중심으로 정리되어 있습니다.


CH11. 원시 타입과 객체 타입의 비교

let primitive = 'hello';
let object = {
  foo = 'world',
};
  • primitiveobject 는 둘 다 식별자다.
    • 따라서 둘 다 메모리 공간 을 기억하는 변수 다.
  • 원시 타입과 객체 타입의 차이는, 메모리 공간에 어떤 값이 저장되는지가 결정한다.
    • primitive 가 기억하는 메모리 공간에는 원시 값 이 저장되어 있다.
    • object 가 기억하는 메모리 공간에는 참조 값 이 저장되어 있다.

1) 원시 타입의 특성: 불변성

원시 타입의 값은 변경 불가능한 값 이다. 다시 말해 read-only다.

let str = "string";

그렇다면, 위 코드에서 원시 타입인 문자열 string 이 저장된 변수 str 의 값은, 변경될 수 없다는 뜻일까?

당연히 그렇지 않다. 변경 불가능하다는 것은 변수가 아니라 값에 대한 진술이다. 앞서 5장에서 명확히 했듯, 변수 란 어떤 값을 저장하기 위해 확보한 메모리 공간 자체다(또는 그 이름까지도 변수라고 부르긴 하지만, 변수에 붙은 '이름' 은 식별자 라고도 한다). 반면 이란 변수에 저장된 데이터로서, 표현식이 평가되어 생성된 결과다. 컴퓨터가 이해할 수 있게 바이너리로 변환되어 저장되는 그 데이터 말이다.

변수에는 재할당을 통해 얼마든지 값을 변경할 수 있다. 중요한 것은, 원시 타입의 값을 재할당할때마다 변수가 기억하는 메모리 주소가 변경된다는 점이다. 어떤 메모리 공간에 원시 값을 한 번 저장하고 나면, 그 공간이 가비지 콜렉터에 의해 비워지기 전까지 저장된 값은 변하지 않는다.

이는 데이터의 신뢰성을 보장하고, 상태 변경 추적을 용이하게 한다.

원시 값의 불변성이 보장되면, 원시 타입 변수의 상태 변경은 곧 기억하는 메모리 주소의 변경이다. 즉, 이전 값에 새로운 값이 덮어씌워지는 것이 아니므로 이전 값이 사라지지 않는다. 그러면 변경 전후 값의 비교 및 변경 추적이 용이해진다. 변수에 저장되어 있던 메모리 주소의 변화 및 그 내역만 확인하면 되기 때문이다.


2) 객체 타입의 특성: 가변성

원시 값과 달리, 객체는 복합적인 자료구조다. 프로퍼티의 값으로 자바스크립트에서 값으로 평가되는 모든 타입을 사용할 수 있으며, 프로퍼티의 개수에도 제한이 없다. 따라서 경우에 따라 크기가 매우 클 수도 있고, 이에 따라 확보해야 할 메모리 공간의 크기를 사전에 정해 두기가 힘들다. 따라서 객체 타입의 값은 변경 가능한 값이다.

앞서 원시 타입과 객체 타입의 변수 모두 메모리 공간을 기억한다고 했다. 다만 그 공간에 원시 값이 저장되어 있느냐, 참조 값이 저장되어 있느냐만 다를 뿐이다.

원시 값을 할당한 변수는 기억하는 메모리 주소를 통해 원시 값 자체에 접근할 수 있다. 즉 바이너리로 저장되어 있는 데이터에 접근가능하다. 반면 참조 값을 할당한 변수는 기억하는 메모리 주소를 통해 참조 값에 접근하는데, 이 값은 생성된 객체가 저장된 메모리 공간의 주소다. 실제 데이터 자체가 아니라, 그 데이터가 저장되어 있는 주소를 기억하고 있는 것이다.

즉 간단히 말해 원시 타입은 값 그 자체를, 객체 타입은 값이 저장되어 있는 메모리 주소를 담고 있다.

자바스크립트는 어째서 데이터 타입을 어째서 불변성과 가변성으로 구분되는 두 종류로 나누었을까? 원시 타입은 불변성이 보장되므로 데이터의 신뢰성과 상태 변경 추적 등에서 이점을 갖지만, 변경 시마다 새로운 메모리 공간을 요구한다는 특징이 있다. 문자열을 제외한 원시 타입은 모두 크기가 정해져 있고, 비교적 가벼우므로 매번 값을 복사해서 새롭게 생성하는 것이 가능하다.

그러나, 앞서 말했듯 객체는 복합적인 자료구조다. 특히 미리 클래스나 타입으로 선언해서 오직 미리 정의된 형태로만 사용가능한 다른 언어의 객체와는 달리, 자바스크립트의 객체는 프로퍼티의 생성과 삭제에 따라 크기가 계속 변하며, 그 크기가 매우 커질 수도 있다. 이는 복사에 많은 비용이 든다는 것을 뜻한다.

동일한 값의 객체를 여러 변수들이 사용하는데, 그 변수들이 모두 각각의 메모리 공간을 갖고 있다면? 그리고 그 객체의 크기가 매우 크다면? 메모리의 효율적 소비가 어렵고 성능을 저하시킬 것이다.

이와 같은 타입 간의 차이는, 값 간의 전달 방식"변수에 변수를 할당했을 때 무엇이 어떻게 전달되는가?" 에서 차이를 발생시킨다.


3) 원시 값의 전달: 값에 의한 전달

변수에 원시 값을 갖는 변수를 할당하면, 할당받는 변수에는 할당되는 변수의 원시 값이 복사되어 전달된다.

let init = 80;
let copy = init;

init = 100;

위 코드에서 copyinit 에 저장된 값인 80 을 복사받아 갖게 된다. 이 때, initcopy 는 각기 다른 메모리 주소를 기억한다. let copy = init 이 런타임 중 엔진에 의해 해석되며 copy 라고 이름붙인 새로운 메모리 공간을 확보하고, init 의 값이었던 80 복사해 이 새로운 공간에 저장한 것이다. 그래서 값은 동일하지만 각기 별개의 값이다. 따라서 init = 100 이라고 변경해도 copy 의 값에는 아무런 영향을 주지 않는다.

즉, 두 변수의 원시 값은 서로 다른 메모리 공간에 저장된 별개의 값이 되어 어느 한쪽에서 재할당을 통해 값을 변경하더라도 서로 간섭할 수 없다.


4) 객체 값의 전달: 참조에 의한 전달

let foo = {
  prop = 'hello';
}

let bar = foo;

변수에 객체를 가리키는 변수를 할당하면, 원본의 참조 값이 복사되어 전달된다. 즉 foobar 는 각기 다른 메모리 주소를 기억하지만, 그 메모리 공간에 저장되어 있는 참조 값은 동일하다. 즉 두 개의 식별자가 하나의 객체를 공유하게 된다. 따라서 원본 또는 사본 중 어느 한쪽에서 객체를 변경하면, 서로 영향을 주고받는다.

이를 방지하기 위해서는 map() 이나 lodashcloneDeep, 또는 배열의 경우 ... 등의 방식을 사용해서 값을 복사해올 수 있다.

profile
하루가 모여 역사가 된다

0개의 댓글