재밌는 글을 하나 읽고 나서 인코딩/디코딩과 관련하여 간단하게나마 정리하고 싶어졌다.
7비트로 표현되는 문자 인코딩 방식이다.
숫자, 알파벳, 특수문자 등 총 128개의 문자를 표현하고, 각각은 0부터 127까지의 정수 값으로 매핑된다.
A: 01000001
B: 01100001
O: 00110000
전 세계의 모든 문자를 일관되게 표현하기 위한 표준이다.
대표적으로 UTF-8, UTF-16, UTF-32 등이 있다.
가변 길이 문자 인코딩 방식으로 ASCII와 호환된다.
1 byte ~ 4 byte까지 다양한 길이를 가진다.
ASCII 문자는 1 byte, 다른 문자는 2 ~ 4 byte이다.
A: 01000001
ä: 11000011 10100100
😊: 11110000 10011111 10010010 10011000
// UTF-8 Encoding
const utf8String = '😊';
const utf8Encoded = new TextEncoder().encode(utf8String);
console.log(utf8Encoded); // Uint8Array [ 240, 159, 152, 138 ]
// UTF-8 Decoding
const utf8Decoded = new TextDecoder().decode(utf8Encoded);
console.log(utf8Decoded); // 😊
기본 다국어 평면(BMP, Basic Multilingual Plane)의 문자는 2 byte로 표현되지만 보충 평면(Supplementary Plane)는 4 byte로 표현된다.
UTF-16을 보다 자세히 이해하기 위해 아래 개념들이 필요하다.
A: 00000000 01000001
ä: 11000011 10100100
😊: 11011011 10001111 10001001 10000000
// UTF-16 Encoding
const utf16String = '😊';
const utf16Encoded = new TextEncoder('utf-16le').encode(utf16String);
console.log(utf16Encoded); // Uint8Array [ 68, 216, 10, 216, 0 ]
// UTF-16 Decoding
const utf16Decoded = new TextDecoder('utf-16le').decode(utf16Encoded);
console.log(utf16Decoded); // 😊
모든 문자를 고정된 4 byte로 표현하는 방식이다.
메모리 사용이 많지만 간단하게 인덱싱이 가능하다.
A: 00000000 00000000 00000000 01000001
ä: 00000000 00000000 11000011 10100100
😊: 00000000 00110011 10001111 10001001
base64 인코딩/디코딩은 바이너리 콘텐츠를 웹에 안전한 텍스트로 표현하기 위해 변환하는 일반적인 형식이다.
인라인 이미지와 같은 데이터 URL에 일반적으로 사용된다.
JS에서 base64 인코딩/디코딩을 위한 핵심 함수는 btoa
와 atob
이다. btoa
는 문자열을 base64로 인코딩된 문자열로 변환하고, atob
는 반대로 base64로 인코딩 된 문자열을 일반 문자열로 디코딩한다.
다만 이 기능은 문자열 내의 각 문자가 실제로 하나의 바이트에 매핑된다는 가정으로 동작하기 때문에 ASCII 문자 또는 단일 바이트로 표현할 수 있는 문자가 포함된 문자열에서만 작동한다.
즉, 유니코드에서는 작동하지 않는다.
실제로 JS에서 'hello⛳❤️🧀' 문자열에 대한 btoa
를 통한 인코딩은 에러를 발생시킨다.
// '⛳': 단일 16비트 코드 단위.
// '❤️': U+2764와 U+FE0F(하트 및 변형)의 두 가지 16비트 코드 단위.
// '🧀' : 32비트 코드 포인트(U+1F9C0), 또는 두 개의 16비트 코드 단위 '\ud83e\uddc0'의 서로게이트(surrogate) 쌍으로 표현할 수 있음.
const validUTF16String = 'hello⛳❤️🧀';
try {
const validUTF16StringEncoded = btoa(validUTF16String);
console.log(`Encoded string: [${validUTF16StringEncoded}]`); // 동작 X
} catch (error) {
console.log(error); // DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.
}
이는 JS가 문자열을 UTF-16 인코딩으로 처리하기 때문에 발생한다.
MDN에서 나온 것처럼 유니코드를 통해 이 인코딩 문제를 해결할 수 있다.
function base64ToBytes(base64) {
const binString = atob(base64);
return Uint8Array.from(binString, (m) => m.codePointAt(0));
}
function bytesToBase64(bytes) {
const binString = String.fromCodePoint(...bytes);
return btoa(binString);
}
const validUTF16String = 'hello⛳❤️🧀';
const validUTF16StringEncoded = bytesToBase64(
new TextEncoder().encode(validUTF16String)
);
console.log(`Encoded string: [${validUTF16StringEncoded}]`); // Encoded string: [aGVsbG/im7PinaTvuI/wn6eA]
const validUTF16StringDecoded = new TextDecoder().decode(
base64ToBytes(validUTF16StringEncoded)
);
console.log(`Decoded string: [${validUTF16StringDecoded}]`); // Decoded string: [hello⛳❤️🧀]
다만 위 코드는 올바른 유니코드에 대해서만 올바르게 동작한다.
론 서로게이트에 대해서는 아래와 같은 일이 발생한다.
// ...
// 함수 선언은 동일
const partiallyInvalidUTF16String = 'hello⛳❤️🧀\uDE75';
const partiallyInvalidUTF16StringEncoded = bytesToBase64(
new TextEncoder().encode(partiallyInvalidUTF16String)
);
console.log(`Encoded string: [${partiallyInvalidUTF16StringEncoded}]`); // Encoded string: [aGVsbG/im7PinaTvuI/wn6eA77+9]
const partiallyInvalidUTF16StringDecoded = new TextDecoder().decode(
base64ToBytes(partiallyInvalidUTF16StringEncoded)
);
console.log(`Decoded string: [${partiallyInvalidUTF16StringDecoded}]`); // Decoded string: [hello⛳❤️🧀�]
최신 브라우저에는 올바른 형식의 문자열인지 확인하기 위한 함수인 isWellFormed가 존재한다.
이를 통해 론 서로게이트로 인해 인코딩/디코딩 과정에서 문자열이 깨지는 것을 방지할 수 있다.
// ...
// 함수 선언은 동일
function isWellFormed(str) {
if (typeof str.isWellFormed !== 'undefined') {
return str.isWellFormed();
} else {
// 구형 브라우저를 위함
try {
encodeURIComponent(str);
return true;
} catch (error) {
return false;
}
}
}
const validUTF16String = 'hello⛳❤️🧀';
const partiallyInvalidUTF16String = 'hello⛳❤️🧀\uDE75';
if (isWellFormed(validUTF16String)) {
const validUTF16StringEncoded = bytesToBase64(
new TextEncoder().encode(validUTF16String)
);
console.log(`Encoded string: [${validUTF16StringEncoded}]`); // Encoded string: [aGVsbG/im7PinaTvuI/wn6eA]
const validUTF16StringDecoded = new TextDecoder().decode(
base64ToBytes(validUTF16StringEncoded)
);
console.log(`Decoded string: [${validUTF16StringDecoded}]`); // Decoded string: [hello⛳❤️🧀]
} else {
// ... 처리
}
if (isWellFormed(partiallyInvalidUTF16String)) {
// ... 처리
} else {
console.log(
`Cannot process a string with lone surrogates: [${partiallyInvalidUTF16String}]`
); // Cannot process a string with lone surrogates: [hello⛳❤️🧀\uDE75]
}