Step 50. 인코딩 탐구 — 글자와 숫자 사이의 다리

Step 50. 인코딩 탐구 — 글자와 숫자 사이의 다리

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★☆☆☆ | 예상 소요 시간 3시간

전제: Step 41~49를 마쳤다. 파이썬 기본 문법과 문자열 다루기를 안다.

  • 준비물: 파이썬이 설치된 컴퓨터. 오늘은 코드보다 실험이 많은 날입니다 — 대화형 모드(python 입력 후 나오는 >>>)를 켜 두고 오세요.
  • 주의: 오늘 실습은 100% 안전합니다. 파일을 바꾸거나 지우는 실험은 없고, 전부 메모리 안의 변환 실험입니다.

컴퓨터는 글자를 모릅니다. 아는 것은 숫자뿐입니다. 그러니 화면에 보이는 ‘A’도 ‘가’도, 내부에서는 전부 숫자입니다. 그럼 ‘A’는 몇 번일까요? 누가 정했을까요? 이 질문이 오늘의 출발점입니다. 글자가 숫자라는 것을 알면, 숫자를 다시 글자로 바꾸는 모든 변환도 보이기 시작합니다 — Base64로 꼬인 문자열, 16진수로 적힌 덤프, 이상한 코드표로 깨진 한글. 전부 "숫자와 글자 사이의 변환표 문제"입니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 문자가 컴퓨터 안에서 숫자로 저장되는 원리를 ord/chr 실험으로 확인한다
  • ASCII, 유니코드, UTF-8, Base64, 16진수(hex)의 관계를 설명한다
  • encode/decode로 str과 bytes를 오간다
  • "Hello"를 번호 나열, hex, Base64로 변환하고 되돌린다
  • 깨진 한글을 "코드표 불일치"로 진단하고 규칙을 맞춰 고친다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3 대화형 모드(>>>) + 짧은 스크립트
오늘의 함수 ord()(글자→번호), chr()(번호→글자), encode()/decode(), bytes.hex()/bytes.fromhex(), base64.b64encode/b64decode
필요한 개념 ASCII, 유니코드, UTF-8, Base64, 16진수, str과 bytes의 구분
오늘의 산출물 viewer.py — 한 문자열을 네 가지 차림새로 보여 주는 변환기

2-1. ASCII — 128개의 번호표

컴퓨터 초기, 미국에서 "영문자에 번호를 매기자"고 정한 표가 ASCII(아스키)입니다. 대문자 A는 65번, 소문자 a는 97번, 숫자 0은 48번. 총 128칸짜리 작은 표로, 지금도 컴퓨터 세계 구석구석에 박혀 있습니다.

2-2. 유니코드와 UTF-8 — 전 세계 문자의 주민등록

128칸으로는 한글도 한자도 못 담습니다. 그래서 전 세계 문자 전부에 번호를 매긴 거대한 표가 유니코드(Unicode)입니다. ‘가’는 44032번 같은 식입니다.

그리고 그 번호를 실제 바이트(저장 단위)로 옮기는 규칙이 UTF-8입니다. 유니코드가 주민등록 번호라면, UTF-8은 그 번호를 봉투에 적는 형식입니다. 우리가 파일을 열 때마다 encoding="utf-8"을 쓰는 이유입니다.

2-3. Base64 — 글자 64개로의 번역

이메일이나 웹은 "글자만 안전하게 지나가는 통로"입니다. 사진 같은 바이너리 데이터는 이 통로를 그대로 지나가지 못합니다. 그래서 모든 데이터를 안전한 글자 64개(A~Z, a~z, 0~9, +, /)로 번역하는 방법이 Base64입니다.

여기서 오늘 가장 중요한 문장: Base64는 암호화가 아닙니다. 번역표가 공개되어 누구나 되돌릴 수 있는 "변환"일 뿐입니다. SGVsbG8=라고 적혀 있으면 비밀이 숨겨진 것이 아니라, 그냥 "Hello"를 다른 글자로 적은 것입니다.

2-4. 16진수(hex) — 바이트의 약식 필기

바이트 하나는 0~255의 숫자입니다. 이것을 16진수 두 글자(00~ff)로 적는 것이 관례입니다 — "48 65 6c 6c 6f"처럼요. 16진수는 0~9에 a~f(10~15)를 붙인 셈법이고, 메모리 덤프, 해시값, 색상 코드(#ff5500)가 전부 이 필기법을 씁니다.


3. 따라 하기

3-1. ord와 chr — 번호표 들여다보기

입력 (대화형 모드, >>>에 하나씩)

ord('A')
ord('a')
ord('0')
chr(65)
chr(97)

출력 (2026-09-09 실측):

65
97
48
'A'
'a'

읽는 법: ord(글자)는 "이 글자의 번호가 뭐니?", chr(번호)는 "이 번호의 글자가 뭐니?"입니다. 서로 거꾸로입니다. 대문자와 소문자의 번호가 32 차이 나는 것(65 vs 97)도 눈여겨보세요.

3-2. 한글의 번호 — 유니코드 확인

입력

ord('가')
ord('힣')
hex(ord('가'))

출력 (2026-09-09 실측):

44032
55203
'0xac00'

읽는 법: ‘가’는 유니코드 44032번입니다. hex()는 십진수를 16진수 표기로 바꿔 주고, 0x는 "이 뒤는 16진수입니다"라는 표지판입니다. 문서에서 "U+AC00" 같은 표기를 본 적 있을 겁니다 — 그것이 이 번호입니다.

예측: chr(44033)은 무슨 글자일까요? ‘가’의 다음 번호니까… (실측 답: ‘각’) 그리고 chr(ord('나') + 1)은요? (실측 답: ‘낙’) 예측하고 확인해 보세요.

3-3. encode와 decode — 글자를 바이트로, 바이트를 글자로

입력

"Hello".encode("utf-8")
"가".encode("utf-8")
b"Hello".decode("utf-8")

출력 (2026-09-09 실측):

b'Hello'
b'\xea\xb0\x80'
'Hello'

읽는 법: encode()는 글자(str)를 바이트(bytes)로, decode()는 바이트를 글자로 되돌립니다. 결과 앞의 b는 "이건 바이트다"라는 표시입니다. ‘가’가 세 개의 바이트(ea b0 80)가 되는 것을 보세요 — UTF-8은 영문은 1바이트, 한글은 3바이트로 적는 규칙입니다. \xea는 "16진수 ea라는 한 바이트"라는 표기입니다.

: str과 bytes의 구분은 나중에 네트워크 통신, 파일 처리, 암호화에서 계속 부딪히는 벽입니다. 오늘 한 번 정면으로 만나 두세요.

3-4. Base64 — 번역하고 되돌리기

입력 (encode_decode.py)

import base64

original = "Hello"
encoded = base64.b64encode(original.encode("utf-8"))
print(encoded)

decoded = base64.b64decode(encoded).decode("utf-8")
print(decoded)

출력 (2026-09-09 실측):

b'SGVsbG8='
Hello

읽는 법: 흐름이 "글자 → 바이트(encode) → Base64 글자(b64encode)"이고, 되돌릴 때는 역순입니다. SGVsbG8=라는 이상한 문자열이 사실 "Hello"였습니다. 끝의 =는 Base64의 특징적인 꼬리(자리 맞춤용)로, 이 꼬리를 보면 Base64를 의심하는 단서가 됩니다.

예측: aGVsbG8gd29ybGQ=는 무엇일까요? 직접 b64decode해 확인하세요. (2026-09-09 실측 답: hello world) Base64를 "읽는" 것은 이렇게 기계에게 되돌리기를 시키는 것입니다.

3-5. hex 변환 — 16진수 필기 연습

입력

data = "Hello".encode("utf-8")
print(data.hex())
print(bytes.fromhex("48656c6c6f").decode("utf-8"))

출력 (2026-09-09 실측):

48656c6c6f
Hello

읽는 법: .hex()는 바이트를 16진수 문자열로, bytes.fromhex()는 거꾸로입니다. "48 65 6c 6c 6f"에서 48은 ‘H’, 65는 ‘e’… 3-1에서 본 ASCII 번호들이 16진수로 적힌 것입니다. 같은 숫자를 십진(72, 101)과 16진(48, 65)으로 적는 두 필기법이 연결됐습니다.

3-6. 종합 변환기 — 네 가지 모습으로 보기

입력 (viewer.py)

import base64

text = input("문자열을 입력하세요: ")
data = text.encode("utf-8")

print("글자별 번호:", [ord(c) for c in text])
print("hex:", data.hex())
print("base64:", base64.b64encode(data).decode())

실행 (2026-09-09 실측, Hello 입력):

문자열을 입력하세요: Hello
글자별 번호: [72, 101, 108, 108, 111]
hex: 48656c6c6f
base64: SGVsbG8=

읽는 법: 한 문자열의 네 얼굴입니다. 글자, 번호 나열, 16진 필기, Base64 — 전부 같은 내용의 다른 표기입니다.

3-7. 코드표가 어긋날 때 — cp949 실험

한국 윈도우에는 UTF-8 말고 오래된 코드표가 하나 더 살고 있습니다. cp949입니다. 이 둘이 뒤섞이면 글자가 깨집니다. 직접 깨뜨려 보고 고쳐 봅시다.

입력 (대화형 모드)

data = "안녕".encode("cp949")
print(data)
print(data.decode("cp949"))

출력 (2026-09-09 실측):

b'\xbe\xc8\xb3\xe7'
안녕

읽는 법: cp949로 번역하면 ‘안녕’은 바이트 네 개(be c8 b3 e7)가 됩니다. 참고로 UTF-8로는 여섯 바이트입니다 — 같은 글자도 코드표에 따라 바이트 수가 다릅니다 (2026-09-09 실측). 같은 코드표로 되돌리면 잘 읽힙니다.

이제 이 바이트를 UTF-8으로 억지로 읽어 보세요. data.decode("utf-8") — 실측 (2026-09-09):

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbe in position 0: invalid start byte

반대 방향도 시험해 봅시다. UTF-8로 적은 한글을 cp949로 읽는 경우입니다. 어떤 글자는 오류가 나고(예: "안녕"은 UnicodeDecodeError: 'cp949' codec can't decode byte 0xec ... — 2026-09-09 실측), 어떤 글자는 오류 없이 엉뚱한 글자로 읽힙니다:

u = "보안".encode("utf-8")
print(u.decode("cp949"))
蹂댁븞

(2026-09-09 실측. "보안"이 "蹂댁븞"으로 — 이런 깨진 글자를 본 적 있을 겁니다.)

"깨짐 = 적을 때와 읽을 때의 코드표가 다르다" — 이것이 오늘의 공식입니다. 코드표가 어긋나면 오류가 나거나, 운 나쁘게 조용히 깨지거나, 둘 중 하나입니다.

: 웹에서 긁어 온 파일, 오래된 프로그램이 만든 파일을 열 때 깨진 글자를 만나게 됩니다. 그때 당황하지 않고 "코드표를 맞추면 된다"고 생각할 수 있게 하는 예방 주사입니다.


4. 미션과 연습문제

미션 — 변환 놀이터 완성과 세 문자열 해독

  1. viewer.py를 발전시켜 변환 방향을 고를 수 있게 합니다. 메뉴: 1. 글자→번호/hex/Base64, 2. Base64→글자, 3. hex→글자
  2. 2, 3번 메뉴에서 잘못된 입력(이상한 Base64, 홀수 자리 hex 등)이 들어와도 죽지 않게 Step 46의 예외처리를 합니다
  3. 아래 세 문자열을 직접 해독해 노트에 적습니다
    • Base64: c2VjdXJpdHk=
    • hex: 637466
    • 번호 나열: [83, 116, 117, 100, 121]
  4. 각 해독에 쓴 명령이나 코드를 README에 적습니다

연습문제

문제 1. ASCII, 유니코드, UTF-8의 관계를 "번호표와 봉투 형식"이라는 구분으로 설명해 보세요.

문제 2. Base64는 왜 암호화가 아닌가요? 변환과 암호화를 가르는 기준을 한 문장으로 말해 보세요.

문제 3. data.decode("utf-8")에서 UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbe ...가 떴습니다. 무슨 상황이며 어떻게 해결하나요?

문제 4. "48656c6c6f".decode()가 오류가 나는 이유와, 올바른 두 다리의 순서를 말해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

변환 놀이터의 뼈대:

import base64

while True:
    print("1. 글자→번호/hex/Base64  2. Base64→글자  3. hex→글자  0. 종료")
    menu = input("선택: ").strip()
    if menu == "0":
        break
    elif menu == "1":
        text = input("문자열: ")
        data = text.encode("utf-8")
        print("번호:", [ord(c) for c in text])
        print("hex:", data.hex())
        print("base64:", base64.b64encode(data).decode())
    elif menu == "2":
        s = input("Base64 문자열: ").strip()
        try:
            print("해독:", base64.b64decode(s).decode("utf-8"))
        except Exception:
            print("Base64 모양이 아니거나, UTF-8 글자가 아닌 데이터입니다.")
    elif menu == "3":
        s = input("hex 문자열: ").strip()
        try:
            print("해독:", bytes.fromhex(s).decode("utf-8"))
        except ValueError:
            print("hex 모양이 아닙니다 (0~9, a~f, 짝수 자리).")
        except UnicodeDecodeError:
            print("바이트는 맞지만 UTF-8 글자가 아닙니다.")
    else:
        print("0~3 중에서 고르세요.")

세 문자열의 해독 (2026-09-09 실측):

c2VjdXJpdHk=  -> security
637466        -> ctf
[83, 116, 117, 100, 121] -> Study

해독에 쓴 명령: base64.b64decode("c2VjdXJpdHk=").decode("utf-8"), bytes.fromhex("637466").decode("utf-8"), "".join(chr(n) for n in [83, 116, 117, 100, 121]).

검증하는 법: ① 메뉴 2에 "!!!" 같은 것을 넣어도 죽지 않고 안내가 뜨는가. ② 메뉴 3에 홀수 자리("486")를 넣으면 ValueError 안내가 뜨는가. ③ 세 문자열의 정답(security / ctf / Study)이 노트에 적혔는가. 셋이 전부 ‘예’이면 완성입니다.

연습문제 해답

문제 1 해답. ASCII는 영문 중심 128칸의 번호표이고, 유니코드는 전 세계 문자 전부에 번호를 매긴 거대한 번호표이며, UTF-8은 그 유니코드 번호를 실제 바이트로 적는 규칙(봉투 형식)입니다. 유니코드가 번호라면 UTF-8은 그 번호의 저장 형식입니다.

문제 2 해답. 번역표가 공개되어 열쇠 없이 누구나 되돌릴 수 있기 때문입니다. 기준: "열쇠 없이 되돌릴 수 있으면 변환, 열쇠가 있어야만 되돌릴 수 있으면 암호화." Base64로 적힌 비밀번호는 비밀번호가 아니라, 이상하게 적힌 비밀번호일 뿐입니다.

문제 3 해답. 그 바이트는 UTF-8 규칙으로 적힌 것이 아닌 상황입니다 — 다른 코드표(cp949 등)로 적혔거나, Base64 디코딩을 빠뜨린 경우입니다. 해결은 "이 바이트가 어떤 규칙으로 만들어졌는가"를 거슬러 올라가 규칙을 맞추는 것입니다. 윈도우 한글 프로그램의 산출물은 cp949인 경우가 많습니다 (3-7절 실측처럼 cp949 바이트 0xbe는 UTF-8로 읽으면 이 오류가 납니다).

문제 4 해답. "48656c6c6f"는 글자(str)이지 바이트가 아니라서 — decode는 바이트에 쓰는 것이기 때문입니다 (2026-09-09 실측: AttributeError: 'str' object has no attribute 'decode'). 올바른 순서: hex 문자열 → bytes.fromhex() → bytes → .decode() → 글자. 두 다리로 건늡니다.

완료 기준 체크리스트

  • [ ] ord와 chr로 글자와 번호를 오갈 수 있다
  • [ ] encode/decode로 str과 bytes를 오갈 수 있다
  • [ ] Base64를 변환(암호화 아님)이라고 설명할 수 있다
  • [ ] hex 표기를 읽고 bytes.fromhex로 되돌릴 수 있다
  • [ ] cp949와 UTF-8의 불일치가 깨짐의 원인임을 실험으로 확인했다
  • [ ] 이상한 문자열을 만나면 꼬리와 글자 구성으로 추측할 수 있다
  • [ ] 미션: 변환 놀이터와 세 문자열 해독을 완료했다

6. 흔한 실수와 해결

벽 1. UnicodeDecodeError가 난다

증상 (2026-09-09 실측):

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbe in position 0: invalid start byte

원인: 그 바이트는 UTF-8 규칙으로 적힌 것이 아닙니다. 다른 코드표(cp949 등)로 적혔거나, Base64 디코딩을 빠뜨린 경우입니다.
해결: "이 바이트는 어떤 규칙으로 만들어졌는가"를 거슬러 올라가세요. 윈도우 한글 프로그램의 산출물은 cp949인 경우가 많습니다. 규칙을 맞추면 열립니다.

벽 2. str과 bytes를 섞어 쓴다

증상 (2026-09-09 실측):

TypeError: can only concatenate str (not "bytes") to str

원인: 글자(str)와 바이트(bytes)는 다른 종족입니다. encode/decode 없이 섞어 쓰면 오류입니다.
해결: 변수가 지금 어느 종족인지 type()으로 확인하는 습관. "네트워크/파일 경계에서는 bytes, 내 프로그램 안에서는 str"로 나눠 생각하고, 경계에서만 encode/decode합니다.

벽 3. Base64를 암호라고 착각한다

증상: Base64로 "암호화"된 중요한 정보를 발견하고 대단한 것을 찾았다고 생각합니다.
원인: 변환과 암호화의 혼동. Base64는 열쇠 없이 누구나 되돌리는 공개 번역입니다.
해결: 기준을 기억하세요 — "열쇠 없이 되돌릴 수 있으면 변환, 열쇠가 있어야만 되돌릴 수 있으면 암호화."

벽 4. hex 문자열을 decode하려 한다

증상 (2026-09-09 실측):

AttributeError: 'str' object has no attribute 'decode'

원인: "48656c6c6f"는 글자(str)이지 바이트가 아닙니다. decode는 바이트에 쓰는 것입니다.
해결: 순서를 기억하세요. hex 문자열 → bytes.fromhex() → bytes → .decode() → 글자. 두 다리로 건늡니다.

벽 5. 깨진 글자 앞에서 당황한다

증상: 파일을 열었더니 蹂댁븞 같은 깨진 글자.
원인: 적을 때와 읽을 때의 코드표가 다른 것입니다 — 3-7절에서 UTF-8로 적은 "보안"을 cp949로 읽으면 정확히 이 모양이 나옵니다 (2026-09-09 실측).
해결: 당황하지 말고 "코드표를 맞추면 된다"를 떠올리세요. 파일을 만든 프로그램(또는 시스템)의 코드표를 추정해 encoding="cp949" 등으로 다시 읽어 봅니다.


7. 정리

오늘의 개념

개념 한 줄 설명
ASCII 영문 중심 128칸의 번호표 (A=65, a=97, 0=48)
유니코드 전 세계 문자 전부의 번호표 (‘가’=44032=U+AC00)
UTF-8 유니코드 번호를 바이트로 적는 규칙 (영문 1바이트, 한글 3바이트)
cp949 한국 윈도우의 오래된 한글 코드표 — UTF-8과 섞이면 깨짐
Base64 모든 데이터를 안전한 글자 64개로 번역 — 변환이지 암호화 아님
hex 바이트를 16진수 두 글자로 적는 약식 필기
str / bytes 글자와 바이트 — encode/decode로만 오감

오늘의 함수

함수 하는 일
ord(글자) / chr(번호) 글자↔번호
hex(숫자) 십진수 → 16진수 표기 (0x 표지)
문자열.encode("utf-8") 글자 → 바이트
바이트.decode("utf-8") 바이트 → 글자
base64.b64encode / b64decode Base64 번역 / 되돌리기
바이트.hex() / bytes.fromhex() hex 필기 / 되돌리기

명령어보다 중요한 감각

이상한 문자열을 만났을 때의 절차를 정리해 둡니다. (가) 꼬리와 글자 구성으로 추측한다 — = 꼬리와 64글자 조합이면 Base64, 0~9와 a~f만 있고 길이가 짝수면 hex. (나) 추측한 규칙으로 되돌려 본다. (다) 안 되면 다른 규칙을 시도한다 — Base64를 풀었더니 또 Base64가 나오는 경우도 흔합니다. (라) 바이트가 나오면 코드표를 맞춰 글자로 읽는다. 이 네 걸음이면 웬만한 변환 문자열은 전부 열립니다.

보안과의 연결: 공격자들은 탐지를 피하려고 명령어를 Base64로 감추고, 방어자는 로그에서 Base64 모양을 찾아 되돌려 봅니다. powershell -enc SGVsbG8= 같은 것을 보고 "-enc 뒤는 Base64구나" 알아보는 것이 오늘의 기술이 만드는 눈입니다. 마지막으로 ‘가’ 한 글자의 여행을 기억하세요 — 유니코드 44032번으로 기억되고, UTF-8로 바이트 세 개(ea b0 80)가 되고, Base64로는 6rCA, hex로는 eab080이 됩니다 (전부 2026-09-09 실측). 전부 같은 ‘가’의 다른 차림새입니다. "데이터가 지금 어느 차림새인가"를 묻는 습관 — 그것이 인코딩 문제의 절반을 풉니다.


전부 체크되면 Step 50 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.