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

Hwang Tae Young·2023년 2월 22일
0

✅ 데이터 타입의 종류

1. JavaScript의 데이터 타입은 크게 두 가지로 나뉜다 - 기본형 & 참조형

  • 기본형: number, string, boolean, null, undefined, symbol
  • 참조형: object(Map, WeakMap, Set, WeakSet), array, function, date, regexp

- 기본형: 할당/연산 시 복제된다

  • 값이 담긴 주소값을 바로 복제 불변성(immutability)를 띈다.

- 참조형: 할단/연산 시 참조된다.

  • 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제합니다.

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

  • 모든 데이터는 바이트 단위의 식별자, 더 정확하게는 메모리 주소값을 통해 서로 구분하고 연결할 수 있다.
  • 변수는 variable로 변할 수 있는 무언가, 즉 변할 수 있는 데이터 (숫자, 문자열, 객체, 배열 등) 입니다.
  • 식별자는 어떤 데이터를 식별하는데 사용하는 이름, 즉 변수명입니다.

✅ 변수선언과 데이터 할당

1. 변수 선언

 let a;
  • 변수 선언은 말그대로 변할수 있는 데이터를 만든다 라는 의미이다. 말 그대로 a라는 애를 선언 한 것이다. 쉽게 말하자면 빈 그릇을 하나 만든 것이다.
    변수를 선언하면 메모리에 비어있는 공간을 확보하고 그 공간에 이름을 붙여주는 것이다.

    메모리 주소 1002 1003 1004 1005
    데이터 이름 : a
    값:

    그렇다면 내용물은 어떻게 채울 수 있을까?

2. 데이터 할당

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

var a = 'abc'; // 변수 선언과 할당을 한 문장으로 표현
  • 이렇게 내용물을 채울 수 있을 것이다. 자바스크립트는 선언 후 할당을 하던, 선언과 할당을 같이하던 결과는 같게 동작한다.

    var의 경우 할당을 한 뒤 선언을 해도 문제가 없지만
    let은 before initialization, const는 SyntaxError: Missing initializer in const declaration 오류가 나온다.

    a = "abc";
    var a;
    console.log(a) // "abc"
    
    a = "abc";
    let a; // ReferenceError: Cannot access 'a' before initialization
    
    a = "abc"
    const a; // SyntaxError: Missing initializer in const declaration 

    이렇게 할 이유도 생각도 잘 안하겠지만, 오류가 나는 이유는 var, let, const의 동작 방식이 다르기 때문이다. 자세한 것은 아래의 링크 참고해주세요!
    링크 : var, let, const의 차이 - 변수 선언 및 할당

그렇다면 이제 데이터할당의 흐름을 알아보자!

메모리 주소 1002 1003 1004 1005
데이터 이름 : a
값: @5004
메모리 주소 5002 5003 5004 5005
데이터 "abc"

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

  • 변수에 바로 값을 대입하지 않고 한 단계를 더 거치는 이유는 데이터 변환을 자유롭게, 메모리를 더욱 효율적으로 관리하기 위함이다.
    • JS는 숫자형 데이터에 대해 64비트의 공간을 확보하지만, 문자열은 특별히 정해진 규격이 없기 때문에 가변적입니다.
      직접 대입을 한다면 확보한 공간에 데이터를 변환할 수 있다면 변환한 데이터를 다시 저장하기 위해서는 확보된 공간을 다시 변환된 데이터의 크기에 맞게 늘리기 작업이 선행되어야 할 것이고 해당 공간이 메모리상 가장 마지막에 있다면 뒤쪽으로 늘리기만하면 되기때문에, 어렵지 않겠지만
      중간쯤에 있다고 한다면 해당 공간보다 뒤에있는 데이터들을 전부 뒤로 옮기고, 이동시킨 주소를 각 식별자에 다시 연결하는 작업을 해야하기때문에 효율면에서 좋지 않기 때문입니다.
var a = 'abc'; 
a = 'abcdef'; 

데이터 변환이 이루어 진다면, 데이터 할당은 아래와같이 기존의 데이터를 사용하는게 아닌 새로운 데이터를 만들어서 그 주소값으로 변경시킨다.

메모리 주소 1002 1003 1004 1005
데이터 이름 : a
값: @5004 -> @5005로 변경
메모리 주소 5002 5003 5004 5005
데이터 "abc" "abcdef"

✅ 기본형 데이터와 참조형 데이터

1. 불변값

  • 변수(variable)상수(constant)를 구분하는 성질은 변경 가능성입니다.
  • 불변값상수는 같은 개념이 아닙니다.
  • 변수와 상수를 구분 짓는 변경 가능성의 대상은 변수 영역메모리 입니다.
    • 한 번 데이터 할당이 이뤄진 변수 공간에 다른 데이터를 재할당할 수 있는지의 여부가 중요합니다.
  • 불변성 여부를 구분할 때의 변경 가능성의 대상은 데이터 영역메모리입니다.
  • 기본형 데이터(숫자, 문자열, boolean, null, undefined, Symbol)는 모두 불변값입니다.
var a = 'abc'; // 변수 a에 "abc"를 할당
a = a + 'def'; // 기존의 "abc"의 값을 "abcdef"로 변경하는 것이 아닌, 아예 새로운 "abcdef" 문자열을 만들어 a에 주소를 저장

var b = 5; // 데이터 영역에 5가 있는지 찾아보고, 없으면 데이터 공간을 만들어 5를 만들어 저장한 뒤 그 주소를 b에 저장
var c = 5; // b에 할당된 데이터 주소값을 그대로 재활용
b = 7; // 기존의 5를 변경하는게 아닌, 7의 값이 있나 찾아보고 없다면 새로 만들어 b에 할당
  • 문자열, 숫자 값도 한번 만들면 다른 값으로 변경할 수 없습니다.
  • 변경은 새로 만드는 동작을 통해서만 이루어집니다.
  • 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않습니다.

    가비지 컬렉터란?

    어떤 데이터에 대해 자신의 주소를 참조하는 변수의 갯수를 ‘참조 카운트' 라고 한다.
    (ex. 주소 @5005을 참조하는 주소는 @1003 하나 이므로 참조 카운트는 1)
    이때, @1003의 데이터 값을 @5005로 변경하면서 (예: a = "abcdef") 해당 데이터(@5004)의 참조 카운트는 0이 된다.
    참조 카운트가 0인 메모리 주소는 가비지 컬렉터의 수거 대상이 된다.
    가비지 컬렉터는 런타임 환경에 따라 특정 시점이나 메모리 사용량 포화 상태 시 자동으로 수거한다.

2. 가변값

  • 참조형 데이터의 기본적인 성질은 가변값이지만, 설정에 따라 변경 불가하게 만들 수도있고, 아예 불변값으로 사용할 수도있습니다.

참조형 데이터의 할당

var obj1 = {
	a: 1,
	b: 'bbb'
}
변수 영역 메모리 주소 1002 1003 1004 1005
데이터 이름 : obj1
값: @5001
데이터 영역 메모리 주소 5001 5002 5003 5004
데이터 @7103 ~ ? 1 "bbb"
객체 @5001의
변수 영억
메모리 주소 7103 7104 7105 7106
데이터 이름 : a
값: @5003
이름 : b
값: @5004

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

  • 기본형 데이터와의 차이는 객체의 변수(프로퍼티)영역이 별도로 존재한다.
  • 객체가 별도로 할애한 영역은 변수 영역일 뿐 데이터 영역은 기존의 메모리 공간을 그대로 활용한다.
  • 데이터 영역에 저장된 값은 모두 불변값이지만, 변수에는 다른 값을 얼마든지 대입 가능하다(가변성).
  • 이러한 점때문에 참조형 데이터는 불변하지 않다고 한다.

참조형 데이터의 프로퍼티 재 할당

var obj1 = {
  a: 1,
  b: 'bbb',
};
obj1.a = 2;
변수 영역 메모리 주소 1002 1003 1004 1005
데이터 이름 : obj1
값: @5001
원래 주소값은 바뀌지 않음
데이터 영역 메모리 주소 5001 5002 5003 5004
데이터 @7103 ~ ? 2
숫자 2를 검색하고 찾지 못해서 데이터 할당
1 "bbb"
객체 @5001의
변수 영억
메모리 주소 7103 7104 7105 7106
데이터 이름 : a
값: @5003 -> @5002 변경
이름 : b
값: @5004
  • 새로운 객체를 만들어 주소값이 바뀌는 것이 아니라, 기존의 객체 내부의 값만 바뀝니다.

3. 변수 복사 비교

var a = 10;
var b = a;

var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
변수 영역 메모리 주소 1002 1003 1004 1005
데이터 이름: a
값: @5001
이름: b
값: @5001
이름 : obj1
값: @5002
이름 : obj2
값: @5002
데이터 영역 메모리 주소 5001 5002 5003 5004
데이터 10 @7103 ~ ? "ddd"
객체 @5001의
변수 영억
메모리 주소 7103 7104 7105 7106
데이터 이름 : c
값: @5001
이름 : b
값: @5003
  • 복사하는 과정은 기본형, 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일합니다.
  • 복사 과정은 동일하지만 데이터 할당 과정 차이가 있기 때문에 변수 복사 이후의 동작에서 차이가 발생합니다.

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

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

b = 15; 
obj2.c = 15;

위 처럼 변경 한 뒤, 비교를 해보면 아래와 같은 결과가 나온다.

a !== b
obj1 === obj2

기본형 데이터인 a와 b는 다른 주소값을 가지게 되었지만, obj1과 obj2는 여전히 같은 주소값을 가지고 있다. 왜 그럴까?

  • 기본형 데이터를 복사한 변수 b의 값을 변경하면 변수의 데이터 값(주소값)만 변경된다.
  • 참조형 데이터인 obj2의 경우 객체의 프로퍼티의 값을 바꾼다면, 현재 보고있는 데이터값(객체의 주소값)이 변경되는 것이 아닌
    객체가 바라보는 주소값이 바뀌는 2단계를 거치기 때문에 여전히 같은 주소값을 가지고 있는 것이다.
변수 영역 메모리 주소 1002 1003 1004 1005
데이터 이름: a
값: @5001
이름: b
값: @5001 => @5004로 변경
이름 : obj1
값: @5002
이름 : obj2
값: @5002 변경 없음
데이터 영역 메모리 주소 5001 5002 5003 5004
데이터 10 @7103 ~ ? 변경 없음 "ddd" 15
객체 @5001의
변수 영억
메모리 주소 7103 7104 7105 7106
데이터 이름 : c
값: @5001 => @5004로 변경
이름 : b
값: @5003

참조형이라도 객체 자체를 변경하는 경우는 값이 달라진다

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

b = 15;
obj2 = { c: 20, d: 'ddd' };
  • "참조형 데이터가 가변값이다" 라는 명제는 데이터 자체를 변경하는 경우가 아니라, 내부의 프로퍼티를 변경할 때만 성립한다.

✅ 불변 객체

  • 불변 객체는 최근 React, Vue.js, Angular 등의 라이브러리&프레임워크 뿐만 아니라, 함수형 프로그래밍, 디자인 패턴 등에서도 매우 중요함
  • 객체도 객체의 프로퍼티가 아닌 데이터 자제를 변경하고자 하면 기본형과 마찬가지로 기존 데이터는 변하지 않습니다.
  • 내부 속성 변경시마다 매번 새로운 객체를 만들어 재할당을 하거나, 자동으로 새로운 객체를 만드는 도구를 활용하면 객체도 불변성을 확보할 수 있습니다.
    ex) immutable.js, immmer js, immutability-helper 등의 라이브러리. ES6의 spread operator, Object.assign 메서드

🤔 불변 객체는 왜 필요할까?

  • 값으로 전달받은 객체를 변경하더라도 원본 객체는 변하지 않아야 하는 상황이 생길때 필요하기 때문이다.
var user = {
  name: 'Jaenam',
  gender: 'male',
};

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

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

if (user !== user2) {
  console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // Jung Jung
console.log(user === user2); // true
  • 이름이 바뀌었을 경우, '유저 정보가 변경되었습니다.'라는 내용이 나와야 하지만 변경되었음에도 객체 내부의 프로퍼티가 바뀐 것이기 때문에 if문을 넘어가 버렸습니다.

🚀 불변 객체를 만드는 간단한 방법

아래와 같이 다른 값을 가질 수 있게, 새로운 객체를 반환하도록 하면 된다.

var user = {
  name: 'Jaenam',
  gender: 'male',
};

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

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

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

하지만, 굳이 바꾸지 않아도 될 gender까지 바꾸게 되었고, 더 많은 정보가 있다면 비 효율적이게 되버리기 때문에 프로퍼티 갯수와 상관없이 복사하는 함수를 만들 수 있다.

var copyObject = function(target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
};
  • 모든 협업하는 개발자가 copyObject의 함수를 사용하여 객체를 변경한다면 user 객체가 곧 불변객체라고 할 수 있지만,
    그 규칙을 지키지 않을수도 있기 때문에 시스템적으로 제약을 거는 것이 더 안전할 것입니다.
  • 또한, 얕은복사만 사용하기 때문에 아쉬운 부분이 생깁니다.

얕은 복사와 깊은 복사

  • 얕은 복사(shallow copy): 바로 아래 단계의 값만 복사합니다.
    • 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주소값만 복사합니다.
    • 원본과 사본이 동일한 참조형 데이터 주소를 가리키므로 사본을 바꾸면 원본도 변경됩니다.

  • 깊은 복사(deep copy): 내부의 모든 값들을 하나하나 찾아서 전부 복사합니다.
    • 중첩된 객체에서 프로퍼티 내부의 값들까지 전부 복사. 재귀로 구현 가능합니다.

깊은 복사

  • 재귀 함수를 이용
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;
};
  • target이 객체인 경우에는 내부 프로퍼티를 순회하며 copyObjectDeep 함수를 재귀적으로 호출하고, 객체가 아닌경우에는 target을 그대로 내보낸다.
  • 이 함수를 이용하면 원본과 사본이 서로 완전히 다른 객체를 참조하게 하여 어느쪽의 프로퍼티를 변경하더라도 다른 쪽에 영향을 주지 않는다.
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 copyObjectViaJSON = function(target) {
  return JSON.parse(JSON.stringify(target));
};
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] } }
  • 객체를 JSON 문법으로 표현된 문자열로 전환했다가 다시 JSON으로 바꾸는 방식
  • 메서드(함수)나 숨겨진 프로퍼티 ex) __proto__, getter/setter등과 같이 JSON으로 변경 할 수 없는 프로퍼티는 전부 무시합니다.
  • httpRequest로 받은 데이터를 저장한 객체를 복사할때 등 순수 한 정보만 다룰 때 활용하기 좋습니다.

✅ undefined와 null

  • undefinednull은 없을을 나타내는 공통점이 있지만, 미세한 차이점도 있다.

1. undefined

  • 사용자가 명시적으로 지정할 수도 있지만, 0값이 존재하지 않을 경우에 자바스크립트 엔진이 자동으로 부여한다.
    • 값을 대입하지 않은 변수, 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
    • 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
    • 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

2. null

  • 만들어진 목적 자체가 비어있음을 명시적으로 나타내고 싶을때 만든 데이터 타입입니다.
  • 한가지 주의사항은 typeof null은 object라고 나오며, 이는 자바스크립트 자체 버그이다. 따라서 변수의 값이 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); // true

참고 : 코어자바스크립트

profile
더 나은 개발자가 되기 위해...☆

0개의 댓글