구리구리구리

[리뷰] 코어 자바스크립트 1장 - 데이터 타입 본문

독서

[리뷰] 코어 자바스크립트 1장 - 데이터 타입

guriguriguri 2023. 5. 30. 21:49

이 글은 코어 자바스크립트 책을 읽으며 정리한 글입니다.

 

데이터 타입

1-1. 데이터 타입에 관한 배경지식

컴퓨터는 메모리에 데이터를 저장한다. 메모리는 많은 비트(bit)들로 구성되어 있으며 각 비트는 고유한 식별자로 구성된다.

 

바이트(byte)

  • 비트로 위치 확인하는 것이 비효율적이며 표현 가능한 개수에 어느정도 제약이 있어도 문제되지 않을만큼의 적정한 공간을 묶기 위해 탄생되었다.
  • 모든 데이터는 바이트 단위 식별자 (메모리 주소값)으로 구분된다.
1bit = 0 또는 1 (2개의 값 표현 가능)
1byte = 8bit (2의 8제곱, 256개의 값 표현 가능)

 

변수와 식별자의 차이점

  • 변수 : 변할 수 있는 데이터
  • 식별자 : 어떤 데이터를 식별하는데 사용하는 이름 (즉, 변수명)

 

1-2. 변수 선언과 데이터 할당

변수 선언시 동작 방식

var a; => 변할 수 있는 데이터를 만드는데 이 데이터의 식별자는 a다

위 코드에서 a는 변할 수 있는 데이터이기에 선언시 undefined 여도  나중에 다른 값으로 변경이 가능하다.

따라서 변수변경 가능한 데이터를 담을 수 있는 공간이라는 개념에 가깝다.

 

위 코드를 메모리 영역에서 작업을 수행하는 흐름 (메모리 구조는 개략적으로 표현)

주소 1001 1002 1003
데이터     이름 : a
데이터 :
  1. 명령 받은 컴퓨터는 메모리에서 빈 공간 하나를 확보한다. (임의로 @1003 부여)
  2. 이 공간의 식별자를 a로 지정한다.
  3. 사용자가 a에 접근시, 컴퓨터는 메모리에서 a라는 식별자를 가진 주소를 검색 후 해당 공간의 데이터를 반환한다.

데이터 할당시 동작 방식

(1) 데이터 할당

// (1)
var a;			// 변수 a 선언
a = "abc";		// 변수 a에 데이터 할당

// (2)
var a = "abc";	// 변수 선언과 할당을 한 문장으로 표현

 

변수 영역 주소 1001 1002 1003
데이터     이름 : a
데이터 : @5004
데이터
영역
주소 5003 5004 5005
데이터   "abc"  
  1. 선언 과정 진행
  2. 데이터를 저장하기 위한 별도의 메모리 공간(@5004)을 다시 확보해 문자열을 저장한다.
  3. 변수 영역에서 식별자 a를 검색한다 (@1003)
  4. 해당 공간(@1003)에 앞서 저장한 문자열 데이터의 주소(@5004)를 @1003의 공간에 대입한다.

데이터의 성질에 따라 변수 영역, 데이터 영역을 나눴으며 이해를 돕기 위한 개념으로 정식 명칭이 아니다.

 

위 과정을 보면 데이터 할당시 번거롭게 한단계를 더 거치는 이유가 뭘까?

JS에서는 문자열 데이터에 정해진 규격이 없고 한글자마다 영어는 1byte, 한글은 2byte로 필요한 메모리 용량이 가변적이다.

만약 미리 확보한 공간 내에서만 데이터 변환이 가능하다면, 변환한 데이터를 다시 저장하기 위해 '확보된 공간을 변환 데이터의 크기에 맞게 늘리는 작업'이 선행되야 하는 단점이 존재한다.

결론적으로는 효율적으로 문자열 데이터의 변환 처리를 위해선 변수, 데이터를 별도의 공간에 저장하는 것이 최적이기 때문이다.

 

(2) 변수 a를 다른 문자열로 변경

변수 영역 주소 1001 1002 1003
데이터     이름 : a
데이터 : @5005
데이터
영역
주소 5003 5004 5005
데이터   "abc" "abcdef"

 문자열 'abc'의 마지막에 'def'를 추가할 경우, 컴퓨터는 'abc'가 저장된 공간에 'abcdef'를 할당하는 것이 아니라 별도의 공간에 문자열을 저장하고, 주소를 변수 공간에 연결한다.

즉, 기존 문자열에 어떤 변환을 가하든 상관 없이 무조건 새로 만들어 별도의 공간에 저장한다.

(기존 (@5004) 데이터는 자신의 주소를 저장하는 변수가 더이상 없을 경우, 가비지 컬렉터의 수거 대상이 된다.)

 

(3) 500개의 변수 생성 후, 모든 변수에 숫자 '5'를 할당

  • 숫자 '5'를 데이터 영역의 별도의 공간에 1번만 저장한다.
  • 모든 변수는 '5'에 대한 주소를 기억한다.
  • 숫자형 데이터는 8바이트가 필요하므로, 주소 공간의 크기가 2바이트라고 한다면 4000(500 * 8)바이트가 아닌 1008(500 * 2 + 8)만 이용하면 된다.

이처럼, 변수 영역과 데이터 영역을 분리하면 중복된 데이터 처리 효율이 높아진다.

 

1-3. 기본형 데이터와 참조형 데이터

  • 변수와 상수를 구분하는 성질은 변경 가능성인데, 이 대상은 변수 영역의 메모리이다. (한번 데이터 할당이 에뤄진 변수 공간에 다른 데이터를 재할당할 수 있는지의 여부가 관건)
  • 불변성 여부를 구분할 때의 변경 가능성의 대상은 데이터 영역이다.
    • 문자열이나 숫자 값도 다른 값으로 변경할 수 없는데, 이것이 불변값의 성질이다. (한번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않음)

(1) 참조형 데이터 변수 할당 방식

var obj1 = {
	a: 1,
	b: 'bbb'
};
변수 영역 주소 1001 1002 1003 1004 1005
데이터   이름 : obj1
데이터 : @5001
     
데이터
영역
주소 5001 5002 5003 5004 5005
데이터 @7103~?   1 'bbb'  
객체 @5001의 변수영역 주소 7103 7104 7105 7106 7107
데이터 이름 : a
데이터 : @5003
이름 : b
데이터 : @5004
     
  1. 변수 영역의 임의의 공간(@1002)에 식별자 obj1을 지정한다.
  2. 임의의 데이터 영역(@5001)에 데이터를 저장하려 했는데, 여러 개의 프로퍼티로 구성된 데이터 그룹이므로 내부 프로퍼티를 저장하기 위한 별도의 변수 영역을 마련하여, 그 영역의 주소를 @5001에 저장한다. (객체의 프로퍼티를 저장하기 위한 메모리 영역은 크기가 정해져 있지 않으며 필요한 시점에 동적으로 확보)
  3. @7103, @7104에 각 식별자 a,b를 지정한다.
  4. 데이터 영역에서 1을 검색 후, 결과가 없으므로 별도의 데이터 영역(@5003)에 1을 저장하고 영역의 주소를 @7103에 저장한다. 문자열 'bbb' 역시 임의로 @5004에 저장 후, 주소를 @7104에 저장한다.

위 과정을 데이터 영역은 모두 불변값이지만 변수에는 얼마든지 다른 값을 대입할 수 있다. 이와 같은 이유로 참조형 데이터는 불변하지 않다(가변적)이라고 하는 것이다.

 

(2) 중첩된 참조형 데이터(객체)의 프로퍼티 할당 방식

var obj = {
	x: 3,
	arr: [1, 2, 3]
};
  1. 변수 영역의 임의의 공간(@1002)에 식별자 obj를 지정한다.
  2. 데이터 영역의 임의의 공간(@5001)에 데이터를 저장하려 했는데 여러개의 프로퍼티로 구성되어 있으므로 별도의 변수 영역(@7103~?)을 마련 후 주소를 @5001에 저장한다.
  3. @7103, @7104에 식별자 x, arr을 지정한다.
  4. 데이터 영역에 3을 검색 후, 검색 결과가 없으므로 데이터 영역의 임의의 공간(@5002)에 3을 저장 후 @7103에 주소를 저장한다.
  5. 데이터 영역의 임의의 공간 @5003에 데이터를 저장하려 했는데 배열 타입의 데이터이므로, 변수 arr을 위한 별도의 변수 영역(@8104~?)을 지정 후 해당 주소를 @5003에 저장하고 @5003 주소를 @7104에 저장한다.
  6. 배열의 요소가 총 3개이므로 3개의 변수 공간을 확보하고 각각의 인덱스를 부여합니다 (0, 1, 2)
  7. 데이터 영역에서 숫자 3을 검색해 (@5002) 그 주소를 @8001에 저장합니다.
  8. 데이터 영역에 숫자 4가 없으므로 @5004에 저장하고, 이 주소를 @8105에 저장합니다.
  9. 데이터 영역에 숫자 5가 없으므로 @5005에 저장하고, 이 주소를 @8106에 저장합니다.

 

(3) 변수 복사 이후 값 변경 결과 비교 (1) - 객체의 프로퍼티 변경 시

var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;

b = 15;
obj2.c = 20;

 

주소 1001 1002 1003 1004 1005 1006
데이터 이름 : a
값 : @5001
이름 : b
값 : @5004
이름 : obj1
값: @5002
이름 : obj2
값 : @5002
   
주소 5001 5002 5003 5004 5005 5006
데이터 10 @7103 ~ ? 'ddd' 15 20  
주소 7103 7104 7105 ....    
데이터 이름 : c
값 : @5005
이름 : d
값: @5003
       

과정은 위에서 많이 설명했었기에 생략하고 코드 전체가 실행되었을 때 메모리 구조를 표로 표현했습니다.

기본형 데이터를 복사한 변수 b의 값을 바꿨더니 @1002의 값이 달라진 반면, 참조형 데이터를 복사한 변수 @1004의 프로퍼티 값 변경시 @1004의 값을 달라지지 않았습니다.

즉, 변수 a와 b는 서로 다른 주소를 보게 됐지만, 변수 obj1과 obj2는 여전히 같은 객체를 바라보고 있습니다.

 

대부분의 JS를 다루는 책에선 '기본형은 값을 복사하고 참조형은 주솟값을 복사한다'라고 설명하지만 사실은 어떤 데이터 타입이든 변수에 할당하기 위해선 주솟값을 복사해야 하기에, 엄밀히 따지면 자바스크립트의 모든 데이터 타입은 참조형 데이터일 수밖에 없습니다.

다만 기본형은 주솟값을 복사하는 과정이 한 번만 이뤄지고, 참조형은 한 단계를 더 거치게 된다는 차이가 존재합니다.

 

(4) 변수 복사 이후 값 변경 결과 비교 (2) - 객체 자체를 변경 시

var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;

b = 15;
obj2 = { c: 20, d: 'ddd' };
주소 1001 1002 1003 1004 1005 1006
데이터 이름 : a
값 : @5001
이름 : b
값 : @5004
이름 : obj1
값: @5002
이름 : obj2
값 : @5006
   
주소 5001 5002 5003 5004 5005 5006
데이터 10 @7103 ~ ? 'ddd' 15 20 @8204 ~ ?
주소 7103 7104 7105 .... 8204 8205
데이터 이름 : c
값 : @5001
이름 : d
값: @5003
    이름 : c
값 : @5005
이름 : d
값: @5003

이번에는 obj2에도 새로운 객체를 할당함으로써 값을 직접 변경함으로써 메모리의 데이터 영역의 새로운 공간에 새 객체가 저장되고 그 주소를 변수 영역의 obj2 위치에 저장했습니다.

이로써, 참조형 데이터가 '가변값'이라고 설명할 때 '가변'은 참조형 데이터 자체를 변경할 경우가 아닌 그 내부의 프로퍼티를 변경할 때만 성립합니다.

1-4. 불변 객체

불변 객체는 보통 값으로 전달 받은 객체에 변경을 가해도 원본 객체는 유지하고 싶은 경우에 사용됩니다. 예를 들면 정보가 바뀐 시점에 알림을 보내거나 바뀌기 전의 정보, 바뀐 후의 정보를 가시적으로 보여줘야 하는 등의 기능 구현하는 경우가 있겠습니다.

불변 객체를 만들기 위한 방법은 다양하게 있지만 대표적으로 immutable.js 같은 라이브러리를 이용해 불변성을 띄는 별도의 데이터 타입과 그에 따른 메소드를 사용할 수 있습니다.

 

(1) 얕은 복사, 깊은 복사

  • 얕은 복사 : 바로 아래 단계의 값만 복사하는 방법입니다.
  • 깊은 복사 : 내부의 모든 값들을 복사하는 방법입니다.

얕은 복사 예시

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

var user = {
  name: 'Raccoon',
  favorite: {
    fruit: 'apple',
    drink: 'coke',
    hobby: 'drive'
  }
}

var user2 = copyObject(user);

user2.name = 'Hello';
console.log(user.name === user2.name); // false

user2.favorite.fruit = 'banana';
console.log(user.favorite.fruit === user2.favorite.fruit); // true

얕은 복사시 단점은 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티 복사시 주솟값만 복사하기에, 사본을 바꾸면 원본도 바뀌는 점이 있습니다.

 

깊은 복사 예시 

var copyObjectDeep = function (target) {
  var result = {};
  if (typeof target === "object" && target !== null) {
    for (var prop in target) {
      result[prop] = copyObjectDeep(target[prop]);
    }
  } else {
    result = target;
  }
  return result;
};

var user = {
  name: "Raccoon",
  favorite: {
    fruit: "apple",
    drink: "coke",
    hobby: "drive",
  },
};

var user2 = copyObjectDeep(user);

user2.name = "Hello";
console.log(user.name === user2.name); // false

user2.favorite.fruit = "banana";
console.log(user.favorite.fruit === user2.favorite.fruit); // false

copyObjectDeep 함수 내에서 target이 객체인 경우 내부 프로퍼티들을 순회하며 copyObjectDeep 함수를 재귀적으로 호출하고, 객체가 아닌 경우 target을 그대로 지정하게끔 했습니다. (target !== null 조건을 붙인 이유는 typeof 명령어가 null에 대해서도 'object'를 반환하는 자바스크립트 버그 때문입니다.)

이 함수를 이용해 객체 복사 후 어느 쪽의 프로퍼티를 변경해도 다른 쪽에 영향을 주지 않습니다.

 

깊은 복사를 수행하는 다른 방법들은 다음과 같습니다.

  1. 위 코드와 같이 범용 함수를 직접 구현해 내부에서 재귀적으로 호출
  2. 객체를 JSON 문법으로 표현된 문자열로 전환 후 다시 JSON 객체로 변경 (메서드, getter/setter같이 JSON으로 변경할 수 없는 프로퍼티는 무시되므로 httpRequest로 받은 데이터를 저장한 객체 복사하는 등 순수힌 정보만 다룰 때 활용하면 좋습니다)

 

1-5. undefined와 null

자바스크립트에서 값이 없음을 나타내는 값은 2가지로 undefined와 null을 사용하는 것입니다.

undefined는 (1) 사용자가 명시적으로 지정할 수도 있고 (2) JS 엔진이 자동으로 부여하는 경우도 있습니다.

JS 엔진은 사용자가 어떤 값을 지정할 것이라고 예상하지만 실제로는 그러지 않았을 때 undefined를 부여하는데 케이스는 다음과 같습니다.

  1. 값이 할당되지 않은 변수에 접근하는 경우
  2. 내부에 존재하지 않는 프로퍼티에 접근하는 경우
  3. return 문이 없거나 호출되지 않은 함수의 실행 결과

 

(1) 2가지 경우의 undefined의 의미

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

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

arr1.map(function (v, i) {
  return v + i;
}); // [Nan, 2]
arr2.map(function (v, i) {
  return v + i;
}); // [empty, 2]

arr1.filter(function (v, i) {
  return !v;
}); // [undefined]
arr2.filter(function (v, i) {
  return !v;
}); // []

arr1.reduce(function (p, c, i) {
  return p + c + i;
}, ""); // undefined011
arr2.reduce(function (p, c, i) {
  return p + c + i;
}, ""); // 11

예제 arr1은 undefined와 1을 직접 할당한 반면 arr2는 빈 배열의 인덱스 1에 값 1을 할당했습니다.

두 배열의 결과값이 다른 이유는 비어 있는 요소는 순환 관련 배열 메소드들의 순회 대상에서 제외되기 때문입니다.

 

이는 배열에서만 일어나는 특이한 현상처럼 보이지만, 사실 배열도 객체이기에 자연스러운 현상입니다.

객체와 마찬가지로 특정 인덱스에 값을 지정해야만 비로소 빈 공간 확보 후 인덱스를 식별자로 지정하고 데이터 영역의 주솟값을 저장하는 등의 동작을 합니다.

즉, 값이 지정되지 않은 인덱스는 '아직은 존재하지 않는 프로퍼티'일 뿐입니다.

 

위 케이스를 보다시피 undefined는 사용자가 명시적으로 부여한 경우와 비어있는 요소에 접근하려 할 때 반환되는 두 경우의 의미를 구분할 수 있습니다.

사용자가 직접 명시 : 값으로 할당된 undefined는 실존 데이터
JS 엔진이 자동으로 부여 : 문자 그대로 값이 없음을 의미

위 문구만을 읽었을 때, 혼란스러울 수 있기에 undefined는 하나의 경우에만 사용하는 것이 좋을 것 같습니다.

따라서 undefined를 직접 할당하지 않고 대신 null을 사용해 명시적으로 나타낼 수 있습니다.

 

(2) null 사용시 주의점

var n = null;
console.log(typeof n); // object

console.log(n == undefined); // true
console.log(n == null); // true

console.log(n === undefined);  // false
console.log(n === null);  // false

null은 typeof가 object라는 JS 버그가 있기에 어떤 변수의 값이 null인지 판별하기 위해 === 일치 연산자를 사용하는 것이 좋습니다.

 


Quiz

1. 아래 코드 실행시 메모리 영역에서 작업을 수행하는 흐름을 변수 영역, 데이터 영역으로 나눠 3단계로 나누어 서술해보세요.

var a = 1;
더보기
  1. 컴퓨터는 변수 영역의 메모리에서 빈 공간을 확보해 해당 공간(@1001)의 식별자를a로 지정한다.
  2. 데이터 영역의 메모리에서 1이라는 데이터가 있는지 검색 후, 결과가 없다면 빈 공간(@5001)을 다시 확보해 1이라는 데이터를 저장한다.
  3. 해당 공간의 주소를 @1001에 저장한다.

2. 참조형 데이터 타입 사용시 주의해야 하는 점과 이유는 무엇일까요?

더보기

참조형 데이터를 변수에 할당시, 실제로는 메모리 영역에서 그 값이 위치한 객체의 주소값을 가리키는 참조값을 저장한다. 즉, 두 변수가 같은 객체를 참조하기에 한 변수의 내부 프로퍼티 변경시 다른 변수의 프로퍼티도 변경될 수도 있다는 점을 주의해야 한다.

3. 참조형 데이터의 내부 프로퍼티를 변경하는 것과 같은 상황에서 불변객체(Immutable Object)를 사용하면 어떤 장단점이 있을까요?

더보기

불변 객체는 내부 프로퍼티 변경시, 기존 객체를 유지하고 새로운 객체를 생성하는 방식으로 동작한다. 즉, 불변 객체는 값이 변경될 수 없기에 오류 케이스가 줄어들지만 불변 객체를 사용함으로써 메모리 공간의 낭비가 발생할 수 있다는 단점도 존재한다.


관련 글

https://github.com/raccoon-ccoder/TIL/blob/main/JavaScript/%5B%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%20%EC%84%A0%EC%96%B8%20%EB%B0%A9%EC%8B%9D%5D%20var%2C%20let%2C%20const.md

 

GitHub - raccoon-ccoder/TIL: Today I Learned

Today I Learned. Contribute to raccoon-ccoder/TIL development by creating an account on GitHub.

github.com