CoreJS - 데이터 타입

SANGKU OH·2020년 11월 23일
2

CoreJavascript

목록 보기
1/10
post-thumbnail

메모리와 데이터

컴퓨터는 모든 데이터를 0 또는 1로 바꿔 기억한다.
0 또는 1만 표현할 수 있는 하나의 메모리 조각 = 비트(bit)
메모리는 매우매우 많은 비트들로 구성되어 있는데, 각 비트는 고유한 식별자를 통해 위치를 확인할 수 있다.

비트의 묶음인 바이트(byte)역시 시작하는 비트의 식별자로 위치를 파악할 수 있다.
모든 데이터는 메모리 주솟값을 통해 서로 구분하고 연결할 수 있다.

식별자와 변수

변수

변할 수 있는 무언가(데이터)

숫자도, 문자열도, 객체도, 배열 모두 데이터!

식별자

어떤 데이터를 식별하는데 사용하는 이름! 즉, 변수명이다.

변수 선언

평소에 하던 그거다.

let a

변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a로 한다.
변수란 결국 변경 가능한 데이터가 담길 수 있는 공간(그릇)으로 생각할 수 있다.
이 그릇에 숫자를 담았다가~ 문자열을 담았다가~~ 등의 다양한 명령을 내릴 수 있다!

데이터 할당

평소에 하던 그거 맞다.
자바스크립트의 엔진은 변수 선언을 한 후에, 데이터를 할당하던, 동시에 할당과 선언을 하던 같은 동작을 수행한다.

let a; // 변수 a 선언
a = 'abc' // 변수 a에 데이터 'abc'할당

let a = 'abc' // 변수 a선언과 데이터 'abc'할당을 한 문장으로 표현

하지만 실제로는 해당 위치에 문자열 'abc'를 직접 저장하지는 않는다.
데이터를 저장하기 위한 별도의 메모리 공간을 다시 확보해서 문자열 'abc'를 저장하고, 그 주소를 변수 영역에 저장하는 식으로 이뤄진다.

1. 변수 영역에서 빈 공간(@1003)을 확보한다.
2. 확보한 공간의 식별자를 'a'로 지정한다.
3. 데이터 영역의 빈 공간(@5004)에 문자열 'abc'를 저장한다.
4. 변수 영역에서 a라는 식별자를 검색한다(@1003)
5. 앞서 저장한 문자열의 주소(@5004)를 @1003의 값에 대입한다.

ex) 문자열 'abc'의 마지막에 'def'를 추가하라고 하면 컴퓨터는 앞서 'abc'가 저장된 공간에 'abcdef'를 할당하는 대신 'abcdef'라는 문자열을 새로 만들어 별도의 공간에 저장하고, 그 주소를 변수 공간에 연결한다. 반대로 'abc'의 마지막 'c'를 제거하라고 해도 새로 만든다.
기존 문자열에 어떤 변환을 가하든 상관 없이 데이터가 다르다면 무조건 새로 만들어 별도의 공간에 저장한다.

반대로 같은 데이터의 경우 변수의 값 부분에 해당 데이터의 주소만 할당해주면 되기 때문에 이러한 데이터 관리방법은 효율적인 메모리의 사용이 가능하다!

데이터 타입

원시형

원시형 타입의 데이터는 Number, String, Boolean, null, undefined, Symbol!
할당이나 연산시 복제되는 형태(추후에 블로깅할 깊은복사, 얇은복사와 관련이 있다.)

값이 담긴 주솟값을 바로 복제

원시형은 불변성의 성질이 있다.

참조형

할당이나 연산시 참조하는 형태

값이 담긴 주솟값들로 이루어진 [묶음]을 가리키는 주솟값을 복제

불변값

변수와 상수를 구분하는 성질은 '변경 가능성' 입니다.

변수와 상수를 구분 짓는 변경 가능성의 대상은 변수 영역 메모리!
한번 데이터 할당이 이뤄진 변수 공간에 다른 데이터를 재할당할 수 있는지 여부가 관건이다.

반면, 불변성 여부를 구분할 때의 변경 가능성의 대상은 데이터 영역의 메모리!

let a = 'abc';
a = a + 'def';

let b = 5;
let c = 5;
b = 7;

변수 a에 문자열 'abc'를 할당했다가 뒤에 'def'를 추가하면 기존의 'abc'가 'abcdef'가 되는 것이 아니다!
새로운 문자열 'abcdef'를 만들어 그 주소를 변수 a의 값으로 저장한다.

변수 b에 5를 할당한다.
그러면 컴퓨터는 일단 데이터 영역에서 5를 찾고, 없으면 그제서야 데이터 공간을 하나 만들어 5를 저장한다.
그 후에 변수 영역에서 b를 찾고, 그 후에 b의 값으로 5의 주소를 저장한다.

변수 c에 5를 할당한다.
데이터 영역에서 5를 찾는다. 위에서 데이터 공간에 5를 만들어 둔 것이 발견 된다.
그 후 변수 영역에서 c를 찾고, 그 후에 c의 값으로 5의 주소를 저장한다.

변수 b의 값을 7로 바꿔보자.
그러면 기존에 저장된 5 자체를 7로 바꾸는 것이 아니라 기존에 저장했던 7을 찾아서 재활용하고, 없으면 새로 만들어 b의 값에 7의 주소를 저장한다.
즉, 5자체가 변하는 것이 아니라 새로운 7이라는 데이터가 생성되고 주소가 변경되는 것이다. 따라서 같은 주소에 있는 데이터의 값 자체가 변하는 것이 아니다!(불변한다!)
한번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.

가변값

기본형 데이터 => 불변값
그렇다면 참조형 데이터는 모두 가변값일까?

참조형 데이터의 할당에 대해 살펴보자!

let obj = {
  a: 1,
  b: 'bbb'
};

  1. 컴퓨터는 우선 변수 영역의 빈 공간(@1002)를 확보하고, 그 주소의 이름을 obj1로 지정한다.
  2. 임의의 데이터 저장 공간(@5001)에 데이터를 저장하려고 보니 여러 개의 프로퍼티로 이뤄진 데이터 그룹이다! 이 그룹 내부의 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고, 그 영역의 주소를(@7103 ~ ?)를 @5001에 저장한다.
  3. @7103 및 @7104에 각각 a와 b라는 프로퍼티 이름을 지정한다.
  4. 데이터 영역에서 숫자 1를 검색한다. 검색 결과가 없으므로 임의로 @5003에 저장하고, 이 주소를 @7103에 저장한다. 문자열 'bbb'역시 임의로 @5004에 저장하고, 이 주소를 @7104에 저장한다.

원시형 데이터와의 차이는 '객체의 변수(프로퍼티) 영역'이 별도로 존재하는 점이다!
그림을 자세히 보면 객체가 별도로 할애한 영역은 변수 영역일 뿐 '데이터 영역'은 기존의 메모리 공간을 그대로 활용하고 있다!
데이터 영역에 저장 된 값은 모두 불변값이다.
그러나, 변수에는 다른 값을 얼마든지 대입할 수 있다.

let obj1 = {
  a: 1,
  b: 'bbb'
};
obj1.a = 2;

obj1의 a 프로퍼티에 숫자 2를 할당하려고 한다.
데이터 영역에서 숫자 2를 검색한다. 검색 결과가 없으므로 빈 공간인 @5005에 저장 후, 이 주소를 @7103(a의 변수주소)
변수 obj1이 바라보고 있는 주소는 @5001로 전혀 변하지 않고있다.
즉 '새로운 객체'가 만들어진 것이 아니라, 기존의 객체 내부 값만 바뀌게 된 것이다!

참조형 데이터의 프로퍼티에 다시 참조형 데이터를 할당하는 경우는?
이를 일컬어 중첩 객체(nested object)라고 한다.

let obj = {
  x: 3,
  arr: [ 3, 4, 5 ]
};
  1. 컴퓨터는 우선 변수 영역의 빈공간(@1002)를 확보하고 구 주소의 이름을 obj로 지정한다.
  2. 임의의 데이터 저장공간(@5001)에 데이터를 저장하려는데, 이 데이터는 여러 개의 변수와 값들을 모아놓은 그룹(객체)이다. 이 그룹의 각 변수(프로퍼티)들을 저장하기 위해 별도의 변수 영역을 마련하고(@7103~?), 그 영역의 주소를 @5001에 저장합니다.
  3. @7103에 이름 x를, @7104에 이름 arr를 지정한다.
  4. 데이터 영역에서 숫자 3을 검색한다. 없으므로 임의로 @5002에 저장 후, 이 주소를 @7103에 저장한다.
  5. @7104에 저장할 값은 배열로서 역시 데이터의 그룹이다. 이 그룹 내부의 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고(@8104~?), 그 영역의 주소 정보를(@8104~?)를 @5003에 저장한다.
  6. 배열의 요소가 총 3개이므로 3개의 변수 공간을 확보하고 각각의 인덱스를 부여한다(0, 1, 2).
  7. 데이터 영역에서 숫자 3을 검색해서(@5002) 그 주소를 @8104에 저장한다.
  8. 데이터 영역에서 숫자 4을 검색 후, 없으므로 @5004에 저장하고, 이 주소를 @8105에 저장한다.
  9. 데이터 영역에서 숫자 5를 검색 후, 없으므로 @5005에 저장하고, 이 주소를 @8106에 저장한다.

자네, 그렇다면 obj.arr[1]을 검색하는 과정은 궁금하지 않나?

  1. obj 검색1: obj라는 식별자를 가진 주소를 찾습니다(@1002).
  2. obj 검색2: 값이 주소이므로 그 주소로 이동합니다(@5001).
  3. obj 검색3: 값이 주소이므로 그 주소로 이동합니다(@7103~?).
  4. obj.arr 검색1: arr이라는 식별자를 가진 주소를 찾습니다(@7104).
  5. obj.arr 검색2: 값이 주소이므로 그 주소로 이동합니다(@5003).
  6. obj.arr 검색3: 값이 주소이므로 그 주소로 이동합니다(@8104~?).
  7. obj.arr[1] 검색 1: 인덱스 1에 해당하는 주소를 찾습니다(@8105).
  8. obj.arr[1] 검색 2: 값이 주소이므로 그 주소로 이동합니다(@5004).
  9. obj.arr[1] 검색 3: 값이 숫자형 데이터이므로 4를 반환한다.

여기까지 잘 이해했다면 next!
만약 이 상태에서 다음과 같이 재할당 명령을 내린다면?

obj.arr = 'str';

참조 카운트:
어떤 데이터에 대해 자신의 주소를 참조하는 변수의 개수

가비지 컬렉터:
런타임 환경에 따라 특정 시점이나 메모리 사용량이 포화 상태에 임박할 때마다 자동으로 수거 대상들을 수거한다. 수거된 메모리는 다시 새로운 값을 할당할 수 있는 빈 공간이 된다.

  1. @5006에 문자열 'str'를 저장하고, 그 주소를 @7104에 저장한다.
  2. @5003은 더이상 자신의 주소를 참조하는 변수가 하나도 없게 된다.
  3. @5003의 참조 카운트는 @7104에 @5003이 저장돼 있던 시점까지는 1이었다가, @7104에 @5006이 저장되는 순간 0이 된다. 참조 카운트가 0인 메모리 주소는 가비지 컬렉터의 수거 대상이 된다!
    즉, @5003이 참조 카운트가 0이 됨에 따라 가비지 컬렉터의 대상이 되고, 이후 언젠가 담겨 있던 데이터인 @8104~? 라는 값이 사라진다. 이 과정에서 연쇄적으로 @8104~?의 각 데이터들의 참조카운트가 0이 되고, 이들 역시 가비지 컬렉터의 대상이 되어 함께 사라진다!

변수 복사 비교

동작 방식을 알았으니, 본격적(?)으로 원시형 데이터와 참조형 데이터의 차이를 확인해보자!

let a = 10;
let b = a;

let obj1 = { c: 10, d: 'ddd' };
let obj2 = obj1;

원시형 데이터

  1. 변수 영역의 빈 공간 @1001를 확보하고, 식별자를 a로 지정한다.
  2. 숫자 10을 데이터 영역에서 검색하고 없으므로 빈 공간 @5001에 저장한 다음, 이 주소를 @1001에 넣는다.

원시형 데이터 복사

  1. 변수 영역 빈 공간 @1002을 확보하고 식별자를 b로 지정한다.
  2. 이제 식별자 a를 검색해 그 값을 찾아와야 한다. @1001에 저장된 값인 @5001을 들고 좀 전에 확보해둔 @1002에 값으로 대입한다.

참조형 데이터

  1. 변수 영역의 빈 공간 @1003를 확보하고 식별자를 obj1로 지정한다.
  2. 데이터 영역의 빈 공간 @5002을 확보하고, 데이터 그룹이 담겨야 하기 때문에 별도의 변수 영역 @7103~을 확보해 그 주소를 저장한다.
  3. @7103에는 식별자 c를, @7104에는 식별자 d를 입력한 다음, c에 대입할 값을 10을 데이터 영역에서 검색한다.
  4. @5001에 이미 저장돼 있으므로 이 주소를 @7103에 연결하고, 문자열인 'ddd'는 데이터의 빈 공간에 새로 만들어서 @7104에 연결한다.

참조형 데이터 복사

  1. 변수 영역의 빈 공간 @1004를 확보하고 식별자 obj로 지정한다.
  2. 식별자 obj1를 검색해(@1003) 그 값인 @5002를 들고, @1004에 값으로 대입한다.

변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일하다.
@1001과 @1002는 모두 값이 @5001이 됐고, @1003과 @1004에는 모두 값이 @5002가 됐다.
그러나 데이터 할당과정에서 이미 차이가 있기 때문에 변수 복사 이후 동작에도 큰 차이가 발생한다.

let a = 10;
let b = a;

let obj1 = { c: 10, d: 'ddd' };
let obj2 = obj1;

b = 15;
obj2.c = 20;

6번째 줄
데이터 영역에 아직 15가 없으므로 새로운 공간 @5004에 저장하고, 그 주소를 든 채로 변수 영역에서 식별자가 b인 주소를 찾는다. @1002의 값이 @5004가 되는 것이다.

7번째 줄
데이터 영역에 아직 20이 없으므로 새로운 공간 @5005에 저장하고, 그 주소를 든 채로 변수 영역에서 obj2를 찾고(@1004), obj2의 값인 @5002가 가리키는 변수 영역에서 다시 c를 찾아(@7103) 그곳에 @5005를 대입한다.

아닛!!!
원시형 데이터를 복사한 변수 b의 값을 바꿨더니 @1002의 값이 달라진 반면, 참조형 데이터를 복사한 변수 obj2의 프로퍼티의 값을 바꾸었더니 @1004의 값은 달라지지 않는다!
즉, 변수 a와 b는 서로 다른 주소를 바라보게 되었으나, 변수 obj1과 obj2는 여전히 같은 객체를 바라보고 있는 상태이다.

a !== b //true
obj1 === obj2 //true

엄밀히 따지자면 어떤 데이터 타입이든 변수에 할당하기 위해서는 주솟값을 복사해야 하기 때문에 자바스크립트의 모든 데이터 타입은 참조형 데이터일 수밖에 없다.
다만 기본형은 주솟값을 복사하는 과정이 한 번만 이뤄지고, 참조형은 한 단계를 더 거치게 된다는 차이가 있다.

변수 복사 이후 값 변경 결과 비교(객체 자체를 변경했을 때)

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

b = 15;
obj2 = { c: 20, d: 'ddd' };

이번에는 b와 마찬가지로 obj2에도 새로운 객체를 할당함으로써 값을 직접 변경했다.
그러면 메모리의 데이터 영역의 새로운 공간에 새 객체가 저장되고 그 주소를 변수 영역의 obj2 위치에 저장한다!
즉 객체에 대한 변경임에도 값이 달라진다!

불변 객체

참조형 데이터의 '가변'은 데이터 자체가 아닌 내부 프로퍼티를 변경할 때만 성립한다.
데이터 자체를 변경하고자 하면 원시형 데이터와 마찬가지로 기존 데이터는 변하지 않는다.
그렇다면 내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나, 자동으로 새로운 객체를 만드는 도구를 활용한다면 객체 역시 불변성을 확보할 수 있다.

값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 하는 경우가 종종 발생한다.
이럴때 사용하는 불.변.객.체
객체의 가변성에 따른 문제점

let user = {
  name: 'sangku',
  gender: 'male'
};

const changeName = (user, newName) => {
  let newUser = user;
  newUser.name = newName;
  
  return newUser;
};

let user2 = changeName(user, 'Jung');

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

console.log(user.name, user2.name); // Jung Jung
console.log(user === user2); // true

user 변수와 user2 변수가 서로 같지 않다는 조건이 성립하면 15번째 줄의 내용이 출력되겠지만 실제로는 출력이 되지 않는다. 즉 user === user2가 성립하는 것이다.

만약 14번째 줄처럼 정보가 바뀐 시점에 알림을 보내야 한다거나, 바뀌기 전의 정보와 바귄 후의 정보의 차이를 가시적으로 보여줘야 하는 등의 기능을 구현하려면 이대로는 안된다...!!

아래와 같이 고쳐보자
객체의 가변성에 따른 문제점의 해결 방법

let user = {
  name: 'sangku',
  gender: 'male'
};

const changeName = (user, newName) => {
  return {
    name: newName,
    gender: user.gedner
  };
};

let user2 = changeName(user, 'Jung');

if (user !== user2) console.log('유저 정보가 변경되었습니다!');
console.log(user.name, user2.name) // sangku Jung
console.log(user === user2) // false

핵심은 changeName 함수가 새로운 객체를 반환하도록 수정 한 것
다만 changeName 함수는 새로운 객체를 만들면서 변경할 필요가 없는 기존의 객체의 프로퍼티(gender)를 하드코딩으로 입력했다. 이런식으로는 대상 객체에 정보가 많을수록, 변경해야 할 정보가 많을수록 사용자가 입력하는 수고가 늘어날 것이다..!
따라서 대상 객체의 프로퍼티 개수에 상관없이 모든 프로퍼티를 복사하는 함수를 만드는 편이 더 좋을 것이다.

기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은 복사)

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

copyObject를 이용한 객체 복사

let user = {
  name: 'sangku',
  gender: 'male'
};

let user2 = copyObject(user);
user2.name = 'Jung';

if (user !== user2) console.log('유저 정보가 변경되었습니다!'); // 유저 정보가 변경되었습니다!
console.log(user.name, user2.name); // sangku, Jung
console.log(user === user2); // false

너무 길어지는 것 같다. 얇은 복사와 깊은 복사는 다음 포스팅에서 ing..!

profile
Prof.Google을 통해 필요한 정보를 이 곳에 insert 🐸

0개의 댓글