다른 언어를 좀 만져본 사람이 자바스크립트로 넘어오면 처음엔 만만해 보인다. 변수 선언하고 값 넣고, 다 아는 얘기 아니냐 싶다. 그러다 어느 날 typeof null'object'라고 찍히는 걸 보고, 뭔가 잘못됐다는 걸 깨닫는다. 이 글은 그 잘못된 것들의 목록이다. 변수가 뭔지는 안다는 전제로, JS가 값을 어떻게 다루는지 그 특유의 구석만 판다.


선언은 세 가지다. 그런데 var는 잊어라. 옛날 코드엔 var가 깔려 있지만, 지금 새로 쓰는 거라면 letconst 두 개만 기억하면 된다. 값이 바뀔 놈은 let, 안 바뀔 놈은 const다. 일단 전부 const로 쓰다가, 재할당이 필요한 순간에만 let으로 바꾸는 습관이 버그를 줄여준다. 실수로 값을 덮어쓰는 사고를 컴파일 단계에서 막아주기 때문이다.


const pi = 3.14;
let count = 0;
count = count + 1; // let은 재할당 OK
console.log(pi, count);
// pi = 3.15; // 이 줄을 켜면 TypeError: Assignment to constant variable.


실행하면 3.14 1이 찍힌다. 마지막 줄 주석을 풀면 TypeError: Assignment to constant variable. (상수에 재할당했다는 오류)가 터지면서 멈춘다. 참고로 const는 재할당을 막을 뿐, 객체 내부까지 얼려주진 않는다. const obj = {} 라고 해도 obj.x = 1은 된다. 이걸 착각해서 왜 안 막히냐고 삽질한 적이 있는데, const가 지키는 건 상자에 붙은 이름표지 상자 안 물건이 아니다.


자료형은 원시값 일곱 개와 객체 하나로 나뉜다. 원시 타입(primitive, 더 쪼갤 수 없는 기본값)은 숫자, 문자열, 불리언, undefined, null, 심볼, BigInt 이렇게 일곱이다. 그 나머지 전부, 배열이든 함수든 날짜든 다 객체다. 어떤 값이 무슨 타입인지 궁금하면 typeof 연산자로 물어보면 된다. 문제는 이 typeof가 가끔 거짓말을 한다는 데 있다.


console.log(typeof 42); // number
console.log(typeof 'hello'); // string
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof null); // object (?!)
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof 10n); // bigint
console.log(typeof Symbol('id')); // symbol


출력은 주석 그대로 number, string, boolean, undefined, object, object, object, function, bigint, symbol 순서로 찍힌다. 눈에 걸리는 게 두 개 있다. 하나는 typeof nullobject라는 것. 이건 JS 초창기부터 있던 버그인데, 고치면 기존 웹이 죄다 깨지는 탓에 20년 넘게 그냥 두고 있는 화석이다. 그러니 null을 검사할 땐 typeof 말고 value === null로 직접 비교해야 한다.


다른 하나는 배열도 object로 나온다는 점이다. 배열인지 확인하고 싶으면 typeof가 아니라 Array.isArray()를 써야 한다. Array.isArray([1,2,3])true, Array.isArray({})false다. 이 둘을 typeof로 구분하려다간 영원히 object만 보게 된다.


null과 undefined는 둘 다 비어있음인데 결이 다르다. undefined는 값을 아직 안 넣은 상태, 시스템이 알아서 붙여주는 기본 공백이다. null은 개발자가 여기 일부러 비워뒀다고 손으로 박아 넣는 값이다. 선언만 하고 값을 안 준 변수는 undefined가 된다.


let a;
console.log(a); // undefined (값을 안 넣음)
let b = null;
console.log(b); // null (내가 비웠다고 명시)
console.log(a === b); // false (둘은 다른 값)


undefined, null, false가 각각 다르게 찍히고, a === bfalse다. 의미가 비슷해 보여도 엄연히 다른 값이라 ===(엄격한 같음)로 비교하면 다르다고 나온다. 실무에서는 함수가 값을 못 찾았을 때 null을 돌려주는 관례가 흔하고, 그냥 아무것도 안 준 인자는 undefined로 들어온다. 이 구분을 알아두면 남의 코드를 읽을 때 의도가 보인다.


숫자는 정수 타입이 따로 없다. 전부 실수다. JS엔 int, float 구분이 없다. 1이든 1.5든 전부 하나의 number 타입, 내부적으로는 64비트 부동소수점 하나로 처리한다. 이 설계 때문에 정밀도 한계가 존재한다. 안전하게 다룰 수 있는 정수는 대략 9천조 언저리까지고, 그걸 넘으면 값이 어긋나기 시작한다.


console.log(0.1 + 0.2); // 0.30000000000000004
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(9007199254740991 + 1); // 9007199254740992
console.log(9007199254740991 + 2); // 9007199254740992 (틀림!)
console.log(1 / 0); // Infinity
console.log(0 / 0); // NaN


보다시피 0.1 + 0.20.3이 아니라 0.30000000000000004다. 이건 JS 버그가 아니라 이진 부동소수점을 쓰는 거의 모든 언어의 공통 운명인데, 돈 계산 같은 데서 이걸 모르고 있으면 1원씩 안 맞는 정산 지옥을 맛본다. 안전 정수 한계를 넘긴 9007199254740991 + 2...992로 나오는 것도 같은 이유다. 아주 큰 정수를 정확히 다뤄야 하면 숫자 뒤에 n을 붙이는 BigInt를 쓴다. 9007199254740993n은 정확히 그 값을 유지한다.


한 가지 더, 0으로 나누면 에러가 아니라 Infinity가 나오고, 0/0처럼 답이 없는 계산은 NaN(Not a Number, 숫자가 아님)이 나온다. 웃긴 건 이 NaN의 타입도 typeof NaN 하면 number라는 점이다. 숫자가 아니라는 값의 타입이 숫자라니, 이쯤 되면 그냥 받아들이는 게 정신 건강에 좋다.


문자열과 객체는 그나마 예상대로 움직인다. 문자열은 +로 이어붙이거나, 백틱을 쓰는 템플릿 리터럴(template literal, 문자열 안에 값을 끼워 넣는 문법)로 조립한다. 객체는 { 키: 값 } 형태로 만들고, 점 표기나 대괄호로 속성에 접근한다.


const who = '세상';
console.log(`안녕 ${who}, ${1 + 2}점`); // 안녕 세상, 3점
const user = { name: '김', age: 20 };
console.log(user.name); // 김
console.log(user['age']); // 20


안녕 세상, 3점이 찍히고, ${} 안의 식은 알아서 계산돼 문자열에 박힌다. 문자열을 +로 덕지덕지 붙이는 것보다 템플릿 리터럴이 훨씬 읽기 편하니 이쪽에 손을 들여놓는 게 좋다. 정리하면 JS의 자료형은 원시값이 복사되고 객체는 참조로 공유된다는 큰 그림 위에서, null과 배열의 typeof 함정, 그리고 모든 숫자가 실수라는 사실만 몸에 새겨두면 초반에 겪는 황당한 버그의 절반은 미리 피한다.