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
컴퓨터 상의 데이터는 결국 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으로 분류되는데,
이 두가지로 분류되는 이유와 차이점은 나중에 살펴보고 우선 각 타입의 특징을 알아보자.
// 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
는 소수점을 포함한 실수를 표현하는 숫자 타입이다.
같은 숫자형 타입인데 int
와 float
을 굳이 나누는 이유는 소수점 아래 숫자의 개수가 무한히 길어질 수 있기 때문에 컴퓨터로 표현하는데 한계가 있다는 것에서부터 시작된다.
아래와 같은 무수히 긴 소수를 변수에 담는다고 생각해보자.
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 인터프리터에 따라 크기가 다를 수도 있다고 한다.
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
는 변수를 통해 실행할 수 있는 함수라고 생각하자)
해당 method
는 string
타입의 데이터만 사용 가능하며, 해당 문자의 원래 숫자값을 반환한다.(단, 문자열이 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만 알아두면 될 것 같다.
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
var a;
let b;
위와 같이 선언만 하고 초기화가 되지 않은 변수들은 JS가 자동으로 undefined
로 초기화를 진행한다.
한마디로 undefined
는 값이 없다는 의미다.
변수를 올바르게 초기화하는 것은 상당히 중요하다.
만약 초기화를 깜빡하고 아래와 같이 undefined
와 number
간의 연산을 진행한다면
undefined + 1 // NaN
NaN
이라는 값이 출력되는 것을 볼 수 있다.
Not a Number의 약자로, "가" - 1
등, 논리적으로 불가능한 연산의 결과값으로 가끔 보이곤 한다.
undefined
처럼 특정 데이터 타입을 갖는 것은 아니고 JS에서 미리 만들어 놓은 변수 중 하나다.
타 언어에서는 연산이 불가능하면 오류를 발생시키지만, JS는 이상한 값이 되더라도 넘어가는 경우가 많기 때문에 더욱 조심해야 한다.
오류를 뱉으면 문제의 원인을 빠르게 파악하고 고칠 수 있지만, 오류를 뱉지 않고 넘어가면 증상 없이 점차 악화되는 병처럼, 소리 소문없이 언젠가 심각한 오류의 원인으로 마주하게 될 수도 있다.
let a = null;
null
은 undefined
와 비슷하면서도 달라서 헷갈릴 수 있는 데이터 타입이다.
둘 다 변수 안에 값이 없는, 비어있는 상태임을 의미한다.
그러나 undefined
는 초기화 되지 않은 변수에 JS가 자동으로 할당하는 반면, null
은 사용자가 변수에 직접 null
을 넣어줘야 한다는 차이가 있다.
의도적으로 변수를 비운 것인지 아닌지의 차이라고 볼 수 있겠다.
절대 중복되지 않고 유일한, 변경 불가능한 값이다.
let s1 = Symbol('unique');
let s2 = Symbol('unique');
s1 === s2; // false
같은 key
로 생성했어도 다른 Symbol
로 인식한다.
이후 보게될 객체의 property
로 사용된다.
Reference Type인 object는 Primitive Types , 혹은 또 다른 object들을 묶어서 만드는 커스텀 타입이다.
아래와 같이 생성할 수 있다.
let obj = {}; // empty object
이렇게 만들면 아무런 값도 갖고 있지 않기 때문에, 빈 오브젝트가 된다.
값을 가진 오브젝트를 만들어보자.
let person = { name: 'a', age: 10 };
오브젝트 내부의 name
과 age
를 Property, 속성이라고도 하는데,
이 때 key
와 value
를 한 쌍으로 본다.
key
를 변수명, value
를 값으로 생각하고 변수 안에 또 다른 변수를 담은 중첩 변수 정도로 이해해도 무방할듯 싶다.
자세한 내용은 추후에 object
에 대한 시리즈에서 작성하겠다.