JavaScript에서 NaN(Not a Number)은 숫자로 표현할 수 없는 연산의 결과를 나타내는 특별한 값입니다. 그런데 이 값에는 처음 접하는 개발자를 혼란에 빠뜨리는 독특한 특성이 있습니다. 바로 NaN이 자기 자신과 비교해도 동등하지 않다는 것입니다. 즉, NaN === NaN은 false를 반환하며, 이는 JavaScript의 모든 다른 값에서는 찾아볼 수 없는 유일한 동작입니다. 이 동작은 버그가 아니라, JavaScript가 채택한 IEEE 754 부동소수점 표준에서 수학적으로 설계된 의도적인 결정입니다.
🔍 핵심 문제점
- 자기 동등 비교 불가:
NaN === NaN과NaN == NaN모두false를 반환하여 일반적인 비교 연산으로는 NaN을 감지할 수 없습니다 typeof결과의 혼란:typeof NaN은"number"를 반환하므로, 타입 확인만으로는 NaN을 구별하기 어렵습니다- 전역
isNaN()의 함정: 전역 함수isNaN()은 인자를 먼저 숫자로 강제 변환하기 때문에isNaN("hello")가true를 반환하는 예상치 못한 결과를 낳습니다 - 조용한 오류 전파: NaN은 산술 연산에서 소리 없이 전파되어, 오류의 근원을 찾기 어렵게 만들고 디버깅 비용을 높입니다
- 배열 탐색 메서드의 한계:
Array.prototype.indexOf()는 내부적으로 동등 비교를 사용하기 때문에 배열 안의 NaN을 찾지 못합니다
💡 실무에서의 영향
NaN 관련 버그는 특히 사용자 입력을 처리하거나 외부 API 데이터를 다루는 실무 환경에서 자주 발생합니다. 예를 들어 사용자가 입력 폼에 문자열을 입력했을 때 이를 숫자로 변환하려 하면 NaN이 생성되고, 이후 계산 로직 전체에 NaN이 전파되어 화면에 NaN 또는 빈 값이 표시되는 문제로 이어집니다. Number.isNaN()을 올바르게 활용하면 이러한 문제를 입력 처리 단계에서 즉시 차단할 수 있습니다. 나아가 NaN의 본질적인 동작 원리를 이해하면 방어적인 코드를 작성하는 습관이 자연스럽게 형성되며, 타입 안전성이 중요한 TypeScript 환경에서도 더 정밀한 타입 가드를 설계하는 데 도움이 됩니다. 이 주제는 단순한 quirk(이상한 동작)를 외우는 것이 아니라, JavaScript 숫자 시스템의 근간을 이해하는 출발점이 됩니다.
핵심 개념
NaN이란 무엇인가
입문
NaN은 “숫자가 아님(Not a Number)“이라는 뜻인데, 신기하게도 숫자 타입으로 분류돼요. 왜 그런지 차근차근 알아볼게요!
🔢 NaN은 어디서 생겨나나요? NaN은 수학적으로 말이 안 되는 계산을 하려고 할 때 생겨요. 예를 들어 “사과”를 숫자로 나누려고 하거나, 음수의 제곱근을 구하려고 할 때처럼요. 컴퓨터가 “이건 계산할 수 없어요!”라고 말하는 대신, NaN이라는 특별한 결과물을 돌려주는 거예요.
🏷️ 왜 NaN이 “숫자” 타입인가요? 이게 가장 혼란스러운 부분이에요! NaN은 “숫자가 아니다”라는 뜻인데, 컴퓨터는 NaN을 숫자 종류로 취급해요. 마치 학교에서 “낙제” 점수도 점수 칸에 기록되는 것처럼, 숫자 연산의 실패 결과도 숫자 칸에 보관돼요. 컴퓨터 입장에서는 “숫자 계산을 했는데 결과가 이상해”라는 뜻이거든요.
🎭 NaN은 특별히 어떤 경우에 나타나나요? 숫자가 아닌 문자열을 숫자로 바꾸려 할 때 (예: “안녕”을 숫자로 변환), 숫자를 0으로 나눌 때(일부 경우), 또는 정의되지 않은 수학 연산(예: 음수의 제곱근)을 할 때 나타나요. 이런 상황에서 컴퓨터는 “결과를 모르겠어요”라는 표시로 NaN을 반환해요.
🤔 NaN을 만나면 왜 당황스러운가요? NaN은 “숫자 아님”이라고 했는데 타입은 숫자라고 하니 처음엔 정말 헷갈려요. 게다가 NaN이 섞인 계산을 계속하면 답이 계속 NaN으로 나와서, 어디서 문제가 시작됐는지 찾기도 어려워요. 마치 병균이 퍼지듯 계산 결과 전체를 오염시키거든요.
중급
NaN(Not a Number)은 숫자 연산이 유효하지 않은 결과를 낼 때 반환되는 특별한 숫자 타입 값입니다. typeof NaN === "number"가 true인 이유는 NaN이 “숫자가 아닌 것”이 아니라, “숫자 연산의 실패를 나타내는 숫자 도메인의 특수 값”이기 때문입니다.
NaN이 생성되는 주요 케이스
NaN은 다음 세 가지 주요 상황에서 발생합니다. 첫째로 숫자로 변환 불가능한 값을 숫자 연산에 사용할 때, 둘째로 Math.sqrt(-1) 같이 실수 범위에서 정의되지 않는 수학 연산을 할 때, 셋째로 undefined를 포함한 산술 연산을 할 때입니다.
// 변환 불가 문자열
Number("hello"); // NaN
parseInt("abc"); // NaN
// 정의되지 않은 수학 연산
Math.sqrt(-1); // NaN
Math.log(-1); // NaN
// undefined 포함 연산
undefined + 1; // NaN
undefined * 2; // NaN
// typeof 확인
typeof NaN; // "number" - 숫자 타입!
왜 typeof NaN이 “number”인가?
IEEE 754 표준은 NaN을 부동소수점 숫자의 특수한 비트 패턴으로 정의합니다. 즉, NaN은 개념적으로는 “숫자가 아님”을 뜻하지만, 구현 차원에서는 부동소수점 표현 공간 안에 존재하는 값입니다. JavaScript는 이 표준을 그대로 채택했기 때문에 typeof NaN이 "number"를 반환하는 것은 의도된 설계입니다.
심화
NaN은 IEEE 754-2008 부동소수점 산술 표준에서 정의한 특수 부동소수점 값(Special Floating-Point Value)으로, ECMAScript 명세는 이를 Number 타입의 멤버로 포함합니다. 이 설계 결정은 IEEE 754를 채택한 모든 언어(Java, Python, C++ 등)에 공통적으로 적용되며, JavaScript에만 국한된 특성이 아닙니다.
IEEE 754 비트 표현과 NaN의 종류 64비트 배정밀도(Double Precision) 부동소수점에서 NaN은 지수(Exponent) 필드의 모든 비트가 1이고 가수(Mantissa/Significand) 필드가 0이 아닌 경우를 나타냅니다. 이론적으로 약 2^52 - 1개의 서로 다른 NaN 비트 패턴이 존재하며, 이를 두 종류로 구분합니다.
Quiet NaN(qNaN)과 Signaling NaN(sNaN) qNaN(Quiet NaN)은 예외를 발생시키지 않고 조용히 전파되는 NaN으로, 대부분의 JavaScript NaN이 이에 해당합니다. sNaN(Signaling NaN)은 부동소수점 예외를 트리거하도록 설계되었지만 JavaScript 엔진은 일반적으로 sNaN을 qNaN으로 변환하여 처리합니다. V8 엔진은 NaN 값을 내부적으로 특정 비트 패턴(canonical NaN)으로 정규화(canonicalize)하는데, 이는 NaN 박싱(NaN-boxing) 최적화 기법과 연관됩니다.
ECMAScript 명세에서의 NaN 정의
ECMAScript 2023 명세 Section 6.1.6.1(The Number Type)에 따르면 Number 타입은 IEEE 754-2008의 64비트 이진 부동소수점 값 집합으로 정의되며, NaN은 이 집합의 유일한 비숫자 멤버로 명시됩니다. typeof 연산자는 Section 13.5.3에 따라 Number 타입 값(NaN 포함)에 대해 문자열 “number”를 반환하도록 명세되어 있습니다.
NaN-boxing 최적화와 엔진 구현 V8, SpiderMonkey 등 현대 JavaScript 엔진은 NaN-boxing(또는 NaN-tagging)이라는 최적화 기법을 사용합니다. 이 기법은 64비트 NaN의 사용되지 않는 비트 패턴 공간을 활용해 포인터와 정수를 저장하는 방식으로, 모든 JavaScript 값을 단일 64비트 워드로 표현할 수 있게 합니다. V8에서 실제 부동소수점 NaN은 특수한 canonical NaN 비트 패턴 하나로 정규화되며, 나머지 NaN 비트 패턴 공간은 힙 포인터와 Small Integer(SMI) 표현에 사용됩니다.
NaN이 자기 자신과 같지 않은 이유
입문
NaN은 자기 자신과 비교해도 “같지 않다”는 결과가 나와요. 이상하게 들리지만, 사실 아주 논리적인 이유가 있어요!
🎲 “모르는 값”끼리 같다고 할 수 있을까요? NaN은 “알 수 없는 숫자”를 표현해요. 비유하자면, 상자 A 안에 뭔가 있는데 열어보지 않았고, 상자 B 안에도 뭔가 있는데 열어보지 않았어요. 두 상자의 내용물이 같다고 말할 수 있나요? 말할 수 없어요! “모른다”는 두 상태가 서로 같다고 확인할 방법이 없거든요.
🧮 수학의 규칙을 따른 거예요 수학자들은 오래전부터 “정의되지 않은 결과끼리는 같다고 볼 수 없다”는 원칙을 세워뒀어요. 예를 들어 0 나누기 0과, 음수 제곱근은 둘 다 “정의 안 됨”이지만 서로 같은 개념이 아니에요. 컴퓨터도 이 원칙을 그대로 따른 거예요.
💡 덕분에 NaN을 찾을 수 있어요
이 특성은 오히려 유용해요! 어떤 값 x가 있을 때 x !== x가 참이라면, 그 값은 반드시 NaN이에요. JavaScript에서 자기 자신과 다른 값은 NaN밖에 없거든요. 마치 비밀 암호처럼, NaN을 찾는 특별한 방법이 되는 거예요.
🌍 JavaScript만의 문제가 아니에요 이 동작은 JavaScript가 특이해서가 아니라, 컴퓨터 과학의 국제 표준(IEEE 754)을 따른 결과예요. Java, Python, C++도 모두 같은 방식으로 동작해요. 그러니 JavaScript가 이상한 게 아니라, 수학적 설계를 충실히 따른 거예요!
중급
NaN === NaN이 false를 반환하는 것은 ECMAScript 명세가 IEEE 754-2008 표준의 동등 비교 규칙을 그대로 따른 결과입니다. IEEE 754는 NaN과의 모든 비교 연산(==, !=, <, >, <=, >=)이 false 또는 unordered를 반환하도록 규정합니다.
수학적 근거 NaN은 단일한 “잘못된 값”이 아니라 “결과를 정의할 수 없는 연산의 결과”를 나타냅니다. 0/0과 Math.sqrt(-1)은 모두 NaN을 반환하지만, 두 연산의 “알 수 없음”이 같은 값을 의미하지는 않습니다. 따라서 NaN끼리 동등하다고 단언할 수 없습니다.
NaN === NaN; // false
NaN == NaN; // false
NaN !== NaN; // true - NaN 감지에 활용 가능!
// NaN과의 모든 비교는 false
NaN > 1; // false
NaN < 1; // false
NaN >= NaN; // false
// NaN 감지 패턴 (Number.isNaN 이전 방식)
function isNaN_legacy(value) {
return value !== value; // NaN만이 자기 자신과 다르다
}
isNaN_legacy(NaN); // true
isNaN_legacy(1); // false
자기 불일치를 활용한 NaN 감지
x !== x는 NaN을 감지하는 유일한 비교 기반 방법입니다. JavaScript의 모든 다른 값은 자기 자신과 항상 동등합니다. 이 패턴은 Number.isNaN()이 ES6에서 도입되기 전에 사용되던 관용 표현이며, 현재도 일부 레거시 코드에서 발견됩니다.
심화
IEEE 754-2008 표준 Section 5.11(Details of comparison predicates)은 NaN을 포함한 비교 연산에서 결과가 “unordered”임을 명시합니다. ECMAScript는 Abstract Equality Comparison(==)과 Strict Equality Comparison(===) 알고리즘 모두에서 이 규칙을 채택합니다.
ECMAScript 명세의 동등 비교 알고리즘 ECMAScript 2023 Section 7.2.15(IsStrictlyEqual)에 따르면, 두 피연산자가 모두 Number 타입일 때 Number::equal(x, y) 추상 연산(Abstract Operation)을 호출합니다. Section 6.1.6.1.13(Number::equal)은 x 또는 y가 NaN이면 false를 반환하도록 명세합니다. 이는 IEEE 754 compareQuietEqual 연산의 명세와 직접 대응합니다.
IEEE 754의 설계 철학 - 전파 가능성 보존
IEEE 754 설계자 William Kahan은 NaN의 자기 불일치 규칙에 대해 명시적인 근거를 제시했습니다. NaN이 자기 자신과 같다면, if (x == x) 조건이 참일 때 x가 정상 숫자임을 보장할 수 없어집니다. 반대로 현재 규칙에서는 x != x라는 단일 조건으로 NaN을 정확히 감지할 수 있으며, 이는 표준화된 NaN 전파 메커니즘과 결합하여 일관된 예외 처리를 가능하게 합니다.
SameValue와 SameValueZero 비교 알고리즘
ECMAScript는 IEEE 754 기반의 ==와 !== 외에도 두 가지 추가 동등 알고리즘을 정의합니다. SameValue(Section 7.2.11)는 Object.is() 구현에 사용되며, NaN을 NaN과 동등하게 취급합니다. SameValueZero는 Map, Set, Array.prototype.includes() 등의 컬렉션 메서드에 사용되며, 마찬가지로 NaN을 NaN과 동등하게 취급합니다. 이로 인해 [NaN].includes(NaN)은 true이지만 [NaN].indexOf(NaN)은 -1을 반환하는 비대칭이 발생합니다.
isNaN()과 Number.isNaN()의 차이
입문
NaN인지 확인하는 방법이 두 가지 있는데, 하나는 함정이 있어요! 어떤 게 안전한지 알아볼게요.
🕳️ 함정이 있는 전통적인 방법
오래된 방법인 isNaN()은 확인하기 전에 몰래 값을 숫자로 바꿔버려요. 마치 “이게 빨간색이야?”라고 물었더니 먼저 빨간 페인트를 칠하고 나서 “응, 빨간색이야!”라고 대답하는 것과 같아요. 문자열 “안녕”을 isNaN에 넣으면, 먼저 숫자로 바꾸려 시도해서 NaN이 되고, 그러면 “맞아, NaN이야!”라고 해버려요. 하지만 처음부터 NaN이 아니었잖아요!
✅ 안전한 현대적인 방법
Number.isNaN()은 훨씬 정직해요. 먼저 “이게 숫자 타입이야?”를 확인하고, 그 다음에 “그리고 NaN이야?”를 확인해요. 숫자 타입이 아니면 바로 “아니야”라고 대답해요. 문자열 “안녕”을 넣으면? 숫자 타입이 아니니까 바로 “NaN 아니야”라고 정확하게 대답해요.
🔍 언제 차이가 나나요? 진짜 NaN인지 확인할 때는 두 방법이 같은 답을 줘요. 하지만 NaN이 아닌 값(문자열, undefined 등)을 넣으면 달라져요. 전통적인 방법은 “이것들도 NaN이야!”라고 잘못 대답하고, 현대 방법은 “이건 NaN이 아니야”라고 정확하게 대답해요.
💼 실생활 비유 전통 방법은 “이 사람이 감기 환자야?”를 물었을 때, 먼저 환자를 추운 곳에 세워두고 기침을 하게 만든 뒤 “응, 감기야!”라고 대답하는 의사 같아요. 반면 현대 방법은 실제로 환자가 이미 기침을 하고 있는지를 그냥 보는 의사예요. 결과가 얼마나 다를 수 있는지 이해되죠?
중급
전역 함수 isNaN()과 Number.isNaN()(ES6, 2015 도입)의 가장 큰 차이는 타입 강제 변환(type coercion)의 유무입니다. 전역 isNaN()은 인자를 먼저 Number()로 변환한 후 NaN 여부를 확인하므로, 원래 NaN이 아닌 값도 true를 반환할 수 있습니다.
// 전역 isNaN() - 타입 강제 변환 후 확인
isNaN(NaN); // true (맞음)
isNaN("hello"); // true (틀림! "hello"는 NaN이 아님)
isNaN(undefined); // true (틀림! undefined는 NaN이 아님)
isNaN(""); // false (틀림! ""은 0으로 변환됨)
isNaN(null); // false (틀림! null은 0으로 변환됨)
// Number.isNaN() - 강제 변환 없이 정확한 확인
Number.isNaN(NaN); // true (정확)
Number.isNaN("hello"); // false (정확)
Number.isNaN(undefined); // false (정확)
Number.isNaN(""); // false (정확)
Number.isNaN(null); // false (정확)
올바른 NaN 감지 전략
실무에서는 항상 Number.isNaN()을 사용하는 것이 권장됩니다. 단, 함수 인자가 이미 숫자 타입임이 보장된 경우에는 두 함수의 결과가 동일합니다. 또한 Number.isNaN()의 폴리필(polyfill, 구형 브라우저 지원을 위한 대체 코드)은 자기 불일치 패턴으로 구현합니다.
// Number.isNaN의 폴리필 구현
if (!Number.isNaN) {
Number.isNaN = function(value) {
return typeof value === 'number' && value !== value;
};
}
심화
전역 isNaN()은 ES1(1997)부터 존재했으며, ECMAScript Section 19.2.3(isNaN)에 따르면 인자에 ToNumber 추상 연산을 적용한 후 IEEE 754 NaN 여부를 확인합니다. Number.isNaN()은 ES6(ES2015) Section 21.1.2.4에 도입되었으며, ToNumber 강제 변환 없이 타입과 NaN 여부를 직접 확인합니다.
내부 알고리즘 비교
전역 isNaN(num)의 명세 알고리즘: (1) num을 ToNumber(num)으로 변환, (2) 변환된 값이 NaN이면 true 반환. 이 과정에서 ToNumber 추상 연산은 "" → 0, null → 0, undefined → NaN, "123" → 123, "hello" → NaN으로 변환하므로, 원래 NaN이 아닌 값이 NaN으로 판정될 수 있습니다.
Number.isNaN(value)의 명세 알고리즘: (1) Type(value)이 Number가 아니면 false 반환, (2) value가 NaN이면 true 반환, (3) 그 외 false 반환. ToNumber 호출이 없으므로 타입 강제 변환이 발생하지 않습니다.
TypeScript 타입 시스템과의 연계
TypeScript에서 Number.isNaN()은 타입 가드(Type Guard) 역할을 명시적으로 수행하지 않습니다. number | string 유니온 타입을 다룰 때는 typeof value === 'number' && Number.isNaN(value) 패턴을 조합하거나, 사용자 정의 타입 가드 함수를 작성하는 것이 권장됩니다. ESLint의 no-restricted-globals 규칙을 통해 전역 isNaN 사용을 팀 레벨에서 제한하는 것도 일반적인 실무 관행입니다.
엔진 최적화 관점
V8 엔진에서 Number.isNaN()은 내장 함수(built-in)로 최적화되어 있으며, Turbofan JIT 컴파일러가 인라인(inline) 처리합니다. IEEE 754 NaN 비트 패턴 확인은 단일 하드웨어 명령어(예: x86의 UCOMISD 명령어)로 처리될 수 있어, 함수 호출 오버헤드가 최소화됩니다.
NaN 전파와 방어적 코딩
입문
NaN은 마치 병균처럼 계산 과정에서 퍼져나가요. 한 번 들어오면 이후 모든 계산 결과를 오염시킬 수 있어요!
🦠 NaN은 어떻게 퍼지나요? 어떤 숫자든 NaN과 연산하면 결과가 NaN이 돼요. 100 + NaN = NaN, 50 * NaN = NaN처럼요. 마치 어떤 음식이든 상한 재료를 섞으면 전체 요리가 망가지는 것과 같아요. 이런 성질을 “NaN 전파”라고 해요.
🔍 어디서 문제가 시작됐는지 찾기 어려워요 만약 계산 과정이 길다면, 마지막에 NaN이 나왔을 때 처음에 어디서 잘못됐는지 찾기가 정말 어려워요. 레시피의 50번째 단계에서 결과물이 이상하다면, 어느 재료가 상했는지 처음부터 다시 확인해야 하는 것처럼요.
🛡️ 어떻게 막을 수 있나요? 가장 좋은 방법은 NaN이 들어올 수 있는 곳(특히 사용자가 입력하는 곳이나 외부에서 데이터가 오는 곳)에서 처음부터 확인하는 거예요. 마치 집 입구에 신발장을 두어 더러운 신발로 집 안을 오염시키지 않는 것처럼, 계산이 시작되기 전에 NaN을 걸러내야 해요.
🎯 NaN 대신 기본값을 쓰는 방법도 있어요 NaN이 발견되면 0이나 다른 적절한 기본값으로 바꿔주는 방법이 있어요. 예를 들어 사용자가 나이를 입력했는데 숫자가 아니라면, NaN 대신 0이나 “알 수 없음”으로 처리할 수 있어요. 이렇게 하면 계산이 계속 NaN을 퍼뜨리는 걸 막을 수 있어요.
중급
NaN은 산술 연산에서 자동으로 전파됩니다. 어떤 산술 연산이든 한 쪽 피연산자가 NaN이면 결과도 NaN이 됩니다. 이 특성으로 인해 연산 초기에 발생한 NaN이 이후 모든 계산 결과를 오염시킬 수 있습니다. 방어적 코딩(defensive coding)의 핵심은 NaN의 진입점을 최대한 초기에 차단하는 것입니다.
const userInput = "abc";
const price = Number(userInput); // NaN
const tax = price * 0.1; // NaN (전파)
const total = price + tax; // NaN (계속 전파)
const discount = total - 1000; // NaN (계속 전파)
console.log(total); // NaN - 어디서 시작됐는지 불명확
console.log(discount); // NaN - 디버깅 어려움
// 패턴 1: 입력 단계에서 유효성 검사
function parsePrice(input) {
const parsed = Number(input);
if (Number.isNaN(parsed)) {
throw new Error(`유효하지 않은 가격: ${input}`);
}
return parsed;
}
// 패턴 2: 기본값 대체 (nullish coalescing 활용)
function safeDivide(a, b) {
const result = a / b;
return Number.isNaN(result) ? 0 : result;
}
// 패턴 3: 배열 필터링
const values = [1, NaN, 3, NaN, 5];
const clean = values.filter(v => !Number.isNaN(v));
// [1, 3, 5]
Array 메서드와 NaN의 주의사항
Array.prototype.indexOf()는 내부적으로 strict equality(===)를 사용하므로 배열의 NaN을 찾지 못합니다. NaN을 포함한 배열 탐색에는 Array.prototype.includes()(SameValueZero 알고리즘 사용) 또는 Array.prototype.findIndex() + Number.isNaN을 사용해야 합니다.
const arr = [1, NaN, 3];
arr.indexOf(NaN); // -1 (찾지 못함!)
arr.includes(NaN); // true (찾음!)
arr.findIndex(Number.isNaN); // 1 (인덱스 반환)
심화
NaN 전파는 IEEE 754-2008 Section 6.2(Operations on NaNs)에서 규정한 NaN 전파 규칙(NaN propagation rule)에 따릅니다. 모든 산술 연산(+, -, *, /, %)에서 피연산자 중 하나라도 NaN이면 결과는 NaN이며, 이는 예외(exception)를 발생시키는 대신 오류를 조용히 전파하는 IEEE 754의 설계 철학을 반영합니다.
IEEE 754 Signal과 Default Exception Handling IEEE 754는 다섯 가지 예외 조건(invalid operation, division by zero, overflow, underflow, inexact)을 정의합니다. NaN 생성은 “invalid operation” 예외에 해당하며, 기본 예외 처리(default exception handling)는 예외를 트리거하는 대신 NaN을 결과로 반환합니다. 이는 하드웨어 부동소수점 연산이 예외를 발생시키지 않고 계속 실행될 수 있게 하여 성능을 유지하는 설계입니다.
실무에서의 NaN 감지 전략
방어적 코딩(defensive programming)의 관점에서 NaN 처리는 세 가지 계층으로 나뉩니다. 첫째로 입력 경계(input boundary)에서 Number.isNaN() 또는 isFinite()를 사용한 조기 유효성 검사, 둘째로 연산 결과에서의 NaN 감지 및 기본값 복원, 셋째로 타입 시스템 활용입니다. TypeScript 환경에서는 number 타입도 NaN을 허용하므로, 브랜드 타입(branded types) 또는 불투명 타입(opaque types) 패턴을 통해 type SafeNumber = number & { readonly _brand: 'SafeNumber' } 형식으로 NaN이 불가능한 숫자 타입을 표현할 수 있습니다.
ECMAScript 컬렉션 API의 비일관성
ECMAScript에서 동등 비교 알고리즘의 선택은 API마다 다릅니다. indexOf, lastIndexOf, ==, ===는 Abstract Equality 또는 Strict Equality를 사용하여 NaN을 찾지 못합니다. includes, Map, Set은 SameValueZero를 사용하여 NaN을 동등하게 취급합니다. Object.is()는 SameValue를 사용하여 NaN을 동등하게 취급합니다. 이 비일관성은 ECMAScript의 역방향 호환성(backward compatibility) 요구사항으로 인해 발생하며, TC39 위원회는 새로운 API에서는 SameValueZero를 기본으로 채택하는 방향으로 진행하고 있습니다.
성능 관점: NaN 체크의 비용
JIT 컴파일된 코드에서 Number.isNaN() 호출은 인라인 처리되어 단일 비교 명령어로 축소됩니다. 그러나 NaN이 빈번하게 발생하는 핫 루프(hot loop)에서는 NaN 전파가 JIT 최적화를 방해할 수 있습니다. V8의 Turbofan은 피연산자가 NaN이 될 수 있음을 감지하면 Speculative Optimization(추측 최적화)의 가정을 무효화하고 덜 최적화된 코드로 폴백(fallback)합니다. 성능이 중요한 수치 연산 코드에서는 NaN의 발생 가능성을 입력 단계에서 제거하는 것이 최적화 측면에서도 유리합니다.