[모던 자바스크립트 Deep Dive] - 6장 정리

2026. 3. 25. 22:39Book Review

반응형

자바스크립트의 모든 값은 데이터 타입을 가진다. 이 말은 당연해 보이지만 실제로 코드를 짤 때 계속 영향을 준다. 숫자 1과 문자열 '1'은 겉보기엔 비슷하지만 전혀 다른 값이다. 숫자는 계산을 위해 존재하고, 문자열은 텍스트를 표현하기 위해 존재한다. 메모리에 저장되는 방식도 다르고, 2진수 형태도 다르며, 해석 방식도 달라진다. 결국 개발자는 의도를 가지고 타입을 구분해서 값을 만들어야 한다.

자바스크립트의 데이터 타입은 크게 원시 타입(Primitive)과 객체 타입(Object)으로 나뉜다.

 

분류 타입

원시 타입 number, string, boolean, undefined, null, symbol, bigint
객체 타입 object (배열, 함수, 객체 등)

숫자 타입 (number)

C나 Java처럼 int, float, double 같은 구분이 없다. 자바스크립트는 전부 Number 하나로 처리한다.

ECMAScript 기준으로 숫자는 배정밀도 64비트 부동소수점 형식을 따른다. 그래서 정수와 실수를 따로 나누지 않고 전부 실수로 처리한다.

3 / 2  // 1.5 (정수끼리 나눠도 실수가 나옴)

 

진법 표현도 가능하다.

0b10  // 2  (2진수)
0o10  // 8  (8진수)
0x10  // 16 (16진수)

 

표현 방식이 다를 뿐, 실제 메모리에는 전부 2진수로 저장되고 꺼낼 때는 10진수 기준으로 해석된다.

 

특수 값 세 가지도 숫자 타입이다.

Infinity   // 양의 무한대
-Infinity  // 음의 무한대
NaN        // Not a Number (숫자가 아님)

 

NaN은 대소문자를 구분한다. nan, NAN으로 쓰면 에러가 난다.

여기서 이상한 점이 있다면 

NaN은 숫자 타입인데 왜 이름이 "숫자가 아님"일까?

 

NaN은 숫자 연산의 결과가 유효하지 않을 때 반환되는 값이다. 예를 들어 'hello' * 2처럼 숫자로 변환할 수 없는 연산을 시도했을 때 나온다. "연산 결과가 표현 불가"라는 뜻을 숫자 타입 안에서 표현하기 위해 만들어진 특수 값이다.

'hello' * 2  // NaN
0 / 0        // NaN
parseInt('abc')  // NaN

 

그리고 NaN은 자기 자신과도 같지 않은 유일한 값이다.

NaN === NaN  // false (!!!)

// 그래서 NaN 확인은 이걸 써야 한다
Number.isNaN(NaN)  // true
Number.isNaN(5)    // false

또 하나 주의할 점. 부동소수점 연산의 정밀도 문제가 있다.

0.1 + 0.2  // 0.30000000000000004 (!!!)
0.1 + 0.2 === 0.3  // false

 

이건 자바스크립트만의 문제가 아니라 IEEE 754 부동소수점 방식을 쓰는 모든 언어에서 발생한다.

정확한 소수 계산이 필요하면 아래처럼 처리한다.

// 방법 1: 정수로 변환해서 계산
(0.1 * 10 + 0.2 * 10) / 10  // 0.3

// 방법 2: toFixed로 반올림
(0.1 + 0.2).toFixed(1)  // '0.3' (문자열로 반환됨에 주의)

 

BigInt 타입

ES2020에서 추가된 BigInt다.

Number 타입이 안전하게 표현할 수 있는 정수의 범위는 -(2^53 - 1)부터 2^53 - 1까지다. 이 범위를 벗어나는 큰 정수를 다뤄야 할 때 BigInt를 쓴다.

Number.MAX_SAFE_INTEGER  // 9007199254740991

// 이 범위를 넘으면 정확도가 깨진다
9007199254740991 + 1  // 9007199254740992 (맞음)
9007199254740991 + 2  // 9007199254740992 (틀림! 같은 값이 나옴)

// BigInt는 숫자 뒤에 n을 붙인다
9007199254740991n + 2n  // 9007199254740993n (정확함)

 

주의할 점은 Number와 BigInt를 직접 연산할 수 없다는 것이다.

1n + 1    // TypeError
1n + 1n   // 2n (같은 타입끼리만 가능)

 

문자열 타입 (string)

문자열은 텍스트 데이터를 표현하기 위한 타입이다. 작은따옴표, 큰따옴표, 백틱으로 감싼다. 감싸는 이유는 자바스크립트 엔진이 감싸지 않으면 키워드나 변수 이름으로 인식하기 때문이다.

 

다른 언어와 비교하면 차이가 보인다. C는 문자열을 문자 배열로, Java는 객체로 다룬다. 자바스크립트는 문자열이 원시 타입이다.

그리고 중요한 특징이 있다. 문자열은 변경 불가능(immutable)하다. 한 번 만들어지면 수정할 수 없다.

let str = 'hello';
str[0] = 'H';
console.log(str);  // 'hello' (바뀌지 않음)

그런데 이런 코드는 왜 되는 걸까?

 

let str = 'hello';
str = 'world';  // 이건 가능하다

헷갈릴 수 있는 부분인데, 'hello'라는 문자열 값 자체를 수정하는 게 아니라 변수 str이 가리키는 값을 새로운 문자열 'world'로 교체하는 것이다. 기존 'hello'는 그대로 남아있고, str의 참조만 바뀐다.

이스케이프 시퀀스

일반 문자열에서 줄바꿈을 표현하려면 이스케이프 시퀀스를 써야 한다.

 

\n 줄바꿈 (LF)
\t
\\ 백슬래시
\' 작은따옴표
\" 큰따옴표
\r 캐리지 리턴 (CR)

 

\r(CR)과 \n(LF)의 차이는 운영체제 역사에서 온다. CR은 타자기의 "커서를 줄 맨 앞으로 이동"에서, LF는 "종이를 한 줄 위로 올림"에서 유래했다. Windows는 \r\n을 줄바꿈으로 쓰고, macOS/Linux는 \n만 쓴다. Git에서 줄바꿈 문자 관련 설정(core.autocrlf)이 있는 이유가 이 때문이다.

템플릿 리터럴

ES6에서 추가된 문자열 문법으로 백틱을 사용한다.

const name = 'kim';
const age = 25;

// 기존 방식
'이름: ' + name + ', 나이: ' + age;

// 템플릿 리터럴
`이름: ${name}, 나이: ${age}`;

 

표현식도 넣을 수 있다. 결과가 문자열이 아니어도 자동으로 문자열로 변환된다.

`1 + 2 = ${1 + 2}`  // '1 + 2 = 3'
`조건: ${true ? '참' : '거짓'}`  // '조건: 참'

 

줄바꿈과 공백도 그대로 유지된다.

const msg = `안녕하세요
반갑습니다`;
// \n 없이도 줄바꿈이 됨

문자열 연산

문자열은 + 연산자로 연결할 수 있다. 피연산자 중 하나라도 문자열이면 +는 덧셈이 아니라 문자열 연결로 동작한다.

'hello' + ' world'  // 'hello world'
'1' + 2             // '12' (문자열 연결)
1 + 2               // 3   (덧셈)
1 + 2 + '3'        // '33' (좌 → 우 순서로 1+2=3, 그 다음 3+'3'='33')
'1' + 2 + 3        // '123'

 

불리언 타입 (boolean)

true와 false 두 값만 가진다. 조건문에서 프로그램 흐름을 제어할 때 사용된다.

Truthy / Falsy에 대해 짚고 넘어가자.

 

자바스크립트는 조건식에 불리언이 아닌 값이 들어오면 자동으로 불리언으로 변환한다. 이때 false로 평가되는 값들을 Falsy, 나머지를 Truthy라고 한다.

 

Falsy 값은 딱 이 7가지다.

false
undefined
null
0
-0
NaN
''  // 빈 문자열

 

이 외의 값은 전부 Truthy다. 빈 배열 []과 빈 객체 {}도 Truthy다. 헷갈리기 쉬운 부분이다.

if ([]) console.log('실행됨');  // 실행됨 (빈 배열도 Truthy)
if ({}) console.log('실행됨');  // 실행됨 (빈 객체도 Truthy)
if ('') console.log('실행됨');  // 실행 안 됨 (빈 문자열은 Falsy)

 

undefined

undefined는 값이 하나다. 변수를 선언만 하고 값을 할당하지 않으면 자바스크립트 엔진이 자동으로 undefined로 초기화한다.

let a;
console.log(a);  // undefined

개발자가 의도적으로 쓰는 값이 아니다. "값이 없다"는 것을 명시하려면 null을 쓰는 게 맞다.

그런데 undefined를 직접 할당하면 안 될까?

let a = undefined;  // 가능은 하지만 권장하지 않음

기술적으로는 가능하지만, undefined는 엔진이 쓰는 값이기 때문에 개발자가 의도적으로 "값 없음"을 표현할 때는 null을 쓰는 게 컨벤션이다. undefined를 직접 할당하면 "엔진이 초기화한 건지, 개발자가 의도한 건지" 구분이 어려워진다.

null

null도 값이 하나다. 의도적으로 값이 없다는 것을 표현할 때 사용한다.

let a = null;

이전에 참조하던 값을 끊는 의미도 있다. 더 이상 해당 값을 참조하지 않겠다는 뜻으로, 자바스크립트 엔진이 이런 값들을 나중에 가비지 컬렉션으로 메모리에서 정리해준다.

null의 typeof가 'object'인 이유는?

typeof null  // 'object' (!!!)

이건 자바스크립트 초창기 버그로 당시 타입 정보를 하위 3비트로 저장했는데 객체의 타입 태그가 000이었고 null도 000(null 포인터)이었기 때문에 잘못 처리됐다. 기존 코드에 영향을 줄까봐 지금까지도 수정되지 않고 있다.

 

그래서 null을 확인할 때는 typeof 대신 일치 연산자(===)를 써야 한다.

const val = null;
typeof val === 'null'  // false (절대 이렇게 하면 안 됨)
val === null           // true  (올바른 방법)

 

Symbol 타입

ES6에서 추가된 타입이다. 유일한 값을 만들 때 사용한다.

const a = Symbol('key');
const b = Symbol('key');

a === b  // false (같은 설명을 넣어도 절대 같은 값이 되지 않음)

 

주로 객체의 프로퍼티 키로 사용해서 충돌을 방지한다.

const id = Symbol('id');
const user = {
  name: 'kim',
  [id]: 123  // Symbol을 키로 사용
};

user[id]  // 123

 

Symbol로 만든 키는 for...in, Object.keys() 같은 일반적인 열거에서 나타나지 않아서 외부에 노출하고 싶지 않은 프로퍼티에 사용한다.

전역 Symbol이 필요할 때는 Symbol.for()를 쓴다.

// Symbol()은 매번 새로운 값
Symbol('key') === Symbol('key')  // false

// Symbol.for()는 같은 키면 같은 값을 재사용
Symbol.for('key') === Symbol.for('key')  // true

 

객체 타입 (Object)

원시 타입 7가지를 제외한 나머지는 전부 객체 타입이다. 배열, 함수, 정규식, 일반 객체 등이 모두 객체 타입에 해당한다.

원시 타입과 객체 타입의 가장 큰 차이는 값의 저장 방식이다.

// 원시 타입 — 값 자체가 복사됨
let a = 10;
let b = a;
b = 20;
console.log(a);  // 10 (영향 없음)

// 객체 타입 — 참조(주소)가 복사됨
let obj1 = { x: 10 };
let obj2 = obj1;
obj2.x = 20;
console.log(obj1.x);  // 20 (같은 객체를 가리키기 때문에 영향 받음)

이 차이 때문에 나중에 함수에 인수를 넘길 때, 배열을 복사할 때 예상치 못한 버그가 생기는 경우가 많다. 의식적으로 이 차이를 기억해두는 게 중요하다.

데이터 타입이 필요한 이유

컴퓨터는 모든 값을 2진수로 저장한다. 예를 들어 이런 값이 있다고 하자.

01000001

 

이걸 숫자로 해석하면 65, 문자로 해석하면 'A'가 된다. 같은 데이터라도 어떻게 해석하느냐에 따라 결과가 완전히 달라진다. 이 해석 기준을 결정하는 게 데이터 타입이다.

또 하나는 메모리 크기다. 자바스크립트 엔진은 데이터 타입에 따라 필요한 만큼의 메모리를 확보한다. 숫자라면 64비트 기준으로 메모리를 잡고, 읽을 때도 그 크기만큼 읽는다.

심벌 테이블

변수 정보를 관리하는 구조다. 쉽게 말하면 변수 관리 장부 같은 개념이다.

let a = 10;

이 코드가 실행되면 내부적으로 이런 정보가 저장된다.

식별자 메모리 주소 타입

a 어떤 주소 number

이후에 a를 사용하면 엔진은 이 테이블을 통해 값을 찾아온다. 식별자를 키로 해서 메모리 주소, 타입, 스코프 정보를 관리한다.

정적 타입 vs 동적 타입

정적 타입 언어는 변수 선언 시 타입을 미리 정한다. C, Java, Kotlin 같은 언어가 해당한다. 타입을 바꿀 수 없고 컴파일 시점에 타입 체크를 수행한다.

// Java
int a = 10;
a = "hello";  // 컴파일 에러

자바스크립트는 다르다. 타입을 선언하지 않고 값을 할당할 때 타입이 결정된다.

let a = 10;
a = 'hello';  // 가능

핵심은 변수는 타입을 가지지 않고 값이 타입을 가진다는 점이다. 값이 바뀌면 타입도 같이 바뀐다. 이걸 동적 타이핑이라고 한다.

typeof는 변수의 타입을 반환하는 게 아니라 현재 값의 타입을 반환한다.

let a = 10;
typeof a;  // 'number'

a = 'hello';
typeof a;  // 'string'

동적 타입의 단점

유연한 대신 위험하다. 코드가 길어질수록 변수의 타입을 추적하기 어려워지고, 잘못된 가정으로 코드를 작성하면 런타임 에러로 이어진다.

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

add(1, 2);      // 3
add('1', 2);    // '12' (의도치 않은 결과)
add(1, null);   // 1   (null이 0으로 변환됨)

그래서 요즘은 자바스크립트에 정적 타입을 추가한 TypeScript를 많이 쓴다.

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

add('1', 2);  // 컴파일 단계에서 에러 잡힘

동적 타입을 쓰면서 안전하게 관리하는 방법은 아래 습관들이다.

- 변수는 꼭 필요한 경우에만 선언한다
- 변수의 스코프는 최대한 좁게 가져간다
- 전역 변수는 피한다
- 값이 변하지 않으면 const를 쓴다
- 의미 있는 이름을 쓴다

결국 가독성이 좋은 코드가 좋은 코드다. 타입이 뭔지 이름만 봐도 유추할 수 있으면 유지보수도 훨씬 수월해진다.

 

참고 자료

모던 자바스크립트 Deep Dive — 이웅모

ECMAScript Specification — https://tc39.es/ecma262/

MDN Web Docs — https://developer.mozilla.org/

반응형