(번역) 자바스크립트에서 `NaN !== NaN`인 이유 (그리고 그 뒤에 숨은 IEEE 754 이야기)

2025-11-162025-11-16
  • 번역

원문: Why NaN !== NaN in JavaScript (and the IEEE 754 story behind it)

항상 같은 것을 반환하는 숫자

오늘 우리는 자바스크립트에서 number 타입으로 식별되는 NaN에 대해 살펴보겠습니다.

> typeof NaN
'number'

응답으로 number 타입을 받습니다. 무엇인가 숫자이려면, 논리적으로 수학 연산을 수행할 수 있어야 합니다. 그러니 NaN에 다른 무언가를 더하거나 최댓값, 최솟값을 확인해 보겠습니다.

> NaN + 1
NaN
> NaN - 1
NaN
> Math.max(NaN)
NaN
> Math.min(NaN)
NaN

보시다시피, 덧셈, 뺄셈, 최댓값/최솟값 확인 후에도 항상 같은 결과를 얻습니다. 그렇다면 왜 이런 값이 필요한 걸까요? 이를 설명하기 위해, Firefox나 V8 내부를 들여다보며 NaN의 사용처와 구현을 찾아보겠습니다.

// Firefox
bool isNaN() const { return isDouble() && std::isnan(toDouble()); }

// V8
if (IsMinusZero(value)) return has_minus_zero();
if (std::isnan(value)) return has_nan();

예시 브라우저의 코드를 보면, NaN을 확인하기 위해 표준 라이브러리의 std::isnan 메서드가 사용됩니다. 이는 NaN이 자바스크립트와 무관하게 등장했다는 것을 시사할 수 있습니다. 그리고 실제로 역사를 거슬러 올라가 보면, NaN의 최초 표준화는 1985년에 등장했으며 IEEE 754라는 번호가 부여되었습니다.

자바스크립트에서 하드웨어 수준까지

이 지식을 바탕으로, 브라우저 코드에서 찾은 내용을 기반으로 NaN이 어떻게 동작하는지 확인하는 간단한 C 프로그램을 작성해 보겠습니다.

> NaN !== NaN
true
> 0 / 0
NaN
#include <math.h>
#include <stdint.h>
#include <stdio.h>

int main() {
    double x = 0.0 / 0.0;

    if (x != x) {
        printf("NaN is not the same\n");
    }
    if (isnan(x)) {
        printf("x is NaN\n");
    }

    uint64_t bits = *(uint64_t*)&x;

    printf("NaN hex: 0x%016lx\n", bits);

    return 0;
}

결과는 자바스크립트에서와 동일합니다!

NaN is not the same
x is NaN
NaN hex: 0xfff8000000000000

우리는 NaN이 다른 프로그래밍 언어에서도 발견된다는 것을 이미 알고 있습니다.

#Python

import math

nan = float('nan')
print(nan != nan)  # True
print(nan == nan)  # False
print(math.isnan(nan))  # True
//C++

#include <iostream>
#include <cmath>

int main() {
    double nan = NAN;
    std::cout << (nan != nan) << std::endl;  // 1 (true)
    std::cout << (nan == nan) << std::endl;  // 0 (false)
    std::cout << std::isnan(nan) << std::endl;  // 1 (true, proper way)
    return 0;
}
//Rust

fn main() {
    let nan = f64::NAN;
    println!("{}", nan != nan);  // true
    println!("{}", nan == nan);  // false
    println!("{}", nan.is_nan());  // true (proper way)
}

하지만 여전히 그것이 무엇을 위한 것인지는 모릅니다.

여전히 이유를 알 수 없으니, 우리의 간단한 프로그램으로 어셈블리 코드를 생성해 보겠습니다 (프롤로그와 스택 프레임 초기화는 건너뛰겠습니다).

# =====================================
#     double x = 0.0 / 0.0;
# =====================================
	pxor	xmm0, xmm0                 # xmm0 = 0.0
	divsd	xmm0, xmm0                 # xmm0 = 0.0 / 0.0 = NaN
	movsd	QWORD PTR -8[rbp], xmm0    # x = NaN

# =====================================
#     if (x != x) {
# =====================================
	movsd	xmm0, QWORD PTR -8[rbp]    # xmm0 = x
	ucomisd	xmm0, QWORD PTR -8[rbp]    # compare x with x (sets PF=1 for NaN)
	jnp	.L2                            # skip if NOT NaN (PF=0)
	# NaN detected - code here
.L2:

# =====================================
#     if (isnan(x)) {
# =====================================
	movsd	xmm0, QWORD PTR -8[rbp]    # xmm0 = x
	ucomisd	xmm0, QWORD PTR -8[rbp]    # compare x with x (sets PF=1 for NaN)
	jnp	.L3                            # skip if NOT NaN (PF=0)
	# NaN detected - code here
.L3:

어셈블리를 접해보지 않은 분들을 위해 설명하자면, xmm0 레지스터는 부동소수점 연산을 수행하는 레지스터입니다. 논리적으로 생각해보면, 우리는 숫자 연산을 수행하고 싶고, CPU는 숫자를 처리하니까, 이런 목적으로 특별히 설계된 레지스터에서 작업하는 게 가장 빠를 수밖에 없겠죠!

또한 NaN을 감지했을 때 플래그를 설정하는 ucomisd 명령어도 볼 수 있습니다.

여기서 어떤 결론을 내릴 수 있을까요? NaN은 자바스크립트 추상화 수준이 아닌 하드웨어 수준에서 구현된다는 것입니다. 그러니 불필요한 추상화를 피하기 위해 어셈블리어로 프로그램을 다시 작성하고, 그 실행 결과를 살펴보겠습니다.

#include <stdio.h>
#include <stdint.h>

int main() {
    double x;
    uint64_t bits;

    __asm__ (
        // double x = 0.0 / 0.0;
        "pxor   xmm0, xmm0\n\t"         // xmm0 = 0.0
        "divsd  xmm0, xmm0\n\t"         // xmm0 = 0.0 / 0.0 = NaN

        // Save results
        "movsd  %0, xmm0\n\t"           // x = NaN
        "movq   %1, xmm0\n\t"           // bits = *(uint64_t*)&x

        : "=m" (x), "=r" (bits)
        :
        : "xmm0"
    );

    int is_not_equal;
    __asm__ (
        // if (x != x)
        "movsd  xmm0, %1\n\t"           // xmm0 = x
        "ucomisd xmm0, %1\n\t"          // compare x with x → PF=1 for NaN
        "setp   al\n\t"                 // al = (x != x)
        "movzx  %0, al\n\t"             // is_not_equal = al

        : "=r" (is_not_equal)
        : "m" (x)
        : "xmm0", "al"
    );

    if (is_not_equal) {                 // if (x != x)
        printf("NaN is not the same\n");
    }

    int is_nan_result;
    __asm__ (
        // if (isnan(x))
        "movsd  xmm0, %1\n\t"           // xmm0 = x
        "ucomisd xmm0, %1\n\t"          // compare x with x → PF=1 for NaN
        "setp   al\n\t"                 // al = isnan(x)
        "movzx  %0, al\n\t"             // is_nan_result = al

        : "=r" (is_nan_result)
        : "m" (x)
        : "xmm0", "al"
    );

    if (is_nan_result) {                // if (isnan(x))
        printf("x is NaN\n");
    }

    printf("NaN hex: 0x%016lx\n", bits);

    return 0;
}

결과는 어떨까요?

NaN is not the same
x is NaN
NaN hex: 0xfff8000000000000

프로그램의 출력은 고수준 언어인 C와 동일합니다.

우리는 NaN이 네이티브로 구현되어 있음을 이미 알고 있으니, ucomisd 명령어를 살펴보겠습니다.

"ucomisd xmm0, %1\n\t"          // compare x with x → PF=1 for NaN

ucomisdUnordered Compare Scalar Double-precision floating-point(순서 없는 스칼라 배정밀도 부동 소수점 비교)의 약자입니다. 이 멋진 명령어는 x86 아키텍처 프로그래머들의 시간과 신경을 절약해 주었습니다. 왜냐하면 CPU 수준에서 이미 숫자 연산 결과가 올바른지 아닌지를 확인해 주기 때문입니다.

NaN !== NaN

주된 이유는 프로그래밍 언어에 isnan() 함수가 아직 존재하지 않던 시절, 프로그래머들에게 x != x 테스트를 사용해 NaN을 감지할 방법을 제공하기 위함이었습니다.

논리적인 관점에서 볼 때, 이는 매우 타당합니다. 왜냐하면 ‘값이 아님’은 ‘값이 아님’과 같을 수 없기 때문입니다.

이것은 버그가 아니라 의도된 설계입니다.

typeof NaN === “number”

NaN은 별도의 타입이 아니라 숫자 시스템(IEEE 754)의 일부입니다. 이는 수학적 연산 오류를 알리는 특별한 숫자 값입니다.

IEEE 754-1985: 이진 부동 소수점 산술 표준

  • 발표: 1985년
  • 저자: 윌리엄 카한(UC 버클리) + IEEE 위원회
  • 정의: NaN, 무한(Infinity), 비정규화된 수(denormalized numbers), 반올림 모드

주요 결정 사항

  • NaN !== NaN (동등 비교 시 항상 false)
  • 지수(Exponent) = 0x7FF, 가수(mantissa) ≠ 0
  • Quiet NaN (qNaN) - 예외를 알리지 않고 연산을 통해 전파됨
  • Signaling NaN (sNaN) - 연산에 처음 사용될 때 예외를 생성함
  • NaN 전파 (NaN과의 모든 연산 → NaN)

NaN은 숫자입니다, 하지만 어떤 종류일까요?

0/0을 나누는 데 왜 부동 소수점 숫자 레지스터가 사용되는지 궁금할 수도 있습니다.

자바스크립트에서 number 타입 값에 대한 연산은 IEEE 754 표준에 따라 연산을 수행하기 위해 배정밀도 부동 소수점 숫자(double)로 표현됩니다.

정수 연산에서 0으로 나누는 것은 명백한 오류입니다. 하지만 부동 소수점 숫자에서는 다음과 같이 정의되지 않은 결과를 초래할 수 있는 많은 경우가 있습니다.

  • 0.0 / 0.0NaN
  • ∞ - ∞NaN
  • 0 * ∞NaN
  • sqrt(-1)NaN

IEEE 754 표준이 없었을 때, 각 하드웨어 제조사는 이러한 상황을 제각기 다르게 처리했으며, 이는 엄청난 코드 이식성 문제를 야기했습니다.

1994년: 펜티엄 FDIV 버그

펜티엄의 부동 소수점 나눗셈 버그 - 일부 나눗셈이 부정확한 결과를 반환했습니다. NaN과 관련된 문제는 아니었지만, 이는 정확한 IEEE 754 구현의 중요성을 보여주었습니다.

인텔은 수백만 개의 프로세서를 교체했으며, 이로 인해 4억 7,500만 달러의 비용이 발생했습니다.

프로그래머의 구원자, NaN

NaN이 하드웨어 수준에서 설정된다는 것을 배웠는데, NaN 이전에는 무엇이 있었을까요?

IEEE 754 표준(1985년) 이전에는 각 하드웨어 제조사가 제각각의 방식을 사용했으며, 이는 보통 0/0 같은 연산이 프로그램 충돌 및 종료로 이어진다는 것을 의미했습니다.

이로 인해 개발자들은 매우 방어적인 프로그래밍을 해야 했습니다. 비행기를 조종하고 있는데, 제어 시스템의 프로그래머가 0/0을 예상하지 못했다고 상상해 보세요. 해당 명령어가 CPU에서 실행되고 ‘Division Error’로 인해 프로그램 전체가 다운됩니다!

인텔과 다른 제조사들은 아키텍처마다 프로그램이 다르게 동작하는 혼란에 질려버렸습니다.

NaN (Not a Number)

왜 다른 해결책 대신 특별한 값을 사용했는지 자문해 볼 수 있습니다.

여러 옵션을 고려해 보겠습니다.

옵션 A: Division Error → 프로그램 충돌 (IEEE 754 이전 방식)

  • 예상치 못한 프로그램 종료 (비행기 예시 참고)
  • 모든 연산 전에 방어적인 프로그래밍 필요

옵션 B: 예를 들어 0을 반환

  • 수학적으로 부정확함
  • 오류를 숨김
  • 이후 계산이 잘못된 결과를 도출함

옵션 C: null 또는 특별한 오류 코드 반환

  • 모든 연산 후 확인 필요
  • 수학적 계산의 연속성을 방해함
  • 결과 타입이 일관되지 않게 됨

옵션 D: 특별한 값 NaN 반환 (IEEE 754가 채택)

  • 값이 계산을 통해 전파됨
  • 프로그램은 계속 실행됨
  • 계산 마지막에 결과를 확인할 수 있음
  • 타입 일관성 유지 (계속 number 타입)

NaN이 없었다면 어땠을까요?

function divide(a, b) {
  // 타입을 확인합니다.
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new Error('Arguments must be numbers');
  }

  // 유효하지 않은 숫자인지 확인합니다.
  if (!isFinite(a) || !isFinite(b)) {
    throw new Error('Arguments must be finite');
  }

  // 나누는 수를 확인합니다.
  if (b === 0) {
    throw new Error('Division by zero');
  }

  return a / b;
}

function calculate(expression) {
  try {
    const result = divide(10, 0);
    return result;
  } catch (e) {
    console.error(e.message);
    return null; // 무엇을 리턴해야 할까요? null? undefined? 0?
  }
}

NaN 덕분에 무엇을 얻었을까요?

function divide(a, b) {
  return a / b; // 나머지는 하드웨어의 몫입니다!
}

function calculate(expression) {
  return divide(10, 0);
}

const result = calculate('10 / 0');
console.log('Result:', result); // Infinity

const badResult = 0 / 0;
if (Number.isNaN(badResult)) {
  console.log('Invalid calculation');
}

요약

NaN은 부동 소수점 계산의 오류 처리에 대한 우아한 해결책입니다.

  • 하드웨어 수준에서 구현됨 (ucomisd 명령어)
  • 1985년부터 IEEE 754 표준의 일부
  • 연산을 통해 전파되며, 계산 마지막에 오류 감지를 허용함
  • NaN !== NaNNaN 감지를 가능하게 하는 의도된 설계임
  • typeof NaN === "number"NaN이 별도 타입이 아닌 숫자 시스템의 일부이기 때문임

References

🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article을 구독해주세요!

Profile picture

emewjin

Frontend Developer

잘못된 내용 혹은 더 좋은 방법이 있으면 언제든지 알려주세요 XD