울면서개발하기
JAVA SCRIPT / NEXACRO

자바스크립트 Number() 가 객체를 숫자로 바꾸는 순서와 nexacro.Decimal

· 2026년 10월 25일 다시 씀

넥사크로의 nexacro.Decimal 값을 비교하다가 Number() 가 객체를 어떻게 숫자로 바꾸는지 정리하게 됐다. 결론부터.

  1. Number(obj), obj * 1, +obj 는 객체를 원시값으로 바꾼 뒤 숫자로 바꾼다. 원시값은 obj[Symbol.toPrimitive] → valueOf() → toString() 순서로 찾고, 원시값이 처음 나온 곳에서 멈춘다.
  2. nexacro.Decimal 은 객체이므로 == 로 서로 비교하면 값이 같아도 false 다. 한쪽 이상을 숫자(또는 문자열)로 바꿔서 비교한다.

문제: 같은 값인데 비교가 false

넥사크로 데이터셋 컬럼 타입이 BIGDECIMAL 이면 getColumn 은 숫자가 아니라 nexacro.Decimal 객체를 돌려준다. A 컬럼(BIGDECIMAL)에 두 행 모두 1 을 넣고 웹 브라우저 콘솔에서 확인해 봤다.

this.Dataset00.getColumn(0, "A");
// ▶ nexacro.Decimal {hi: 1, lo: 0}

this.Dataset00.getColumn(0, "A") == this.Dataset00.getColumn(1, "A");
// false

Number(this.Dataset00.getColumn(0, "A")) == this.Dataset00.getColumn(1, "A");
// true

trace 로 찍으면 문자열 1 만 보여서 객체라는 것을 알 수 없다. trace 는 값을 문자열로 바꿔서 찍기 때문이다. 타입을 보려면 웹 브라우저로 실행해서 console.log 를 쓴다.

첫 번째 비교가 false 인 이유는 단순하다. == 와 === 모두 양쪽이 객체이면 같은 객체(참조)인지만 본다. 행마다 다른 객체이므로 값이 같아도 false 다.

두 번째 비교가 true 인 이유는 한쪽이 원시값(숫자)이기 때문이다. 객체와 원시값을 == 로 비교하면 객체를 원시값으로 바꾼 뒤 비교한다. 그럼 nexacro.Decimal 은 어떻게 원시값이 될까.

원인: 객체를 원시값으로 바꾸는 규칙

명세(ECMAScript)의 ToPrimitive 규칙을 요약하면 이렇다. 바꾸려는 쪽이 원하는 타입을 힌트로 넘긴다.

연산 힌트 시도 순서
Number(obj), obj * 1, +obj, obj - 0 "number" valueOf → toString
String(obj), `${obj}` "string" toString → valueOf
obj + "", obj == 1 "default" valueOf → toString (Date 만 toString 먼저)
  • 객체에 Symbol.toPrimitive 메서드가 있으면 그것만 호출하고 힌트를 인자로 넘긴다(ES2015 부터).
  • 없으면 표의 순서대로 호출하되, 원시값을 돌려준 첫 메서드의 결과를 쓰고 멈춘다. 둘 다 원시값을 돌려주지 않으면 TypeError 다.
  • 얻은 원시값을 다시 숫자로 바꾼다. 문자열이면 앞뒤 공백을 무시하고 해석하며, 숫자가 아닌 글자가 섞이면 NaN 이다.

콘솔에서 확인해 보면 이렇다.

// valueOf 가 원시값을 돌려주면 toString 은 호출되지 않는다
Number({ valueOf() { return 1; }, toString() { return 3; } });   // 1

// valueOf 가 객체를 돌려주면 toString 으로 넘어간다 (기본 valueOf 도 객체 자신을 돌려준다)
Number({ valueOf() { return {}; }, toString() { return "3"; } }); // 3

// Symbol.toPrimitive 가 있으면 그것이 우선이다
const price = {
  [Symbol.toPrimitive](hint) {
    return hint === "number" ? 42 : "42원";
  },
};
Number(price);  // 42
`${price}`;     // "42원"

// 원시값을 끝내 얻지 못하면 TypeError
Number({ valueOf() { return {}; }, toString() { return {}; } });  // TypeError

// 문자열 → 숫자
Number("");      // 0
Number(" 12 ");  // 12
Number("12px");  // NaN  (parseFloat("12px") 는 12)

Number() 와 * 1, 단항 + 는 거의 같지만 BigInt 에서 다르다. Number(1n) 은 1 이지만 +1n 은 TypeError 다.

nexacro.Decimal 의 경우

웹 환경 콘솔에서 nexacro.Decimal.prototype.toString 을 입력해 보면 toString 이 정의되어 있다. 앞부분은 이런 모양이다.

function () {
  if (this.isZero()) {
    return "0";
  }
  if (this.isNaN()) {
    return "NaN";
  }
  // ...
}

별도의 valueOf 를 찾지는 못했다. 그렇다면 기본 valueOf 가 객체 자신을 돌려주므로(원시값이 아님) toString() 이 호출되고, 그 결과 "1" 이 숫자 1 로 바뀐다고 보면 위의 결과가 설명된다. == 비교에서도 같은 순서로 "1" 이 나오고, "1" == 1 은 true 다.

해결: 비교할 때는 명시적으로 바꾼다

var a = this.ds_main.getColumn(0, "AMT");
var b = this.ds_main.getColumn(1, "AMT");

a == b;                    // false — 객체끼리는 참조 비교
Number(a) === Number(b);   // 숫자로 비교
String(a) === String(b);   // 문자열로 비교

한쪽만 바꿔서 == 로 맞추는 방식도 동작은 하지만, 읽는 사람이 암묵적 변환을 알아야 한다. 양쪽을 같은 타입으로 바꾸고 === 로 비교하는 편이 의도가 분명하다.

Number() 로 바꾸면 자바스크립트의 double 정밀도(유효숫자 약 15~17자리)로 내려간다. 자릿수가 아주 큰 금액이나 소수 자릿수가 긴 값을 정확히 비교해야 한다면 문자열로 비교하는 쪽을 검토한다.

nexacro.Decimal 은 왜 있을까

자바스크립트 숫자는 IEEE 754 double 이라 0.1 + 0.2 가 0.3 이 아니다.

0.1 + 0.2; // 0.30000000000000004

같은 데이터셋에 0.1, 0.2 를 넣고 넥사크로의 getSum 으로 합계를 구하면 nexacro.Decimal 이 나오는데, hi 가 정확히 0.3 이다.

this.Dataset00.getSum("A");
// ▶ nexacro.Decimal {hi: 0.3, lo: 1.1102230246251564e-17}

hi 와 lo 두 숫자로 값을 나눠 담아 일반 숫자 연산보다 정밀도를 높이는 방식으로 보인다(내부 구현 문서는 확인하지 못했다). 다른 상용 UI 도구에도 비슷한 숫자 객체가 있는 것을 보면, 금액처럼 정확한 계산이 필요한 업무 화면을 위한 장치로 이해하고 있다.

다시 쓰며 바로잡은 점

  • 원문은 “Number 는 valueOf() 와 toString() 을 순서대로 호출한다”고 했는데, 정확히는 원시값이 나오는 첫 메서드에서 멈춘다. Symbol.toPrimitive 가 가장 먼저라는 점과 힌트에 따라 순서가 바뀐다는 점(문자열 변환은 toString 먼저)을 더했다.
  • Number() 와 * 1·+ 의 BigInt 차이, double 정밀도 한계를 덧붙였다.
  • nexacro.Decimal 이 toString 으로 변환된다는 부분은 valueOf 가 따로 없다는 전제의 추론임을 밝혔다.