Javascript

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

복습의 목적으로 정재남님의 저서 '코어 자바스크립트'를 읽고 정리한 글입니다.

더 자세하고 정확한 내용은 책을 참고하시길 바랍니다.

 

질문

이 챕터를 읽고 난 후, 다음 질문에 답할 수 있어야 한다.

  • 자바스크립트의 자료형은 어떻게 분류할 수 있는가?
  • 숫자 또는 문자열의 데이터를 위해 몇 바이트의 메모리가 확보되는가?
  • 변수는 무엇이고 식별자란 무엇인가?
  • (데이터 타입에 따라) 데이터는 어떻게 메모리에 할당되는가?
  • 변수와 상수을 구분짓는 특징은 무엇인가?
  • 불변성이란 무엇인가?
  • 메모리에 남아있는 미사용 데이터는 어떻게 처리되는가?
  • 기본형 데이터를 갖는 변수 복사, 참조형 데이터를 갖는 변수 복사에는 어떤 차이가 있는가?
  • 얕은 복사와 깊은 복사의 차이는 무엇인가?
  • 불변 객체를 어떻게 만들 수 있는가?
  • undefined 는 언제 반환되는가?
  • null 은 어떤 용도로 사용되는가?
  • { typeof null }은 어떻게 출력되는가?

 

자바스크립트의 자료형

자바스크립트에서 자료형은 크게 두 갈래로 분류할 수 있다.

  • 기본형(primitive type): number, string, boolean, null, undefiend, symbol
  • 참조형(refernece type): object(array, function, Date, RegExp, Map, Set...)

기본형은 값이 담긴 주솟값을 바로 복제하지만,

참조형은 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제한다.

자세한 내용은 아래에서 설명한다.

 

메모리 확보

자바스크립트에서는 숫자를 위해 8바이트의 메모리를 확보하기 때문에 형 변환에 자유로운 편이다.

문자열은 특별히 정해진 규격이 없다.

 

미리 확보된 공간에서 데이터를 바꿔야 한다면, 메모리 공간을 늘리는 작업이 선행돼야 하지만 자바스크립트에서는 정해진 메모리 규격이 없기 때문에 가변적으로 메모리를 사용할 수 있다.

 

변수명과 식별자

변수는 변할 수 있는 데이터?

let a;

데이터를 할당하지 않고 변수를 선언하기만 해도 메모리에는 별도의 공간이 확보된다.

식별자는 그 공간의 이름이다.

 

따라서 변수는 변할 수 있는 데이터를 담을 수 있는 공간이라고 이해하는 것이 더 정확하다.

필요할 때 데이터를 할당하여 새로운 주솟값을 가리킬 수 있다.

 

데이터 할당

let a;

a = 'alpha'

변수를 선언하면 메모리에 a라는 식별자를 갖는 공간이 확보되고 문자열이 할당된다. 단, 문자열 자체가 해당 메모리 주소에 직접 할당되지 않는다는 점에 유의해야 한다. 그 주소에는 'alpha'라는 문자열이 존재하는 데이터 주솟값이 할당된다. 한 번 거쳐가는 셈인데, 번거롭게 보일 수 있지만 이는 데이터 변환을 자유롭게 할 수 있게 함과 동시에 메모리를 더 효율적으로 관리하기 위한 방법이다. 만약 새롭게 변환된 데이터가 할당되면, 기존의 데이터의 주솟값이 아닌 새로운 데이터의 주솟값을 가리키기만 하면 된다. 즉 변수 영역과 데이터 영역이 따로 존재하는 셈이다.

 

참조형의 경우에는 좀 더 복잡하다. 기본형은 데이터의 주솟값을 가리키지만, 참조형은 객체의 변수 영역을 가리키는 주소값을 가리킨다.

const obj = { 
  a: 'alpha',
  b: 'beta' 
}

변수를 선언하고 객체를 할당하게 되면, obj라는 식별자를 갖는 메모리 공간이 확보된다. 그 곳에 할당된 값은 "값이 담긴 주소 값들을 포함하는 덩어리를 가리키는 주소 값"이다. 한 문장의 글로 표현하기엔 무리가 많다. 만약 메모리 주소를 임의로 'x번지'라고 가정해 본다면,

 

  • 1번지(변수 영역) > 이름: obj, 값: 13번지 - 값이 담긴 주소 값을 포함하는 데이터를 가리키는 주소 값
  • 13번지(데이터 영역) > 101번지, 102번지 - 값이 담긴 주소 값을 포함하는 데이터
  • 101번지(변수 영역) > 이름: a, 값: 14번지 - 값이 담긴 주소 값
  • 102번지(변수 영역) > 이름: b, 값: 15번지 - 값이 담긴 주소 값
  • 14번지(데이터 영역) > 'alpha' - 문자열
  • 15번지(데이터 영역) > 'beta' - 문자열

이처럼 기본형과는 달리 첫 번째 데이터 영역이 또 다른 변수 영역들의 주소 값을 갖고 있는 것이 특징이다.

 

변수와 상수를 구분짓는 특징

변수는 새로운 데이터를 할당할 수 있지만(= 변수 영역에서 새로운 데이터의 주솟값을 할당할 수 있지만),

상수에는 새롭게 데이터를 할당할 수 없다.

 

불변성이란 무엇인가

변수와 상수를 구분짓는 것이 변수 영역에 관한 것이라면,

불변성은 데이터 영역에 관한 것이다.

 

기본형 데이터인 숫자, 문자, boolean 등은 모두 불변값이다. 만약 데이터 변환을 위해 새로운 문자열을 할당한다면, 내부적으로는 데이터를 직접 바꾸는 것이 아닌 새로운 데이터를 위한 공간을 확보하여 할당한 다음 그 주소 값을 변수영역이 가리키도록 한다. 실제로는 데이터가 변하는 것이 아니라 새로운 데이터가 재할당되는 것이다. 그러나 참조형 데이터인 객체의 경우, 변수 영역이 가리키는 데이터가 실제 데이터가 아닌, 실제 데이터가 존재하는 주솟값을 포함하는 각각의 변수 영역을 참조하기 때문에 가변적이라고 할 수 있다. 아래에서 변수 복사 비교를 통해 둘의 차이를 확인할 수 있다.

 

가비지 컬렉터

기본형 데이터는 불변하기 때문에 한번 만들어진 데이터는 그 자리에 그대로 남아있게 된다. 만약 아무도 참조하지 않는 데이터라면 가비지 컬렉터가 런타임 환경에 따라 특정 시점에 자동으로 그 대상들을 수거하게 된다.

 

변수 복사 비교

let a = 1;
let b = a;

b = 2;

console.log(a===b); //false

변수를 복사하게 되면 같은 데이터 영역을 가리키는 변수 영역(named b)이 새롭게 생성된다.

만약 b에 새로운 데이터를 할당하게 되면 이제는 서로 다른 데이터 영역을 가리키기 때문에 a와 b는 더 이상 같지 않다.

let a = { 
  a: 3,
  b: 5 
}; 

let b = a;

b.a = 6;

console.log(a===b); //true

참조형 데이터의 변수 복사에서는 상황이 다르다. b에 a를 복사하고 나서 속성을 변경했는데 여전히 a와 b가 같다는 것을 확인할 수 있다. b의 프로퍼티를 바꿨는데 a의 프로퍼티도 바뀐 것이다. 즉 사본을 바꾸면 원본이 바뀌고, 원본을 바꾸면 사본이 바뀐다. 변수를 복사할 때 데이터 영역이 갖는 주솟값만 얕게 복사하는, 이른바 얕은 복사만 이루어지기 때문이다. 이 문제를 해결하기 위해선 내부 프로퍼티를 재귀적으로 복사해 주는 깊은 복사가 이뤄져야 한다.

 

얕은 복사와 깊은 복사

객체의 가변성을 해결하기 위해 내부 프로퍼티를 순회하면서 새로운 객체에 복사할 수 있다.

const user = {
  name: 'noname',
  age: 18,
};

const copyObject = function(target) {
  const result = {};
  for (let prop in target) {
    result[prop] = target[prop];
  }
  return result;
};

const user2 = copyObject(user);

user2.age = 20;

console.log(user === user2); //false

 

하지만 만약 객체의 depth가 한 단계만 더 깊어진다면,

const user = {
  name: 'noname',
  age: 18,
  info: {
    blog: 'noname.blog.com',
    github: 'github.com',
    email: 'email@mail.com',
  }
};

const copyObject = function(target) {
  const result = {};
  for (let prop in target) {
    result[prop] = target[prop];
  }
  return result;
};

const user2 = copyObject(user);

user2.info.blog = '/';

console.log(user === user2); // false
console.log(user2.info.blog); // '/'
console.log(user1.info.blog); // '/'

한 단계만 더 깊게 들어가도 원본과 사본에 함께 변하는 것을 확인할 수 있다.

 

이럴 때는 더 깊은 복사가 필요하다.

const user = {
  name: 'noname',
  age: 18,
  info: {
    blog: 'noname.blog.com',
    github: 'github.com',
    email: 'email@mail.com',
  }
};

const copyObjectDeep = function(target) {
  let result = {};
  if (typeof target === 'object' && target !== null) {
    for (let prop in target) {
      console.log(target[prop]);
      result[prop] = copyObjectDeep(target[prop]);
    }
  }
  else {
    result  = target;
  }
  return result;
};

const user2 = copyObjectDeep(user);

user2.info.blog = '/';

console.log(user === user2); // false

console.log(user2.info.blog); // '/'
console.log(user1.info.blog); // 'noname.blog.com'

 

재귀적으로 깊은 복사를 구현하고 이를 활용하여 불변 객체를 만들 수 있다.

예측 가능하고 신뢰할 수 있는 코드를 작성하기 위해서 불변성을 지키는 것은 중요하다.

 

undefined는 언제 반환되는가?

  • 함수의 리턴값이 없을 때
  • 값이 할당되지 않은 변수에 접근할 때
  • 객체 내부에 존재하지 않는 프로퍼티에 접근할 때

 

null은 언제 사용되는가?

let arr = []
arr.length = 3;
console.log(arr); // [ <3 empty items> ]

let arr = [undefined undefined undefined]
console.log(arr); // [undefined undefined undefined]

빈 배열에 길이가 주어지게 되면 empty item이 출력되는 것을 확인할 수 있다. 이처럼 비어있는 요소는 배열의 순회 대상에서 제외된다. 반대로 undefined는 순회가능한 대상으로 마치 그 자체가 값처럼 여겨진다.배열은 무조건 length 프로퍼티의 개수만큼 빈 공간을 확보하고 각 공간에 인덱스를 이름으로 지정할 것이라고 생각하기 쉽지만, 실제로는 객체와 마찬가지로 특정 인덱스에 값을 지정할 때 비로소 빈 공간을 확보하고 인덱스를 이름으로 지정하고 데이터의 주솟값을 저장하는 등의 동작을 합니다.

 

배열은 무조건 length 프로퍼티의 개수만큼 빈 공간을 확보하고 각 공간에 인덱스를 이름으로 지정할 것이라고 생각하기 쉽지만, 실제로는 객체와 마찬가지로 특정 인덱스에 값을 지정할 때 비로소 빈 공간을 확보하고 인덱스를 이름으로 지정하고 데이터의 주솟값을 저장하는 등의 동작을 합니다.

 

undefined가 마치 값과 같이 할당 가능하고 순회의 대상이 될 수 있다는 점에 주목해야 한다. 이렇게 활용된 undefined는 자바스크립트 엔진이 반환해주는 undefined와 서로 성격이 다르다. 자바스크립트 엔진이 뱉어주는 undefined는 해당 프로퍼티 또는 값이 존재하지 않음을 의미하는 반면, 어딘가에 할당된 undefined는 실존하는 데이터처럼 여겨지기 때문이다.

 

이러한 사실이 혼란을 야기하기도 하는데, 이는 undefined는 할당하는 방식으로 사용하지 않음으로써 간단히 해결될 수 있다. 대신에 값이 없음을 명시적으로 표현하기 위해서는 undefined가 아닌 null을 할당해주면 된다.

typeof null

console.log(typeof null); //object

null의 타입검사를 하게 되면 object라고 나오는데 이는 자바스크립트의 자체 버그이다.

만약 어떤 변수가 null인지의 여부를 판단하기 위해서는 일치 연산자(===)를 활용해야 한다.