개발자를 위한 진법 변환 가이드: 2진수·16진수·8진수·10진수
어느 오후, 디버거에서 0x7FFF5FBFF8C0을 들여다보다가 CSS 파일을 열어 #FF5733을 매만지고, 이어서 터미널에서 chmod 755를 입력합니다. 세 가지 서로 다른 수 표현이지만 바탕에 깔린 산술은 같습니다. 16진수와 2진수 사이의 암산이 매번 걸리거나, 유닉스 권한이 왜 8진수를 쓰는지 궁금했다면, 여기서 그 연결 고리를 정리합니다.
일상적인 프로그래밍에서 마주치는 네 가지 진법, 외워 둘 만한 세 가지 변환 방법, 자바스크립트·파이썬·Go·C 코드를 차례로 짚어 봅니다. 이론을 건너뛰고 바로 변환하려면 진법 변환기를 여세요. 2~36진법을 임의 정밀도로 처리합니다.
모든 개발자가 쓰는 네 가지 진법
각 진법은 역사적 우연이 아니라 실용적인 이유로 프로그래밍에 쓰입니다.
2진수 (Base 2) — 기계의 언어
두 개의 숫자 0과 1만 사용합니다. 트랜지스터는 켜짐 아니면 꺼짐이고, 이 물리적 제약이 곧 2진수를 만듭니다. 비트마스크, 기능 플래그, 비트 연산, IP 서브넷 계산을 다룰 때 직접 마주칩니다.
2진수는 금방 길어집니다. 10진수 255는 2진수로 11111111이어서 세 자리가 여덟 자리로 늘어납니다. 개별 비트 위치가 중요한 경우가 아니라면 프로그래머가 원시 2진수를 직접 쓸 일이 드문 이유입니다.
8진수 (Base 8) — 유닉스의 축약 표기
0부터 7까지 여덟 개의 숫자를 씁니다. 8진수 한 자리는 정확히 2진수 3비트에 대응하며, 이 때문에 유닉스 파일 권한이 8진수를 씁니다. chmod 755는 세 개씩 묶은 권한 비트 세 그룹을 읽기 좋은 세 자리에 압축한 것입니다.
8진수는 PDP-11 시대에 더 큰 비중을 차지했습니다. 당시 기계 워드가 3비트 그룹으로 깔끔하게 나뉘었기 때문입니다. 오늘날에는 유닉스 권한에 주로 쓰이고, 가끔 0으로 시작하는 C 리터럴에도 등장합니다(0177을 10진수 177로 착각한 오프바이원 버그가 한두 건이 아닙니다).
10진수 (Base 10) — 사람의 기본값
0부터 9까지 열 개의 숫자를 씁니다. 사람의 머리가 기본으로 쓰는 체계입니다. 포트 번호, 배열 인덱스, HTTP 상태 코드, 픽셀 크기가 모두 10진수입니다. 컴퓨터는 10진수로 생각하지 않지만 사람은 그렇게 생각하므로, 사용자에게 노출되는 숫자는 10진수로 나옵니다.
16진수 (Base 16) — 바이트 단위 작업의 공용어
0-9와 A-F, 열여섯 개의 기호를 씁니다. 16진수 한 자리가 정확히 2진수 4비트(니블)를 표현해 2진 데이터를 가장 간결하게 나타냅니다. 16진수 두 자리는 언제나 1바이트입니다.
메모리 주소(0x7FFF5FBFF8C0), CSS 색상(#FF5733), MAC 주소(00:1A:2B:3C:4D:5E), UUID 포매팅, 해시 다이제스트에서 16진수를 만납니다.
네 체계 사이에서 값을 확인해 보려면 진법 변환기에 아무 진법으로 값을 입력하세요. 다른 진법 값이 즉시 갱신됩니다.
진법 변환의 원리: 세 가지 핵심 방법
진법 변환에 도구가 꼭 필요한 건 아닙니다(물론 쓰면 더 빠릅니다). 다음 세 가지 방법이 모든 경우를 덮습니다.
방법 1 — 자릿값 전개 (임의 진법 → 10진수)
모든 자릿값 체계는 같은 방식으로 동작합니다. 각 자리의 숫자에 밑(base)의 거듭제곱을 곱하고 더합니다. 지수는 오른쪽 끝에서 0부터 시작합니다.
2진수 1011:
1×2³ + 0×2² + 1×2¹ + 1×2⁰
= 8 + 0 + 2 + 1
= 11
16진수 FF:
15×16¹ + 15×16⁰
= 240 + 15
= 255
대부분의 언어는 파싱 함수로 이를 처리합니다.
parseInt('1011', 2) // 11
parseInt('FF', 16) // 255
parseInt('755', 8) // 493
방법 2 — 반복 나눗셈 (10진수 → 임의 진법)
반대 방향으로 가려면 10진수를 대상 진법으로 반복해서 나누고, 나머지를 모읍니다. 나머지는 아래에서 위로 읽습니다.
10진수 255 → 16진수:
255 ÷ 16 = 15 나머지 15 (F)
15 ÷ 16 = 0 나머지 15 (F)
→ 위로 읽기: FF
10진수 42 → 2진수:
42 ÷ 2 = 21 나머지 0
21 ÷ 2 = 10 나머지 1
10 ÷ 2 = 5 나머지 0
5 ÷ 2 = 2 나머지 1
2 ÷ 2 = 1 나머지 0
1 ÷ 2 = 0 나머지 1
→ 위로 읽기: 101010
bin(42) # '0b101010'
hex(255) # '0xff'
oct(493) # '0o755'
방법 3 — 비트 그룹핑 (2진수 ↔ 16진수/8진수 직접 변환)
숙련된 개발자가 가장 자주 쓰는 방법입니다. 16 = 2⁴, 8 = 2³이므로 산술 없이 비트를 그룹으로 묶기만 해도 2진수와 16진수(또는 8진수) 사이를 오갈 수 있습니다.
2진수 → 16진수: 오른쪽부터 니블(4비트)로 묶습니다. 필요하면 왼쪽 그룹을 0으로 채웁니다.
2진수: 1010 1111
16진수: A F
→ AF
2진수 → 8진수: 오른쪽부터 3비트씩 묶습니다.
2진수: 111 101 101
8진수: 7 5 5
→ 755
니블 대응표는 외워 둘 만한 가치가 있습니다.
| 2진수 | 16진수 | 2진수 | 16진수 |
|---|---|---|---|
0000 | 0 | 1000 | 8 |
0001 | 1 | 1001 | 9 |
0010 | 2 | 1010 | A |
0011 | 3 | 1011 | B |
0100 | 4 | 1100 | C |
0101 | 5 | 1101 | D |
0110 | 6 | 1110 | E |
0111 | 7 | 1111 | F |
이 표가 손에 익으면 2진수에서 16진수로의 변환은 한눈에 읽어 내는 작업이 됩니다.
언어별 진법 변환
네 가지 주요 언어에서 진법 변환을 어떻게 처리하는지 정리했습니다.
자바스크립트 / 타입스크립트
// 파싱: 임의 진법 문자열 → 숫자
parseInt('FF', 16) // 255
parseInt('101010', 2) // 42
parseInt('755', 8) // 493
// 포매팅: 숫자 → 임의 진법 문자열
(255).toString(16) // 'ff'
(42).toString(2) // '101010'
(493).toString(8) // '755'
// 리터럴
const bin = 0b11111111; // 255
const oct = 0o377; // 255
const hex = 0xff; // 255
// 2⁵³을 넘어가는 값에는 BigInt 사용
const big = BigInt('0xFFFFFFFFFFFFFFFF');
big.toString(2) // 1이 64개
big.toString(10) // '18446744073709551615'
파이썬
# 10진수 → 다른 진법 (접두사가 붙은 문자열 반환)
bin(255) # '0b11111111'
oct(493) # '0o755'
hex(255) # '0xff'
# 다른 진법 → 10진수
int('11111111', 2) # 255
int('FF', 16) # 255
int('755', 8) # 493
# 패딩이 적용된 포매팅 출력
f'{255:08b}' # '11111111' (8자리 2진수, 0으로 패딩)
f'{255:02x}' # 'ff' (2자리 16진수, 소문자)
f'{255:02X}' # 'FF' (2자리 16진수, 대문자)
# 파이썬 정수는 기본적으로 임의 정밀도
big = int('F' * 64, 16) # 256비트 숫자, 오버플로 없음
Go
package main
import (
"fmt"
"strconv"
)
func main() {
// 포매팅: int → 임의 진법 문자열
fmt.Println(strconv.FormatInt(255, 16)) // "ff"
fmt.Println(strconv.FormatInt(255, 2)) // "11111111"
fmt.Println(strconv.FormatInt(493, 8)) // "755"
// 파싱: 임의 진법 문자열 → int
n, _ := strconv.ParseInt("FF", 16, 64) // 255
fmt.Println(n)
// Printf 동사(verb)
fmt.Printf("%b %o %x %d\n", 255, 255, 255, 255)
// 11111111 377 ff 255
}
C
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main() {
// 10진수, 8진수, 16진수 출력
printf("%d %o %x\n", 255, 255, 255);
// 255 377 ff
// 임의 진법 파싱
long val = strtol("FF", NULL, 16); // 255
long bin = strtol("101010", NULL, 2); // 42
// 2진수 printf는 기본 제공되지 않음 — 수동 추출:
uint8_t byte = 0xAF;
for (int i = 7; i >= 0; i--)
putchar(((byte >> i) & 1) ? '1' : '0');
// 10101111
return 0;
}
실무에서 만나는 진법 변환
진법 변환이 책 속 이론이 아니라 업무가 되는 다섯 가지 상황입니다.
1. 메모리 주소 디버깅
디버거에 포인터 값이 0x7FFF5FBFF8C0으로 찍혀 있다고 합시다. 끝 두 바이트를 2진수로 변환하면 1000 1100 0000이 나오고, 뒤쪽 0이 여섯 개라는 점에서 이 주소가 64바이트 경계에 정렬되어 있음이 드러납니다. 정렬은 캐시 성능, SIMD 연산, 메모리 매핑 I/O에 영향을 줍니다. 16진수 표기는 이런 패턴을 한눈에 보여 줍니다.
포인터 산술도 16진수로 따지는 편이 이해하기 쉽습니다. 기준 주소에서 0x100 떨어진 위치는 정확히 256바이트이고, 많은 임베디드 시스템에서 한 페이지에 해당합니다. 10진수로 보면 이 관계가 잘 드러나지 않습니다.
2. CSS 16진수 색상 ↔ RGB
색상 #FF5733은 16진수 쌍으로 묶인 세 바이트입니다.
| 쌍 | 16진수 | 10진수 | 채널 |
|---|---|---|---|
FF | FF | 255 | 빨강 (최대) |
57 | 57 | 87 | 초록 |
33 | 33 | 51 | 파랑 |
#F00 같은 축약 표기는 #FF0000으로 확장되어 순수한 빨강이 됩니다. 8자리 형태인 #FF573380은 알파 채널을 더한 것으로, 80(10진수 128)은 약 50% 불투명도입니다.
16진수와 10진수의 대응 관계를 알아 두면 채널 값을 일정 수치만큼 올리거나 내릴 때 컬러 피커를 덜 쓰게 됩니다.
3. 유닉스 파일 권한 (8진수)
chmod 755는 다음과 같이 나뉩니다.
7 → 111 → rwx (소유자: 읽기 + 쓰기 + 실행)
5 → 101 → r-x (그룹: 읽기 + 실행)
5 → 101 → r-x (기타: 읽기 + 실행)
권한 비트 세 개(읽기=4, 쓰기=2, 실행=1)가 8진수 한 자리(0-7)에 대응하므로 8진수 한 자리가 정확히 한 권한 그룹을 표현합니다. 자주 쓰이는 패턴입니다.
| 8진수 | 2진수 | 권한 | 일반적인 용도 |
|---|---|---|---|
755 | 111 101 101 | rwxr-xr-x | 실행 파일, 디렉터리 |
644 | 110 100 100 | rw-r--r-- | 일반 파일 |
700 | 111 000 000 | rwx------ | 비공개 스크립트 |
600 | 110 000 000 | rw------- | SSH 키, 비밀값 |
4. 네트워크 서브넷 계산
/24 서브넷 마스크는 2진수에서 앞쪽에 1이 24개 이어진다는 뜻입니다.
11111111.11111111.11111111.00000000
→ 255.255.255.0
네트워크 주소를 구하려면 IP와 마스크를 2진수로 AND 연산합니다.
192.168.1.37 → 11000000.10101000.00000001.00100101
255.255.255.0 → 11111111.11111111.11111111.00000000
AND 결과 → 11000000.10101000.00000001.00000000
→ 192.168.1.0 (네트워크 주소)
네트워크 엔지니어는 일상적으로 10진수(IP 표기), 2진수(서브넷 계산), 16진수(패킷 캡처) 사이를 오갑니다. 눈으로 확인하고 싶을 땐 진법 변환기에 옥텟 단위로 값을 넣어 보세요.
5. 해시 다이제스트와 UUID 읽기
d41d8cd98f00b204e9800998ecf8427e 같은 MD5 해시는 16진수 32자이며, 16바이트(128비트)를 표현합니다. 16진수 두 자리가 1바이트에 해당합니다.
UUID는 8-4-4-4-12 형태의 16진수 패턴을 따릅니다.
550e8400-e29b-41d4-a716-446655440000
하이픈으로 구분된 16진수 32자로, 같은 128비트를 다른 형태로 나타낸 것입니다. 버전 니블은 13번째 자리에 있으며 41d4의 4가 UUID v4임을 나타냅니다.
해시는 MD5·SHA 해시 생성기에서, UUID는 UUID 생성기에서 만들 수 있습니다. 두 도구 모두 여기서 다룬 2진 표현에 그대로 대응하는 16진수 출력을 내보냅니다.
16진수를 넘어서: Base 36, Base 64, 그리고 커스텀 진법
표준 네 진법이 대부분의 작업을 덮지만, 특수한 맥락에서 등장하는 것도 있습니다.
Base 36 — 간결한 영숫자 인코딩
Base 36은 10개의 숫자와 26개의 문자(A-Z)를 모두 써서 대소문자 구분 없는 영숫자 표현 중 가장 간결한 형태를 만듭니다. URL 단축 서비스, 유튜브 영상 ID, 단축 링크, 간결한 DB 키에 자주 쓰입니다.
(1000000).toString(36) // 'lfls'
parseInt('lfls', 36) // 1000000
10진수 1,000,000이 네 글자로 줄어듭니다. URL에 안전한 짧은 식별자로는 이보다 더 줄이기 어렵습니다.
Base 64 — 데이터 인코딩 (진법이 아님)
Base64는 또 다른 진법처럼 보이지만 목적이 다릅니다. 위치 기반 체계로 수를 표현하는 대신 임의의 2진 데이터(이미지, 파일, JWT 토큰)를 ASCII 텍스트로 인코딩합니다. A-Z, a-z, 0-9, +, / 총 64개의 기호를 씁니다.
계산 방식도 진법 변환과 다릅니다. Base64는 입력을 3바이트(24비트) 블록 단위로 처리해 6비트 문자 4개를 출력합니다. 인코딩 방식이지 수 체계가 아닙니다.
Base64 데이터 인코딩·디코딩에는 Base64 인코더/디코더를 쓰세요.
임의 진법 (2-36)과 등장 사례
그 밖에도 야생에서 가끔 만나는 진법이 있습니다.
- Base 12 (십이진법): 시간(12시간제), 수량(한 다스). 일부 수학자들은 12가 더 많은 약수를 가지므로 일상용으로는 10진수보다 12진수가 낫다고 주장합니다.
- Base 60 (육십진법): 시간(60초, 60분)과 각도(360°). 바빌로니아 수학에서 이어져 내려왔습니다.
- Base 32: 사람이 읽기 쉬운 식별자를 위한 Crockford Base32 인코딩(I, L, O처럼 혼동하기 쉬운 문자를 제외). 지오해싱에서도 쓰입니다.
진법 변환기는 2부터 36까지 모든 정수 진법을 지원합니다.
비트 연산과 진법 변환
2진수 이해는 숫자 변환을 넘어 시스템 프로그래밍, 게임 개발, 권한 시스템의 비트 연산으로 이어집니다.
const READ = 0b100; // 4
const WRITE = 0b010; // 2
const EXEC = 0b001; // 1
// OR로 권한 결합
const perms = READ | WRITE; // 0b110 = 6
// AND로 특정 권한 확인
(perms & READ) !== 0 // true — 읽기 보유
(perms & EXEC) !== 0 // false — 실행 없음
// XOR로 권한 토글
perms ^ WRITE // 0b100 = 4 — 쓰기 제거
// 시프트 연산
1 << 3 // 0b1000 = 8 (1을 왼쪽으로 3비트 시프트)
0xFF >> 4 // 0b00001111 = 15 (오른쪽 4비트 시프트 = 16으로 나눔)
기능 플래그, 하드웨어 레지스터, 네트워크 프로토콜, 그래픽스 프로그래밍은 모두 비트 연산에 기댑니다. 2진수가 손에 익으면 비트 조작은 평범한 논리처럼 읽힙니다.
자주 묻는 질문
프로그래밍에서 쓰이는 주요 네 가지 진법은 무엇인가요?
2진수(base 2), 8진수(base 8), 10진수(base 10), 16진수(base 16)입니다. 2진수는 하드웨어에서 데이터가 물리적으로 존재하는 형태입니다. 8진수는 유닉스 파일 권한에 대응합니다. 10진수는 사람을 향한 기본값입니다. 16진수는 2진수를 읽기 좋게 압축한 형태이며, 16진수 한 자리는 정확히 4비트입니다.
2진수를 16진수로 어떻게 변환하나요?
2진수 자리를 오른쪽부터 네 개씩 묶고, 필요하면 왼쪽 그룹을 0으로 채웁니다. 각 그룹을 대응시키면 됩니다. 0000=0, 0001=1, …, 1010=A, …, 1111=F. 예: 2진수 10101111 → 그룹 1010 1111 → 16진수 AF. 16 = 2⁴이기 때문에 이 방법이 성립합니다.
16진수를 10진수로 어떻게 변환하나요?
각 16진수 자리에 16의 거듭제곱(오른쪽 끝이 0)을 곱한 뒤 더합니다. A=10, B=11, C=12, D=13, E=14, F=15. 예: 16진수 FF = 15×16¹ + 15×16⁰ = 240 + 15 = 255. 코드로는 자바스크립트 parseInt('FF', 16), 파이썬 int('FF', 16)입니다.
프로그래머는 왜 2진수 대신 16진수를 쓰나요?
간결해서입니다. 16진수 한 자리가 정확히 4비트에 대응해, 2진수 11111111 00001010은 16진수로 FF0A가 되어 훨씬 짧고 읽기 쉽습니다. 16진수는 메모리 주소, CSS 색상(#FF5733), MAC 주소, 해시 출력, UUID 포매팅의 표준 표기입니다.
니블이란 무엇이고, 왜 16진수 변환에 중요한가요?
니블은 2진수 4비트, 즉 반 바이트입니다. 니블 하나가 정확히 16진수 한 자리에 대응해, 2진수에서 16진수로의 변환은 니블 단위 조회로 끝납니다. 1바이트 = 2니블 = 16진수 두 자리. 이 4비트 대응 덕분에 16진수가 바이트 단위 데이터 표기의 표준이 됐습니다.
유닉스 파일 권한은 왜 8진수로 쓰나요?
그룹마다 권한 비트가 세 개(읽기=4, 쓰기=2, 실행=1), 그룹은 세 개(소유자, 그룹, 기타)입니다. 2³ = 8이므로 3비트 그룹 하나가 정확히 8진수 한 자리에 대응합니다. 755는 소유자=7(rwx), 그룹=5(r-x), 기타=5(r-x). 8진수는 3비트 그룹핑에 가장 자연스러운 진법입니다.
자바스크립트에서 2⁵³보다 큰 숫자를 어떻게 다루나요?
BigInt를 씁니다. 리터럴 뒤에 n을 붙이거나 BigInt() 생성자를 쓰세요. BigInt('0xFFFFFFFFFFFFFFFF').toString(2)는 64비트 2진 문자열 전체를 반환합니다. 표준 Number는 9,007,199,254,740,991(2⁵³ - 1)을 넘으면 정밀도가 손실됩니다. 진법 변환기는 내부적으로 BigInt를 써서 정밀도 손실 없이 임의 크기 숫자를 처리합니다.
16진수를 2진수로 어떻게 변환하나요?
같은 니블 대응표를 역방향으로 적용해, 16진수 한 자리를 4비트 2진수로 치환하면 됩니다. 0=0000, 1=0001, …, A=1010, …, F=1111. 예: 16진수 AF → A(1010) + F(1111) → 2진수 10101111. 코드로는 자바스크립트 parseInt('AF', 16).toString(2) 또는 파이썬 bin(int('AF', 16))[2:]입니다.
CSS 16진수 색상은 왜 3자리, 6자리, 8자리가 있나요?
세 형태 모두 같은 RGB 모델에 대응하지만 정밀도와 알파를 다르게 인코딩합니다. 3자리 #F0A는 축약형으로, 각 자리를 두 번 반복해 #FF00AA로 확장됩니다. 6자리 #FF00AA는 표준 RGB 형태이며 채널마다 16진수 두 자리를 씁니다. 8자리 #FF00AA80은 네 번째 바이트로 알파 채널을 더하며 80(10진수 128)은 약 50% 불투명도입니다. 최신 브라우저는 세 가지를 모두 받아들이고, 디자이너는 의도를 해치지 않는 한 가장 짧은 형태를 고릅니다.