%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%8D%B8%EB%84%A4%EC%9D%BC-038


요즘 자바스크립트 면접 스터디를 하고 있는데, 데이터 타입에 대한 질문이 나왔다.

물론 실제 면접에서 해당 질문이 나올 확률은 매우 낮다. 하지만 나는 원시 타입과 객체 타입에 대한 차이를 정확하게 모르고 있었다..!

따라서 코어자스 1장을 다시 읽고, 정리글을 작성해보려 한다.

항상 기초가 중요함을 잊지 말자.





01. 데이터 타입의 종류

자바스크립트의 데이터 타입에는 두 종류가 있다.

  • 기본형 (원시 타입, primitive type) - 숫자, 문자열, 불리언, null, undefined, 심볼
  • 참조형 (객체 타입, reference type) - 객체, 배열, 함수, 날짜, 정규표현식 등


이 둘은 할당이나 연산 시 다른 점을 가진다.

  • 기본형은 값이 담긴 주솟값을 바로 복제
  • 참조형은 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주소값을 복제


이 둘의 차이를 명확하게 이해하기 전에 잠시 메모리와 데이터에 대한 배경지식을 먼저 훑고 지나가자.





(*) 배경지식

1. 메모리와 데이터

  • 비트 - 0 또는 1로 표현할 수 있는 하나의 메모리 조각 (0, 1의 2가지 값 표현 가능)
  • 바이트 - 8개의 비트를 묶은 메모리 단위 (2^8개의 값 표현 가능)


정적 타입 언어

정적 타입 언어란, 타입 즉 자료형을 컴파일 시에 결정하는 언어를 말한다. 정적 타입 언어에는 C, C++, Java 등이 있다.

이러한 정적 타입 언어는 메모리의 낭비를 최소화하기 위해 데이터 타입별로 할당할 메모리 영역을 2바이트, 4바이트 등으로 나누어 미리 정해놓는다.


예를 들어 2바이트 정수형 타입 (short)은 0을 포함한 -32768 ~ +32768의 숫자만 허용한다.
만약 사용자가 범위를 벗어나는 정수를 입력하면, 오류가 발생한다.

따라서 사용자는 직접 4바이트 정수형 타입(int) 등으로 직접 형변환을 함으로써 오류를 해결할 수 있다.


동적 타입 언어

메모리 용량이 과거보다 월등히 커진 상황에서 자바스크립트가 등장했다.


동적 타입 언어란, 타입 즉 자료형을 실행 시에 결정하는 언어를 말한다. 따라서 타입을 미리 지정하는 것이 아닌, 변수만 선언하여 값을 지정할 수 있다. 동적 타입 언어에는 JavaScript, Python 등이 있다.


동적 타입 언어는 메모리 공간을 정적 타입 언어에 비해 넉넉하게 할당한다.

예를 들어 숫자의 경우 정수형인지 부동소수형인지 구분하지 않고 64비트 (8바이트)를 확보한다.




2. 식별자와 변수

  • 식별자 - 어떤 값을 구별해서 식별할 수 있는 이름, 즉 변수, 함수, 클래스의 이름을 뜻한다.
  • 변수 - 변할 수 있는 무언가, 변경 가능한 데이터가 담길 수 있는 공간


💡
정리하자면, 모든 데이터는 비트, 바이트 단위의 메모리 공간을 할당받는다. 바이트와 비트는 고유한 식별자로 해당 메모리 주소를 파악할 수 있다.

즉 모든 데이터는 바이트 단위의 식별자, 즉 메모리 주솟값을 통해 서로 구분하며 연결할 수 있다.





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

1. 변수 선언

예를 들어 해당 변수를 선언하면, 메모리 영역의 변화는 다음과 같다.

var a;

Untitled

컴퓨터는 메모리에서 비어있는 공간 하나를 확보하고, 이 공간의 이름을 식별자인 a로 지정한다.

이후에 사용자가 a에 접근하고자 하면, 컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색해 해당 공간에 담긴 데이터를 반환하게 된다.




2. 데이터 할당

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

var a = "abc"; //   변수 선언과 할당을 한 문장으로 표현

선언 과정은 위에서 살펴본 것과 같으며, 할당 과정의 경우 a라는 주소를 검색해 그곳에 문자열 ‘abc’를 할당함으로써 이루어진다.


생각과 다르게 실제로는 해당 위치에 ‘abc’라는 값을 직접 넣지 않는다.

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


Untitled 1


💡
변수 영역에 직접 값을 대입하지 않는 이유는, 데이터 변환을 자유롭게 할 수 있게 함과 동시에, 메모리를 더욱 효율적으로 관리하기 위함이다.

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

메모리 공간에 여러 값이 들어있고, 중간에 위치한 값의 크기를 늘렸다 가정해보자.
공간의 크기를 늘릴 뿐 아니라, 뒤쪽에 저장된 데이터를 늘린 공간만큼 뒤로 옮겨야 하는 번거로운 작업이 함께 이루어져야 한다.

따라서 효율적으로 데이터의 변환을 처리하기 위해서 변수와 데이터를 별도의 공간에 나누어 저장하는 것이 최적의 방법이다.


var a = "abc";
a = a + "def";

해당 코드와 같이 문자열이 변경되면, ‘abcdef’라는 문자열을 새로 만들어 별도의 공간에 저장하고, 그 주소를 변수 공간에 연결하게 된다.


Untitled 2





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

1. 불변값

기본형 데이터 (원시 타입) 데이터는 모두 불변값이다. 그렇다면 불변값은 무엇일까?


변수와 상수를 구분하는 성질은 ‘변경 가능성’이다.

변수 영역 메모리를 바꿀 수 있으면 변수, 바꿀 수 없으면 상수이다.

하지만 불변값, 즉 불변성 여부를 구분할 때의 변경 가능성의 대상은 데이터 영역 메모리이다.


Untitled 3



// Ex 1

var a = "abc";
a = a + "def";

해당 코드에서 변수 a‘abc’를 할당했다가 뒤에 ‘def’를 추가하면 기존의 ‘abc’‘abcdef’로 바뀌는 것이 아니다.

새로운 문자열 ‘abcdef’를 만든 후, 그 주소값을 변수 a에 저장하게 된다.

따라서 ‘abc’‘abcdef’는 별개의 데이터이다.


// Ex 2

var num1 = 5;
var num2 = 5;

num1 = 10;

변수 num1에 숫자 5를 할당하게 되면, 컴퓨터는 데이터 영역 메모리에서 5의 값을 찾고, 없을 시 임의에 메모리 공간에 5의 값을 할당한다. 그 이후 변수 num1에 해당 메모리 주소를 할당한다.

num2에도 숫자 5를 할당하게 되면, 데이터 영역 메모리에서 5의 값을 가진 메모리 주소를 찾아 num2에 할당하게 된다.

마지막 줄의 경우, 10의 값을 새로운 메모리 공간에 할당하고 num1에 해당 메모리 주소를 할당하게 된다.

따라서 5, 10 모두 다른 값으로 변경될 수 없으며, 새로운 값의 할당은 새 메모리 공간을 확보하며 이루어진다.


💡
이처럼 문자열 값도, 숫자 값도 값을 변경할 수 없다.
변경은 새로 만드는 동작을 통해서만 이뤄진다.

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




2. 가변값

참조형 데이터(객체 타입)은 가변값으로 사용할 수 있다.


참조형 데이터의 선언 및 할당 과정은 다음과 같다.

var obj1 = {
  a: 1,
  b: "bbb",
};


Untitled 4

그림에서 확인할 수 있듯이, 참조형 데이터와 기본형 데이터와의 차이는 ‘객체의 변수(프로퍼티) 메모리 영역’이 별도로 존재한다는 것이다.


만약 해당 코드로 a 프로퍼티에 다른 값을 대입한다면 어떻게 될까?

var obj1 = {
  a: 1,
  b: "bbb",
};

obj1.a = 2;

Untitled 5

변수 obj1이 바라보고 있는 주소값인 @5001은 바뀌지 않았다.

즉 새로운 객체가 만들어진 것이 아닌, 기존의 객체 내부의 값만 바뀌게 된다.


💡
객체가 별도로 할애한 영역은 변수 영역일 뿐, ‘데이터 영역’은 기존의 메모리 공간을 그대로 활용한다. 또한 데이터 영역의 저장된 모든 값은 불변값이다.
하지만 변수에는 얼마든지 다른 값을 대입할 수 있다.

= 따라서 참조형 데이터는 불변하지 않다(가변값이다).


(+) 가비지 컬렉터

불변값인 데이터 메모리 영역의 값들이 계속해서 쌓이면, 메모리의 낭비가 일어날것이다.

따라서 가비지 컬렉터(Garbage collector, GC)가 존재한다.


가비지 컬렉션이란 런타임 환경에 따라 특정 시점이나 메모리 사용량이 포화 상태에 임박할 때, 메모리의 수거 대상들을 수거(collecting)하는 메모리 관리 기법이다.


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

가비지 컬렉터는 어떠한 데이터의 참조 카운트가 0일 때, 수거 대상으로 판단하며 특정 시점에 이를 수거해 메모리를 관리한다.




3. 변수 복사 비교

기본형 데이터와 참조형 데이터의 변수 복사 변화를 살펴보자.

var a = 10;
var copyA = a;

var obj = { c: 10, d: "str" };
var copyObj = obj;

Untitled 6

기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 것은 동일하다.


하지만 변수 복사 이후, 데이터 할당 과정에서 큰 차이가 발생한다.

var a = 10;
var copyA = a;

var obj = { c: 10, d: "str" };
var copyObj = obj;

copyA = 15;
copyObj.c = 20;

Untitled 7

변수 acopyA는 서로 다른 주소를 바라보고 있게 되었으나,

변수 objcopyObj의 경우에는 여전히 같은 객체를 바라보고 있다.

a !== copyA;
obj === copyObj;

이 결과가 바로 기본형과 참조형의 가장 큰 차이점이다.


기본형과 참조형의 값을 복사하는 과정은 동일하지만, 기본형은 주솟값을 복사하는 과정이 한번만 일어나고, 참조형은 한 단계를 더 거치게 된다는 차이가 존재한다.



만약 참조형 데이터의 프로퍼티를 변경하는 것 외에, 객체 자체를 새롭게 할당하게 되면 어떻게 될까

var obj = { c: 10, d: "str" };
var copyObj = obj;

copyObj = { c: 20, d: "new str" };

이 경우에는 copyObj에도 새로운 객체를 할당함으로써 값을 직접 변경했다.

따라서 메모리의 데이터 영역에 새로운 공간에 새 객체가 저장되고, 그 주소를 변수 영역의 copyObj에 저장하게 된다.

따라서 객체에 대한 변경임에도 변수에 할당되는 값이 달라질 수 있다.


💡
따라서 참조형 데이터가 ‘가변값’이라고 설명할 때의 ‘가변’은 참조형 데이터 자체를 변경할 경우가 아닌 그 내부의 프로퍼티를 변경할 때만 성립한다.





04. 불변 객체

1. 얕은 복사와 깊은 복사

  • 얕은 복사 - 바로 아래단계의 값(주소값)만 복사
  • 깊은 복사 - 내부의 모든 값들을 전부 복사


얕은 복사로 생성한 원본과 사본의 경우, 동일한 주소값을 참조한다. 따라서 사본의 프로퍼티 변경 사항이 원본에도 반영되는 특징이 있다.

깊은 복사의 경우, 실제 데이터가 저장된 주소 값까지 전부 복사하기 때문에 사본과 원본은 아예 다른 객체가 된다.


💡
특정 객체를 복사할 때 객체 내부의 모든 값을 복사해서 완전히 새로운 데이터를 만들고자 할때,
객체의 프로퍼티 중에서 그 값이 기본형 데이터일 경우에는 그대로 복사하면 되지만
참조형 데이터는 다시 그 내부의 프로퍼티들을 복사해야 한다.

이 과정을 참조형 데이터가 있을 때마다 재귀적으로 수행해야만 비로소 깊은 복사가 된다.

깊은 복사를 수행함으로써 불변 객체를 만들 수 있다.




2. JSON

JSON을 사용해 간단하게 깊은 복사를 수행할 수 있다.

원본 객체를 JSON 문법으로 표현된 문자열로 전환했다가 다시 JSON 객체로 바꾸면, 원본과 관련이 없는 새로운 객체가 생성된다.


다만 다음과 같은 프로퍼티들은 JSON으로 변경할 수 없다.

  • 메서드
  • 숨겨진 프로퍼티 __proto__
  • getter/setter


var copyObject = function (target) {
  return JSON.parse(JSON.stringify(target));
}

var originalObj = {
  a: 1,
  b: {
    c: null,
    d: [1, 2],
    func1: function () { console.log('function1'); }
    func2: function () { console.log('function2'); }
  }
}

var copyObj = copyObject(originalObj);

copyObj.a = 3;  // 복사한 배열 변경
copyObj.b.c = 4; // 복사한 배열 변경
originalObj.b.d[1] = 3; // 원본 배열 변경

console.log(originalObj);
// { a: 1, b: { c: null, d: [1, 3], func1: f(), func2: f() } }
console.log(copyObj);
// { a: 3, b: { c: 4, d: [1, 2] } }

마지막 출력 결과를 보면, 복사된 배열에는 메서드가 존재하지 않으며

두 객체는 서로 영향을 미치지 않는 독립적인 객체임을 알 수 있다.





05. undefined와 null

  • undefined - 자바스크립트 엔진이 비어있는 값에 대해 자동으로 부여하는 값
  • null - 비어있는 값임을 명시적으로 나타낼 때 사용하는 값




1. undefined

undefined는 다음과 같은 경우에 자바스크립트 엔진이 자동으로 부여한다.

  • 값을 대입하지 않은 변수 (var 키워드 변수 선언 시)
  • 객체 내부의 존재하지 않는 프로퍼티에 접근
  • return 문이 없거나 호출되지 않는 함수의 실행 결과


💡
undefined는 배열이나 객체에 임의로 할당하게 되면, 하나의 값으로써 동작한다.
비어있는 배열을 출력했을 때의 empty items와 다르게 undefined는 순회의 대상이 된다.

즉 자바스크립트 엔진이 반환해주는 undefined는 문자 그대로 값이 없음을 나타내며
임의로 할당한 undefined는 실제로 존재하는 데이터로써 동작하게 된다.

따라서 직접 undefined를 할당하는 것은 지양해야 하며, 비어있음을 알리기 위해서는 같은 의미의 null을 사용해야 한다.




2. null

사용자가 명시적으로 ‘비어있음’을 나타내고 싶을 때 null을 사용한다.


하지만 nulltypeof null의 값이 object로 출력되는 특징이 있다.

따라서 어떤 데이터의 값이 null인지 확인하기 위해서는 엄격 동등 연산자(일치 연산자, ===)를 사용해야 한다.





06. 정리

자바스크립트 데이터 타입

  • 기본형(원시타입) - 불변값
  • 참조형(객체타입) - 가변값


변수와 식별자

  • 변수 - 변경 가능한 데이터가 담길 수 있는 공간 (메모리)
  • 식별자 - 변수의 이름 또는 무언가를 식별할 수 있는 고유한 이름


변수 선언 및 할당

  • 기본형 데이터 할당
    → 별도에 공간에 값 저장 후 해당 공간의 주소를 변수의 값에 할당
  • 참조형 데이터 할당
    → 참조형 데이터 내부 프로퍼티들을 위한 변수 영역을 별도로 확보
    → 확보된 주소를 변수에 연결
    → 확보한 변수 영역에 각 프로퍼티의 식별자 저장
    → 각 데이터를 별도의 공간에 저장해서 그 구조를 식별자와 매칭

할당 과정에서 기본형과 차이가 생긴 이유는, 참조형 데이터가 여러개의 프로퍼티(변수)를 모은 ‘그룹’이기 때문이다. 이 차이로 인해 참조형 데이터를 ‘가변값’으로 여긴다.


불변 참조형 데이터

  • 깊은 복사 - 내부 프로퍼티들을 전부 복사하는 방법

깊은 복사를 사용해 참조형 데이터를 불변값으로 사용할 수 있다.


undefined와 null

  • undefined - 값이 없을 경우, 자바스크립트 엔진이 자동으로 부여하는 ‘없음’의 의미
  • null - 사용자가 값이 ‘없음’을 명시적으로 나타낼 수 있는 값

본래의 의미에 따라 사용자가 없음을 표현하기 위해서는 null을 사용하는 것이 지향된다.






출처

코어 자바스크립트 1장

댓글남기기