목차
- 1. 최초의 문자 셋, 아스키코드
- 2. 유니코드의 등장
- 3. 유니코드로 문자열 다루기
- 4. 인코딩하기
- 5. 디코딩하기
- 6. 입출력과 인코딩
- 7. 소스 코드의 인코딩
컴퓨터는 기본적으로 0과 1, 즉 숫자만 처리할 수 있다.
그렇다면 우리가 입력하는
A
B
가
나
★
같은 문자는 컴퓨터가 어떻게 이해할까?
문자를 특정 숫자와 연결해 놓는 방법을 사용한다.
예를 들어
65 → A
66 → B
67 → C
처럼 문자와 숫자를 서로 대응시키는 것이다.
이러한 문자와 숫자의 대응표를 문자 셋(Character Set) 이라고 한다.
초기에는 컴퓨터 회사마다 서로 다른 문자 셋을 사용했다.
예를 들어 어떤 컴퓨터에서는
65 → A
라고 사용하지만 다른 컴퓨터에서는
65 → X
라고 사용한다면 서로 문서를 주고받을 때 문제가 발생한다.
이러한 문제를 해결하기 위해 만들어진 표준이 바로
ASCII(American Standard Code for Information Interchange)
이다.
ASCII는 총 128개의 문자를 표현한다.
주로 다음과 같은 문자들이 포함되어 있다.
예를 들어 파이썬에서 ord()를 사용하면 문자의 숫자 값을 확인할 수 있다.
ord('A')
실행 결과
65
반대로 숫자를 문자로 변환하려면 chr()를 사용한다.
chr(65)
실행 결과
'A'
즉,
'A'
↓
ord()
↓
65
↓
chr()
↓
'A'
와 같은 관계이다.
ASCII는 영어를 표현하는 데에는 문제가 없었지만 한계가 있었다.
ASCII가 표현할 수 있는 문자는 총 128개뿐이기 때문이다.
따라서
한글
중국어
일본어
아랍어
등을 모두 표현하기에는 부족했다.
그래서 각 나라에서는 각각 다른 문자 셋을 만들었다.
예를 들어
한국 → KSC5601
일본 → JIS
중국 → GB2312
와 같은 방식이다.
하지만 또 다른 문제가 생겼다.
각 나라가 서로 다른 문자 셋을 사용하면
한국에서 만든 파일
↓
일본 컴퓨터에서 열기
↓
글자가 깨짐
과 같은 문제가 발생할 수 있다.
이러한 문제를 해결하기 위해 등장한 것이 유니코드(Unicode) 이다.
유니코드는 쉽게 말하면
전 세계에서 사용하는 문자에 공통된 번호를 부여한 문자 체계
라고 생각하면 된다.
따라서 하나의 문자열 안에서도
Hello
안녕하세요
こんにちは
你好
와 같은 여러 나라의 문자를 함께 사용할 수 있다.
파이썬 3에서는 일반적인 문자열을 모두 유니코드 문자열로 처리한다.
예를 들어
a = "안녕하세요"
에서 a의 자료형을 확인하면
print(type(a))
결과
<class 'str'>
이다.
즉 파이썬에서 우리가 일반적으로 사용하는
"Hello"
"Python"
"한글"
등은 모두 str 객체이다.
여기서 중요한 개념이 등장한다.
파이썬 내부에서는 문자열을 유니코드 문자열로 처리하지만, 파일에 저장하거나 네트워크로 전송할 때는 실제 바이트(Byte) 데이터가 필요하다.
즉,
문자열(str)
↓
실제 저장하거나 전송할 수 있는
바이트(bytes)
로 변환해야 한다.
이 과정을 인코딩(Encoding) 이라고 한다.
파이썬에서는 encode()를 사용한다.
a = "Life is too short"
b = a.encode('utf-8')
print(b)
print(type(b))
실행 결과
b'Life is too short'
<class 'bytes'>
원래 a는
str
자료형이었다.
하지만
a.encode('utf-8')
을 실행하면
bytes
자료형으로 변환된다.
즉,
str
↓ encode()
bytes
이다.
다음 결과를 보면
b'Life is too short'
문자열 앞에 b가 붙어 있다.
이 b는
이 데이터가 일반 문자열(
str)이 아니라 바이트 문자열(bytes) 이라는 의미이다.
예를 들어
a = "Python"
b = b"Python"
print(type(a))
print(type(b))
결과
<class 'str'>
<class 'bytes'>
두 값은 화면에서 비슷하게 보여도 자료형은 서로 다르다.
이번에는 한글을 사용해 보자.
a = "한글"
print(a.encode("utf-8"))
실행하면 다음과 같은 바이트 값이 출력된다.
b'\xed\x95\x9c\xea\xb8\x80'
여기서
\x
뒤의 두 문자는 16진수로 표현된 1바이트 값이라고 생각하면 된다.
다음 코드를 실행해 보자.
a = "한글"
a.encode("ascii")
오류가 발생한다.
UnicodeEncodeError
왜 그럴까?
ASCII에는 한글 문자가 정의되어 있지 않기 때문이다.
즉,
ASCII
├─ A → O
├─ B → O
├─ 1 → O
└─ 한 → X
이므로 한글을 ASCII 방식으로 표현할 수 없다.
같은 한글이라도 어떤 인코딩 방식을 사용하느냐에 따라 실제 바이트 값은 달라질 수 있다.
예를 들어
a = "한글"
print(a.encode("utf-8"))
print(a.encode("euc-kr"))
두 결과는 서로 다르다.
왜냐하면
UTF-8 방식
EUC-KR 방식
이 문자를 바이트로 변환하는 규칙이 서로 다르기 때문이다.
쉽게 생각하면
"한글"
UTF-8이라는 규칙으로 변환
↓
바이트 A
EUC-KR이라는 규칙으로 변환
↓
바이트 B
처럼 같은 문자라도 인코딩 규칙에 따라 다른 바이트 값이 만들어질 수 있다.
인코딩과 반대되는 과정이 디코딩(Decoding) 이다.
인코딩이
문자열 → 바이트
라면 디코딩은
바이트 → 문자열
이다.
파이썬에서는 decode()를 사용한다.
a = "한글"
b = a.encode("euc-kr")
print(b.decode("euc-kr"))
실행 결과
한글
과정을 보면
"한글"
↓
encode("euc-kr")
↓
bytes
↓
decode("euc-kr")
↓
"한글"
이다.
다음 코드를 살펴보자.
a = "한글"
b = a.encode("euc-kr")
현재 b는 EUC-KR 규칙으로 만들어진 바이트 데이터이다.
따라서 다시 문자열로 변환할 때도
b.decode("euc-kr")
처럼 EUC-KR을 사용해야 한다.
그런데 다음과 같이 작성한다면
b.decode("utf-8")
잘못된 규칙으로 바이트를 해석하게 된다.
따라서
UnicodeDecodeError
와 같은 오류가 발생할 수 있다.
UTF-8로 인코딩
↓
UTF-8로 디코딩
EUC-KR로 인코딩
↓
EUC-KR로 디코딩
처럼 사용한 인코딩 방식을 알고 있어야 제대로 디코딩할 수 있다.
이 부분이 가장 중요하다.
유니코드 문자열(str)
│
│ encode("utf-8")
↓
바이트 문자열(bytes)
│
│ decode("utf-8")
↓
유니코드 문자열(str)
즉,
str → bytes
bytes → str
이다.
코드로 보면 다음과 같다.
text = "안녕하세요"
data = text.encode("utf-8")
text2 = data.decode("utf-8")
print(type(text))
print(type(data))
print(type(text2))
결과
<class 'str'>
<class 'bytes'>
<class 'str'>
나는 다음처럼 기억하면 편할 것 같다.
encode
문자를 컴퓨터가 저장하거나 전송할 수 있는
바이트로 포장
decode
바이트를 사람이 사용하는
문자열로 해석
따라서
문자열
↓
encode
↓
바이트
↓
파일 / 네트워크
↓
바이트
↓
decode
↓
문자열
의 흐름으로 생각하면 된다.
프로그램에서 인코딩 문제가 많이 발생하는 부분이 파일이나 네트워크 등 외부 데이터와 통신할 때이다.
기본적인 원칙은 다음과 같이 생각할 수 있다.
외부에서 데이터 입력
↓
bytes
↓
decode
↓
str
↓
프로그램 내부에서 처리
↓
str
↓
encode
↓
bytes
↓
외부로 출력
즉 프로그램 내부에서는 가능한 한 일반 문자열인 str을 사용하고, 외부에서 데이터를 받거나 보낼 때 인코딩과 디코딩을 처리하는 방식이다.
파일을 읽을 때 open()의 encoding 옵션을 이용할 수 있다.
예를 들어 EUC-KR로 저장된 파일이라면
with open("euc_kr.txt", encoding="euc-kr") as f:
data = f.read()
처럼 작성할 수 있다.
파일을 읽은 data는 파이썬에서 일반 문자열인 str로 사용할 수 있다.
print(type(data))
<class 'str'>
data = data + "\n" + "추가 문자열"
프로그램 내부에서는 일반 문자열로 작업한다.
with open("euc_kr.txt", mode="w", encoding="euc-kr") as f:
f.write(data)
정리하면 다음과 같다.
EUC-KR 파일
↓
open(... encoding="euc-kr")
↓
파이썬 str
↓
문자열 처리
↓
open(... encoding="euc-kr")
↓
EUC-KR 파일
텍스트 파일을 다룰 때는 가능하면 인코딩을 직접 지정하는 습관을 들이는 것이 좋다.
with open("test.txt", "r", encoding="utf-8") as f:
data = f.read()
파일을 저장할 때도 마찬가지이다.
with open("test.txt", "w", encoding="utf-8") as f:
f.write("안녕하세요")
특히 운영체제나 실행 환경이 달라지면 기본 인코딩 차이 때문에 한글이 깨지는 문제가 발생할 수 있으므로
encoding="utf-8"
처럼 명시적으로 작성하면 문제를 줄일 수 있다.
파이썬으로 작성한 .py 파일 역시 하나의 텍스트 파일이다.
따라서 소스 코드 파일에도 인코딩 방식이 존재한다.
파이썬 3에서는 기본적으로 소스 코드를 UTF-8로 처리한다.
따라서 일반적인 경우에는 다음 코드를 따로 작성할 필요가 없다.
# -*- coding: utf-8 -*-
하지만 UTF-8이 아닌 다른 인코딩으로 저장된 소스 파일이라면 인코딩을 명시할 수 있다.
예를 들어
# -*- coding: euc-kr -*-
처럼 작성할 수 있다.
마지막으로 str과 bytes의 차이를 정리해 보자.
| 구분 | str | bytes |
|---|---|---|
| 의미 | 유니코드 문자열 | 실제 바이트 데이터 |
| 예시 | "한글" | b'\xed...' |
| 주 사용 위치 | 프로그램 내부 | 파일, 네트워크 등 |
| 변환 | .encode() | .decode() |
| 변환 결과 | bytes | str |
가장 중요한 관계는
str
↓ encode()
bytes
bytes
↓ decode()
str
이다.
문자와 숫자를 대응시킨 초기 표준 문자 셋이다.
65 → A
66 → B
하지만 표현할 수 있는 문자가 적어 한글과 같은 문자를 표현할 수 없다.
전 세계의 문자를 하나의 체계로 표현하기 위한 문자 표준이다.
파이썬 3의 일반 문자열은 유니코드 문자열이다.
a = "한글"
print(type(a))
<class 'str'>
문자열을 바이트로 변환한다.
text.encode("utf-8")
str → bytes
바이트를 문자열로 변환한다.
data.decode("utf-8")
bytes → str
encode()
문자열(str) ────────────────→ 바이트(bytes)
↑ │
│ │
└──────────────────────────────┘
decode()
예를 들어
text = "한글"
data = text.encode("utf-8")
result = data.decode("utf-8")
print(text)
print(data)
print(result)
한글
b'\xed\x95\x9c\xea\xb8\x80'
한글
파이썬 내부에서는
str을 사용하고, 파일 저장이나 네트워크 전송처럼 외부와 데이터를 주고받을 때 필요에 따라encode()와decode()를 이용해bytes와 변환한다.
사람이 보는 문자
↓
str
↓
encode()
↓
bytes
↓
파일 / 네트워크
↓
bytes
↓
decode()
↓
str
↓
사람이 보는 문자