[JavaScript] 2. Data Types

100tick·2022년 12월 12일
0

JavaScript Deep Dive

목록 보기
2/16
post-thumbnail

JS의 데이터 타입

JS는 8가지 데이터 타입을 제공한다.(ES2020 기준)

// Primitive Types
1 			// number
"abc"	 	// String
true 		// boolean
undefined 	// undefined
null		// null
Bigint(1)	// BigInt
Symbol()	// Symbol

// Object(Reference) Types
Object()	// object

1. 모든 것은 bit

컴퓨터 상의 데이터는 결국 0, 1로 이루어진 bit의 덩어리일 뿐이다.
데이터 타입이란 해당 타입이 몇 bit의 공간을 사용할 것이며, 그것을 어떻게 해석할 것인지 구별하기 위해 존재한다.

예를 들어 97을 이진수로 나타내면 1100001이 된다.
이 수를 4byte 공간에 저장하면 00000000 00000000 00000000 01100001이 된다.
8byte 공간에 저장하면 당연히 00000000 00000000 00000000 00000000 00000000 00000000 00000000 01100001이 될 것이다.

00000000 00000000 00000000 01100001의 비트 패턴이 저장된 변수를 콘솔에 출력한다면, 97이 보일 것이다.

만약 이 97문자로 출력한라면?

a를 출력할 것이다.

이렇듯 같은 비트 패턴도 데이터 타입이 무엇인가에 따라 다르게 출력될 수 있다.

이 사실을 숙지하고 각 데이터 타입을 자세히 살펴보자.

데이터 타입은 크게 Primitive/Reference Type으로 분류되는데,
이 두가지로 분류되는 이유와 차이점은 나중에 살펴보고 우선 각 타입의 특징을 알아보자.


1. Primitive Types

1.1 number

// golang
var a int = 1
var b float64 = 1.0

a == b // invalid operation: a == b (mismatched types int and float64)

위 코드는 JS가 아닌 Go로 작성된 코드이다.
Go 뿐만 아니라 대부분의 언어는 정수/실수에 따라 데이터 타입이 나뉜다는 것을 보여주기 위해 예시를 들은 것이다.

let a = 1;
let b = 1.0;
a === b; // true

그리고 다음이 JS 코드다.

Go와 달리 a는 정수, b는 실수임에도 불구하고 비교 연산이 가능하다.
JS의 숫자형 데이터는 정수, 실수를 따로 나누지 않고 number라는 타입 하나로 모든 숫자를 표현하기 때문이다.

이와 상반되게 Go를 비롯한 여타 대부분의 언어에서 숫자 타입은 크게 int, float으로 나뉘며 int는 정수, float는 소수점을 포함한 실수를 표현하는 숫자 타입이다.

같은 숫자형 타입인데 intfloat을 굳이 나누는 이유는 소수점 아래 숫자의 개수가 무한히 길어질 수 있기 때문에 컴퓨터로 표현하는데 한계가 있다는 것에서부터 시작된다.

아래와 같은 무수히 긴 소수를 변수에 담는다고 생각해보자.
let a = 0.12345678901234567890123456789012345678901234567890...;
지면상 생략했지만 a가 소수점 아래 100자리까지 존재한다면 어느 정도의 용량이 필요할까.

비교 대상이 필요할테니 일반적인 정수형 타입, int가 요구하는 용량과 표현 가능한 수의 범위부터 알아보자.

int는 32bit 이진수로 2의 32승, 즉 4,294,967,296가지 수를 표현할 수 있다.
음수까지 표현하므로 실제 범위는 −2,147,483,648 ~ 2,147,483,647가 된다.
(bit까지 표기해서 타입명을 int32라고 쓰기도 하며, 더 큰 범위가 필요한 경우 int64를 사용한다. 타입에 bit를 표기하지 않으면 보통 32bit라고 보면 된다.)

대부분의 경우 32bit int로 충분하지만 부족하다면 int64를 사용한다.
18,446,744,073,709,551,615가지 수를 표현할 수 있으며, 음수를 포함할 경우 실제 범위는 -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807가 된다.
(int32는 겨우 21억까지였지만, int64는 무려 923경까지 표현이 가능하다.)

그런데 이 거대한 숫자도 자릿수는 겨우 19자리에 불과하다.
256bit 쯤 돼야 77자리를 표현할 수 있는데, 여전히 부족하다.

100자리를 표현하려면 적어도 300~400bit를 써야 한다는 이야긴데, 수지가 안맞다.
용량이 일반적인 정수의 10배가 넘어가는 매우 비효율적인 방법인 것이다.

구글, 아마존 등의 세계적인 IT기업들도 용량을 조금이라도 줄이기 위해 계속해서 막대한 투자를 하고 있는데, 이런 비효율적인 방식을 누가 용납할까?

아니, 애초에 소수점 아래 수 자리조차 사용할 일이 그렇게 많지 않다.

주식 가격이 올랐을 때, 11.13173448222911944491129393%가 올랐습니다. 라는 메세지를 보고 싶은 사람은 아무도 없을 것이다.
11.1%가 올랐습니다. 정도면 충분할 것이다.

고로 효율적인 사용을 위해 float 타입의 소수들은 IEEE 754라는 부동소수점 표준 방식을 따르며, 이 방식을 통해 구현된 수들은 합리적인 용량을 갖고, 소수점 아래 15~17자리까지 표현할 수 있다.(64bit 기준)

다만 정확도는 매우 떨어진다.
계좌 이체, 탄도 계산 등 오차가 있어서는 안되는 연산에는 절대로 사용되면 안된다.
(실제로 float64 오류로 인해 미사일 궤도 오류 등 커다란 사고가 있었음)

부동소수점의 정밀도가 얼마나 낮은지 알아보기 위해 당장 0.1 x 0.1 연산을 JS에서 해보자.
(JS의 숫자형 타입은 기본적으로 IEEE 754 즉, 위에서 언급한 float64과 같은 형식이다)

0.1 * 0.1 // 0.010000000000000002

납득할 수 없는 결과가 나오지만, 이것이 현실이다.

고도의 정밀한 연산이 필요하다면 다른 언어를 사용하거나 BigInt 타입을 사용하는 것이 좋으며,
어떤 언어를 사용하더라도 float, double 등의 부동소수점 타입은 정밀도가 매우 떨어진다라는 사실을 항상 기억해두는 것이 좋다.
혹시라도 오차가 결코 있어서는 안될 매우 중요한 연산에 이것이 사용되는 불상사가 없도록 하기 위해서.

참고로 number 타입은 대체로 32bit 환경에서 4byte, 64bit는 8byte를 차지하지만 JS 인터프리터에 따라 크기가 다를 수도 있다고 한다.

1.2 string

string에 대한 내용 중 잘못 알고 있던 부분이 있어서 정정한다.
JS의 문자열은 UTF-8이 아닌 UTF-16 기반이라고 한다.
https://exploringjs.com/impatient-js/ch_unicode.html#encodings-used-in-web-development-utf-16-and-utf-8

프로그래밍에서 string은 문자열 데이터 타입을 의미한다.
숫자형 데이터 타입과의 차이는 작은 따옴표 ' ' 혹은 큰 따옴표 " " 혹은 ` ` 안에 문자들을 넣어줘야 string으로 인식한다는 것이다.

1 // number
'1' // string

'abc' // string
"cde" // string
`fgh` // string
// error ' ', " " 두 개를 섞어 쓰면 안 됨
'abc" 

위에서 97을 문자 형태로 출력하면 a가 된다는 이야기를 했다.
실제로 그런지 확인해보자.
방법은 아래와 같다.

'a'.charCodeAt(); // 97

charCodeAt()string 타입의 변수가 호출할 수 있는 method다.
(method는 변수를 통해 실행할 수 있는 함수라고 생각하자)

해당 methodstring 타입의 데이터만 사용 가능하며, 해당 문자의 원래 숫자값을 반환한다.(단, 문자열이 2글자 이상이면 맨 앞 1글자의 숫자값만 반환하고 나머지는 무시된다.)

문자처럼 보이도록 출력하지만, 문자도 결국 숫자로 이루어진 데이터고, 각 문자마다 대응되는 숫자값을 미리 정해놓았기 때문이다.
어떤 문자가 어떤 숫자에 대응될 것인지를 정해서 만들어 놓은 것이 바로 ASCII, UTF-8등의 문자 인코딩 방식이다.
https://en.wikipedia.org/wiki/ASCII

string은 4byte or 8byte로 고정된 number와 달리 문자의 개수가 증가할수록 더 많은 byte를 차지한다.
또한 각 문자가 갖는 크기는 언어에 따라 달라질 수 있다.
최근의 프로그래밍 언어들은 대부분 UTF-8 인코딩 방식을 사용하는데, 극한의 효율을 위해 문자별 가변 크기를 갖기 때문이다.
아래 코드를 보자.

new Blob(['']).size; // 0
new Blob(['a']).size; // 1
new Blob(['가']).size; // 3
new Blob(['😊']).size; // 4

new Blob()이 무엇인지는 신경쓰지 말고, 이것이 문자열이 몇 byte인지 알려준다는 것에 집중하자.

빈 문자열은 0byte, 알파벳은 1byte, 한글은 3byte, 이모티콘은 4byte의 공간을 차지한다.

new Blob(['a가']).size; // 4(1 + 3)

알파벳(1byte)과 한글(3byte)을 하나의 문자열로 연결해도 크기가 각 문자를 더한 값(4byte)과 일치한다.

앞서 말한 UTF-8이라는 가변형 인코딩 방식으로 동작하기 때문인데, 켄 톰슨, 롭 파이크라는 우리보다 아득하게 앞서 활동한 업계의 거장(C, Go, Unix 등을 개발하기도 했다)들이 설계하였고, 원리가 생각보다 간단하니 자세한 내용은 직접 찾아보면 좋을듯하다.

이것을 알면 IP주소의 Class도 바로 이해할 수 있으므로 함께 찾아보면 일석이조일 것이다.
별로 관심이 없다면 알파벳 1byte, 한글 3byte만 알아두면 될 것 같다.

1.3 boolean

let a = true;
let b = false;

참(true) or 거짓(false) 2가지를 나타낼 수 있는 데이터 타입이다.
2가지 경우만 표현하면 되므로 이론상 1bit만 있어도 표현할 수 있다.

그러나 컴퓨터는 최소 1byte 단위로 데이터를 처리하고, 앞서 살펴본 메모리 주소의 단위도 1byte로 나누어져 있기 때문에 사실상 1byte(8bit)가 가장 작은 단위라고 보면 된다.(극한의 효율을 위해 1byte 안의 각 bit를 하나의 boolean으로 보고 총 8개로 쪼개서 사용하는 bit flag 같은 경우도 있지만 그정도의 최적화가 JS에서 요구 되지는 않을듯)

결론은 boolean은 데이터의 최소 단위인 1byte지만, 아래 첨부된 링크에 작성된 글에 따르면, CPU가 한번에 읽어오는 단위는 아키텍쳐에 따라 4byte, 8byte 등이 되고, 상황에 따라 Alignment가 일어나면 4byte(32bit CPU) 혹은 8byte(64bit CPU)가 될 수도 있다고 한다.
https://shevchenkonik.com/blog/memory-size-of-boolean

1.4 undefined

var a;
let b;

위와 같이 선언만 하고 초기화가 되지 않은 변수들은 JS가 자동으로 undefined로 초기화를 진행한다.

한마디로 undefined값이 없다는 의미다.

변수를 올바르게 초기화하는 것은 상당히 중요하다.
만약 초기화를 깜빡하고 아래와 같이 undefinednumber간의 연산을 진행한다면

undefined + 1 // NaN

NaN이라는 값이 출력되는 것을 볼 수 있다.
Not a Number의 약자로, "가" - 1 등, 논리적으로 불가능한 연산의 결과값으로 가끔 보이곤 한다.
undefined처럼 특정 데이터 타입을 갖는 것은 아니고 JS에서 미리 만들어 놓은 변수 중 하나다.

타 언어에서는 연산이 불가능하면 오류를 발생시키지만, JS는 이상한 값이 되더라도 넘어가는 경우가 많기 때문에 더욱 조심해야 한다.

오류를 뱉으면 문제의 원인을 빠르게 파악하고 고칠 수 있지만, 오류를 뱉지 않고 넘어가면 증상 없이 점차 악화되는 병처럼, 소리 소문없이 언젠가 심각한 오류의 원인으로 마주하게 될 수도 있다.

1.5 null

let a = null;

nullundefined와 비슷하면서도 달라서 헷갈릴 수 있는 데이터 타입이다.
둘 다 변수 안에 값이 없는, 비어있는 상태임을 의미한다.

그러나 undefined는 초기화 되지 않은 변수에 JS가 자동으로 할당하는 반면, null은 사용자가 변수에 직접 null을 넣어줘야 한다는 차이가 있다.

의도적으로 변수를 비운 것인지 아닌지의 차이라고 볼 수 있겠다.

1.6 Symbol

절대 중복되지 않고 유일한, 변경 불가능한 값이다.

let s1 = Symbol('unique');
let s2 = Symbol('unique');

s1 === s2; // false

같은 key로 생성했어도 다른 Symbol로 인식한다.
이후 보게될 객체의 property로 사용된다.


2. Reference Types

2.1 object

Reference TypeobjectPrimitive Types , 혹은 또 다른 object들을 묶어서 만드는 커스텀 타입이다.
아래와 같이 생성할 수 있다.

let obj = {}; // empty object

이렇게 만들면 아무런 값도 갖고 있지 않기 때문에, 빈 오브젝트가 된다.

값을 가진 오브젝트를 만들어보자.

let person = { name: 'a', age: 10 };

오브젝트 내부의 nameageProperty, 속성이라고도 하는데,
이 때 keyvalue를 한 쌍으로 본다.
key를 변수명, value를 값으로 생각하고 변수 안에 또 다른 변수를 담은 중첩 변수 정도로 이해해도 무방할듯 싶다.

자세한 내용은 추후에 object에 대한 시리즈에서 작성하겠다.

0개의 댓글