[Javascript]자바스크립트에서의 이모지(Emoji) 파싱
유니코드
우선 본론에 들어가기 앞서 유니코드에 대해 약간의 지식이 필요합니다.
- 기본적으로 알아두어야할 용어는 다음과 같습니다.
- 코드 포인트(Code point)
- 특정 유니코드 문자의 숫자 표현입니다. - 문자 코드(Character Code)
- 코드 포인트의 또 다른 명칭입니다. - 코드 유닛(Code Unit)
- 지정된 인코딩 형식 내에서 레퍼토리의 각 문자를 인코딩하는데 사용되는 비트시퀀스입니다.
Javascript의 경우 UTF-16을 사용합니다. - 10진법(Decimal)
- 10진수로 코드포인트를 표현하는 방법입니다. - 16진법(Hexadeciaml)
- 16진수로 코드포인트를 표현하는 방법입니다.
예를 들어 설명해 봅시다. 'A'라는 문자가 있습니다.
문자 A는 코드포인트 65(10진법) 또는 41(16진법)로 표현됩니다.
// 문자열에서 10진수 코드포인트로
'A'.codePointAt(0)
> 65
// 10진법에서 16진법으로
Number(65).toString(16);
> 41
// 16진법에서 10진법으로
parseInt('41',16)
> 65
// 또는
0x0041
> 65
// 10진법 코드 포인트에서 문자열로
String.fromCodePoint(65)
> 'A'
// 16진법 코드 포인트에서 문자열로
'\u0041'
> 'A'
codePointAt 과 fromCodePoint는 이모지를 포함하는 UTF-16 인코딩이 16비트보다 큰 유니코드 문자를 처리할 수 있도록 ES2015에 도입 된 새로운 메서드입니다. 이모티콘을 제대로 처리하지 못하는 charCodeAt 대신 사용하는 것이 좋습니다.
다음 예시를 통해 확인해보겠습니다.
console.log("😀".charCodeAt(0))
// 55357 출력, 값이 일치하지 않음
console.log("😀".codePointAt(0))
// 128512 출력, 값 일치
자바스크립트에서 16진수 표현에 대해 주의해야 할 몇 가지 내용
- 모든 16진수 코드포인트는 4자리입니다.
- 코드가 4자 미만인 경우 0으로 채워야합니다.
// 유효하지 않습니다.
\u41
// 올바른 표현방식입니다.
\u0041
- 모든 16진수 코드포인트는 대소문자를 구분하지 않습니다.
// 다음 내용은 같습니다.
"\uD83D"
"\ud83d"
- 두 가지 형식으로 표현 가능합니다.
- Javascript에서 16진수는 \u0041 과 0x0041 두가지 방식으로 표현 할 수 있습니다.
브라우저 콘솔에서는 다음과 같은 내용이 표시됩니다.
String.fromCodePoint(0x0041);
> 'A'
'\u0041';
> 'A'
이모지(Emoji)
원래 코드 포인트의 범위는 영어 알파벳을 포함하는 16비트였습니다. 하지만 이제는 기존 범위 외에도 선택할수 있는 유니코드평면이 16개 추가되어 총 17개의 유니코드 평면을 가지게 되었습니다.
BMP를 넘어선 나머지 평면은 이모지를 포함하는 아스트랄 평면(Astral Plane)이라고 합니다. 이모지는 다국어 보조 평면(Supplementary Multilingual Plane)인 평면1(Plane 1)에 존재하고 있습니다.
다음의 결과값은 무엇일까요?
"😀".length
정답은 1이 아닌 2입니다!
자바스크립트에서 문자열은 16비트 코드 포인트의 시퀀스입니다. 이모지는 BMP위에 인코딩되기 때문에 서로게이트(Surrogate) 쌍이라고도 하는 한 쌍의 코드 포인트로 표현된다는 것을 의미합니다.
예를 들어 😀인 0x1F600 은 다음과 같이 표현됩니다.
"\uD83D\uDE00"
위의 서로게이트 쌍을 브라우저의 콘솔에 복사하면 😀이 표시됩니다. Javascript는 이 문자쌍을 길이 2로 해석하기 때문에 다음과 같은 작업을 수행할 수 없습니다.
"abc😀".split('')
> ["a", "b", "c", "�", "�"]
그렇다면 서로게이트 쌍을 얻을 수 있는 방법은 없을까요? 아래 코드를 보시면 다양한 방법을 확인할 수 있습니다.
function toUTF16 (codePoint) {
var TEN_BITS = parseInt ( '1111111111', 2);
function u (codeUnit) {
return '\\ u'+ codeUnit.toString (16) .toUpperCase ();
}
if (codePoint <= 0xFFFF) {
return u (codePoint);
}
codePoint-= 0x10000;
// 최상위 10 비트로 이동하려면 오른쪽으로 이동
var leadSurrogate = 0xD800 + (codePoint >> 10);
// 최하위 10 비트를 얻기 위해 마스크
var tailSurrogate = 0xDC00 + (codePoint & TEN_BITS);
return u (leadSurrogate) + u (tailSurrogate);
}
// 이모티콘을 십진수로 표현
"😀".codePointAt (0)
> 128512
// 10진수를 이모지로
String.fromCodePoint (128512)
> "😀"
// 10 진수에서 16 진수로 변환하려면
// toUTF16을 사용할 수 있습니다.
// 10 진수에서 16 진수로
toUTF16 (128512)
> "\ uD83D \ uDE00"
// 이모지를 16 진수로
"\ uD83D \ uDE00"
> "😀"
자바스크립트 내의 이러한 제한 때문에 이모티콘이 포함된 문자열을 분석하려면 약간의 방법이 필요합니다.
정규식 작성
정규식을 작성하기 위해 이모지의 유니코드 범위를 알아야 합니다.
위키백과 이모지 항목에는 다음과 같은 범위의 이모지가 있습니다. (이 중 많은 이모지는 미래의 이모지가 지정될 범위의 값도 가지고 있는 것으로 추정됩니다.)
- Dingbats(딩뱃 이미지폰트) (U+2700 to U+27BF, 33 out of 192 of which are emoji)
- Miscellaneous Symbols and Pictographs(기타기호 및 그림) (U+1F300 to U+1F5FF, 637 of 768 of which are emoji)
- Supplemental Symbols and Pictographs(추가기호 및 그림) (U+1F900 to U+1F9FF, 80 out of 82 of which are emoji)
- Emoticons(이모티콘) (U+1F600 to U+1F64F)
- Transport and Map Symbols(운송 및 지도 기호) (U+1F680 to U+1F6FF, 92 out of 103 of which are emoji)
- Miscellaneous Symbols(기타기호) (U+2600 to U+26FF, 77 out of 256 of which are emoji)
딩벳
- 범위는 U + 2700에서 U + 27BF까지이므로 정규식은 다음과 같습니다.
[\u2700-\u27bf]
/[\u2700-\u27bf]/.test('')
> true
기타기호 및 그림
- 범위는 U + 1F300에서 U + 1F5FF까지이며 다음과 같은 서로 게이트 쌍이 있습니다.
toUTF16(0x1F300)
> "\uD83C\uDF00"
toUTF16(0x1F5FF)
> "\uD83D\uDDFF"
// The regex for this range, from lodash’s implementation
[\ud800-\udbff][\udc00-\udfff]
/[\ud800-\udbff][\udc00-\udfff]/.test(String.fromCodePoint(0x1F5FF))
> true
/[\ud800-\udbff][\udc00-\udfff]/.test(String.fromCodePoint(0x1F300))
> true
추가기호 및 그림
- U + 1F900에서 U + 1F9FF까지, 다음과 같은 서로 게이트 쌍을 사용합니다.
toUTF16(0x1F910)
> "\uD83E\uDD10"
toUTF16(0x1F9C0)
> "\uD83E\uDDC0"
//기타 기호 정규식 재사용
/[\ud800-\udbff][\udc00-\udfff]/.test(String.fromCodePoint(0x1F910))
> true
/[\ud800-\udbff][\udc00-\udfff]/.test(String.fromCodePoint(0x1F9C0))
> true
이모티콘
- U + 1F600에서 U + 1F64F까지, 다음과 같은 서로 게이트 쌍을 사용합니다.
toUTF16(0x1F600)
> "\uD83D\uDE00"
toUTF16(0x1F64F)
> "\uD83D\uDE4F"
// regex
/[\ud800-\udbff][\udc00-\udfff]/.test(String.fromCodePoint(0x1F600))
> true
/[\ud800-\udbff][\udc00-\udfff]/.test(String.fromCodePoint(0x1F64F))
> true
운송 및 지도 기호
- U + 1F680 부터 U + 1F6FF까지, 다음과 같은 서로 게이트 쌍을 사용합니다.
toUTF16(0x1F680)
> "\uD83D\uDE80"
toUTF16(0x1F6FF)
> "\uD83D\uDEFF"
// regex
/[\ud800-\udbff][\udc00-\udfff]/.test(String.fromCodePoint(0x1F680))
> true
/[\ud800-\udbff][\udc00-\udfff]/.test(String.fromCodePoint(0x1F6FF))
> true
기타 기호
- U + 2600 부터 U + 26FF까지, 다음과 같은 서로 게이트 쌍을 사용합니다.
toUTF16(0x2600)
> "\u2600"
toUTF16(0x26FF)
> "\u26FF"
// regex
/[\u2600-\u26FF]/.test(String.fromCodePoint(0x2600))
> true
/[\u2600-\u26FF]/.test(String.fromCodePoint(0x26FF))
> true
최종 정규식
또 다른 기타 기호
- 상단의 정규식으로도 걸러지지 않는 이모지들이 있는데, 이는 Wikipedia 의 Unicode Block Emoji 항목 하단 에서 이유를 찾을 수 있습니다.
Additional emoji can be found in the following Unicode blocks: Arrows (8 codepoints considered emoji), Basic Latin (12), CJK Symbols and Punctuation (2), Enclosed Alphanumeric Supplement(41), Enclosed Alphanumerics (1), Enclosed CJK Letters and Months (2), Enclosed Ideographic Supplement (15), General Punctuation (2), Geometric Shapes (8), Latin-1 Supplement (2), Letterlike Symbols (2), Mahjong Tiles (1), Miscellaneous Symbols and Arrows (7), Miscellaneous Technical (18), Playing Cards (1), and Supplemental Arrows-B (2).
쉽게 말하자면 위에 서술된 유니코드 블럭에서도 이모지가 존재하기 때문에 위의 정규식으로 걸러지지 않는 이모지가 존재할 수 있다는 내용입니다. 그러므로 서술된 유니코드 블럭과 위에서 설명한 모든 내용을 포함한 정규식은 다음과 같습니다.
(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])
이 글이 javascript에서 이모지를 파싱하는데 조금의 도움이 되었길 바랍니다.