이진수 변환

Binary
10 1010
Octal
52
Decimal
42
Hexadecimal
2a

2의 보수

8-bit
0010 1010
16-bit
0000 0000 0010 1010
32-bit
0000 0000 0000 0000 0000 0000 0010 1010
64-bit
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 1010

입력한 내용은 저장, 기록, 공유되지 않습니다.

숫자를 이진수, 팔진수, 십진수, 십육진수 사이에서 변환하고 텍스트와 이진수도 서로 바꿀 수 있습니다. 64비트를 훨씬 넘어서도 값이 정확합니다.

사용 방법

  1. 입력할 진법을 고르고 값을 입력합니다.
  2. 다른 모든 진법이 함께 표시되고 아래에 2의 보수가 나옵니다.
  3. 텍스트 모드로 바꾸면 글자와 이진수를 서로 변환할 수 있습니다.

계산이 정확한 이유

여기서의 모든 변환은 크기 제한도 반올림도 없는 BigInt로 이루어집니다. 구현상의 각주처럼 들리지만, 사실 이것이 웹의 대부분의 변환기와 이 도구를 가르는 가장 큰 차이입니다.

JavaScript 숫자는 IEEE 754 배정밀도 부동소수점 수이며, 정수를 정확히 나타낼 수 있는 것은 2의 53제곱에서 1을 뺀 9,007,199,254,740,991까지입니다. 뻔한 방식으로 작성한 변환기는 그 아래에서는 맞고, 그 위에서는 조용히 틀립니다. 이는 나쁜 종류의 실패입니다. 오류도 경고도 없이, 길이와 모양은 맞지만 정답은 아닌 결과가 나옵니다.

이 한계는 수시로 부딪칠 만큼 낮습니다. 64비트 전체 값은 한참 위에 있고, Snowflake 생성기의 데이터베이스 ID, 나노초 타임스탬프, 하드웨어 레지스터 덤프, 해시, 64비트 메모리 주소에서 읽은 값도 모두 그렇습니다. 굳이 변환해 볼 만큼 큰 숫자라면, 바로 그 지점에서 일반 변환기는 믿을 수 없게 됩니다.

2의 보수

네 가지 진법 아래에는 값을 부호 있는 8, 16, 32, 64비트 정수로 저장했을 때의 모습을 보여 줍니다. 이는 2진수 앞에 마이너스 기호를 붙이는 것과 다르며, 메모리를 읽을 때 사람들이 걸려 넘어지는 부분이 바로 그 차이입니다.

2의 보수에서 음수는 절댓값의 모든 비트를 뒤집고 1을 더해 만듭니다. 8비트에서 -1은 11111111, -128은 10000000입니다. 따로 떼어 낼 부호 비트도, 음수 0도 없고, 부호를 특별 취급하지 않아도 덧셈이 그대로 동작합니다. 주요 프로세서가 모두 이 방식으로 정수를 저장하는 이유입니다.

범위가 비대칭인 이유도 여기서 설명됩니다. 8비트가 담는 범위는 -128부터 128이 아니라 -128부터 127인데, 0이 양수 쪽 자리 하나를 차지하기 때문입니다. 값이 들어가지 않는 비트 폭이라면 조용히 잘라 내지 않고 그렇다고 알려 줍니다. 잘린 결과는 완벽하게 유효해 보이지만 그렇지 않기 때문입니다.

출력 읽는 법

긴 값은 한눈에 읽을 수 있도록 묶어서 보여 줍니다. 2진수는 8자리(1바이트)씩, 16진수는 두 자리씩, 10진수는 세 자리씩입니다. 묶음은 화면 표시용일 뿐이며 복사할 때는 제거됩니다.

입력도 같은 방향으로 일부러 너그럽게 받습니다. 0x, 0b, 0o 접두사는 받아들이고 무시하며, 구분자로 쓰인 밑줄, 공백, 쉼표도 마찬가지입니다. 그래서 소스 코드나 16진수 덤프에서 그대로 붙여 넣은 리터럴도 미리 정리하지 않고 쓸 수 있습니다.

텍스트를 2진수로

텍스트 모드는 텍스트를 UTF-8로 인코딩한 뒤 각 바이트를 8비트로 씁니다. 반대 방향에서는 비트를 바이트로 다시 읽어 UTF-8로 디코딩합니다.

다른 방법, 즉 각 문자의 코드 유닛을 그대로 쓰는 방식은 대부분의 텍스트-2진수 변환기가 쓰는 방식인데, 두 곳에서 깨집니다. U+00FF를 넘는 문자는 8비트를 넘어 더 이상 바이트 단위로 맞지 않습니다. 더 나쁜 것은, 대부분의 이모지를 포함한 기본 다국어 평면(BMP) 밖의 문자가 JavaScript에서 두 개의 서로게이트 코드 유닛으로 저장된다는 점입니다. 이를 하나씩 따로 다루면 의미 없는 반쪽 두 개가 되어 받는 쪽에서 아무것도 디코딩되지 않습니다. UTF-8을 거치면 두 문제를 모두 피할 수 있으며, 그래서 이모지가 여기서는 왕복되고 다른 곳에서는 안 되는 경우가 많습니다. 한글도 여기서는 문제없이 왕복됩니다.

하나의 수를 네 가지로 적기

2,025를 봅시다. 2진수로는 11111101001, 8진수로는 3751, 16진수로는 7E9입니다. 양은 하나도 달라지지 않았고, 그것을 적는 데 쓴 진법만 달라졌습니다. 어떤 진법이든 각 자리는 자릿값입니다. 7E9는 7×256 더하기 14×16 더하기 9이고, 11111101001은 비트가 켜진 2의 거듭제곱들의 합입니다.

16진수가 자리를 잡은 이유는 16이 2의 네제곱이라 한 자리가 정확히 4비트이고 대응이 외우기 쉽기 때문입니다. F는 1111, 8은 1000, A는 1010입니다. 메모리 덤프, 색상 값, 해시, MAC 주소가 10진수가 아니라 16진수로 쓰이는 이유가 이것입니다. 계산하지 않고도 자릿수에서 비트를 바로 읽을 수 있으니까요.

8진수는 거의 한 곳, 유닉스 파일 권한에만 남아 있습니다. 이유는 같습니다. 한 자리가 3비트여서 읽기, 쓰기, 실행에 딱 맞아떨어지므로 755는 rwxr-xr-x이고 찾아볼 것이 없습니다.

비트 연산, 그리고 마스크가 하는 일

값을 2진수로 보면 비트 연산자의 뜻은 분명해집니다. AND는 양쪽 모두에서 켜진 비트만 남기므로 플래그를 검사하는 방법이 됩니다. value AND 0b0100이 0이 아니면 정확히 세 번째 비트가 켜져 있다는 뜻입니다. OR는 비트를 켜고, XOR는 뒤집고, NOT은 전부 반전하며, 왼쪽이나 오른쪽 시프트는 2의 거듭제곱으로 곱하거나 나눕니다.

플래그를 따로 떨어진 불리언이 아니라 한 비트씩 저장하는 이유가 여기 있습니다. 권한 필드, 기능 토글 묶음, 네트워크 패킷의 옵션이 정수 하나에 들어가고, 하나를 확인하는 일은 명령 하나면 됩니다. AND에 쓰는 상수를 마스크라고 부르며, 이를 2진수나 16진수로 적으면 10진수로는 결코 드러나지 않는 의도가 눈에 보입니다.

시프트에는 알아 둘 함정이 하나 있습니다. 대부분의 언어에서 부호 있는 값의 오른쪽 시프트는 부호 비트를 유지하므로, -8을 오른쪽으로 시프트하면 큰 양수가 아니라 -4가 됩니다. 두 가지를 모두 제공하는 언어는 다른 기호를 쓰며, JavaScript의 >>> 연산자가 그 예입니다.

수가 메모리에 놓이는 방식

7E9라고 적힌 값은 그것이 어떻게 저장되는지 말해 주지 않습니다. 폭이 중요합니다. 같은 수라도 타입에 따라 1, 2, 4, 8바이트를 차지하고, 폭에 담기지 않는 값은 커지는 대신 잘리거나 한 바퀴 돕니다.

바이트 순서도 중요합니다. 여러분이 만나는 거의 모든 기계인 리틀엔디언은 최하위 바이트를 먼저 저장하므로, 32비트 값 0x000007E9는 16진 덤프에서 E9 07 00 00으로 보입니다. 빅엔디언은 그 반대이며 네트워크 프로토콜이 규정한 순서라 네트워크 바이트 순서라고 부르고, 모든 소켓 경계마다 변환 함수가 존재하는 이유이기도 합니다. 어느 쪽을 보고 있는지 모른 채 덤프를 읽으면 자신만만한 헛소리가 나옵니다.

같은 폭의 부호 없는 타입과 부호 있는 타입은 같은 비트를 다르게 해석합니다. 0xFF는 부호 없는 바이트로는 255이고 부호 있는 바이트로는 -1입니다. 어느 쪽이 맞는지는 바이트 어디에도 적혀 있지 않고, 오직 타입만이 정합니다.

부동소수점은 완전히 다른 체계다

위의 이야기는 모두 정수에 관한 것입니다. double은 부호 비트 하나, 11비트 지수, 52비트 가수로 저장되며, 수를 분수 곱하기 2의 거듭제곱으로 나타냅니다. 그런 형태로 정확히 표현되지 않는 값은 표현 가능한 가장 가까운 값으로 반올림됩니다.

0.1 더하기 0.2가 0.3이 아니라는 유명한 결과의 설명은 이것이 전부입니다. 10분의 1은 2진수에서 순환 소수이고, 3분의 1이 10진수에서 그런 것과 같습니다. 그래서 살짝 어긋난 값으로 저장되고, 살짝 어긋난 두 값을 더하면 살짝 어긋난 값이 나옵니다. 계산은 옳습니다. 틀린 것은 10진 소수가 2진 저장을 무사히 통과하리라는 기대입니다.

여기서 실무 규칙이 따라옵니다. 돈은 부동소수점으로 저장하지 말고 정수 단위나 십진 타입을 쓸 것. 부동소수점은 같음이 아니라 허용 오차로 비교할 것. 그리고 double에서는 2^53을 넘는 정수가 정밀도를 잃는다는 사실을 기억할 것. 이 변환기가 임의 정밀도 연산을 쓰는 이유가 그것입니다.

이 진법들을 실제로 만나는 곳

16진수는 바이트를 사람에게 보여 주는 곳이면 어디든 나타납니다. CSS의 색은 3바이트의 16진수, MAC 주소는 6바이트, SHA-256 다이제스트는 32바이트로 64자입니다. 메모리 주소, 운영 체제와 하드웨어의 오류 코드도 모두 16진수인데, 비트 경계가 자릿수와 맞아떨어지기 때문입니다.

2진수는 개별 비트가 의미를 갖는 곳에 등장합니다. 넷마스크, 하드웨어 레지스터, 권한 비트, 기능 플래그, 그리고 QR 코드나 UTF-8 같은 형식의 인코딩 표가 그렇습니다. UTF-8을 읽는 것이 좋은 예입니다. 0으로 시작하는 바이트는 ASCII, 110으로 시작하면 2바이트 시퀀스의 시작, 10으로 시작하면 이어지는 바이트입니다. 이 구조는 10진수에서는 보이지 않고 2진수에서는 뻔히 드러납니다.

8진수는 대체로 파일 권한과 몇몇 오래된 형식으로 물러났지만, 기억해 둘 함정이 하나 남아 있습니다. C, ES5 이전의 JavaScript, 그리고 여러 설정 형식에서 0으로 시작하는 숫자는 8진수이므로 0755는 칠백오십오가 아닙니다. 요즘 JavaScript는 명시적인 0o 접두사를 쓰고, YAML 1.2는 앞자리 0 규칙을 아예 없앴는데, 이는 파일 모드가 10진수로 읽히는 또 다른 놀라움의 원인이 됩니다.

자주 묻는 질문

아주 큰 수도 정확한가요?
네. 모든 연산에 BigInt를 사용하므로 자릿수와 무관하게 정확합니다. 일반 JavaScript 숫자로 만든 변환기는 2^53(약 9천조)을 넘으면 정밀도를 잃고 그럴듯하지만 틀린 답을 내놓습니다. 완전한 64비트 값은 이미 그 한계를 넘기 때문에 생각보다 자주 문제가 됩니다.
2의 보수는 무엇이고 왜 보여주나요?
부호 있는 정수가 메모리에 실제로 저장되는 형식입니다. 8비트 2의 보수에서 -1은 부호 비트에 1이 아니라 11111111입니다. 레지스터 덤프를 읽거나 프로토콜을 디버깅할 때 필요한 것이 이 저장 형식입니다. 값이 들어가지 않는 비트 폭은 조용히 잘라내지 않고 그렇다고 표시합니다.
0x나 0b 접두사가 붙은 값도 붙여넣을 수 있나요?
네. 이런 접두사는 인식해 무시하며 자릿수 구분용 밑줄, 공백, 쉼표도 마찬가지라 소스 코드에서 그대로 붙여넣을 수 있습니다.
텍스트는 어떻게 이진수로 변환되나요?
먼저 UTF-8로 인코딩한 뒤 각 바이트를 8비트로 적습니다. 그래서 한국어, 일본어, 중국어, 이모지가 정확히 왕복됩니다. 문자 코드를 직접 읽는 도구는 ASCII 밖에서 깨지고 이모지 하나가 손상된 두 조각이 됩니다.

마지막 업데이트:

문제 신고

진행 단계, 예상 결과, 실제 결과를 적어 주세요. 비밀 정보와 개인 정보는 삭제해 주세요.

관련 도구