반응형

바쁜 분은 이것만

  1. 문자열 전체가 숫자여야 하면 Number(), "12px"처럼 앞부분만 읽으려면 parseInt(s, 10) / parseFloat(s)
  2. Number("")와 Number(null)은 0이 나온다 — 빈 값 검사를 먼저
  3. NaN 확인은 Number.isNaN(), 253 넘는 정수는 BigInt로

문자열을 숫자로 바꾸는 Number(), parseInt(), parseFloat(), 단항 +는 빈 문자열, 단위가 붙은 값, null에서 결과가 갈립니다. Node.js v26에서 실행해 비교했어요.

1. 결과표: 같은 입력, 네 가지 변환

const inputs = ["12px", "", " 12 ", "0x1F", "1e3", "12.5", null, undefined, "1,000"];
const show = (v) => (typeof v === "string" ? JSON.stringify(v) : String(v));
console.log("입력".padEnd(10), "Number".padEnd(8), "parseInt".padEnd(9), "parseFloat".padEnd(11), "+");
for (const v of inputs) {
  console.log(
    show(v).padEnd(10),
    String(Number(v)).padEnd(8),
    String(parseInt(v)).padEnd(9),
    String(parseFloat(v)).padEnd(11),
    String(+v)
  );
}
입력         Number   parseInt  parseFloat  +
"12px"     NaN      12        12          NaN
""         0        NaN       NaN         0
" 12 "     12       12        12          12
"0x1F"     31       31        0           31
"1e3"      1000     1         1000        1000
"12.5"     12.5     12        12.5        12.5
null       0        NaN       NaN         0
undefined  NaN      NaN       NaN         NaN
"1,000"    NaN      1         1           NaN

단항 +는 Number()와 같은 규칙입니다(차이는 6번의 BigInt뿐). 결국 Number 계열 vs parse 계열로 나뉘어요.

2. Number는 "전체", parseInt/parseFloat는 "앞에서부터"

  • "12px": Number는 전체가 숫자 형식이어야 해서 NaN, parseInt는 숫자가 아닌 글자(p)에서 멈추고 12를 돌려줍니다.
  • "" / null: Number는 0, parse 계열은 NaN. 빈 입력창이 0으로 계산되는 버그가 여기서 나와요.
  • "0x1F": parseFloat는 16진수를 몰라서 0까지만 읽습니다.
  • "1e3": parseInt만 e에서 멈춰 1이에요.
  • "1,000": 콤마는 숫자로 안 봅니다. parseInt가 조용히 1을 돌려주는 쪽이 더 위험해요(7번).

3. parseInt에 radix(진법)를 안 주면

console.log(parseInt("08"));
console.log(parseInt("0x1F"));
console.log(parseInt("1F"));
console.log(parseInt("1F", 16));
console.log(parseInt("0b101"));
console.log(parseInt("101", 2));
console.log(parseInt("12", 0));
console.log(parseInt("12", 1));
console.log(parseInt("12", 37));
8
31
1
31
0
5
12
NaN
NaN

현재 엔진에서 "08"은 8입니다. 옛 엔진은 0으로 시작하면 8진수로 읽기도 했지만 ES5부터 빠졌어요. 지금 남은 문제는 다른 쪽입니다.

  • "0x"로 시작하면 자동으로 16진수: 입력이 "0x1F"면 오류 대신 31이 들어가요.
  • "0b101"은 0: 0b/0o 접두사는 모릅니다. 2진수는 radix 2로 지정해요.
  • radix는 2~36: 0은 10으로 취급, 1이나 37은 NaN. arr.map(parseInt)의 NaN도 index가 radix로 들어가서 생깁니다(따로 정리한 글이 있어요).

10진수는 항상 parseInt(s, 10)으로 씁니다. 또 parseInt는 인자를 먼저 문자열로 바꿔 읽어요.

console.log(parseInt("1e3"), parseFloat("1e3"));
console.log(parseInt(0.0000005));
console.log(parseInt(1e21));
1 1000
5
1

0.0000005는 "5e-7"이 되어 5로 읽힙니다. 숫자 소수점 버리기는 Math.trunc()가 맞아요.

4. NaN 판별: isNaN vs Number.isNaN

const show = (v) => (typeof v === "string" ? JSON.stringify(v) : String(v));
const vals = [NaN, "abc", undefined, "", "12"];
for (const v of vals) {
  console.log(show(v).padEnd(10), "isNaN:", String(isNaN(v)).padEnd(6), "Number.isNaN:", Number.isNaN(v));
}
const n = Number("12px");
console.log(n === NaN, Number.isNaN(n));
NaN        isNaN: true   Number.isNaN: true
"abc"      isNaN: true   Number.isNaN: false
undefined  isNaN: true   Number.isNaN: false
""         isNaN: false  Number.isNaN: false
"12"       isNaN: false  Number.isNaN: false
false true

전역 isNaN()은 먼저 숫자로 바꾼 뒤 검사해서 "abc"도 true, 빈 문자열은 false예요. Number.isNaN()은 변환 없이 정말 NaN일 때만 true입니다. NaN은 자기 자신과도 달라서 === NaN은 항상 false예요.

5. 정수인지 확인: Number.isInteger

console.log(Number.isInteger(5), Number.isInteger(5.0), Number.isInteger(5.5));
console.log(Number.isInteger("5"), Number.isInteger(Number("5")));
console.log(Number.isInteger(Infinity), Number.isInteger(NaN));
console.log(Number.isSafeInteger(2 ** 53), Number.isSafeInteger(2 ** 53 - 1));
true true false
false true
false false
false true

정수·실수 구분이 없어 5.0도 정수입니다. 문자열은 변환하지 않아 "5"는 false. isSafeInteger는 다음 절의 안전 범위까지 확인해요.

6. 큰 수: MAX_SAFE_INTEGER를 넘으면 값이 바뀐다

console.log(Number.MAX_SAFE_INTEGER);
const id = "9007199254740993";
console.log(parseInt(id));
console.log(Number(id));
console.log(parseInt("12345678901234567890"));
console.log(9007199254740992 === 9007199254740993);
9007199254740991
9007199254740992
9007199254740992
12345678901234567000
true

Number는 64비트 부동소수점이라 253 - 1까지만 정수를 정확히 표현합니다. 그 위로는 가까운 값으로 바뀌어 ...993이 ...992가 되고, 다른 두 수가 같다고 나와요. 긴 주문번호·ID를 parseInt하면 조용히 다른 값이 됩니다(0.1+0.2 소수 오차와는 다른 문제). 이럴 땐 BigInt를 씁니다.

const id = "9007199254740993";
const big = BigInt(id);
console.log(big);
console.log(big + 1n);
try { console.log(big + 1); } catch (e) { console.log(e.name + ": " + e.message); }
try { BigInt("12px"); } catch (e) { console.log(e.name + ": " + e.message); }
console.log(JSON.stringify({ id: String(big) }));
try { JSON.stringify({ id: big }); } catch (e) { console.log(e.name + ": " + e.message); }
9007199254740993n
9007199254740994n
TypeError: Cannot mix BigInt and other types, use explicit conversions
SyntaxError: Cannot convert 12px to a BigInt
{"id":"9007199254740993"}
TypeError: Do not know how to serialize a BigInt

BigInt는 숫자 뒤에 n을 붙이고, 일반 숫자와 섞으면 TypeError입니다. BigInt("12px")는 에러를 내고, JSON.stringify도 못 하니 보낼 땐 문자열로 바꿔요.

1번에서 말한 +와 Number()의 차이가 여기서 나옵니다. Number(10n)은 10이지만 +10n은 TypeError: Cannot convert a BigInt value to a number예요.

7. 실전: 빈 값·콤마 처리 함수

const price = "1,000원";
console.log(Number(price.replace(/[^0-9.-]/g, "")));
const toNum = (s) => {
  const t = String(s ?? "").trim();
  if (t === "") return null;
  const n = Number(t);
  return Number.isNaN(n) ? null : n;
};
for (const v of ["12", " 12 ", "", "12px", null, "1e3"]) console.log(JSON.stringify(v), "->", toNum(v));
1000
"12" -> 12
" 12 " -> 12
"" -> null
"12px" -> null
null -> null
"1e3" -> 1000

콤마·단위는 숫자 외 문자를 지운 뒤 Number로 바꿉니다. "비어 있음"과 "0"을 구분하려면 빈 값을 먼저 거르고, 실패는 Number.isNaN으로 확인해요.

정리

상황 쓰는 법 주의
문자열 전체가 숫자여야 함 Number(s) 또는 +s "", null은 0
"12px" 앞부분 정수 parseInt(s, 10) radix 항상 지정
숫자 소수점 버리기 Math.trunc(n) parseInt(5e-7)은 5
NaN인지 Number.isNaN(n) 전역 isNaN은 변환 후 검사
253 넘는 정수 BigInt(s) Number와 섞기·JSON 불가

참고: MDN Web Docs — Number() constructor, parseInt(), parseFloat(), Unary plus (+), isNaN(), Number.isNaN(), Number.isInteger(), Number.MAX_SAFE_INTEGER, BigInt / ECMAScript Language Specification 21.1 Number Objects, 19.2.5 parseInt

반응형

+ Recent posts