JS - 데이터 타입

박상준

2024년 11월 01일

2

JavaScript

core javascript를 공부하기에 앞서서...

전부터 사두었던 코어 자바스크립트를 다시금 공부를 해봐야겠다는 판단이 든다. 고졸에 비전공자인 내가 그 간극을 줄이기 위해선 이런 핵심적인 내용을 우선적으로 알고 있어야 한다고 생각한다. 단순히 책을 옮겨 적으면서 학습하는것이 아니라 이 글을 읽고 누군가는 조금이나마 도움이 될수 있도록 하는 것이 목표이다..!

데이터 타입의 종류

데이터 타입에 대해 알아보기 전에 어떤 데이터 타입이 있는지 간단하게 정리해보겠다. 크게 두가지 타입으로 기본형(primitive type)참조형(reference type)데이터로 나눠진다.

  • 기본형 : number, boolean, string, null, undefined, symbol
  • 참조형 : Object, Array, Function, Date, RegExp, Map, Set

이렇게 다양한 데이터들이 두가지 타입으로 나눠진다고 보면 된다. 데이터 타입 이미지 그럼 이 두가지 데이터는 어떤 기준으로 구분할까? 기준은 데이터를 복제할때 나타난다. 기본형과 참조형 둘다 복제할때 데이터를 저장하는 저장소의 주소값을 저장하고 복제할때에도 둘다 저장소의 주소값을 복사하는 형식이다. 하지만 기본형은 데이터가 담긴 주소값을 바로 복제하고, 참조형은 여러 데이터들의 주소값이 묶인 주소값을 복제한다.

지금 뭔가 말장난하는것 같지만 지금은 두개의 차이가 어떻게 되는지 구분하기 위함이니 이렇게만 설명하고 넘어가겠다. 이후에 데이터가 어떻게 메모리에 저장되고, 주소값을 참조하는지 알아가면서 명확하게 표현해보겠다.

메모리에 저장되는 데이터

데이터가 어떻게 메모리에 저장되는지 간략하게 알아보겠다. 우선 우리가 사용하는 숫자나 언어는 컴퓨터가 바로 알아들을수가 없다. 그래서 우리는 컴퓨터의 언어인 0과 1로 변환시켜줘야한다. 이렇게 변환된 데이터를 메모리에 저장함으로써 여러 데이터를 컴퓨터가 저장하는 것이다.

이렇게 0또는 1을 메모리에 저장하기 위한 공간을 비트(bit)라고 한다. 비트는 고유한 식별자를 가지고 있어서 어디에 어떤 값이 있는지 알수 있지만 0과 1로만 이뤄져 있기 때문에 많은 데이터를 관리하고 찾기란 쉽지 않다. 그래서 효율을 위해 8개의 bit를 묶어서 바이트(byte)라는 단위가 생기게 되었다. 즉, 1바이트는 8비트이다. 메모리 이미지 이렇게 만들어진 바이트의 위치를 파악하는 법은 쉽다. 바이트 첫번째에 위치하는 비트의 고유한 식별자를 안다면 어디에 어떤 값이 저장되어 있는지 찾을 수 있는것이다. 위에서 얘기한 메모리 주소값이라는 개념은 비트의 고유한 식별자를 의미하는 것이다.

C/C++, Java의 정적 타입 언어는 메모리 낭비를 최소화하기 위해 데이터 타입별로 메모리 영역을 정해두었다. 하지만 메모리의 압박에서 자유로워진 현재 자바스크립트는 메모리 공간을 넉넉하게 잡아 두었다. 숫자형의 경우 8바이트, 64비트의 메모리를 확보하고 그 안에 데이터를 저장한다. 추가적으로 만약 소수를 저장한다면 일정 단위 이상은 올림이나 내림 처리를 하는것을 권장한다. 왜냐하면 무한 소수라면 저장가능한 비트의 수가 제한적이기 때문에 오차범위가 발생하기 때문이다. 나중에 이 내용만 따로 정리해보겠다.

변수 선언과 데이터 할당

데이터가 어떻게 메모리에 저장되는지 알았으니 변수에 어떤 식으로 데이터가 할당되는지 알아보겠다.

var a;

우선 변수를 하나 선언해주겠다. 변수란 변할 수 있는 데이터를 의미한다. 그래서 변할 수 있는 데이터를 담는 공간 정도로 이해하면 될것 같다. 변수선언 이미지

실제로는 더 복잡한 과정을 거쳐 데이터가 저장되겠지만 데이터 타입을 이해하기 위한 정도로만 이해하기 위해 대략적으로 표현하겠다.

a라는 변수를 선언하였고 이 변수에 데이터를 저장하기 위해 임의의 저장소를 하나 선택한 상황이다. 선언만 했기 때문에 아직 아무건 값이 들어있지 않다. 만약 이 저장소에 값이 들어가게 된다면 a는 식별자가 되어서 저장소에 저장된 값을 알수가 있게 된다.

var = a;
a = '123';

이제 만든 변수에 문자열을 넣어보겠다. 왠지 1004주소에 바로 '123'이 들어갈것 같지만 그렇지가 않다. 데이터 할당 이미지 '123'이라는 값을 저장하기 위한 다른 저장소를 확보하고 그 저장소에 데이터를 저장한 후, 저장소의 주소값을 할당하고자 하는 주소에 값으로 넣어주는 것이다.

a라는 변수를 선언함으로 공간을 확보한다(1004) -> '123'이라는 데이터를 다른 공간에 저장한다(3002) -> a라는 식별자를 가진 공간을 찾는다. -> '123' 데이터를 저장한 주소를 a식별자 공간에 저장한다.

그렇다면 왜 바로 저장을 안하고 굳이 한단계를 거치는 걸까? 가장 큰 이유는 메모리를 효율적으로 관리하기 위함이다. 예시를 들어보자면 만약 8바이트의 용량을 가진 10이라는 숫자 데이터를 똑같이 100개의 변수에 할당한다고 가정해보자. 각각 저장한다면 100*8, 즉 800바이트의 용량이 필요하다. 반면에 메모리 주소를 참조하게 되면 한번만 데이터 할당을 하고, 주소 공간 크기인 2바이트(16비트기준)에 주소를 저장하여 100x2+8인 208바이트의 용량이 필요하게 된다.

두번째로 문자열의 경우 가변적이고 문자의 길이마저 가변적이기 때문이다. 만약 일정 용량만큼 문자를 저장하기로 설정했다고 가정했을때 용량을 넘어서는 경우 문제가 생긴다. 만약 뒤에 아무런 데이터가 없다면 문제가 없겠지만 뒤에 다른 데이터들이 존재한다면 데이터의 주소가 전부 바뀌는 비효율적인 메모리 사용이 발생한다.

기본형 데이터와 참조형 데이터 비교

어떻게 데이터가 메모리에 저장되는지 이유까지 알아보았다. 이제 두가지의 차이점을 이해하는데 도움이 될것이다.

불변값

두 데이터의 차이는 불변성에 있다. 기본형 데이터는 불변성이 있는 불변값이다. 불변성의 예시를 들어보겠다.

var a= 'abc';
a = a + 'def';

a라는 변수를 선언하고 메모리 공간에 'abc'문자열을 저장한 후 메모리 주소값을 a에 저장한 상태이다. 이제 여기에서 'def'를 추가하면 어떻게 될까? 불변성 이미지 기존의 abc메모리에 저장된 데이터가 abcdef로 바뀌는 것이 아닌 abcdef라는 데이터를 별도로 저장해서 할당한다. 이렇게 처음 만들어진 데이터는 바꿀수 없다는 것이 불변성을 가진다는 것이다.

그럼 사용하지 않으면 어떻게 될까? 메모리에서 GC, 가비지 컬렉팅을 통해 할당되지 않은 데이터는 일정 시간이 지나면 삭제된다.

가변값

불변값과 반대로 가변, 변할수 있다면 가변값이다.

var obj = {
  a : 1,
  b : 'abc'
};

참조형 데이터를 하나 만들었다고 가정했을때 메모리 테이블을 보면 참조형 데이터 테이블 obj라는 변수를 선언하고 객체의 프로퍼티를 담을 메모리 주소(3000)를 참조한다. 그리고 프로퍼티를 담은 메모리에는 프로퍼티에 대한 메모리 주소가 저장된다. a라는 프로퍼티를 선언하고 a에 할당할 1이 저장되어 있는 메모리 주소를 저장한다. b도 마찬가지로 저장한다.

기본형 데이터는 메모리 주소를 바로 참조하는 반면에 참조형 데이터는 객체의 프로퍼티 영역이 별도로 존재한다. 이 영역은 직접 값을 저장하는 것이 아닌 기본형 데이터를 저장한 메모리 주소를 저장한다는 점이다. 그래서 메모리 주소는 변할수 있다는 점에서 가변성이 있다는 것이다.

var obj = {
  a : 1,
  b : 'abc'
};

obj.a = 2;

객체의 값을 한번 바꿔보면 Image obj 자체는 변함이 없지만 내부 프로퍼티의 메모리 주소는 바뀐 것을 볼수가 있다. 이렇게 불변성과 가변성이 어떤 의미를 가지는지 알수가 있다.

변수 복사 비교

변수를 복사하고 값을 바꾼후에 비교하면 이 성질에 의해 데이터가 어떻게 변하는지 알기 쉽다.

var a = 10;
var b = a;

var obj = { c : 10, d : 'abc' };
var obj2 = obj;

하나는 기본형 데이터를 복사하고 하나는 참조형 데이터를 복사해보겠다. Image 이런 식으로 데이터가 저장된다. 좀 복잡해 보이긴 하지만 천천히 둘러보면 a를 복사한 b는 a와 같은 메모리 주소를 참조하고, obj를 복사한 obj2도 같은 객체 데이터 주소를 참조한다. 이제 이 값을 바꿔보겠다.

b = 15;
obj2.c = 20;

복사를 했으니 전부다 바뀔지 복사본만 바뀔지 헷갈릴 것이다. 하지만 불변성과 가변성을 이해하고 있다면 금방 답이 추론된다. Image a와 b는 기본형 데이터를 저장하고 있기 때문에 불변성에 의해서 다른 값으로 바뀐것을 볼수가 있다. 하지만 obj와 obj2는 객체 내부 프로퍼티의 메모리 주소만 바뀌었고 객체 데이터의 메모리는 바뀐것이 없기 때문에 결국 같은 메모리를 바라보게 된다.

console.log(a); // 10
console.log(b); // 15
console.log(obj); // { c : 20, d : 'abc' }
console.log(obj2); // { c : 20, d : 'abc' }

a !== b // true
obj === obj2 // true

기본형 데이터는 복사를 해도 경우는 불변성에 의해 다른 데이터가 되었고, 참조형 데이터는 복사를 해도 가변성에 의해 같은 데이터로 유지가 되는 것을 볼수 있다.

불변 객체 만들기

그럼 객체같은 참조형 데이터는 복사를 할수 없을까? 아니다. 완전히 다른 메모리 주소인 참조형 데이터를 만들어 줌으로서 복사가 가능하다.

var obj = { c : 10, d : 'abc' };
var obj2 = obj;

obj2 = { c : 15, d : 'abc' };

console.log(obj===obj2); // false

obj와 obj2둘다 c와 d라는 프로퍼티를 가진 객체이다. 내부의 프로퍼티의 값이 같지만 실제로는 완전히 다른 객체이기 때문에 다른 데이터이다. 이런 방식으로 완전히 다른 객체를 만들어서 할당해줌으로서 참조형 데이터의 복사가 가능하다. 함수화 시켜서 객체 내부의 프로퍼티를 전부 복사하는 로직을 작성해보겠다.

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

for...in을 사용해서 객체 프로퍼티를 전부 복사하는 것이다.

매번 이 로직을 작성할 필요는 없다. 자바스크립트 메소드를 사용하면 된다.

  • spread(...)
  • 객체인 경우 : Object.assign()
  • 배열인 경우 : data.slice()

하지만 이 로직만으로 완전히 복사는 안된다. 만약 객체 내부에 또다른 배열이나 객체가 있는 경우 프로퍼티에 있는 객체까지는 복사를 못하기 때문이다.

얕은 복사, 깊은 복사

복사에도 종류가 있나 싶지만 차이가 있다.

  • 얕은 복사 : 바로 아래 단계의 값만 복사
  • 깊은 복사 : 내부의 모든 값을 전부 복사

위에서 만들었던 객체의 경우는 얕은 복사로도 복사가 된다. 하지만 이중 객체인 경우 얕은 복사로는 완전히 복사되지 않는다.

var obj = {
  a : 10,
  b : {
      c : 'abc',
      d : 'def'
   }
};
var obj2 = copyObject(obj);
obj.a = 20;
obj2.b.c = '123';

console.log(obj.a); // 10
console.log(obj2.a); // 20
console.log(obj.b.c); // '123'
console.log(obj2.b.c); // '123'

바로 아래단계에 있는 a는 잘 복사가 되었지만 이중으로 들어있는 객체의 내부 프로퍼티까지는 복사하지 못한 모습이다. 그래서 저렇게 내부에 있는 모든 값을 복사하는 것이 깊은 복사이다. copyObject처럼 내부의 값들까지 전부 복사하도록 로직을 작성해도 되지만 다른 방법만 적어보겠다.

우선 단순히 데이터로만 이뤄진 객체를 복사한다면 JSON으로 변환시키면 된다.

var obj = {
  a : 10,
  b : {
      c : 'abc',
      d : 'def'
   }
};
var obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj.b===obj2.b); //false

JSON으로 변환하게 되면 메모리 참조가 끊긴 상태로 데이터가 변환된다. 그래서 완전히 다른 객체를 만들어서 할당하는 것이다. 이 방법은 몇가지 단점이 있다. JSON으로 변환하지 못하는 function,setter,getter는 복사하지 못한다는 점이다. 그래서 일반적인 데이터를 복사할때 사용하면 유용하다.

undefined과 null

자바스크립트에서 없음을 나타내는 값들이다. 둘다 없다는 의미지만 완전히 같은 의미를 가지진 않는다.

undefined

undefined라는 단어 자체에서 알수 있듯이 정의되지 않음을 의미한다. 사용자가 직접 지정할 수도 있지만 자바스크립트 엔진이 자동으로 부여하는 값이기도 하다.

  • 값을 대입하지 않은 변수
  • 객체 내부의 존재하지 않는 프로퍼티에 접근할 때
  • return문이 없거나 호출되지 않는 함수의 실행 결과

이 세가지 경우에 자동으로 undefined를 부여한다.

var a;
var obj = { a : 1 };
var func = function() {};
var b = func()

console.log(a); // undefined
console.log(obj.b); // undefined
console.log(b); // undefined

그렇다면 길이를 정한 배열에 아무런 값이 없는 경우는 어떨까?

var arr = [];
arr.length = 3;
console.log(arr); // [empty x 3]

undefined도 아니고 empty라는 값이 출력된다. 배열의 길이를 3으로 지정하면서 그만큼의 메모리를 확보했지만 아무런 값도 지정되지 않았다는 의미이다.

var arr = new Array(3);
console.log(arr); // [empty x 3]

이 경우도 같다. 생성자 함수를 통해 생성했지만 아무런 값이 지정되지 않았다는 것이다. empty는 undefined와 다른 출력 결과를 보여주기도 한다.

var arr = [undefined, 1];
var arr2 = [];
arr2[1] = 1;

arr.forEach(function (v, i) { console.log(v, i); }); // undefined, 0 / 1, 1
arr2.forEach(function (v, i) { console.log(v, i); }); // 1, 1

이렇게 empty는 아무런 값도 지정되지 않은 공간이기 때문에 순회를 할수도 없는 것이다. 그래서 배열을 순회하는 로직을 작성해도 건너뛰고 동작하게 된다. 좀더 쉽게 얘기하자면 배열도 결국 객체이다. 아무런 값을 지정하지 않았기 때문에 아직은 존재하지 않는 프로퍼티가 되는 것이다. 결국 배열도 인덱스값을 식별자로 가지는 객체라고 생각하면 좋을 것 같다.

null

비어있는 값에 대해서 undefined나 empty와 같은 상황이 발생하니 사용자가 구별하기 쉽도록 도와주는 것이 null이다. 비어있음을 직접 표현하고 싶다면 undefined이 되도록 놔두는 것이 아니라 null을 지정해주면 된다. 그러면 비어있는 값에 대해서는 null이 적용되고, 우리가 통제가능한 범위를 넘어가는 값들에 대해선 undefined가 되어 쉽게 파악가능하다.

주의할점! null은 typeof null이 object이다. 자바스크립트 버그이기 때문에 조심해야 한다. 그래서 null인지 확인하기 위해서 typeof null과 같은 방법을 사용하면 안된다. 그래서 null을 확인할때는 일치 연산자(===)을 사용해서 확실하게 null임을 확인해줘야 한다.

마무리

나름 코어 자바스크립트의 내용을 잘 정리해보려고했는데 잘 되었는지 모르겠다. 하지만 부족하거나 나중에 새롭게 알게 되는 부분이 있다면 다시 정리하면 되니까 걱정하지 말아야겠다. 이후엔 실행 컨텍스트에 대해서 정리할 것이다. 자바스크립트에서 어떤 방식으로 변수가 사용되고 함수가 실행되는지 이해하게 될것이다.

개의 댓글