ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JavaScript] 1. 데이터 타입
    개발자 이야기/JavaScript 2021. 7. 9. 00:14

    1. 데이터 타입의 종류

    기본형(Primitive type) vs 참조형(Reference type)

    기본형

    • 문자열, 숫자, boolean, null, undefined + (ES6에서 추가된) Symbol
    • 기본형은 불변성(immutability)을 가진다.

     

    참조형

    • 객체와 객체의 하위 분류에 속하는 배열, 함수, 날짜, 정규표현식 + (ES6에서 추가된) Map, WeakMap, Set, WeakSet 등

     

    기본형과 참조형을 구분하는 기준은 복제하는 대상이다. 기본형값이 담긴 주솟값을 바로 복제하는 반면 참조형은 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제한다는 점이다.

     

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

    • 변수 : 변할 수 있는 데이터, 변경 가능한 데이터가 담길 수 있는 공간 or 그릇
    • 식별자 : 변수명, 어떤 데이터를 식별하는 데 사용하는 이름

     

    변수 생성의 3단계

    선언 - 초기화 - 할당

    • 선언 : 실행 컨텍스트에 변수 객체를 등록
    • 초기화 : 변수 객체에 등록된 변수를 위해 메모리에 공간 확보, 보통 undefined로 초기화
    • 할당 : undefined로 초기화된 변수에 실제 값을 할당

     

    변수 선언의 동작 원리

    var a; // undefined

    위 코드의 의미는, "변할 수 있는 데이터를 만든다. 그리고 이 데이터의 식별자는 a로 한다."

    위 코드의 명령을 받은 컴퓨터는 메모리에서 비어있는 공간 하나를 확보하고, 이 공간의 식별자를 a라고 지정하는데까지가 변수의 선언 과정이다.

     

    이후 a에 접근하려고 한다면

    1.컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색하여 2.해당 공간에 담긴 데이터를 반환한다.

     

    데이터 할당

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

     

    위 코드에서 문자열 'abc'는 주소 검색으로 찾은 식별자 a의 주소가 가리키는 공간에 직접적으로 저장되지 않는다.

     

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

     

    변수 영역에 값을 직접 대입하지 않고 굳이 한 단계 더 거치는 이유

    데이터 변환을 자유롭게 할 수 있게 하기 위해서 + 메모리를 효율적으로 관리하기 위해서

    • 만약 미리 확보한 공간 내에서만 데이터 변환을 할 수 있다면 변환한 데이터를 다시 저장하기 위해 '확보된 공간을 변환된 데이터 크기에 맞게 늘리는 작업'이 선행되어야 한다. 

     

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

    불변값

    변수상수를 구분하는 성질 : 변수 영역 메모리의 변경 가능성

    • 한 번 데이터 할당이 이뤄진 변수 공간에 다른 데이터를 재할당할 수 있는지 여부 = 재할당 가능 여부

     

    불변성을 구분하는 성질 : 데이터 영역 메모리의 변경 가능성

     

    기본형 데이터(문자열, 숫자, boolean, null, undefined, Symbol)는 모두 불변값이다.

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

    위 코드의 동작과정을 살펴보자.

    먼저 변수 a에 문자열 'abc'를 할당려고 할 때 컴퓨터는 데이터 영역에서 'abc'을 찾고, 없으면 그제서야 데이터 공간을 하나 만들어 'abc'를 저장한다. 두 번째 줄에서는, 기존 'abc'가 'abcdef'로 바뀌는 것이 아닌 새로운 데이터 공간을 만들어 문자열 'abcdef'를 저장한 후 그 주소를 변수 a에 저장한다. 즉, 'abc'와 'abcdef'는 완전히 별개의 데이터다. 변경은 새로 만드는 동작을 통해서만 이뤄진다. 이것이 불변값의 성질이다. 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.

     

    가변값

    참조형 데이터의 기본적인 성질은 가변값이나 설정에 따라서 변경 불가능한 경우도 있고, 처음부터 불변값으로 활용하는 방안도 있다.

    참조형 데이터의 할당 과정을 살펴보면, 

    여러 개의 프로퍼티로 이뤄진 데이터 그룹을 저장하기 위해서 별도의 변수 영역(객체 @5001의 변수 영역)을 마련하고, 그 영역의 주소(@7103~?)를 @5001에 저장한다.

     

    기본형 데이터와의 차이는 '객체의 변수(프로퍼티) 영역'이 별도로 존재한다는 점이다. 하지만 데이터 영역은 기존의 메모리 공간을 그대로 활용하고 있다. 또한, 데이터 영역에 저장된 값은 모두 불변값이지만, 프로퍼티에는 다른 값을 얼마든지 대입할 수 있다. 이러한 이유로 '참조형 데이터는 가변값이다'라고 흔히 생각한다. 

     

    참조 카운트

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

    참조 카운트가 0인 메모리 주소는 가비지 컬렉터(garbage collector, GC)의 수거 대상이 된다.

    가비티 컬렉터는 런타임 환경에 따라 특정 시점이나 메모리 사용량이 포화 상태에 임박할 때마다 자동으로 수거 대상들을 수거한다.

    수거된 메모리는 다시 새로운 값을 할달할 수 있는 빈 공간이 된다.

    let a = {
      x: 3,
      arr: [3, 4, 5]
    };
    
    a.arr = 'mok';

     

    기본형 데이터와 참조형 데이터의 차이

    변수 복사 비교

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

     

    변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일하다. @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;

    기본형 데이터를 복사한 변수 b의 값을 바꿨더니 @1002의 값이 @5001에서 @5004로 변경되었다. 반면, 참조형 데이터를 복사한 변수 obj2의 프로퍼티 값을 바꿨지만 @1004의 값은 변하지 않았다. 기본형의 주솟값을 복사하는 과정은 한 번 이뤄지고, 참조형의 경우에는 한 단계 더 거치기 때문이다. 이러한 점이 기본형과 참조형 데이터의 가장 큰 차이점이다.

     

    변수 복사 후 객체 자체를 변경

    위 예제(객체의 프로퍼티 변경)와는 다르게 이번에는 객체 자체를 변경해보았다.

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

    이렇게 객체 자체를 직접 변경하게 되면 새로운 객체를 위한 변수 영역(@8204~?)이 새롭게 할당되고, 해당 변수 영역을 저장할 데이터 영역(@5006)도 새롭게 할당되면서 그 값이 obj2의 값에 저장된다. 서로 바라보는 객체 자체가 다르기 때문에 obj1 !== obj2가 성립한다.

     

    즉, 참조형 데이터가 가변값이라고 설명될 때의 가변은 참조형 데이터 자체를 변경할 경우가 아닌 그 내부의 프로퍼티를 변경할 경우에만 성립한다는 것을 알 수 있다.

     

    4. 불변 객체(Immutable Object)

    객체 지향 프로그래밍에 있어서 불변 객체는 생성 이후 그 내부의 상태가 변하지 않는 객체를 말한다.

    최근 React, Vue, Angular 등의 라이브러리나 프레임워크, 함수형 프로그래밍, 디자인 패턴 등에서 매우 중요한 기초 개념이다.

     

    불변 객체는 언제 필요할까?

    값으로 전달 받은 객체를 변경하더라도 원본 객체는 변하지 않아야 하는 경우

    const user = {
      name: 'Mok',
      gender: 'male',
    };
    
    const changeName = (user, newName) => {
      const newUser = user;
      newUser.name = newName;
      return newUser;
    };
    
    const user2 = changeName(user, 'Tom');
    
    if (user !== user2) {
      console.log('User information has changed.');
    }
    
    console.log(`USER1 : ${user.name}`);
    console.log(`USER2 : ${user2.name}`);
    console.log(`USER1 === USER2? ${user === user2}`);

    출력값은 아래와 같다.

    USER1 : Tom
    USER2 : Tom
    USER1 === USER2 ? true

    위 코드는 객체의 가변성으로 인한 문제점을 보여주는 예시다. const newUser = user의 의미는 newUser와 user가 가리키는 주소값이 같아진다는 것이다. 그 결과로, if (user !== user2)이란 조건이 성립되지 않아 User information has changed. 문자열이 출력되지 않았음을 알 수 있다.

     

    만약 정보가 변경된 시점에 알람을 보내야 한다거나, 변경되기 전과 후의 정보를 보여주는 기능을 구현해야 할 경우에는 이와 같은 가변성이 문제가 된다. 이를 해결하기 위해 변경 전과 후에 서로 다른 객체를 바라보게 만들어야 한다. 아래 코드로 객체의 가변성에 따른 문제점을 해결할 수 있다.

    const user = {
      name: 'Mok',
      gender: 'male',
    };
    
    const changeName = (user, newName) => {
      return {
        name: newName,
        gender: user.gender,
      };
    };
    
    const user2 = changeName(user, 'Tom');
    
    if (user !== user2) {
      console.log('User information has changed.');
    }
    
    console.log(`USER1 : ${user.name}`);
    console.log(`USER2 : ${user2.name}`);
    console.log(`USER1 equal USER2 ? ${user === user2}`);

    출력값은 아래와 같다.

    User information has changed.
    USER1 : Mok
    USER2 : Tom
    USER1 equal USER2 ? false

    위 코드처럼, changeName 함수가 return값으로 새로운 객체를 반환하도록 수정한다면 가변성 문제를 어느 정도 해결할 수 있다.

    하지만 위의 해결 방안은 완벽하지 않다. 만약 대상 객체에 많은 프로퍼티가 많을수록, 또는 변경해야 할 프로퍼티가 많을수록 사용자가 하드코딩해야 할 수고가 늘어난다. 그러므로 위 방법 보다 좀 더 좋은 방안으로는 대상 객체의 프로퍼티 개수에 상관 없이 모든 프로퍼티를 복사하는 함수를 만드는 것이다.

    const copyObject = function (target) {
      let result = {};
      for (let prop in target) {
        result[prop] = target[prop];
      }
      return result;
    };
    
    const user = {
      name: 'Mok',
      gender: 'male',
    };
    
    const user2 = copyObject(user);
    user2.name = 'Tom';
    
    if (user !== user2) {
      console.log('User information has changed.');
    }
    
    console.log(`USER1 : ${user.name}`);
    console.log(`USER2 : ${user2.name}`);
    console.log(`USER1 equal USER2 ? ${user === user2}`);

    출력값은 아래와 같다.

    User information has changed.
    USER1 : Mok
    USER2 : Tom
    USER1 equal USER2 ? false

    위 코드에서 copyObject 함수는 for in 문법을 이용해 result 객체에 target 객체의 프로퍼티들을 복사하는 함수다.

    위 해결 방안의 단점은 협업하는 모든 사람들과 객체 내부의 변경이 필요할 때 copyObject 함수를 사용하기로 협의하고 그 규칙을 지킨다는 전제하에서만 user 객체를 불변 객체로 바라볼 수 있다.

     

    이보다는 시스템적으로 제약을 거는 방법이 더 안전한 해결 방안이 될 수 있다.

    immutable.js, baobab.js 등의 라이브러리 자체에서 불변성을 지닌 별도의 데이터 타입과 메서드를 이용해 불변 객체를 만들 수 있다.

     

    얕은 복사와 깊은 복사

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

    얕은 복사가 이뤄질 경우, 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때는 주솟값만 복사하기 때문에 해당 프로퍼티의 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 된다.

     

    즉, 어떤 객체를 복사할 때 객체의 프로퍼티 중 기본형 데이터라면 그대로 복사해도 무방하지만 참조형 데이터의 경우에는 다시 그 내부의 프로퍼티들을 복사해야 깊은 복사가 이뤄진다고 볼 수 있다.

     

    아래 코드는 copyObject 함수를 깊은 복사 방식으로 고친 코드다.

    const copyObjectDeep = function(target) {
      let result = {};
      if (typeof target === 'object' && target !== null) {
        for (let prop in target) {
          result[prop] = copyObjectDeep(target[prop]);
        }
      } else {
        result = target;
      }
      
      return result;
    };
    
    let obj = {
      a: 1,
      b: {
        c: null,
        d: [1, 2]
      }
    };
    
    let 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} } }

    copyObjectDeep 함수 내 target이 객체인 경우, 내부 프로퍼티들을 순회하며 copyObjectDeep 함수를 재귀적으로 호출하고, 객체가 아닌 경우 target을 그대로 result로 지정하게끔 하였다. 이렇게 하면 원본과 사본이 서로 완전히 다른 객체를 참조할 수 있게 된다.

     

    target !== null 조건을 추가한 이유는 typeof 명령어가 null에 대해서도 'object'를 반환하기 때문(자바스크립트 자체 버그)이다.

     

     

    5. undefined vs. null

    자바스크립트에서 '없음'을 나타내는 값 두 가지 : undefined, null

     

    사용 목적

    • undefined : 어떤 변수에 값이 존재하지 않을 경우
    • null : 사용자가 명시적으로 '없음'을 표현하기 위해 대입한 값, '비어있음'을 명시적으로 나타내고 싶을 때 사용하자

     

    null의 경우, typeof null이 object라는 점을 주의해야 한다.(자바스크립트 자체 버그)

    그러므로 어떤 변수의 값이 null인지 여부를 판별하기 위해서는 typeof, 동등 연산자(==) 대신 '일치연산자(===)'를 사용해야 한다.

    동등 연산자로 비교할 경우, null과 undefined가 서로 같다고 판단한다.

     

    undefined는 값이 존재하지 않을 때 자바스크립트 엔진이 자동으로 부여한다.

    자바스크립트 엔진은 사용자가 응당 어떤 값을 지정할 것이라고 예상되는 상황임에도 실제로는 지정되지 않았을 때 undefined를 반환한다.

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

     

     

    참고자료

    '개발자 이야기 > JavaScript' 카테고리의 다른 글

    [JavaScript] new 연산자  (0) 2021.07.19
    [JavaScript] 4. 콜백 함수  (0) 2021.07.15
    [JavaScript] 3. this  (0) 2021.07.13
    [JavaScript] 함수 선언식 vs 함수 표현식  (0) 2021.07.10
    [JavaScript] 2. 실행 컨텍스트  (0) 2021.07.10

    댓글

Designed by Tistory.