0xFF, 0b1010, chmod 755, #3B82F6. 개발하다 보면 진법이 다른 숫자를 하루에도 몇 번씩 마주치는데, 막상 손으로 바꾸려면 "나머지를 위에서부터 읽던가, 아래서부터 읽던가" 헷갈리더라고요. 결론부터 말하면 모든 진법은 "자리값 × 숫자"의 합이고, 10진수를 다른 진법으로 바꿀 땐 그 진법으로 계속 나눈 나머지를 아래에서 위로 읽으면 돼요. 2진수와 16진수 사이는 계산할 필요도 없이 4비트씩 묶어서 한 글자로 바꾸면 끝이에요. 156은 2진수 10011100, 16진수 0x9C 예요.
이 글에서는 자리값이라는 기본 원리, 나눗셈 변환법, 2진수 ↔ 16진수 4비트 묶음 단축법, 8진수가 아직 쓰이는 곳, 음수를 표현하는 2의 보수, 색상 코드·파일 권한·바이트·비트마스크 같은 실무 예, 그리고 JavaScript와 파이썬에서 진법을 바꾸는 코드와 함정까지 순서대로 정리할게요. 숫자를 바로 넣어 보고 싶다면 진법 변환기에서 비트 배열까지 함께 볼 수 있어요.
핵심 요약
N진수의 각 자리는 N의 거듭제곱이에요. 2진수 10011100은 128 + 16 + 8 + 4 = 156이고, 16진수 9C는 9 × 16 + 12 = 156이에요.
10진수 → N진수는 N으로 나눈 나머지를 모아 아래에서 위로 읽어요. 156을 16으로 나누면 몫 9, 나머지 12(C)라서 9C예요.
2진수 ↔ 16진수는 4비트가 16진수 한 자리라서 오른쪽부터 4자리씩 끊으면 바로 바뀌어요. 1001 1100 → 9 C.
음수는 2의 보수로 저장해요. 비트를 모두 뒤집고 1을 더하면 되고, 8비트에서 −5는 11111011(0xFB)이에요.
진법의 원리, 자리값만 알면 돼요
우리가 쓰는 10진수 156 은 사실 1 × 100 + 5 × 10 + 6 × 1이에요. 각 자리가 10의 거듭제곱(10², 10¹, 10⁰)이라는 뜻이죠. 진법이 바뀌어도 원리는 똑같고 밑(base)만 바뀌어요.
- 2진수: 자리값이 1, 2, 4, 8, 16, 32, 64, 128 … 쓰는 숫자는 0과 1
- 8진수: 자리값이 1, 8, 64, 512 … 쓰는 숫자는 0~7
- 16진수: 자리값이 1, 16, 256, 4096 … 쓰는 숫자는 0~9와 A(10)~F(15)
그래서 어떤 진법이든 10진수로 바꾸는 방법은 하나예요. 각 자리 숫자에 자리값을 곱해 다 더하면 됩니다.
| 표기 | 계산 | 10진수 |
|---|---|---|
2진수 10011100 | 128 + 16 + 8 + 4 | 156 |
16진수 9C | 9 × 16 + 12 × 1 | 156 |
8진수 234 | 2 × 64 + 3 × 8 + 4 | 156 |
16진수 FF | 15 × 16 + 15 | 255 |
2진수 11111111 | 128 + 64 + … + 1 | 255 |
2진수를 10진수로 바꿀 땐 1이 있는 자리의 값만 더하면 돼서 더 쉬워요. 그래서 2의 거듭제곱(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024)은 외워 두면 평생 써먹습니다. 2¹⁰ = 1,024라서 "1K", 2¹⁶ = 65,536, 2³² = 약 43억이라는 것도 같이 기억해 두면 좋아요.
진법이 작을수록 자릿수가 길어져요
같은 값을 적더라도 밑이 작으면 자릿수가 늘어나요. 컴퓨터는 2진수로 저장하지만 사람이 읽을 땐 16진수로 적는 이유가 여기에 있어요.
10진수를 2진수·16진수로, 나눗셈 변환법
10진수를 N진수로 바꿀 땐 N으로 계속 나누고 나머지를 모아요. 몫이 0이 될 때까지 나눈 다음, 마지막에 나온 나머지부터 거꾸로(아래에서 위로) 읽으면 됩니다.
156을 2진수로 바꿔 볼게요.
| 나누기 | 몫 | 나머지 |
|---|---|---|
| 156 ÷ 2 | 78 | 0 |
| 78 ÷ 2 | 39 | 0 |
| 39 ÷ 2 | 19 | 1 |
| 19 ÷ 2 | 9 | 1 |
| 9 ÷ 2 | 4 | 1 |
| 4 ÷ 2 | 2 | 0 |
| 2 ÷ 2 | 1 | 0 |
| 1 ÷ 2 | 0 | 1 |
나머지를 아래에서 위로 읽으면 10011100 이에요. 처음 나온 나머지가 가장 오른쪽(1의 자리)이라는 것만 기억하면 순서를 헷갈리지 않아요. 첫 나눗셈의 나머지는 "2로 나누어떨어지는가", 즉 짝수·홀수를 알려 주니까 당연히 맨 끝자리가 되는 거죠.
16진수는 16으로 나누면 돼요. 156 ÷ 16 = 몫 9, 나머지 12. 9 ÷ 16 = 몫 0, 나머지 9. 아래에서 위로 읽으면 9, 12 → 9C예요. 나머지가 10 이상이면 A~F로 바꿔 적는 게 유일한 차이예요.
큰 수는 "빼기 방식"이 더 빠를 때도 있어요
머릿속으로 할 땐 나눗셈보다 가장 큰 2의 거듭제곱부터 빼는 방식이 편할 때가 있어요. 156에서 128을 빼면 28, 28에서 16을 빼면 12, 12에서 8을 빼면 4, 4에서 4를 빼면 0. 뺀 자리(128, 16, 8, 4)에 1, 나머지 자리에 0을 적으면 1001 1100 이 바로 나와요.
2진수 ↔ 16진수, 4비트씩 묶으면 끝
16 = 2⁴이라서 2진수 4자리가 16진수 정확히 1자리에 대응해요. 그래서 2진수와 16진수 사이에서는 10진수를 거칠 필요가 없어요.
- 2진수를 오른쪽 끝에서부터 4자리씩 끊어요. 왼쪽 끝이 4자리가 안 되면 앞에 0을 채워요.
- 각 4자리 묶음을 아래 표에서 찾아 한 글자로 바꿔요.
| 4비트 | 16진수 | 4비트 | 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 |
예를 들어 110101101 은 오른쪽부터 끊으면 1 1010 1101 이고, 앞을 채우면 0001 1010 1101 → 1AD예요. 반대로 16진수 3F 는 3 → 0011, F → 1111이라 0011 1111 이 됩니다. 표를 다 외울 필요는 없고, 8·4·2·1 자리값만 기억하면 1011 = 8 + 2 + 1 = 11 = B처럼 바로 계산돼요.
8진수도 같은 원리예요. 8 = 2³이라 3비트씩 묶으면 돼요. 10011100 을 3비트씩 끊으면 010 011 100 → 2, 3, 4 → 8진수 234예요.
8진수는 어디에 쓰일까, chmod 755
요즘 8진수를 직접 볼 일은 거의 리눅스 파일 권한뿐이에요. 권한은 소유자·그룹·기타 사용자 세 묶음에 각각 읽기(r=4)·쓰기(w=2)·실행(x=1) 3비트가 있어서, 3비트 = 8진수 한 자리가 딱 맞거든요.
| 8진수 | 2진수 | 권한 | 의미 |
|---|---|---|---|
| 7 | 111 | rwx | 읽기 + 쓰기 + 실행 |
| 6 | 110 | rw- | 읽기 + 쓰기 |
| 5 | 101 | r-x | 읽기 + 실행 |
| 4 | 100 | r-- | 읽기만 |
그래서 chmod 755 는 111 101 101, 즉 rwxr-xr-x 이고, 644 는 110 100 100 = rw-r--r-- 예요. 이 값을 10진수로 읽으면 755가 아니라 493이라는 게 함정이에요. 파이썬에서 os.chmod(path, 755) 라고 쓰면 엉뚱한 권한이 걸리고, 반드시 0o755 처럼 8진수 접두어를 붙여야 해요.
2의 보수, 컴퓨터가 음수를 저장하는 방법
비트에는 마이너스 기호가 없어요. 그래서 컴퓨터는 맨 앞 비트를 부호처럼 쓰는 2의 보수 방식을 써요. 만드는 법은 간단해요.
- 양수를 2진수로 적어요. 5 →
00000101 - 모든 비트를 뒤집어요. →
11111010 - 1을 더해요. →
11111011(= 0xFB)
이렇게 하면 8비트에서 −5는 11111011 이에요. 이 방식이 좋은 이유는 덧셈 회로 하나로 뺄셈까지 된다는 거예요. 5 + (−5)를 그대로 더하면 00000101 + 11111011 = 1 00000000 이 되고, 8비트를 넘친 맨 앞 1은 버려지니 결과는 0이에요.
같은 8비트 11001000 도 부호 없는(unsigned) 정수로 읽으면 200이고, 부호 있는(signed) 정수로 읽으면 200 − 256 = −56이에요. 비트는 같고 해석만 다른 것이죠. 바이너리 파일이나 네트워크 패킷을 읽을 때 부호 여부를 잘못 고르면 이런 식으로 값이 완전히 달라져요.
부호 있는 정수에서 최댓값에 1을 더하면 최솟값으로 넘어가요. 8비트 127 + 1 = −128이 되는 거죠. 이게 정수 오버플로예요. 32비트 정수의 최댓값 2,147,483,647(0x7FFFFFFF)을 넘는 순간 음수로 바뀌는 것도 같은 원리이고, 유닉스 시간이 2038년에 32비트를 넘는 문제도 여기서 나와요. 자세한 건 유닉스 타임스탬프와 2038년 문제 글에 정리해 두었어요.
실무에서 만나는 2진수·16진수
색상 코드 #RRGGBB
#FF5733 은 16진수 두 자리씩 R=FF(255), G=57(87), B=33(51)이에요. 1바이트가 16진수 2자리라서 색상 채널 하나가 딱 두 글자에 들어가요. HEX·RGB·HSL을 언제 어떻게 바꿔 쓰는지는 HEX RGB HSL 차이와 변환 공식 글에서 자세히 다뤘어요.
바이트와 헥스 덤프
파일이나 패킷을 바이트 단위로 보여 주는 헥스 덤프는 바이트 하나를 16진수 두 자리로 적어요. 알파벳 A 는 0x41(2진수 01000001), 한글 한 은 UTF-8로 ED 95 9C 3바이트예요. 한글이 왜 3바이트인지는 UTF-8과 EUC-KR 한글 바이트 차이 글에서 비트 단위로 풀어 두었어요.
비트마스크와 플래그
여러 개의 켜짐/꺼짐 상태를 정수 하나에 담을 때 비트 하나씩을 플래그로 써요.
const READ = 0b001, WRITE = 0b010, EXEC = 0b100;
let perm = READ | WRITE; // 0b011 = 3, 두 비트를 켬
const canWrite = (perm & WRITE) !== 0; // true, 특정 비트 확인
perm &= ~WRITE; // 0b001, 특정 비트 끔
perm ^= EXEC; // 0b101, 특정 비트 뒤집기
| 연산 | 예시 (2진수) | 결과 | 용도 |
|---|---|---|---|
AND & | 1010 AND 0110 | 0010 (2) | 특정 비트 확인·추출 |
| OR (세로 막대) | 1010 OR 0110 | 1110 (14) | 비트 켜기 |
XOR ^ | 1010 XOR 0110 | 1100 (12) | 비트 뒤집기 |
왼쪽 시프트 << | 5 << 2 | 10100 (20) | × 2ⁿ |
오른쪽 시프트 >> | 20 >> 2 | 101 (5) | ÷ 2ⁿ (내림) |
그 밖에 자주 보는 16진수
- 메모리 주소·에러 코드:
0x7FFF5FBFF8AC,0x80070005처럼 길게 나와요. - UUID: 128비트를 16진수 32자리로 적은 거예요.
- 매직 넘버: 파일 맨 앞 몇 바이트로 형식을 구분해요. PNG는
89 50 4E 47, PDF는25 50 44 46(%PDF)로 시작해요.
JavaScript·파이썬으로 진법 변환하기
JavaScript
(156).toString(2); // "10011100"
(156).toString(16); // "9c" → .toUpperCase()로 "9C"
parseInt("9C", 16); // 156
parseInt("10011100", 2); // 156
0b10011100, 0x9C, 0o234; // 리터럴은 모두 156
(5).toString(2).padStart(8, "0"); // "00000101" 자릿수 맞추기
(-5 >>> 0).toString(16); // "fffffffb" 32비트 2의 보수로 보기
주의할 점 몇 가지가 있어요.
parseInt는 두 번째 인자(radix)를 항상 적으세요. 생략하면"0x1A"는 16진수로 읽지만 다른 문자열은 10진수로 읽어서 의도와 달라질 수 있어요.parseInt("12abc", 16)처럼 중간에 맞지 않는 글자가 있으면 거기서 멈추고 앞부분만 읽어요("12abc"는 16진수로 전부 유효해서 76476이지만"12xyz"는 18). 검증이 필요하면 정규식으로 먼저 확인하세요.- 비트 연산자(AND·OR·XOR·NOT·시프트)는 숫자를 32비트 부호 있는 정수로 바꿔서 계산해요.
0xFFFFFFFF | 0이 −1이 되는 이유예요. 부호 없이 보려면>>> 0을 붙이세요. - 일반 숫자는 2⁵³(9,007,199,254,740,992)까지만 정확해요. 64비트 ID나 해시를 다룰 땐
BigInt("0xFFFFFFFFFFFFFFFF")처럼 BigInt를 쓰세요.
파이썬
bin(156), oct(156), hex(156) # ('0b10011100', '0o234', '0x9c')
int("9C", 16), int("10011100", 2) # (156, 156)
format(156, "08b"), format(156, "02X") # ('10011100', '9C')
format(-5 & 0xFF, "08b") # '11111011' 8비트 2의 보수
bin(-5) # '-0b101' ← 2의 보수가 아니에요!
파이썬 정수는 크기 제한이 없어서 bin(-5) 는 2의 보수 대신 마이너스 기호 + 절댓값으로 보여 줘요. 8비트·32비트 같은 고정 폭의 비트 모양이 필요하면 -5 & 0xFF, -5 & 0xFFFFFFFF 처럼 마스크를 씌워야 해요. 거꾸로 0xFB를 부호 있는 8비트로 읽으려면 x - 256 if x >= 128 else x 를 쓰면 됩니다.
소수는 2진수로 정확히 안 떨어져요
0.5(= 2⁻¹)나 0.25는 2진수로 딱 떨어지지만 0.1은 0.000110011001100… 처럼 끝없이 반복돼요. 그래서 컴퓨터에서 0.1 + 0.2는 정확히 0.3이 아니라 0.30000000000000004가 됩니다. 돈 계산처럼 정확해야 하는 곳은 정수(원·센트 단위)나 Decimal 타입을 쓰세요.
자주 묻는 질문
10진수를 2진수로 바꿀 때 나머지는 어느 방향으로 읽나요?
마지막에 나온 나머지부터, 즉 아래에서 위로 읽어요. 첫 번째 나머지는 짝수·홀수를 알려 주는 1의 자리라서 맨 오른쪽에 와요. 156이면 나머지가 0,0,1,1,1,0,0,1 순서로 나오고 거꾸로 읽은 10011100이 답이에요.
2진수를 16진수로 바꾸는 가장 빠른 방법은요?
오른쪽 끝에서부터 4자리씩 끊고, 각 묶음을 8·4·2·1 자리값으로 더해 한 글자로 바꾸면 돼요. 10 이상은 A~F로 적어요. 10진수를 거칠 필요가 없어서 길이가 길어도 빠르게 바꿀 수 있어요.
2의 보수는 어떻게 구하나요?
양수의 비트를 모두 뒤집고 1을 더하면 돼요. 8비트에서 5(00000101)를 뒤집으면 11111010, 1을 더하면 11111011이고 이게 −5예요. 다시 같은 과정을 거치면 원래 5로 돌아와요.
chmod 755는 왜 8진수인가요?
권한이 소유자·그룹·기타 세 묶음이고 각각 읽기·쓰기·실행 3비트라서, 3비트를 한 자리로 적는 8진수가 딱 맞기 때문이에요. 7은 rwx, 5는 r-x예요. 코드에서는 10진수 755가 아니라 0o755처럼 8진수로 적어야 해요.
16진수에서 A~F는 무슨 뜻인가요?
16진수는 한 자리에 0부터 15까지 들어가야 하는데 숫자 기호는 0~9뿐이라서, 10~15를 A~F로 적기로 한 거예요. A=10, B=11, C=12, D=13, E=14, F=15이고 대소문자는 구분하지 않아요.
정리
진법 변환의 원리는 "자리값 × 숫자의 합" 하나예요. N진수 → 10진수는 자리값을 곱해 더하고, 10진수 → N진수는 N으로 나눈 나머지를 아래에서 위로 읽으면 돼요. 2진수와 16진수 사이는 4비트, 8진수는 3비트씩 묶으면 계산 없이 바뀌고, 음수는 비트를 뒤집고 1을 더한 2의 보수로 저장돼요.
실무에서는 색상 코드, 바이트 덤프, 파일 권한, 비트마스크에서 이 원리가 그대로 쓰여요. 코드에서는 JS의 parseInt radix 생략과 32비트 비트 연산, 파이썬의 bin(-5) 출력 방식만 조심하면 됩니다. 값을 빠르게 확인하거나 비트 배열을 눈으로 보고 싶다면 진법 변환기를 쓰고, 색 변환은 색상 코드 변환기에서 이어서 해 보세요.