typeof null이 object인 이유는?

JavaScript 초창기 설계 결정으로 남은 typeof null의 역사적 버그를 이해하고, 하위 호환성 유지를 위해 수정되지 않은 이유를 학습합니다

입문 15분 typeof null 역사적 버그 하위 호환성

typeof null"object"를 반환하는 현상은 JavaScript 역사상 가장 유명한 설계 버그 중 하나입니다. null은 분명히 “값이 없음”을 나타내는 원시 타입임에도 불구하고, typeof 연산자는 이를 객체로 분류합니다. 이 동작은 1995년 JavaScript가 처음 설계될 당시의 구현 방식에서 비롯된 것으로, 이후 수십 년이 지난 지금까지도 수정되지 않은 채 언어 명세에 그대로 남아 있습니다. 표면적으로는 단순한 언어 quirk처럼 보이지만, 이 동작을 제대로 이해하지 못하면 실제 프로젝트에서 찾기 어려운 버그를 만들어 낼 수 있습니다.

🔍 핵심 문제점

  • 타입 판별 오류: typeof null === "object"가 참이므로, typeof만으로는 null과 실제 객체를 구분할 수 없습니다
  • 역사적 구현 잔재: 초기 JavaScript 엔진이 값을 메모리에 저장할 때 사용한 타입 태그 방식이 null을 객체와 동일한 태그(0)로 처리한 것이 원인입니다
  • 수정 불가능한 하위 호환성: ECMAScript 위원회는 이 버그를 인지하고 있지만, 수정할 경우 전 세계 수많은 기존 코드가 동시에 망가지기 때문에 수정하지 않기로 결정했습니다
  • null 체크 패턴의 복잡성: 안전한 null 판별을 위해 === null 비교를 별도로 사용해야 하며, typeof 단독으로는 충분하지 않습니다
  • 신입 개발자의 흔한 함정: 이 동작을 모르는 상태에서 typeof를 이용해 null 여부를 확인하려 하면, 조용한 버그가 발생하여 디버깅에 상당한 시간을 낭비하게 됩니다

왜 중요한가?

실무에서 외부 API 응답이나 사용자 입력을 처리할 때, 값이 null인지 실제 객체인지를 정확히 판별하는 일은 매우 흔한 요구사항입니다. typeof null의 동작을 모르는 개발자는 typeof를 이용한 타입 체크 로직에서 예기치 않은 분기를 경험하고, 원인 파악 없이 오랜 시간을 디버깅에 허비하게 됩니다. 특히 React나 Vue 같은 프레임워크에서 컴포넌트에 전달되는 props나 상태값이 null인 경우를 처리하는 코드, 그리고 TypeScript와 함께 사용할 때 타입 좁히기(type narrowing) 로직을 작성하는 과정에서 이 함정에 빠지기 쉽습니다. 더 나아가 이 버그가 왜 수정되지 않는지, 즉 하위 호환성이라는 언어 설계의 핵심 원칙을 이해하면, JavaScript 생태계 전반의 진화 방식과 ECMAScript 명세가 어떻게 관리되는지에 대한 깊은 통찰을 얻을 수 있습니다. typeof null 하나를 제대로 이해하는 것은 단순한 지식 습득이 아니라, JavaScript를 언어 수준에서 다루는 개발자로 성장하는 중요한 출발점입니다.


핵심 개념

The Type Tag Bug

입문

typeof null"object"를 돌려준다는 건 JavaScript가 처음 만들어질 때부터 생긴 실수예요. 왜 이런 실수가 생겼는지, 왜 지금도 고치지 않는지 알아볼게요!

🏷️ 컴퓨터는 값을 어떻게 구별하나요? 컴퓨터는 모든 것을 숫자로 기억해요. 값을 저장할 때 “이게 숫자야”, “이게 글자야”라는 표시표(태그)를 같이 붙여 두는 방식을 사용해요. 마치 도서관에서 책마다 스티커를 붙여 소설은 빨강, 과학은 파랑으로 구분하는 것처럼요.

🐛 1995년에 무슨 실수가 있었나요? JavaScript를 처음 만든 브렌던 아이크는 단 10일 만에 언어를 만들었어요. 그때 컴퓨터 메모리에 값을 저장할 때, 객체(object)에는 “0”이라는 태그를 붙였어요. 그런데 null이라는 값은 원래부터 메모리 주소가 0으로 시작하는 특별한 빈 값이었기 때문에, 컴퓨터가 null을 보고 “오, 태그가 0이네? 객체구나!” 하고 착각해 버린 거예요.

🌍 왜 지금도 고치지 않나요? 전 세계에 이미 수백만 개의 웹사이트와 프로그램이 typeof null === "object"라는 동작을 믿고 만들어져 있어요. 만약 지금 고치면, 그 모든 사이트가 동시에 망가질 수 있어요. 마치 이미 완성된 건물의 기초 공사를 다시 뜯어고치려면 건물 전체를 무너뜨려야 하는 것과 같아서, 그냥 놔두기로 한 거예요.

📋 그래서 typeof null은 뭐라고 알려줘야 하나요? JavaScript를 배우는 모든 사람이 꼭 외워야 할 특별 규칙이에요. “typeof null"object"를 돌려주지만, 이건 버그야!” 라고요. 실제로 null은 객체가 전혀 아니에요. “아무 값도 없음”을 나타내는 특별한 빈 표시예요.

중급

typeof null === "object"는 1995년 JavaScript 초기 구현에서 비롯된 타입 태그 버그입니다. 당시 Brendan Eich는 값을 표현할 때 낮은 비트(low-order bits)를 타입 태그로 사용했는데, 객체의 태그 값이 0이었고, C 언어 전통의 null 포인터(NULL pointer) 역시 메모리 주소 0x0으로 표현되었습니다. 그 결과 null이 객체와 동일한 타입 태그를 갖게 되어, typeof 연산자가 "object"를 반환하게 되었습니다.

하위 호환성과 수정 불가 원칙 ECMAScript 위원회(TC39)는 이 버그를 수정하려는 시도(ES3.1, 2006년경)를 실제로 검토했지만, 전 세계 웹 코드베이스와의 하위 호환성(backward compatibility) 파괴를 이유로 기각했습니다. 현재도 ECMAScript 명세는 이 동작을 “역사적 이유에 의한 고의적 anomaly”로 기술합니다.

// typeof null의 예상치 못한 결과
console.log(typeof null);        // "object" (버그!)
console.log(typeof undefined);   // "undefined"
console.log(typeof {});          // "object"
console.log(typeof []);          // "object"

// null은 원시 타입(primitive)임에도 "object"를 반환
console.log(null === null);      // true
console.log(null instanceof Object); // false - null은 객체가 아님

typeof 연산자의 타입 태그 메커니즘 초기 JavaScript 엔진의 값 표현 방식에서 각 값의 최하위 1~3비트가 타입 식별자 역할을 했습니다. 객체는 비트 패턴 000, 정수는 001, 부동소수점은 010, 문자열은 100, 불리언은 110이었습니다. null은 C 언어 관습대로 0x00000000(모든 비트가 0)이었으므로, 타입 태그도 000이 되어 객체와 구분이 불가능했습니다.

심화

typeof null === "object"는 ECMAScript 명세 Section 13.5.3 (The typeof Operator)에 명시된 사양이지만, 그 기원은 명세가 아닌 1995년 Netscape Navigator 2.0의 SpiderMonkey 전신 인터프리터 구현에 있습니다. 이 버그는 C 언어 기반 값 표현(value representation) 체계의 타입 태그 충돌에서 비롯됩니다.

초기 JavaScript 값 표현의 타입 태그 인코딩 초기 Mocha/LiveScript 인터프리터는 각 JSValue를 32비트 워드로 표현했으며, 최하위 3비트(low-order 3 bits)를 타입 태그(type tag)로 사용했습니다. 공식적으로 문서화된 태그 매핑은 다음과 같습니다:

  • 000: 객체 참조(object reference)
  • 001: 정수(integer, 31비트 정수)
  • 010: 부동소수점(double)
  • 100: 문자열(string)
  • 110: 불리언(boolean)
  • 특수값 -1(0xFFFFFFFF): undefined

null은 C 언어 관습인 NULL 포인터(0x00000000)로 표현되었고, 결과적으로 타입 태그 비트가 000이 되어 typeof 연산자의 태그 검사 로직이 이를 객체로 분류했습니다. 이 동작은 ECMAScript 1st Edition(1997)부터 공식 명세에 포함되었으며, Section 11.4.3 (ES1) 이후 현재까지 유지됩니다.

TC39의 수정 시도와 기각 배경 2006년 ES3.1 작업 중 Waldemar Horwat이 typeof null"null"로 수정하는 제안을 제출했습니다. 그러나 실증적 웹 호환성 테스트(web compatibility testing)에서 다수의 실제 웹사이트가 typeof value === "object"로 null과 객체를 동시에 처리하는 패턴에 의존하고 있음이 확인되었고, TC39는 웹 호환성 원칙(web compatibility principle)에 따라 기각했습니다. 이 결정은 TC39의 핵심 설계 원칙인 “Don’t break the Web”을 명시적으로 따른 사례로, HTML Living Standard (WHATWG)와 함께 운영되는 브라우저 생태계 전체의 안정성 우선 정책을 반영합니다.

SpiderMonkey와 V8의 현재 구현 현대 엔진에서는 더 이상 32비트 태그 인코딩을 사용하지 않지만, typeof null의 반환값은 명세 준수 목적으로 하드코딩되어 있습니다. V8(Chromium)의 src/compiler/typer.cc에서 kNull 타입에 대한 typeof 결과를 명시적으로 "object"로 반환하도록 처리하며, SpiderMonkey(Firefox)도 동일한 방식을 취합니다. 이 구현은 성능 최적화와 무관하게 순전히 하위 호환성 보장을 위한 명세 이행입니다.

Proper Null Checking

입문

typeof만으로는 null을 찾아낼 수 없어요. null인지 아닌지 정확하게 확인하는 올바른 방법을 배워볼게요!

🔎 typeof로 null을 확인하면 왜 안 되나요? typeof null"object"를 돌려주기 때문에, typeof만 쓰면 “진짜 객체”와 “null” 중 어느 쪽인지 구분할 수가 없어요. 마치 가방 색깔만 보고 내용물을 판단하려 하는데, 빈 가방과 물건이 든 가방이 같은 색이라서 구분이 안 되는 것과 같아요.

✅ null인지 확인하는 올바른 방법은요? === null을 사용하면 돼요. 이건 “이 값이 정확히 null이에요?”라고 직접 묻는 방법이에요. 마치 가방을 직접 열어서 비어있는지 확인하는 것처럼, 값을 직접 null과 비교하는 거예요.

⚠️ == null은 어떤가요? == (등호 두 개)를 쓰면 null과 undefined 둘 다 포함해서 확인해요. 마치 “완전히 비었거나 아무것도 없는” 것을 한꺼번에 확인하는 것과 같아요. 상황에 따라 유용할 때도 있지만, 정확히 null만 확인하고 싶다면 === null(등호 세 개)을 써야 해요.

🛡️ 객체인지 null인지 안전하게 구분하려면요? 진짜 객체가 필요할 때는 두 가지를 함께 확인해요. “null이 아니면서, typeof가 object인지” 두 조건을 모두 확인하는 거예요. 이렇게 하면 null은 걸러내고 진짜 객체만 통과시킬 수 있어요.

중급

typeof를 단독으로 사용하면 null과 실제 객체를 구분할 수 없습니다. null 여부를 안전하게 확인하려면 엄격한 동등 비교(=== null)를 사용해야 합니다.

null 확인의 세 가지 패턴

  • value === null: 정확히 null만 판별 (권장)
  • value == null: null 또는 undefined 모두 판별 (느슨한 동등 비교)
  • !value: falsy 값 전체 (null, undefined, 0, "", false 포함) — 부정확하므로 주의
const data = null;
const obj = { name: "Alice" };

// 잘못된 방법: typeof만으로는 구분 불가
console.log(typeof data === "object"); // true  (null인데!)
console.log(typeof obj  === "object"); // true  (실제 객체)

// 올바른 방법 1: 엄격한 비교
console.log(data === null); // true
console.log(obj  === null); // false

// 올바른 방법 2: null과 undefined 모두 체크
console.log(data == null); // true
console.log(obj  == null); // false
function isNonNullObject(value) {
  return typeof value === "object" && value !== null;
}

console.log(isNonNullObject(null));       // false
console.log(isNonNullObject({}));         // false? No → true
console.log(isNonNullObject([]));         // true (배열도 객체)
console.log(isNonNullObject("hello"));    // false

API 응답 처리의 실전 패턴 외부 API에서 받은 데이터가 null일 수 있을 때, 프로퍼티 접근 전 null 가드(null guard)를 적용하는 것이 안전합니다. ES2020의 옵셔널 체이닝(?.)은 이 패턴을 간결하게 표현하는 방법입니다.

const response = null; // API 응답이 null인 경우

// 위험: TypeError 발생 가능
// console.log(response.data.name);

// 안전: null 가드
if (response !== null && response.data) {
  console.log(response.data.name);
}

// 간결한 표현: 옵셔널 체이닝
console.log(response?.data?.name); // undefined (에러 없음)

심화

value === null은 Abstract Equality Comparison(추상 동등 비교) 알고리즘이 아닌 Strict Equality Comparison(엄격 동등 비교, ECMAScript Section 7.2.16)을 적용합니다. null의 엄격 동등 비교는 SameValueNonNumeric 알고리즘을 거치며, null은 오직 null과만 엄격하게 동일합니다. 이 특성이 안전한 null 판별의 기반입니다.

Strict Equality vs Abstract Equality의 null 처리 value == null (Abstract Equality, Section 7.2.15)은 null과 undefined를 상호 동등하게 처리하는 특별 규칙을 포함합니다. 이는 null == undefined === true로 이어지며, 명세에서 의도적으로 설계된 동작입니다. 반면 value === null은 타입 변환(type coercion) 없이 [[Type]][[Value]]를 동시에 비교하여 null만을 정확히 식별합니다.

Null 가드 패턴의 타입 시스템 관점 TypeScript에서 value !== null 가드는 컴파일러의 Control Flow Analysis(제어 흐름 분석)를 통해 타입 좁히기(type narrowing)를 유발합니다. T | null 유니온 타입의 변수에 대해 if (value !== null) 블록 내부에서 타입이 T로 좁혀집니다. 이는 TypeScript 2.0에서 도입된 Strict Null Checks(--strictNullChecks) 플래그와 연동되며, null과 undefined를 독립적인 타입으로 취급하는 사운드 타입 시스템(sound type system)의 핵심 기제입니다.

isNonNullObject 패턴의 표준화 typeof value === "object" && value !== null 패턴은 ECMAScript 명세 자체의 내부 추상 연산(abstract operation)인 IsObject(value)의 동작과 사실상 동일합니다. 명세의 IsObjectType(value) === Object를 검사하지만, typeof의 quirk로 인해 사용자 코드에서는 null 제외 조건을 명시적으로 추가해야 합니다. Array.isArray(), instanceof, Object.prototype.toString.call()은 각각 특화된 타입 판별을 제공하므로, 요구사항에 따라 조합하여 사용합니다.

Null vs Undefined Type Distinction

입문

JavaScript에는 “아무것도 없음”을 표현하는 방법이 두 가지가 있어요. 바로 nullundefined인데, 이 둘은 비슷해 보이지만 용도가 달라요. 특히 typeof로 확인했을 때 결과가 달라서 구분 방법을 알아두면 아주 유용해요!

📭 undefined는 어떤 값인가요? undefined는 “아직 아무것도 담지 않은 빈 상자” 예요. 변수를 만들었는데 아무 값도 넣지 않으면 자동으로 undefined가 들어가요. 택배가 오기 전 텅 빈 우편함 같은 거예요. JavaScript가 “이 상자는 만들었는데, 뭘 넣어야 할지 아직 몰라요”라고 할 때 써요.

🚫 null은 어떤 값인가요? null은 “일부러 비워 놓은 빈 상자”예요. 누군가가 의도적으로 “여기엔 아무것도 없어”라고 표시한 거예요. 택배 상자에 “내용물 없음”이라는 라벨을 붙인 것처럼요. 개발자가 직접 “이 칸은 비어있음”을 표현할 때 써요.

🔍 typeof로 확인하면 어떻게 달라요? 여기서 중요한 차이가 생겨요! typeof로 확인해보면, undefined"undefined"라고 정확히 알려줘요. 그런데 null은 앞서 배운 버그 때문에 "object"라고 잘못 알려줘요. 그래서 null을 typeof로 확인하면 안 되고, 반드시 === null로 확인해야 한다는 거예요!

💡 실제로 언제 각각을 쓰나요? 서버에서 데이터를 받아오기 전까지는 변수가 undefined 상태일 수 있어요. 그런데 서버가 “이 사람에게 해당하는 데이터가 없어요”라고 알려줄 때는 null을 보내줘요. 즉, undefined는 “아직 몰라요”, null은 “없다는 게 확실해요”의 차이예요.

중급

nullundefined는 모두 “값이 없음”을 표현하지만, typeof 결과와 의미론적 용도가 다릅니다.

구분nullundefined
typeof 결과"object" (버그)"undefined"
의미의도적으로 비어있음값이 할당되지 않음
발생 원인개발자가 명시적으로 할당변수 선언만 하고 미할당, 없는 프로퍼티 접근
엄격 동등null === null → trueundefined === undefined → true
느슨한 동등null == undefined → trueundefined == null → true
let a;
let b = null;

console.log(typeof a); // "undefined"
console.log(typeof b); // "object" (버그!)

// 정확한 판별
console.log(a === undefined); // true
console.log(b === null);      // true
console.log(a == null);       // true  (null == undefined도 true)
console.log(b == undefined);  // true  (null == undefined도 true)

함수 반환값에서의 null vs undefined 함수가 명시적으로 아무것도 반환하지 않으면 undefined를 반환합니다. 반면 “결과가 없음”을 명시적으로 표현하고 싶을 때는 개발자가 직접 null을 반환합니다. 이 관례를 지키면 호출자가 “반환값이 없었는지(undefined)“와 “결과가 없다고 알려준 것인지(null)“를 구분할 수 있습니다.

function findUser(id) {
  if (id === 1) return { name: "Alice" };
  return null; // 찾지 못했음을 명시적으로 표현
}

function doSomething() {
  // return 없음 → undefined 반환
}

const user = findUser(99);
const result = doSomething();

console.log(user === null);       // true  - "없음"이 확실
console.log(result === undefined); // true  - 반환값 자체가 없음

심화

ECMAScript 명세에서 null과 undefined는 서로 다른 원시 타입(primitive type)으로 정의됩니다. ECMAScript Section 6.1.1은 Undefined Type을, Section 6.1.2는 Null Type을 별도로 명세하며, 각각 단 하나의 값(undefined, null)만을 가집니다.

Abstract Equality에서의 null/undefined 동치 규칙 ECMAScript Section 7.2.15 (IsLooselyEqual) 알고리즘에는 다음 규칙이 명시됩니다: x == y에서 x가 null이고 y가 undefined이면 true를 반환하고, 반대의 경우도 마찬가지입니다. 이 규칙은 타입 변환(type coercion) 없이 직접 정의된 특별 케이스로, 두 값을 느슨하게 동일하게 취급하는 의도적 설계입니다. 이를 활용하면 value == null로 null과 undefined를 동시에 필터링할 수 있습니다.

Undefined의 typeof 안전성과 null의 취약성 typeof undefined는 선언되지 않은 변수에 대해서도 "undefined"를 반환하고 ReferenceError를 발생시키지 않습니다. 이는 typeof 연산자의 특수 처리로, ECMAScript Section 13.5.3에 명시됩니다. 이와 달리 null은 typeof가 "object"를 반환하므로, typeof를 이용한 null 가드는 의도대로 작동하지 않습니다. TypeScript --strictNullChecks 환경에서 이 차이는 컴파일러 수준의 Control Flow Analysis에 의해 정확히 추적되며, nullable 타입(T | null | undefined)의 각 분기는 독립적으로 좁혀집니다.

실제 코드베이스에서의 null/undefined 관례 Douglas Crockford의 “JavaScript: The Good Parts”와 Airbnb JavaScript Style Guide는 undefined를 개발자가 직접 할당하지 말 것을 권장합니다. undefined는 JavaScript 엔진이 “값 없음” 상태를 나타내기 위해 사용하는 예약된 값으로, 개발자가 “없음”을 표현할 때는 null을 사용하는 것이 관례입니다. 이 관례는 Google Closure Compiler와 Flow Type 등 정적 분석 도구들이 null과 undefined를 별도의 nullable 마커로 취급하는 방식과도 일치합니다.