개발 이야기3분 읽기

진법 변환과 정수 파싱이 어긋나는 자리

parseInt('0b101')은 0이고 1 << 32는 1입니다. 언어마다 다른 파싱 규칙과 비트 폭의 경계를 정리했습니다.

16진수 문자열을 정수로 바꾸는 일은 함수 하나면 끝나 보입니다. 그 함수들이 접두어를 서로 다르게 취급하고, 언어마다 규칙이 달라서 값이 조용히 갈립니다.

JavaScript의 두 함수

parseIntNumber가 다르게 동작합니다.

입력 parseInt(x) Number(x)
'0x1f' 31 31
'0b101' 0 5
'010' 10 10
'1e3' 1 1000
'42abc' 42 NaN
'' NaN 0

parseInt('0b101')이 0인 이유는 0까지 읽고 b에서 멈추기 때문입니다. parseInt('1e3')이 1인 것도 같습니다 — 지수 표기를 모릅니다.

Number('')가 0인 것도 자주 걸립니다. 폼에서 빈 값이 들어오면 0으로 저장됩니다.

parseInt는 진법을 항상 명시합니다.

parseInt('08', 10)   // 8

옛 브라우저에서 0 접두어를 8진수로 읽던 동작은 사라졌지만, 두 번째 인자를 쓰는 습관은 남겨 두는 편이 안전합니다. 이유가 하나 더 있습니다.

['1', '2', '3'].map(parseInt)   // [1, NaN, NaN]

map이 두 번째 인자로 인덱스를 넘기고, 그것이 parseInt의 진법 자리에 들어갑니다. parseInt('2', 1)은 1진법이라 NaN, parseInt('3', 2)는 2진법에 3이 없어 NaN입니다. map(Number)map(s => parseInt(s, 10))을 씁니다.

파이썬은 접두어를 요구한다

int(x, 0)은 접두어로 진법을 판단합니다. int(x)는 10진수만 받습니다.

입력 int(x, 0) int(x)
'0x1f' 31 ValueError
'0b101' 5 ValueError
'010' ValueError 10
' 42 ' 42 42
'42abc' ValueError ValueError

int('010', 0)이 오류인 것이 특징입니다. 파이썬 3에서 0 접두어는 8진수가 아니고, 그렇다고 10진수로도 받지 않습니다. 8진수는 0o10으로 적어야 합니다.

JavaScript와 정반대의 성향입니다. JavaScript는 읽을 수 있는 데까지 읽고 넘어가고, 파이썬은 형식이 어긋나면 예외를 냅니다. 사용자 입력을 다룰 때는 예외를 내는 쪽이 다루기 쉽습니다.

같은 값의 여러 표기

십진 255
이진 11111111    0b11111111
팔진 377         0o377
십육진 ff        0xff

폭 맞춤 : 11111111  0x00ff
36진법  : 73

36진법은 0-9a-z를 다 씁니다. 짧은 식별자를 만들 때 쓰기도 하지만 대소문자 구분이 없어 혼동되기 쉽습니다.

JavaScript의 비트 연산은 32비트다

숫자는 64비트 부동소수점인데, 비트 연산자를 쓰는 순간 32비트 정수로 변환됩니다.

1 << 31      // -2147483648   최상위 비트가 부호가 된다
1 << 32      // 1             시프트 양이 32로 나눈 나머지가 된다
2**31 | 0    // -2147483648
-1 >>> 0     // 4294967295    >>> 만 부호 없는 결과를 낸다

1 << 32가 1인 것은 시프트 양에 & 31이 적용되기 때문입니다. 32비트를 넘는 비트마스크를 만들려면 BigInt를 써야 합니다.

정수 정밀도 한계도 별개로 있습니다.

Number.MAX_SAFE_INTEGER      // 9007199254740991
2**53 === 2**53 + 1          // true

음수와 2의 보수

언어마다 음수의 이진 표기를 다르게 보여 줍니다.

파이썬  bin(-1)    -0b1              부호를 따로 붙인다
        -1 & 0xff  0b11111111        폭을 정해야 2의 보수가 나온다
        -128 & 0xffffffff  0xffffff80

JS      (-255).toString(2)   "-11111111"
        BigInt.asIntN(8, 255n)   -1n     8비트로 해석
        BigInt.asUintN(8, -1n)   255n

파이썬 정수는 폭이 없어 bin(-1)-0b1입니다. 하드웨어의 8비트 11111111을 보고 싶으면 & 0xff로 폭을 지정해야 합니다.

프로토콜 문서에 int32, uint16 같은 타입이 적혀 있으면 그 폭으로 마스킹한 값을 봐야 합니다. 폭을 지정하지 않은 채 음수를 보면 다른 값처럼 보입니다.

색과 IP 주소

진법 변환이 실무에서 자주 나오는 두 자리입니다.

색상   #4f46e5  →  R 79, G 70, B 229
IPv4   192.168.1.1  →  0xC0A80101  →  3232235777

IP를 정수로 저장할 때 부호 있는 32비트에 넣으면 128.0.0.0 이상이 음수가 됩니다. 부호 없는 타입이나 64비트를 씁니다.

확인 순서

  1. parseInt에 두 번째 인자가 있는지 봅니다. 없으면 넣습니다
  2. map(parseInt)를 검색합니다
  3. 빈 문자열이 0으로 들어가는 경로가 있는지 봅니다
  4. 비트 연산이 32비트를 넘는지 확인합니다. 넘으면 BigInt입니다
  5. 음수를 이진수로 확인할 때는 폭을 먼저 정합니다. 8비트인지 32비트인지에 따라 표기가 다릅니다

tools.onuel.dev진법 변환기에 값을 넣으면 2·8·10·16진수를 한 번에 보여 주고, JavaScript 숫자 범위를 넘는 큰 값도 처리합니다. 바이트 단위로 대조해야 하면 Hex 인코딩을, 비트 패턴을 눈으로 볼 때는 텍스트 ↔ 2진수 변환을 함께 쓰면 됩니다.

  • #진법
  • #정수
  • #JavaScript