JavaScript의 데이터 타입(Data Type), 원시형과 비원시형의 차이

Jihan·2024년 6월 28일
0

1. JavaScript의 데이터 타입(Data Type)

어떤 프로그래밍 언어를 배우든, 항상 첫 몇 시간 이내에서는 데이터 타입을 배우게 된다.

우리는 시스템을 구축하기 위해 프로그래밍을 하는데, 시스템의 작은 단위인 함수가 작동하기 위해서는 이를 위한 변수가 필요하다. 또, 변수를 저장하기 위해서는 메모리에 이를 위한 공간을 할당해야 하며, 메모리에 저장되어 있는 2진수의 변수들을 읽고, 작성자가 의도한 형태로 읽어내기 위해서는 어디서부터 어디까지 하나의 변수인지, 그리고 이 2진수 덩어리는 어떻게 해석되는지 등을 사전에 정해두어야 한다.

우리는 이러한 약속을 데이터 타입이라고 부르고, 프로그래밍 언어들은 각각 자체적인 데이터 타입 체계를 가지고 있다. 위에서 흘러내려온 것처럼 특정 프로그래밍 언어를 사용해서 시스템을 구축하기 위해서는 그 프로그래밍 언어가 가지고 있는 데이터 타입 체계를 이해해야 한다.

또한 JavaScript는 런타임에 언어의 타입이 정해지는 약타입 언어이다. 심지어 새로운 값이 할당되는 것만으로도 변수의 타입이 재정의된다.

let data = '1'; // typeof data === 'string'
data = 1; // typeof data === 'number'

이러한 방식은 개발자가 알아서 명시적이지 않은 타입 캐스팅 등에 주의를 기울여야 한다는 것을 의미하는데, 우리는 이러한 점들을 고려하여 JavaScript 코드를 작성할 필요가 있다. 기본적으로 JavaScript가 지원하는 데이터 타입에는 어떤 것들이 있는지 살펴보도록 하자.

1.1. Primitive(원시형)와 Non-Primitive(비원시형)

위의 사진과 같이 JavaScript에는 크게 두 가지 데이터 타입이 있다. 바로 Primitive와 Non-primitive인데, Primitive는 한국말로 원시형이라 부르니 Non-primitive는 비원시형이라 부르면 적절할 것 같다.

원시형 변수와 비원시형 변수는 변수의 생성과 참조 방식에 차이가 있다.

원시형 변수의 경우 변수를 선언하고 값을 할당할 때 변수에 값을 직접 저장하지만, 비원시형 변수의 경우 변수를 선언하고 값을 할당할 때 값을 메모리에 저장하고 그 값을 가리키는 참조값을 생성해 변수에 저장한다.

let x = 0;
const y = x;
x = 20;
console.log(x, y);
// 20, 10

위 코드에서 020은 원시형 값인 Number형이므로, xy는 할당 당시에 원시형 변수로 캐스팅되며 그 값을 각각 직접 저장하게 된다. 따라서 위와 같은 코드에서도 xy의 값은 서로 의존하지 않게 된다.

const x = { a: 0 };
const y = x;
x.a = 1;
console.log(x, y);
// { a: 1 } { a: 1 }

하지만 위의 코드에서는 { a: 0 }이 비원시형 값인 Object형이므로, x에 이를 할당할 때 값을 메모리에 저장하고 그 참조값을 x에 저장하게 된다. 따라서 yx를 할당하면 y도 같은 참조값을 가지게 되어 호출하였을 때 같은 메모리의 값을 불러오게 된다.

이것은 개인적인 생각인데, 이와 같은 차이는 데이터의 확장 가능성에서 시작된 설계로 생각된다. 원시형 값들의 경우 일정 범위 안에서 그 값들이 전부 표현되거나, 값들이 가질 수 있는 범위가 한정되어 있다. 이는 값들이 할당될 수 있는 메모리 영역을 제한할 수 있다는 것을 의미하고, 그 메모리의 크기는 변수에 저장할 수 있는 단위에 의존할 것이다. 하지만 비원시형 값들의 경우 그 값들이 어떤 특정한 메모리 범위 내에서 전부 선언될 수 있다는 보장이 없다. 따라서 이러한 값들을 변수에 직접 저장하기에는 변수에 저장할 수 있는 메모리의 크기를 벗어날 수 있기 때문에, 참조값만을 저장하고 그 실제 값은 힙 메모리에서 동적할당 혹은 링킹을 통해 이를 해결하려는 것이다.

아무튼 원시형 데이터 타입과 비원시형 데이터 타입에는 구체적으로 어떤 데이터 타입들이 있는지 살펴보자.

1.2. Primitive

1.2.1. Number

다른 프로그래밍 언어에서 정수를 int형, 실수를 float형 등으로 구분하는 것과 다르게 JavaScript에서는 이를 구분하지 않는 숫자 자료형이 존재한다. Number형 변수는 숫자를 IEEE 754 표준에 의해서 double-precision 64bit format(64비트 이중 정밀도의 부동소수점 형식)으로 저장한다.

Number형 변수는 정확하게 18,437,736,874,454,810,627 (=2^64-2^53+3)개의 값들을 가질 수 있는 것으로 알려져 있다. 이는 Number 자료형이지만 값이 숫자가 아닐 경우를 나타내는 NaN(Not a Number)의 값이 될 수 있는 9,007,199,254,740,991(2^53 - 2)개의 값들을 2^64개의 비트로 나타낼 수 있는 부동소수점 값의 범위에서 제외한 것이다. 이에 대한 자세한 정보는 ECMAScript의 Number Type 명세를 살펴보도록 하자.

NaN과 Infinity값들로 계산 결과가 정의되지 않거나 표현할 수 없는 범위의 값들을 나타낼 수 있다.

const notANumber = NaN;
const positiveInfinity = Infinity;
const negativeInfinity = -Infinity;

1.2.2. BigInt

ECMAScript 명세에는 BigInt 형이 Number 형에 포함되어 있다. BigInt는 말 그대로 큰 숫자를 표현하기 위한 데이터 타입이다. 계산기에 큰 숫자에 대한 연산을 계속해나가다 보면, 1.5e+24와 같은 표현을 볼 수 있다(https://ko.wikipedia.org/wiki/과학적_기수법). 이는 저장공간이나 출력공간에 저장 또는 출력할 수 있는 값의 범위를 벗어났기 때문에 특정 범위의 정확도를 포기하면서 큰 값을 표기하는 방식이다. Number 형도 충분히 큰 값에 대해서 이러한 지수 표기법이 차용되지만, JavaScript에서는 이러한 방식으로 큰 정수값을 표현하기 위해서 BigInt 자료형이 존재한다.

BigInt형은 실수 범위를 포함시키는 것과 연산과 저장의 정밀도 등을 일부 포기하고 저장 가능한 값의 범위를 늘린 숫자 자료형이라고 생각하면 된다.

MDN 문서에서는

BigInt 타입은 임의 정밀도로 정수를 나타낼 수 있는 JavaScript 숫자 원시 값입니다. BigInt로 Number의 안전한 정수 제한(Number.MAX_SAFE_INTEGER)을 넘어서는 큰 정수도 안전하게 저장하고 연산할 수 있습니다.

+*-**% 연산자를 BigInt에서도 사용할 수 있습니다. 금지된 연산자는 >>>뿐 입니다. BigInt는 Number와 엄격하게 같지는 않지만 느슨하게 유사합니다.

고 이를 설명한다.

코드상으로는 아래와 같이 숫자 뒤에 n을 추가하여 해당 값이 BIgInt형임을 나타낼 수 있다.

const x = 10n
console.log(x + 1) // TypeError
console.log(x + 1n) // 11n
console.log(typeof x) // 'bigint'

이렇게 BigInt형으로 선언된 숫자는 아래와 같이 정수의 값의 범위를 벗어나 비교연산자에서 예상하지 못한 값을 출력하는 Number형과 다르게 더 큰 범위의 정수 값에도 그 연산자의 결과를 보장받는 등, 큰 수의 표현에 이점이 생긴다.

console.log(9007199254740992 === 9007199254740993); // true
console.log(9007199254740992n === 9007199254740993n); // false

1.2.3. Boolean

논리 값을 저장하는 변수의 데이터 타입인 Boolean형이다. true와 false의 두 가지 값만을 가질 수 있으며, 논리 연산의 결과값이면서 조건문의 동작조건으로 적용할 수 있다.

const x = true;
if(x) console.log('true') // true

추가로 JavaScript에서 지원하는 논리 연산자는 아래 세 가지이다.

연산자의미사용법반환값
&&논리곱(AND)expr1 && expr2“둘 다 true인가?”를 반환
||논리합(OR)expr1 || expr2“true가 있는가?”를 반환
!논리부정(NOT)!expr“false인가?”를 반환

표현식과 연산자 - JavaScript | MDN

논리값을 반환하는 식들을 논리 연산자로 연산할 때, 위 페이지의 논리 연산자 항목 아래에 위치한 단락 평가(short-circuit)라는 내용에 대해 알고 있으면 좋을 것 같다. 요약하자면, JavaScript엔진들은 논리 연산자를 왼쪽에서 오른쪽의 순서로 평가한다. 따라서 ||(OR)연산자의 앞에 truthy가 오거나, &&(AND)연산자의 앞에 falsy가 오면 그 뒤의 식들은 실행도 하지 않고 각각 참과 거짓으로 인식하고 조건을 평가한다.

1.2.4. String

ECMAScript 표준에서 String형은 16bit의 unsigned integer value가 이어져있는 데이터 묶음이다. 최대 길이는 2^53-1이며, 일반적으로 UTF-16 인코딩된 문자열 값을 갖는다. 특징적인 부분은 딱히 없다. 일반적으로 사용할 때는 길이의 제한에 대해서 느끼기도 어려우며, 다른 자료형보다 추상화되지 않은 상태로 값이 유지되고 출력되기 때문에 비교적 다루기 쉽기 때문이다. MDN의 문서에서는 이러한 부분들 때문에 String자료형을 너무 남용하는 것에 대해 조심하라고 이야기한다.

규칙을 통해, 어떤 자료구조라도 문자열로 표현할 수 있습니다. 그러나 그게 좋은 방법은 아닙니다. 예를 들어, 구분자를 사용하면 (물론 JavaScript 배열이 더 적합하겠지만) 문자열로 리스트를 흉내낼 수도 있을 것입니다. 그러나 구분자를 리스트의 요소로 사용하는 순간 리스트가 망가지고 맙니다. 이제 구분자를 구분하기 위해 이스케이프 문자를 선택하고, 등등... 이 모든 것이 각자의 규칙을 필요로 하고 불필요한 유지보수 부담이 발생합니다.

문자열은 텍스트 데이터에만 사용하세요. 복잡한 데이터를 표현해야 할 땐 문자열을 구문 분석하고 적합한 추상화를 사용하세요.

1.2.5. Symbol

고유한 객체 키의 생성을 위해 사용되는 변수의 타입이다.

심볼의 경우 해당 심볼을 구분하기 위한 특정 값을 생성자 내부에 입력하여 생성하는데, 같은 값으로 심볼을 생성하여도 서로 다른 고유 값을 가지게 된다. 또한, JS의 object는 키 값으로 string과 symbol만을 사용할 수 있도록 하고 있다. 따라서 사용자가 임의로 참조하지 못하는 특정한 객체의 값을 private하게 생성하기 위해 사용되는 변수 타입이며, 특정 값에 심볼 값을 담아서 사용하지 않고 바로 객체에서 심볼키에 대한 값을 선언하여 할당할 경우 해당 키를 어떻게 해서도 참조할 수 없게 된다.

const value = Symbol("symbol");
console.log(typeof value);
// "symbol"

Symbol("value") === Symbol("value")
// false

const obj = {};
obj[value] = "value";
console.log(obj);
// {Symbol(symbol):"value"}

const value2 = Symbol("symbol");
console.log(obj[value2])
// undefined

1.2.6. Undefined

선언되었지만 값이 할당되지 않았거나, 메서드와 선언 또는 함수의 반환에서 변수가 값을 할당받지 못했을 때 해당 변수의 타입을 말한다. Boolean 형이 true와 false를 가질 수 있는 것처럼, Undefined형은 undefined라고 불리는 하나의 값만을 가질 수 있다.

const undefinedVar;
console.log(undefinedVar) // undefined

1.3. Non-primitive(Objects)

비원시형 변수들은 위에서 언급한 것과 같이 변수의 값을 직접 저장하지 않고, 변수의 값이 저장된 위치를 참조하는 참조값을 저장하고 있다. 비원시형 변수가 참조하는 비원시형 값들은 힙 메모리에 저장되며, 힙 메모리는 크기가 동적으로 조정될 수 있기 때문에 비원시형 값들의 크기 또한 동적으로 조정될 수 있다. 저장하려는 데이터의 크기가 동적으로 변할 수 있는 경우를 커버하기 위해서 이러한 방식으로 설계되었으며, 크기나 구조에 제한이 없기 때문에 사용하기에 따라서 데이터의 양과 복잡성이 자유도 높게 증가될 수 있다.

비원시형 변수들이 비원시형 값들의 참조값만을 저장한다는 것 때문에 우리는 얕은 복사, 깊은 복사에 대한 고민과 얕은 비교, 깊은 비교에 대한 고민을 선행하고 이를 활용하여야 한다.

또한 JavaScript에서 비원시형 변수들은 모두 object로 구현되어 있다. 따라서 typeof예약어를 통해 반환되는 비원시형 값들의 자료형은 모두 “object”이며, 간단하게 비원시형 자료형과 원시형 자료형을 비교하기 위해서 일반적으로 typeof를 활용하게 된다.

1.3.1. Object

객체는 JavaScript의 기본 비원시형 자료형으로, 키-값 쌍으로 이루어진 컬렉션 형식이다. 이 키-값 쌍을 속성(property)이라 부른다.

객체 속성의 키는 string형과 symbols형이 될 수 있지만, 객체 속성의 값의 경우 객체 자체를 포함한 모든 자료형을 사용할 수 있기 때문에, 복잡한 형태의 자료구조 구축이 가능하게 만들어준다.

객체를 생성하고 사용하는 기본적인 예시는 다음과 같다.

const Obj = { key: 'value', a: 'b' };

console.log(Obj.key); // 'value'

Obj.newKey = 'newValue';

console.log(Obj); // { key: "value", a: "b", newKey: "newValue" }

// 키-값 쌍 삭제
delete Obj.a;

console.log(Obj); // { key: "value", newKey: "newValue" }

또한 객체의 값을 대괄호 표기법을 사용하여 동적으로 접근하거나 수정할 수도 있다.

const dynamicKey = 'key';
console.log(Obj[dynamicKey]); // 'value'

Obj[dynamicKey] = 'updatedValue';
console.log(Obj.key); // 'updatedValue'

1.3.2. Function

함수는 JavaScript에서 객체의 한 종류로, 코드 블록을 캡슐화하여 재사용 가능한 단위로 만들 수 있다. 함수는 선언식, 표현식 등 다양한 방식으로 정의할 수 있으며, 일급 객체로 취급되어 다른 함수의 인수나 반환값으로 사용할 수 있다.

function add(a, b) {
  return a + b;
}

const sub = function(a, b) {
  return a - b;
};

const mul = (a, b) => a * b;

또한 함수는 객체이기 때문에 프로퍼티를 가질 수 있다.

function greet() {
  console.log('Hello!');
}

greet.language = 'English';
console.log(greet.language); // 'English'

1.3.3. Null

null은 의도적으로 값이 없음을 나타내는 특수한 값이다. 이는 변수가 객체를 참조하지 않음을 명확히 하기 위해 사용된다. nulltypeof 연산자를 사용할 때 object로 간주되며, 이는 JavaScript의 초기 설계 결함으로 알려져 있다.

const nullVar = null;
console.log(typeof nullVar); // 'object'

1.3.4. Array

Array 객체는 순서가 있는 값들의 리스트로, 각 값은 인덱스로 접근할 수 있다. 배열은 다양한 내장 메서드를 제공하여 배열의 요소를 조작하거나 정보를 얻을 수 있게 한다.

const arr = [1, 2, 3];
console.log(arr[0]); // 1

arr.push(4);
console.log(arr); // [1, 2, 3, 4]

arr.pop();
console.log(arr); // [1, 2, 3]

1.3.5. Date

Date 객체는 날짜와 시간을 나타내기 위해 사용된다. 현재 날짜와 시간뿐만 아니라 특정 날짜와 시간을 설정할 수도 있다.

const now = new Date();
console.log(now.toISOString()); // 현재 날짜와 시간을 ISO 8601 형식으로 출력

const specificDate = new Date('2020-01-01');
console.log(specificDate.toDateString()); // 'Wed Jan 01 2020'

1.3.6. RegExp

RegExp 객체는 정규 표현식을 나타내며, 문자열에서 패턴을 검색하거나 대체하는 데 사용된다. 정규 표현식은 문자열의 특정 패턴을 찾거나 검증하는 데 유용하다.

const regex = /hello/i;
console.log(regex.test('Hello world!')); // true

const replaced = 'Hello world!'.replace(regex, 'Hi');
console.log(replaced); // 'Hi world!'

1.3.7. Map, WeakMap

Map 객체는 키-값 쌍을 저장하며, 키의 원시 자료형 또는 객체를 사용할 수 있다. Map은 객체와 달리 키의 삽입 순서를 기억하며, 동일한 키가 중복되지 않는다.

const map = new Map();
map.set('key1', 'value1');
map.set('key2', 'value2');

console.log(map.get('key1')); // 'value1'
console.log(map.size); // 2

WeakMap 객체는 키로 객체만을 사용하고, 키로 설정된 객체에 대한 약한 참조를 유지한다. 이는 키로 설정된 객체가 더 이상 참조되지 않을 때 가비지 컬렉션의 대상이 될 수 있게 한다.

const weakMap = new WeakMap();
const obj = {};
weakMap.set(obj, 'value');

console.log(weakMap.get(obj)); // 'value'

1.3.8. Set, WeakSet

Set 객체는 중복되지 않는 값들의 집합을 저장하며, 값은 원시 자료형 또는 객체일 수 있다. Set은 값의 삽입 순서를 기억하며, 배열과 유사하지만 중복 값을 허용하지 않는다.

const set = new Set();
set.add(1);
set.add(2);
set.add(2);

console.log(set.has(1)); // true
console.log(set.size); // 2

WeakSet 객체는 객체의 집합을 저장하고, 각 객체에 대한 약한 참조를 유지한다. WeakSet에 저장된 객체가 더 이상 참조되지 않을 때 가비지 컬렉션의 대상이 될 수 있다.

const weakSet = new WeakSet();
const obj1 = {};
weakSet.add(obj1);

console.log(weakSet.has(obj1)); // true

1.3.9. 왜 이렇게 많지?

JavaScript에는 객체를 기반으로 하는 다양한 내장 자료형들이 있다.

로우레벨의 자료형으로 따질 때는 이들 모두 object를 기반으로 하여 구현되어있기 때문에, typeof 연산자로 평가하면 object로 반환된다. 엔진의 입장에서는 이들 모두 object인 것이다.

그러니 이렇게 다양한 자료형들을 모두 분류할 필요 없이, JavaScript의 object, class, prototype 등 언어가 가진 특징들을 기반으로 필요에 맞게 가장 적절한 형태로 구현되어 있는 자료형들이라고 보면 된다. 또한 각 자료형들을 다룰 때 필요한 빌트인 함수들도 함께 프로토타입으로 제공되고 있다. 위에서 소개한 아이들은 특히 그 중에서도 많이 사용되며 JavaScript를 공부할 때 꼭 한 번은 마주쳐야 하는 아이들이다.

각 자료형마다 인터페이스가 어떤 식으로 구축되게 된 자체적인 히스토리들도 있고(Date…), 능숙하게 다루기 위해서 반드시 알아야 할 지엽적인 자체개념들이 있기 때문에 각 자료형마다 시간을 들여 공부할 필요가 있다.

또한 우리는 object, class, prototype 등의 개념을 활용하여 필요에 따라서 자료형을 직접 구축할 수도 있다. 필요에 따라 적절한 자료형을 활용하거나 생성하여 활용하는 연습을 꾸준히 하도록 해보자.

profile
DIVIDE AND CONQUER

0개의 댓글

관련 채용 정보