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

데이터 타입

시작

오늘은 자바스크립트의 원론적인 개념에 대해 공부한 내용을 정리한다.

데이터 할당

우선 자바스크립트내 모든 데이터는 byte단위의 식별자인 메모리 주소값을 통해 서로 구분이 된다.
간혹 자바스크립트를 그냥 사용만 하다가 변수와 식별자의 차이를 모르는 경우가 있는데 차이는 다음과 같다.

js
var result = 3;
// 위 구문에서 result는 식별자 3은 변수

또한 변수는 다음과 같이 할당한다.

js
/** 풀어 쓴 변수 할당 방식 */
var value;
value = 'value'; // 할당

/** 붙여 쓴 변수 할당 방식 */
var value = 'value';

데이터 할당의 경우 변수 영역에는 식별자를 저장, 데이터 영역에는 할당된 데이터가 저장된다. 식별자는 데이터 영역에 저장된 데이터의 주소를 값으로 가진다. 식별자는 일종의 키(key)이다.
식별자의 경우 2byte 데이터는 타입에 따라 다르지만 숫자는 8byte이다. 이와 같이 저장하는 방식은 메모리의 공간을 보다 덜 차지할 수 있기 때문이다.

자바스크립트의 데이터 타입 구분

자바스크립트에서 데이터 타입은 기본형과 참조 형으로 나뉜다. 기본형과 참조형은 다음과 같이 구분이 가능하다.

복제 방법의 구분

기본형"
주로 원시 타입의 데이터가 저장되며 값이 담긴 주소값을 바로 복제한다.

참조형"
객체(Object)를 저장하며 값이 담긴 주소값들로 이루어진 묶음(객체)을 가리키는 주소값을 복제한다. 내부 프로퍼티의 주소는 직접 참조하지 않는다.

불변성에 의한 구분

데이터 영역 에 저장된 데이터가 불변이느냐에 따라 구분이 가능하다.

불변값과 불변성
아래와 같이 2번째 라인에서 값을 다시 할당하는 경우 데이터 영역에 새로운 공간에 abcdef가 저장되고 식별자는 해당 주소를 참조하도록 변경된다.
즉 이전에 존재하던 abc가 저장된 영역은 불변하다고 볼 수 있다. 물론 이 공간은 더 이상 사용되지 않으므로 메모리 공간의 효율을 위해 가비지 컬렉팅에 의해 언젠가 소멸된다.

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

가변값과 가변성
기본형의 경우 불변, 참조형의 경우 가변한다.
다음 코드의 경우 객체이다. 객체는 프로퍼티를 저장할 별도의 공간을 가지게 된다. 이 별도의 공간에서 프로퍼티의 식별자 즉, 키 값이 저장되고 데이터 영역에 값이 저장된 주소를 참조하여 변수 영역'obj1' 식별자의 값으로 존재한다. 즉 주소를 저장한다.
그래서 객체의 경우 불변성 여부를 따지자면 그렇지 않다고 볼 수 있다. obj1의 값인 프로퍼티가 수정된다면 별도의 공간에서 프로퍼티의 값이 새로운 데이터 영역의 주소를 참조하므로 가변이 된다.

js
var obj1 = {
  a: 1,
  b: 'bbb',
}

즉, 변수 영역, 데이터 영역은 공통으로 존재하지만 참조형 데이터의 경우 프로퍼티를 위한 별도의 공간을 가지게 된다. 물론 프로퍼티의 값을 즉, 새로운 데이터 영역의 공간 주소가 할당된다면, 이전 공간도 가비지 콜렉터에 의해 소멸이 된다.

[그림 첨부 예정]

데이터 영역 값의 변경

다음과 같은 경우도 존재한다. 다음 코드에서 b는 a와 같은 데이터영역의 주소를 참조한다.
또한 obj2는 obj1과 같은 별도의 공간(프로퍼티를 저장한 공간)을 참조한다. 만일 이 상태에서 값이 변경된다면 어떻게 될까?

js
// 기본형 데이터
var a = 10;
var b = a;

// 참조형 데이터
var obj1 = { c: 10, d: 'ddd'};
var obj2 = obj1;

기본형과 참조형의 복사

다음과 같이 값이 변경되었다고 생각하자.

js
b = 15;
obj2.c = 20;

우선 기본형의 경우 데이터 영역내 새로운 공간에 15를 저장한다. 참조형의 경우 데이터 영역내 새로운 공간에 20을 저장하고 해당 메모리 주소를 별도의 공간에 저장된 c식별자의 값을 해당 주소로 변경한다. 그럼 원본 변수와 비교를 진행해보자. 우선 b는 새로운 값을 참조한다. 즉 a===bfalse이다.
단 객체의 경우 예상과는 달리 true가 결과값으로 나온다. 이유는 별도의 공간에 저장된 프로퍼티 식별자의 값이 둘다 변경되었기 때문이다. 이로 인해 생길 문제가 있다. 분명 가변성이지만 이 경우의 코드는 불변성으로 만들어줘야 할 경우가 있다.

js
a === b // false 다르다.
obj1 === obj2 // true 같다.

즉, obj1의 프로퍼티도 새로운 데이터영역의 주소를 참조한다. 이제 obj2의 프로퍼티 값을 바꾸어도 obj1에 영향이 가지 않도록 변경해보도록 하자.

불변 객체 생성하기

별도의 공간에 새로운 프로퍼티를 저장해 원본 객체와 다른 객체를 생성해보도록 하자.

js
var user = {
  name: 'wonjang',
  gender: 'male'
}
// input: user객체, 바꿀 이름 output: 이를 바꿔서 복사한 그 객체
var changeName = function(user, newName){
  var newUser = user;
  newUser.name = newName;
  return newUser;
}

var user2 = changeName(user, 'twojang');
// 아직까진 불변의 객체는 만들지 못했다. 객체의 값의 주소.
// 즉 별도의 공간에 저장된 같은 프로퍼티가 저장된 주소는 같기 때문이다.
if(user !== user2){
  console.log('유저 정보가 변경되었습니다.')
}

위 코드에서 6~10 라인에서 함수는 매개변수로 user, newName이 존재한다. 이 매개변수로 새로운 객체를 만들고, name을 설정한 후 이를 반환하는 함수를 볼 수 있다. 여기서 15번 라인의 if구문은 작동하지 않고 넘겨버린다.

아래는 이 문제에 대한 개선 코드이다. 다음 코드는 if문이 정상적으로 동작한다.

js
var user = {
  name: 'wonjang',
  gender: 'male',
}

var changeName = function(user, newName){
  // 새로운 객체를 return
  return {
    name: newName,
    gender: user.gender,
  };
};

var user2 = changeName(user, 'twojang');

if(user !== user2){
  console.log('유저 정보가 변경되었습니다.')
}

다만 여기서도 문제는 존재한다. 지금은 프로퍼티가 2개이지만, 1500개가 담긴 프로퍼티의 경우 추가로 코드를 작성해야한다. 즉, 비효율적이다. 결국 좋은 코드는 아니다. 더 나은 방법을 살펴보도록 하자.

얕은 복사를 이용한 더 효율적인 방법

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

target이라는 매개변수를 받아서 for...in 을 통해 인덱스를 하나씩 순회하여 result에 반영해주고 이를 반환한다. 즉, 추가적인 코드를 작성할 필요가 없다.

위 코드를 이전 코드에 적용하면 다음과 같은 코드가 완성된다.

js
var user = {
  name: 'wonjang',
  gender: 'male',
}

var user2 = copyObject(user);
user2.name = 'twojang';

if(user !== user2){
  console.log('유저 정보가 변경되었습니다.');
}

이 코드에서 user2는 완전히 다른 객체이다. 불변 객체와 효율적인 코드를 작성해보았지만 문제는 여전히 존재한다. 객체내 프로터티가 저장하는 값이 원시 타입이 아닌 경우 즉, 객체인 경우(배열 등) 문제가 발생한다. 이때는 깊은 복사가 필요하다.

얕은 복사와 깊은 복사

얕은 복사와 깊은 복사는 다음과 같이 정의할 수 있다.

얕은 복사"
바로 아래 단계의 값만 복사. 중첩된 객체의 경우(객체내 프로퍼티가 객체를 값으로 하는 경우) 참조형 데이터가 저장된 프로퍼티를 복사할 때, 주소값만 복사한다.

깊은 복사"
내부 모든 값들을 하나하나 다 찾아서 복사한다. 재귀를 통해 구현할 수 있다.

다음과 같은 중첩된 객체를 깊게 복사해보도록 하자.

js
var user = {
  name: 'wonjang',
  urls: {
    portfolio: 'https://github.com/WoogLim',
    blog: 'https://www.wooglim.dev/',
    facebook: 'http://facebook.com/wooglim'
  }
}

재귀를 이용한 중첩된 객체에 대한 깊은 복사

js
var user = {
  name: 'wonjang',
  urls: {
    portfolio: 'https://github.com/WoogLim',
    blog: 'https://www.wooglim.dev/',
    facebook: 'http://facebook.com/wooglim'
  }
}

var copyObjectDeep = function(target) {
  var result = {};
  if(typeof target === 'object' && target !== null){
    for(var prop in target){
      // 자기 자신을 호출 즉, 재귀한다.
      // 우선 맨처음은 조건을 만족하므로 재귀한다.
      // 두번째의 경우 프로퍼티가 object인 경우 다시 재귀한다.
      result[pop] = copyObjectDeep(target[prop]);
    }
  } else {
    // 재귀하여 호출했을때 객체가 아니라면 값을 할당한다.
    result = target;
  }
  return result;
}

var user2 = copyObject(user);
user2.urls = copyObject(user.urls);

user.urls.portfolio = 'https://github.com/limgeon'
console.log(user.urls.portfolio === user2.urls.portfolio2);

user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog);

이제 모든 문제가 해결되었지만 위 방법의 경우 코드가 일단 어렵다. 보다 쉽게 깊은 복사를 이루어낼 방법은 없을까? 답은 JSON을 이용하는 것이다.
이 JSON의 경우 다음에 정리해보도록 하자. 만일 정리가 된다면 이어서 링크를 포함시키도록 하겠습니다.

null과 undefined

번외로 null과 undefined는 둘다 없음을 의미한다.

undefined

undefined는 자바스크립트 엔진에서 값이 할당되지 않은 경우 자동으로 부여한다.

js
var a;
console.log(a); // undefined

var obj = { a: 1};
console.log(obj.a);
console.log(obj.b);
// console.log(b) // ReferenceError

var func = function() { };
var c = func();
console.log(c); // undefined

비어있는 요소와 undefined를 보자면 undefined는 하나의 타입이다. 다음 코드를 보면 마지막 arr3은 undefined값을 출력한다. 즉 비어있다와 undefined는 다르다.
empty의 경우 공간은 존재하지만 값은 존재하지 않는다.

js
var arr1 = []
arr1.length = 3;
console.log(arr1); // [<3 empty items]

var arr2 = new Array[3];
console.log(arr2); // [<3 empty items]

var arr3 = [undefined, undefined, undefined]
console.log(arr3); // [undefined, undefined, undefined]

다음 코드를 살펴보자 배열의 메서드를 이용한 undefined와 empty의 차이를 알아보도록 하자.

js
var arr1 = [undefined, 1];
var arr2 = [];
arr2[1] = 1;

//forEach
arr1.forEach((v, i) => {console.log(v, i);});
arr2.forEach((v, i) => {console.log(v, i);});

//map
arr1.map(function(v, i) => v + i);
arr2.map(function(v, i) => v + i);

//filter
arr1.filter(function(v) => !v);
arr2.filter(function(v) => !v);

//redece
arr1.redece(function(p, c, i) => {p + c + i}, '');
arr2.redece(function(p, c, i) => {p + c + i}, '');

공통점을 살펴보자면 empty의 경우 skip하지만 undefined의 경우 정상적으로 수행하는것을 볼 수 있다.

즉, undefined는 필요에 의해 할당한건지, 엔진에 의해 반영된 건지 구분지을순 없다. 때문에 명시적으로 없다라는 의미를 부여하고 싶다면 undefined를 사용하지 않는다.

null

null은 위에 말한 없다라는 것을 명시적으로 표현할 때 사용한다. 또한 null은 원시 타입이 아닌 객체로 만일 조건을 부여한다면 object인지만 검사해선 안되고 추가 작업이 필요하다.

js
var no = null;
if(typeof no === object && var != no)