데이터 타입

Ordinary·2023년 5월 22일
0

1장에서는 기본형과 참조형으로 분류하여 간단히 자바스크립트에서 사용하는 데이터 타입에 대해서 배우게 됩니다. 또한, 기본형과 참조형 데이터의 차이점을 알아보고 왜 데이터의 불변성을 유지하는 것이 필요한 지 이유를 살펴봅니다.

기본형과 참조형의 차이

자바스크립트는 데이터를 기본형과 참조형으로 구분합니다.

기본형참조형
numberobject
stringarray
booleanfunction
nulldate
undefinedRegExp
symbolmap, weakMap, set, weakSet

기본형과 참조형 데이터의 차이는 다음과 같습니다.

  • 기본형: 값을 저장한 공간의 주소값을 참조한다.
  • 참조형: 데이터 그룹의 주솟값을 참조한다. 데이터 그룹의 각 요소들은 값을 저장한 공간의 주솟값을 참조한다.

즉, 주솟값을 참조한다는 점에서는 동일하나, 값을 바로 참조하고 있는지, 데이터 그룹이라는 단계를 하나 더 가지고 있는 지에 따라서 두 데이터 타입을 구분할 수 있고, 이런 점은 값의 변경, 할당, 복사, 비교 동작 방식에 차이를 만들어 냅니다.

두 데이터 타입의 차이를 살펴보기 전에, 이해를 돕기 위해서 배경 지식을 채우고 가도록 하겠습니다.

식별자와 변수

식별자는 어떤 데이터를 식별하는 데 사용하는 이름으로 변수명이라고도 합니다. 변수는 식별자가 식별하는 것, 즉 데이터 자체를 의미합니다. 숫자도 데이터고, 문자열, 객체, 배열 모두 데이터입니다.

  • 식별자: 변수 이름. 값을 저장하기 위해 확보한 공간에 붙여진 이름
  • 변수: 데이터, 값
//식별자 = a, 변수 = "abc"
var a = "abc";

자바스크립트 내에서의 변수 선언과 데이터 할당

자바스크립트에서는 식별자를 통해 이름이 붙여진 공간과 실제 변수값이 저장되는 공간이 분리되어 있습니다. 이를 간단히 “변수영역”, “데이터 영역”이라고 구분짓겠습니다. 데이터 영역에서는 실제 변수값이 저장되고 변수 영역에서는 값이 저장된 데이터 영역의 주솟값이 저장됩니다.

기본형 변수의 선언과 데이터 할당이 어떻게 이뤄지는지 살펴보겠습니다.

var a = "abc";

  1. 변수 영역에 1003 주소지 공간을 확보하고 식별자(이름)을 a라고 한다.
  2. 데이터 영역에서 5004 주소지 공간을 확보하고 값 abc을 저장한다.
  3. 변수 영역 1003에 앞서 저장한 데이터 영역의 주소지 5004를 저장한다.

참조형 즉, 객체, 배열의 경우 변수 선언과 데이터 할당은 어떻게 되는 지 살펴보면 다음과 같습니다.

var obj = {
	a: 1,
	b: "bbb"
}

  1. 변수 영역에 빈 공간 1003을 확보하고 obj라는 이름을 지정한다.
  2. 데이터 영역에 데이터 그룹(a, b)에 대한 따로 변수 영역 71037014를 확보하고 obj에 대한 데이터 영역 5001에 해당 주소값을 저장한다.
  3. a, b에 저장할 값에 대해서 별도의 데이터 영역(5004, 5005)을 확보한 다음 1bbb를 저장한다.
  4. 7103701450045005를 저장한다.

이렇게 변수 영역과 데이터 영역으로 나누어서 값을 저장함으로써 2가지 이점을 얻을 수 있습니다.

  1. 데이터 변환이 자유롭습니다.

    자바스크립트의 경우, 숫자형은 8바이트이지만 문자열은 특별한 제약이 없습니다. 만약 주소가 아닌 값 자체를 저장하게 된다면 문자열의 내용이 바뀔 때마다 메모리 상에서 변환된 데이터에 맞게 공간을 다시 확보해야 하고 이에 맞춰서 모든 데이터의 주소를 다시 재정렬해야 합니다.

    데이터 영역을 따로 나누어 관리함으로써, 값이 변경될 때 새로 데이터 영역을 할당하고, 변수 영역에서는 주소값만 교체하는 것으로 데이터 변환을 할 수 있습니다.

  2. 메모리 관리를 효율적으로 할 수 있습니다.

    500개의 변수를 생성하고 모두 5라는 값으로 초기화할 때, 500개의 값에 대한 메모리 공간을 가지고 있는 것보다 동일한 데이터에 대해 같은 주소값을 참조하게 하는 것이 훨씬 효율적입니다.

기본형과 참조형의 변수 복사 비교

기본형과 참조형의 차이를 더 확실히 비교하기 위해서 변수 생성 및 복사 이후에 복사된 데이터의 변경이 될 때 어떻게 이뤄지는지 살펴보겠습니다.

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

표

우선 기본형 데이터a의 메모리 할당 과정은 이전과 동일합니다. 따라서 변수 b에 대해서 복사 과정을 살펴보면 다음과 같습니다.

  1. 변수 영역 빈 공간 1002 확보 및 b라고 식별자 지정
  2. a라는 이름의 변수 검색 및 저장하고 있는 값 @5001을 저장.

참조형 데이터obj1의 할당 과정도 이전과 동일합니다. 다음은 obj2의 복사 과정입니다.

  1. 변수 영역에 빈 공간 1004를 확보 및 obj2라고 식별자 지정
  2. obj1이라는 변수 이름 검색 및 저장하고 있는 값 @5002 저장.

그렇다면, 복사한 데이터의 값 즉, bobj2의 값을 변경하게 되었을 때 각각 참조하고 있는 주소값은 어떻게 다라질까요? 다음의 코드를 실행한다고 했을 때 메모리는 다음과 같습니다.

b = 15;
obj2.c = 20;

기본형 데이터인 변수 b의 경우에는 5001번지의 1015로 변경할 수 없기 때문에(불변성) 별도의 공간에 15를 할당한 후, 5004번지 주소값을 새로 저장하게 됩니다.

obj2의 경우에는 데이터 그룹의 변수 c가 가리키는 주소값은 5005로 바뀌었으나, 궁극적으로 obj2가 가리키는5002번지에서의 참조값은 기존의 @7103~로 변경되지 않았습니다. 즉, 별도의 변수 영역이라는 단계를 하나 더 갖기 때문에 데이터 영역의 값이 변경되지 않았음에도(@7103~) 이전과 다른 값인 20 으로 바꿀 수 있는 것입니다. 이를 가변성 이라고 합니다.

가변성과 불변성

데이터가 불변한가, 불변하지 않은가(가변성)의 기준이 되는 것은 데이터 영역에서 저장하고 있는 값을 변경할 수 있는지에 대한 여부입니다.

앞서 기본형값을 저장하고 있는 b의 경우, 저장한 값을 변경할 때는 데이터 영역을 가리키는 메모리 주소값에 변경이 일어납니다. 이는 기존에 저장하고 있던 값이 10에서 20으로 변경하는 것이 아니라 새로운 영역을 할당해 20을 저장하기 때문입니다. 즉 기존값은 변경이 불가능하기 때문에 다른 메모리 영역에 할당해서 새로운 주소값을 저장하는 것입니다.

객체 obj2의 경우, 가리키고 있는 데이터 영역 5002번에 대해서는 전혀 변경이 없지만, c 프로퍼티의 값이 15에서 20로 바뀐 것을 알 수 있습니다. 그 내부의 c의 값은 기본형으로 불변하다고 할 수 있지만, 객체를 저장하고 있는 obj의 경우 기존 데이터 영역의 변화 없이 값이 변경되었음으로 불변하지 않다고 말할 수 있습니다(가변성)

불변 객체

기존의 객체와 같은 참조형 데이터는 값을 복사하더라도 참조하는 값이 동일하기 때문에 내부 프로퍼티의 변화를 잘 감지하기 힘들다거나, 복사된 객체에서의 변경이 원본 객체에도 영향을 미치는 등의 문제점이 있습니다.

만약, 원본 데이터는 값이 변경되지 않도록 두고(불변 객체), 내부 프로퍼티의 변경이 필요할 때는 새로운 객체를 만들어서 할당하게 한다면, 가변성으로 인한 여러 문제를 해결할 수 있습니다.

var obj2 = {...obj1, c: 20};

불변 객체란, 생성 후 값을 변경할 수 없는 객체를 의미하는데, 불변 객체가 필요한 경우는 다음과 같습니다.

  • 정보의 업데이트가 있을 때, 알림을 보내야하는 경우
  • 바뀌기 전 정보와 바뀐 정보의 차이를 가시적으로 보여줘야 하는 경우

불변성을 지킴으로써 얻는 이점은 다음과 같습니다.

  1. 객체 참조 주소값부터 다르므로 깊은 비교를 할 필요가 없어집니다.
    얕은 비교만으로도 객체 구분이 가능하므로 계산 리소스를 줄일 수 있다.
  2. 가변성을 가진 객체의 경우, 원본 데이터가 변경될 때 이를 참고하고 있는 다른 데이터도 같이 변경되기 때문에 예상치 못한 오류(사이드 이펙트)가 발생될 수 있다. 불변성을 통해서 이런 점을 방지할 수 있다.

React에서의 불변 객체

React의 경우, 상태 변화를 감지할 때 변경 전, 후의 props의 업데이트를 얕은 비교를 통해서 감지합니다. 즉, 객체 속성 하나하나를 비교한는 것이 아니라 각 객체의 참조값(주소값)만을 비교하여 상태 변화를 감지하는 것입니다.

Todo 프로젝트를 진행한다고 해봅시다.

/*
type todo = {
	id: string,
	label: string
}
*/

// todoHome.js
onUpdate(todoId, label) {
  this.todos.update(todoId, label);
}

// todos.js
update(todoId, label) {
    var todo = this._todos.find((todo) => todo.id === todoId);

    todo.update(label);
    this.emit('update');
}

shouldComponentUpdate는 컴포넌트가 리렌더링이 될지 여부를 판단하기 위해 호출되는 함수로, 새로 props나 state가 전달 혹은 변경될 때 호출됩니다. (class Component에서 구현 가능)

만약, todo라는 prop을 받는 컴포넌트가 있다고 할 때, 새로운 todo 로 변경될 때마다 리렌더링이 이뤄져야 하기 때문에 하단의 코드와 같이 작성할 수 있습니다. 하지만, nextProps의 todo와 현재 props의 todo가 같은 참조를 하고 있면 해당 조건문은 변경의 유무와 상관없이 항상 거짓을 반환하고 리렌더링은 스킵되게 됩니다.

//todoItem.js

shouldComponentUpdate(nextProps, nextState) {
  return (
    nextProps.todo !== this.props.todo
  );
}

따라서, todo객체의 불변성을 확보해서 상태 변화를 감지할 수 있게 바꿔줘야 하고, 이를 익숙히 사용하는 함수형 컴포넌트에 대입해보면 간단히 다음과 같이 생각해볼 수 있습니다.

export default function App() {
  const [todos, setTodos] = useState([
    { label: "a" },
    { label: "b" },
    { label: "c" }
  ]);

  const handleButtonClickUpdate = () => {
    const copy = todos;
    //가변성
	copy[0].label = "changed";
    setTodos(copy);

	//불변성
    setTodos([{ label: "changed" }, ...copy.slice(1)]);
  };

  return (
    <div className="App">
      <button onClick={handleButtonClickUpdate}>click</button>
      {todos.map((todo, i) => (
        <TodoItem key={i} todo={todo} />
      ))}
    </div>
  );
}

undefined vs null

undefinednull은 자바스크립트에서 모두 없음 을 나타내는 값입니다. undefined의 경우 자바스크립트 엔진에서 특정한 경우에 자동으로 반환해주는 값이기도 함으로 코드 구현에 있어 의미를 명확히 구분할 수 있도록 되도록이면 null을 사용하는 것을 권장합니다.

undefined가 반환되는 경우는 다음과 같습니다

  • 초기화하지 않은 변수에 접근할 때
  • 객체 내부에 존재하지 않은 프로퍼티에 접근할 때
  • return문이 없는 함수의 실행 결과

즉, 값이 비어있음을 명시적으로 표현하기 위해 null을 쓰게 되면, 자바스크립트 엔진에서 반환하는 undefined에 대한 의미와 상황(ex) 값을 대입하지 않은 변수에 접근한 상황)들에 대해 명확히 구분지을 수 있습니다.

참고

Component – React
React와 불변객체

0개의 댓글