데이터 타입 | 기본형과 참조형 : 코어 자바스크립트 씹뜯맛즐 하기

코어 자바스크립트를 씹뜯맛즐해 보는 시간이다.
이미 널리고 널린 게 해당 책에 대한 정리 글이지만, 그걸 읽는다고 내 것이 되는 게 아니니
나는 나만의 언어로, 방식으로 이해하며 정리하는 방법을 택했다.
 


 

데이터 타입의 종류

기본형 : 숫자 (number), 문자 (string), 불리언 (boolean), null, undefined 등
참조형 : 객체 (+ Map, WeakMap, Set, WeakSet), 배열, 함수, 날짜, 정규 표현식 등

 
자바스크립트의 데이터 타입은 위와 같이 분류할 수 있다.
여기서 기본형과 참조형은 할당이나 연산 시 복제되는지, 아니면 참조되는지를 가지고 나누게 된다.
엄밀하게 따지면 기본형과 참조형 모두 복제를 하지만, 복제하는 대상이 다르다.
 
위의 기준이 무슨 말인지 명확하게 이해하려면
컴퓨터가 데이터를 메모리에 저장하는 방법에 대해서 먼저 알아볼 필요가 있다.
 

컴퓨터가 데이터를 메모리에 저장하는 방법

예를 들어 a라는 변수에 1이라는 값을 할당할 때, 컴퓨터는 이 데이터를 어떻게 저장할까?
완벽하지는 않지만 대략의 프로세스는 다음과 같다
 

먼저 컴퓨터는 일정 메모리 공간을 확보하고 이 공간의 이름 (식별자)을 a라고 지정한다.
또한 컴퓨터는 다른 메모리 공간을 확보해 해당 메모리의 값으로 1을 저장한다.
그다음 할당 과정에서 컴퓨터는 a라는 이름을 가진 메모리에
1의 값을 가진 메모리 주소를 찾아 a 식별자가 있는 메모리의 값으로 1이 저장된 주소값을 저장한다.
컴퓨터가 이렇게 데이터를 저장하는 이유는 불필요한 메모리 낭비를 방지하기 위해서다.
 
데이터의 크기에 따라서 컴퓨터가 확보하는 메모리의 크기도 달라지게 되는데,
만약 변수를 생성할 때 확보하는 메모리가 1MB고, 데이터의 크기가 5MB라고 가정하자.
그리고 변수 두 개에 동일한 데이터를 저장한다고 가정해 보자.
 

let a = 1234567890;
let b = 1234567890;

만약 이와 같은 상황에서 변수를 생성할 때 만든 메모리에 값으로 직접 데이터를 넣는 방법과
데이터 메모리를 따로 확보하고 주소값을 넣는 방법을 비교해 보면
직접 넣을 때는 12MB 메모리가 필요하고, 주소값을 넣는 방법은 7MB 정도 필요하게 될 것이다.
이러한 이유로 컴퓨터는 주소값을 저장하는 방식으로 데이터를 저장한다.
 

기본형

기본형 데이터 (숫자 (number), 문자 (string), 불리언 (boolean), null, undefined 등)는
해당 값이 변하지 않는다라는 특징이 있다.
 

let a = 1;
a = 2;
console.log(a)      // 2

위에 코드를 보면 a에 할당된 값이 재할당을 통해 변하는 것을 볼 수 있다.
그렇다면 값이 변하는 거 아닌가?라는 생각을 할 수 있는데
데이터 타입이라고 하는 것은 a 변수의 값이 아닌 1이라는 데이터 그 자체다.
값이 변하지 않는다는 것은 1이라는 데이터가 변하지 않는다는 것을 의미한다.
 
컴퓨터가 데이터를 저장하는 방식을 떠올려보면
위의 코드에서 1과 2라는 데이터를 저장하기 위해 각각의 메모리를 확보한 뒤,
a 식별자가 있는 메모리에는 각각의 주소값을 값으로 저장했을 것이다.
변하는 것은 a 변수를 담당하는 메모리에 담긴 주소가 변경된 것이지
데이터 자체가 변한 것은 아니라는 의미다. 그래서 해당 값이 변하지 않는다고 하는 것이다.
 

let a = 1;
let b = 3;

a = 2;
b = 2;

또한 기본형 데이터는 할당이나 연산 시 복제가 된다고 설명했는데 위 코드를 통해 확인해 보자.
 
이 코드를 보면 a와 b 변수에 동일하게 2를 할당한다.
이 과정에서 a, b 식별자가 지정된 메모리의 값에는 2라는 값이 저장되는 것이 아닌,
2라는 특정 값을 저장하고 있는 메모리의 주소가 저장이 된다.
할당 시 복제가 된다는 말은 이 주소값이 복제가 된다는 것이다.
  

참조형

참조형 데이터 (객체 (+ Map, WeakMap, Set, WeakSet), 배열, 함수, 날짜, 정규 표현식 등)는
기본형 데이터와 다르게 기본적으로는 가변값을 가진다라는 특징이 있다.
 

let obj = {a:1, b:2};
obj.a = 2;

이 코드를 보면 obj 객체에 프로퍼티로 a와 b가 있고, 각각의 프로퍼티에는 1과 2라는 값이 할당되어 있다.
그 후 obj 객체의 a 프로퍼티에 2라는 값을 재할당 했다.
 
식별자와 특정 값에 대한 메모리를 확보하는 것은 기본형과 동일하다.
그러나 여러 개의 프로퍼티로 이루어져 있는 데이터 그룹이 식별자의 값으로 할당되는 경우
주소 목록이 해당 식별자의 값으로 저장이 된다.
 

 
그림을 통해 확인해 보면 가변값을 가진다에 대한 의미를 해석해 보자.
앞서 기본형 데이터는 데이터 그 자체, 위의 그림에서 찾아보면 301, 302 메모리에 할당된 값이다.
하지만 참조형 데이터는 위의 그림에서 찾아보면 201~202 메모리를 의미한다.
이 메모리에 값에는 주소값이 할당되어 있다.
 
즉, 어떠한 연산이나 재할당을 통해 이 주소값은 변할 수 있기에 가변값을 가진다라고 해석할 수 있겠다.
결국 이러한 데이터의 특징 때문에 문제가 발생하기도 하는데 어떤 문제가 있는지 알아보자.
 

728x90

 

데이터 타입에 따라 발생할 수 있는 문제

먼저 기본형 데이터를 복제할 때 과정을 살펴보자.
 

let a = 1;
let b = a;
a = 2;

console.log(b);

위의 코드를 보면 a 변수에 1을 할당하고, b 변수에는 a를 할당했다.
그다음 a 변수에 2를 재할당했다.
 
변수 b에 a를 할당했다는 것은 a의 값을 b에게 할당했다는 의미가 되고,
결과적으로 b의 값 영역에는 1의 메모리 주소를 할당했다는 의미가 된다.
 
b에 a를 할당했다고 해서 a와 b가 끈끈하게 연결된 것이 아닌
a가 가지고 있는 데이터 주소를 준거뿐이라 a 변수에 다른 값 주소를 할당해도
b에게는 영향을 주지 않는다.
 
기본형 데이터의 복제가 아무 문제가 없다는 것은
참조형 데이터를 복제할 때 문제가 발생한다는 의미가 된다.
 

const obj = { name:"um" };

const obj2 = obj;
obj2.name = "test";

console.log(obj.name);

위의 코드에서 출력값은 "test"라는 문자가 출력된다..
분명 기존 객체와 다른 객체의 프로퍼티에 값을 재할당 했는데 왜 기존 프로퍼티까지 영향을 받는 걸까?
 

데이터를 저장할 때 식별자가 있는 메모리에 주소값이 저장되는 것과

참조형 데이터는 가변값이라는 것을 다시 한번 생각해 보자.

 

위의 흐름에 따르면 obj 객체와 obj2 객체는 동일한 주소값을 가지고 있다.

프로퍼티의 주소가 같다는 의미가 된다.

그래서 해당 프로퍼티의 값을 재할당 할 경우 같은 프로퍼티 주소를 가지고 있는 두 객체는

동시에 영향을 받을 수밖에 없는 상태가 된다.

 

얕은 복사와 깊은 복사

이렇게 참조형 데이터를 복제할 때, 메모리의 주소값 복제한 경우를 얕은 복사 (Shallow Copy)라고 한다.

이러한 얕은 복사는 데이터의 불변성을 지키기 어려워 다양한 이슈를 발생시키곤 하는데,

이러한 문제를 해결하기 위해 깊은 복사 (Deep Copy)를 해줘야 한다.

깊은 복사는 복제 시 새로운 메모리 공간을 확보해서 데이터를 완전히 복사하는 것을 의미한다.

 

깊은 복사의 방법으로 몇 가지 방법이 있는데, 그중 대표적으로 전개 연산자 (Spread Operator)가 있다.

 

const obj = { name:"um" }

const obj2 = { ...obj }
obj2.name = "test"

console.log(obj.name)        // "um"

전개 연산자를 사용하게 되면 참조형 데이터를 복제할 때 발생했던 문제가 해결되는 것을 볼 수 있다.

그러나 사실 이 방법도 온전한 깊은 복사에는 한계가 있다.

바로 2차원 이상의 객체는 다시 얕은 복사가 수행된다는 점이다.

 

const obj = { name:"um", list: { title:"copy" } };

const obj2 = { ...obj }
obj2.list.title = "deep"

console.log(obj.list.title)       // "deep"

위와 같이 2차원 객체의 프로퍼티 값을 변경할 경우에는 얕은 복사가 발생해

동일한 주소값을 바라보게 되는 경우가 발생하게 된다.

결과적으로 전개 연산자는 1차원의 객체를 복제할 때 유용한 방식이다.

 

그렇다면 2차원 이상의 객체를 복제할 때 어떻게 해야 할까?

 

const obj = { name: "um", list: { title: "copy" } };

  function deepCopyObj(origin) {
    let obj = {};

    for (let key in origin) {
      if (typeof origin[key] === "object") {
        obj[key] = deepCopyObj(obj[key]);
      } else {
        obj[key] = origin[key];
      }
    }

    return obj;
  }

  const obj2 = deepCopyObj(obj);

  obj2.list.title = "test"

  console.log(obj)             //  { name: "um", list: { title: "copy" } }

먼저로는 재귀함수를 사용하는 방법이 있다.

객체의 프로퍼티가 객체인지 아닌지를 판단해 재귀적으로 새로운 메모리를 확보해

객체를 생성한 뒤 할당하는 방식으로 깊은 복사를 할 수 있다.

 

그 외에도 JSON.parse()와 JSON.stringify()와 같은 메서드를 이용하는 방법도 있다.

 


 

포스팅 작성에 참고한 감사한 글들

코어 자바스크립트 : 챕터 1 - 데이터 타입

지니의 기록 : 참조 타입의 얕은 복사와 깊은 복사

MDN 문서 : 전개 연산자