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

김율이·2023년 4월 2일
post-thumbnail

1-1. 데이터 타입의 종류

JavaScript의 데이터 타입

  • 기본형(primitive type)
    - number, string, boolean, null, undefined, symbol
    - 할당이나 연산시 복제
    - 값이 담긴 주솟값을 바로 복제
    - 불변성(immutability)을 띈다.
  • 참조형(reference type)
    - object(Map, WeakMap, Set, WeakSet), array, function, date, regexp
    - 할당이나 연산시 참조
    - 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제

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

  • 자바스크립트는 숫자형 데이터에 대해 64비트(8바이트)의 공간을 확보하고 있다.
    • 문자열은 전체 글자수가 가변적이기 때문에 특별히 정해진 규격이 없다.
      ex) 영어 1바이트, 한글 2바이트
  • 모든 데이터는 바이트 단위의 식별자, 즉, 메모리 주솟값(memory address)을 통해
    서로 구분하고 연결할 수 있다.
  • 변수(variable): 변할 수 있는 데이터(숫자, 문자열, 객체, 배열)
    - 변경 가능한 데이터가 담길 수 있는 공간
  • 식별자(identifier): 어떤 데이터를 식별하는 데 사용하는 이름, 변수명

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

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

var a = 'abc' // 변수 선언과 할당을 한 문장으로 표현
  • 할당 과정에서 a라는 이름을 가진 주소를 검색해서 문자열 'abc'를 할당하면 될 것 같지만 실제로는 해당 위치에 문자열 'abc'를 직접 저장하지 않는다.

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

💡 변수 영역에서 값을 직접 대입하지 않고 한 단계를 더 거치는 이유?
- 데이터 변환을 자유롭게 할 수 있게 하고, 메모리를 효율적으로 관리하기 위함
- 미리 확보한 공간 내에서만 데이터 변환을 할 수 있다면 변환한 데이터를 다시
저장하기 위해서 확보된 공간을 변환된 데이터 크기에 맞게 늘리는 작업이 필요함
- 즉, 가변적인 문자열 데이터의 변환을 처리하려면 변수와 데이터를 별도의
공간에 나누어 저장
하는 것이 좋음
- 기존 문자열에 변환이 발생하면 무조건 새로 만들어 별도의 공간에 저장함

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

1-4-1. 불변값

  • 변수(variable)와 상수(constant)를 구분하는 성질은 '변경 가능성'
    • 불변값과 상수를 명확히 구분해야 함
  • 변수와 상수를 구분 짓는 변경 가능성의 대상은 변수 영역 메모리
  • 불변성 여부를 구분 짓는 변경 가능성의 대상은 데이터 영역 메모리
  • 기본형 데이터(숫자, 문자열, boolean, null, undefined, Symbol)는 모두 불변값
var a = 'abc'; // 변수 a에 'abc' 데이터 주소 할당
a = a + 'def'; // 기존 'abc'에 'def'를 추가하는 것이 아니라, 
// 새로운 문자열 'abcdef'를 만들어 그 주소를 변수 a에 저장

var b = 5; // 변수 b에 숫자 5 할당 
// 데이터 영역에서 5 찾고, 없으면 데이터 공간 만들어 저장
var c = 5; // 이미 만들어놓은 값이 있으니 위의 주솟값 재활용
b = 7; // 기존에 저장된 5를 7로 바꾸는 것이 아니라, 
//이미 만들어 놓은 7이 있다면 재활용하고 없으면 새로 만들어서 b에 저장

💡 불변성의 성질
변경은 새로 만드는 동작을 통해서만 이뤄지기 때문에 문자열 값, 숫자 값 모두 한 번 만든 값을 바꿀 수 없다.

  • 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.

1-4-2. 가변값

  • 참조형 데이터의 경우, 기본적인 성질은 가변값인 경우가 많다.
  • 하지만, 설정에 따라 변경 불가능하거나 아예 불변값으로 활용할 수 있다.
  • 기본형 데이터와의 차이는 '객체의 변수(프로퍼티) 영역'이 별도로 존재한다는 것.
  • 객체가 별도로 할애한 영역은 변수 영역일 뿐 '데이터 영역'은 기존의 메모리 공간을 그대로 활용하고 있다. → 데이터 영역에 저장된 값은 모두 불변값
  • BUT, 변수 영역에는 다른 값을 대입할 수 있기 때문에 참조형 데이터는 불변(immutable)하지 않는다(=가변값이다)라고 하는 것이다.

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

💡 가비지 컬렉터(garbage collector, GC)
- 참조 카운트가 0인 메모리 주소는 가비지 컬렉터의 수거 대상이 된다.
- 가비지 컬렉터는 런타임 환경에 따라 특정 시점이나 메모리 사용량이 포화 상태에 임박할 때마다 자동으로 수거 대상들을 수거(collecting)한다.
- 수거된 메모리는 다시 새로운 값을 할당할 수 있는 빈 공간이 된다.

1-4-3. 변수 복사 비교

  • 변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일하다.
  • BUT, 데이터 할당 과정에서 차이가 있기 때문에 변수 복사 이후의 동작에 큰 차이가 발생한다.
  • 기본형은 주솟값을 복사하는 과정이 한 번만 이뤄지고, 참조형은 한 단계를 더 거치게 된다.
  • 참조형 데이터가 ‘가변값'일 때는 참조형 데이터 자체를 변경할 경우가 아니라, 내부의 프로퍼티를 변경할 때만 성립한다.

1-5. 불변 객체

1-5-1. 불변 객체를 만드는 간단한 방법

  • 불변 객체(immutable object)는 최근의 React, Vue.js, Angular 등의 라이브러리나 프레임워크에서뿐만 아니라 함수형 프로그래밍, 디자인 패턴 등에서도 매우 중요한 기초

💡 왜 불변 객체가 필요할까?
값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 하는 경우가 발생함

객체의 가변성에 따른 문제점

var user = {
  name: 'Yuli',
  gender: 'female'
};

var changeName = function (user, newName) {
  var newUser = user;
  newUser.name = newName;
  return newUser;
};

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

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

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

객체의 가변성에 따른 문제점의 해결 방법

var user = {
  name: 'Yuli',
  gender: 'female'
};

var chageName = function (user, newName) {
  return {
    name: newName,
    gender: user.gender
  };
};

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

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

console.log(user.name, user2.name); // Yuli Kim
console.log(user === user2); // false

💡 위 해결 방법의 미흡한 점
chageName 함수는 새로운 객체를 만들면서 변경할 필요가 없는 기존 객체의 프로퍼티(gender)를 하드코딩으로 입력했다. 이 방식은 대상 객체에 정보가 많거나 변경해야 할 정보가 많을수록 사용자가 입력하는 수고가 늘어날 것이다.

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

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

copyObject를 이용한 객체 복사

var user = {
  name: 'Yuli',
  gender: 'female'
};

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

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

console.log(user.name, user2.name); // Yuli Kim
console.log(user === user2); // false

💡 위 해결 방법의 미흡한 점
협업하는 모든 개발자들이 user 객체 내부의 변경이 필요할 때는 무조건 copyObject 함수를 사용하기로 합의하고 그 규칙을 지킨다면 불변 객체이지만, 아예 프로퍼티 변경을 할 수 없게 시스템적으로 제약을 거는 편이 안전하다. 또한, 얕은 복사만을 수행하는 것이 가장 아쉬운 점이다.

1-5-2. 얕은 복사와 깊은 복사

  • 얕은 복사(shallow copy)는 바로 아래 단계의 값만 복사하는 방법
  • 깊은 복사(deep copy)는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법

중첩된 객체에 대한 얕은 복사

var user = {
  name: 'Yuli'
  urls: {
	portfolio: 'https://github.com/yyuli',
  	blog: 'https://velog.io/@yulikim'
  }
};

var user2 = copyObject(user);

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

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

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

💡user 객체에 직접 속한 프로퍼티에 대해서는 복사해서 완전히 새로운 데이터가 만들어진 반면, 한 단계 더 들어간 urls의 내부 프로퍼티들은 기존 데이터를 그대로 참조

중첩된 객체에 대한 깊은 복사

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

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

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

객체의 깊은 복사를 수행하는 범용 함수

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 obj = {
  a: 1,
  b: {
       c: null,
       d: [1, 2]
     }
};

var obj2 = copyObjectDeep(obj);

obj2.a = 3;
obj2.b.c = 4;
obj.b.d[1] = 3;

console.log(obj); // { a: 1, b: { c: null, d: [ 1, 3 ] } }
console.log(obj2); // { a: 3, b: { c: 4, d: { '0': 1, '1': 2 } } }

JSON을 활용한 간단한 깊은 복사

var obj = {
  a: 1,
  b: {
       c: null,
       d: [1, 2],
       func1: function () { console.log(3); }
     },
       func2: function () { console.log(4); }
};

var obj2 = copyObjectviaJSON(obj);

obj2.a = 3;
obj2.b.c = 4;
obj.b.d[1] = 3;

console.log(obj); // { a: 1, b: { c: null, d: [ 1, 3 ], func1: f() }, func2: f() }
console.log(obj2); // { a: 3, b: { c: 4, d: [ 1, 2 ] } }
  • BUT, 메서드(함수)나 숨겨진 프로퍼티인 __ proto _ 나 getter/setter 등과 같이 JSON으로 변경할 수 없는 프로퍼티들은 모두 무시한다.
  • HTTPRequest로 받은 데이터를 저장한 객체를 복사할 때 등 순수한 정보만 다룰 때 활용하기 좋은 방법이다.

1-6. undefined와 null

1-6-1. undefined

자동으로 undefined 를 부여하는 경우

  1. 값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
  2. 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
  3. return 문이 없거나 호출되지 않는 함수의 실행 결과
var a;
console.log(a);  // (1) undefined. 값을 대입하지 않은 변수에 접근

var obj = { a: 1 };
console.log(obj.a);  // 1
console.log(obj.b);  // (2) 존재하지 않는 프로퍼티에 접근
console.log(b);  // c.f) ReferenceError: b is not defined

var func = function() { };
var c = func();  // (3) 반환(return)값이 없으면 undefined를 반환한 것으로 간주
console.log(c);  // undefined

💡 'undefined'의 의미 구분

  • 사용자가 명시적으로 부여한 경우
    • undefined가 '비어있음'을 의미하긴 하지만 하나의 값으로 동작하기 때문에 이때의 프로퍼티나 배열의 요소는 고유의 키값(프로퍼티 이름)이 실존하게 되고, 순회의 대상이 된다.
  • 비어있는 요소에 접근하려고 할 때 반환되는 경우
    • 해당 프로퍼티 내지 배열의 키값(인덱스) 자체가 존재하지 않음을 의미한다.
  • 즉, 값으로써 어딘가에 할당된 undefined는 실존하는 데이터인 반면, 자바스크립트 엔진이 반환해주는 undefined 문자 그대로 값이 없음을 나타낸다.

1-6-1. null

  • '비어있음'을 명시적으로 나타내고 싶을 때 undefined가 아닌 null을 사용한다.
  • typeof null이 object라는 자바스크립트 자체 버그를 주의해야 한다.
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);  // true

💡 undefined와 null의 비교
동등 연산자(equally operator)(==)로 비교할 경우, null과 undefined가 서로 같다고 판단하기 때문에 정확히 판별하기 위해서는 일치 연산자(identity operator)(===)를 사용해야 한다.

참조

  • 정재남, 코어 자바스크립트, 위키북스, 2019, p. 1-35

0개의 댓글