본문으로 건너뛰기

JavaScript emoji utils

무료2018-09-15#JS#Solution#emoji length#string including emoji#JavaScript emoji表情#emoji表情截断#emoji字符长度

JavaScript 의 문자열 처리는 어려워 보이지 않지만, emoji 를 만날 때까지

서문에

JavaScript 의 문자열 처리는 어려워 보이지 않지만, emoji 를 만날 때까지:

[caption id="attachment_1801" align="alignnone" width="626"]javascript-emoji-issues javascript-emoji-issues[/caption]

??무슨 일이 일어난 것인가?도대체 어떻게 된 것인가?

Unicode 부호화부터 설명해야 합니다……

一.Unicode 부호화

The Unicode codepoint range goes from U+0000 to U+10FFFF which is over 1 million symbols, and these are divided into groups called planes. Each plane is about 65000 characters (16^4). The first plane is the Basic Multilingual Plane (U+0000 through U+FFFF) and contains all the common symbols we use everyday and then some. The rest of the planes require more than 4 hexadecimal digits and are called supplementary planes or astral planes.

즉, Unicode 가 지원하는 부호화 범위는 U+0000 에서 U+10FFFF 로, 100 만 개가 넘는 기호에 대응할 수 있습니다(0x10FFFF === 1114111). 이러한 기호들은 16 개의*평면(panel)*으로 그룹화되어 있어, 각 평면에 65536(16^4 === 65536)개가 배치됩니다

그 중에서,常用 기호들은 모두 첫 번째 평면(U+0000 에서 U+FFFF)에 배치되어 있으므로, *기본 다언어 평면(Basic Multilingual Plane, 약칭 BMP)이라고 불립니다. 나머지 평면 중의코드 포인트 값(codepoint, 즉 기호에 대응하는 Unicode 부호화 값)*은 모두 4 자리(16 진수)보다 크며, *보조 평면(supplementary plane)*이라고 불립니다

P.S. 보조 평면에는매우 대단해 보이는이름도 있어, astral plane(성계?성계 위면?)이라고 불립니다

I have no idea if there's a good reason for the name "astral plane." Sometimes, I think people come up with these names just to add excitement to their lives.

더욱이, 기본 다언어 평면 내의 65536 개 위치의 입주율은 100% 가 아니며, 일부 위치를专门 비워 불시의 수요에 대비하고 있습니다. 예를 들어, 새로운 특수한 의미의 기호를 추가하거나 확장하기 위해서입니다

예를 들어 UTF-16 중의*서로게이트 페어(surrogate pairs)*의 개념으로, 2 개의 4 자리(16 진수)작은 코드 포인트 값으로 1 개의 큰 코드 포인트 값(4 자리보다 큼)을 나타냅니다. 기본 다언어 평면에서 보조 평면으로의 매핑의 일종입니다. 이것이 가능한 이유는 다음과 같습니다:

基本多言語平面内、U+D800 から U+DFFF までのコードポイント区間は Unicode 文字にマッピングされないよう永久に保留されています。UTF-16 はこの保留された 0xD800-0xDFFF 区間のコードポイントを利用して補助平面の文字のコードポイントを符号化します。

二.JavaScript 중의 Unicode

JS 중의 Unicode 문자에는 3 종류의 표현 방법이 있습니다:

'A' === '\u0041' === '\x41' === '\u{41}'

그 중에서 \xU+0000 에서 U+00FF 까지만 사용되며, \u 는 임의의 Unicode 문자(U+0000 에서 U+10FFFF)에 적용할 수 있지만, 4 자리보다 큰(U+FFFF 보다 큼)경우, 꽃괄호({})로 16 진수 시퀀스를 감싸야 합니다:

The \x can be used for most (but not all) of the Basic Multilingual Plane, specifically U+0000 to U+00FF. The \u can be used for any Unicode characters. The curly braces are required if there are more than 4 hexadecimal digits and optional otherwise.

주의, \u{} 이스케이프 구문은 ES 2015 에서 정의되었으며, UnicodeEscapeSequence 라고 불립니다. 이전에는 2 개의 작은 Unicode 로 1 개의 큰 Unicode 를 나타냈습니다. 예를 들어:

'' === '\u{1F4A9}'
'' === '\uD83D\uDCA9'

\uD83D\uDCA9 가 서로게이트 페어로, <H,L> 의 형태를 하고 있으며, 이둘의 변환 관계는 다음과 같습니다:

let C, L, H;
C = 0x1F4A9;

// 公式:大 Unicode 转代理对儿
H = Math.floor((C - 0x10000) / 0x400) + 0xD800;
L = (C - 0x10000) % 0x400 + 0xDC00;

[H, L].map(v => '\\u' + v.toString(16).toUpperCase()).join('')
"\uD83D\uDCA9"

더욱이, JS 에서는 16 비트 부호 없는 정수 값을 1 문자로 간주하므로, 1 개의 emoji 는 여러 문자로 간주될 수 있습니다:

The phrase code unit and the word character will be used to refer to a 16-bit unsigned value used to represent a single 16-bit unit of text.

Unicode character only refers to entities represented by single Unicode scalar values: the components of a combining character sequence are still individual "Unicode characters", even though a user might think of the whole sequence as a single character.

P.S. JavaScript 의 Unicode 지원 및 ES 규범의 관련 내용에 대해서는, JavaScript's internal character encoding: UCS-2 or UTF-16? 참조

正規表現中의 Unicode

큰 Unicode(U+FFFF 보다 큼)가 JS 중에서 2 개의 작은 Unicode(서로게이트 페어)로 표현된다면, 당연히 이러한 정규 표현식을 쓰고 싶어집니다:

> /[\uD83D\uDCA9-\uD83D\uDE0A]/.test('')
Uncaught SyntaxError: Invalid regular expression: /[\uD83D\uDCA9-\uD83D\uDE0A]/: Range out of order in character class

이러한 range 를 식별할 수 없다는 오류가 발생하지만, 어떻게 정규 표현식으로 큰 Unicode 문자 범위를 기술할 수 있을까요?

JS 는 u flag 를 제공하여 이 문제를 해결합니다:

u Unicode; treat pattern as a sequence of Unicode code points

/[\uD83D\uDCA9-\uD83D\uDE0A]/u.test('')
/[-]/u.test('')

유사하게, .(닷은 임의의 문자에 매치)가 서로게이트 페어 형태의 큰 Unicode 에 매치하고 싶다면, u flag 를 활성화해야 합니다:

> /foo.bar/.test('foobar')
false
> /foo.bar/u.test('foobar')
true

P.S././u 는 서로게이트 페어 형태의 emoji 에만 매치할 수 있으며, 다른 형태는 불가능합니다. 예를 들어:

> /foo.bar/u.test('foo2??bar')
false

P.S. 더 많은 관련 예제에 대해서는, Astral ranges in character classes 참조

fromCodePoint 과 fromCharCode

String.fromCodePointString.fromCharCode 의 차이는, 전자가 더 큰 범위의 16 진수 Unicode 부호화를 지원한다는 것입니다. 예를 들어:

> String.fromCodePoint(0x1F4A9)
""
> String.fromCharCode(0x1F4A9)
"?"

하지만 fromCodePoint 는 ES 2015 규범에서 정의되었으며, 호환성은 fromCharCode 보다 좋지 않습니다. 0x0000-0xFFFF 범위의 65536 개 Unicode 문자에 대해서는, fromCharCode 의 사용을 권장합니다

三.emoji 부호화

Unicode 와 유사하게, emoji 도 일종의 부호화 규칙이며, 대응하는 규범이 있고, 많은 버전이 존재합니다:

Emoji 12.0
Emoji 11.0
Emoji 5.0
Emoji 4.0
Emoji 3.0
Emoji 2.0
Emoji 1.0

그 중에서 12.0 은 2019 년에 릴리스 예정이며, 최신의 11.02018-02-07 에 릴리스되었습니다

HTML, CSS 규범과 유사하게, 신버전 규범에서 추가된 emoji 가 모두 구현된 것은 아니며, HTML, CSS 보다 심각한 호환성 문제에 직면하고 있습니다:

  • 규범 버전:emoji 규범의 릴리스가 빈번하며, 여러 버전이 공존

  • 플랫폼 차이:Web 브라우저 환경 외에, emoji 는 플랫폼의 네이티브 지원(각종 화면 표시 디바이스)에 의존

  • Unicode 에의 의존:emoji 는 Unicode 를 기초로建立되었으며, Unicode 규범에 의존

예를 들어, SMS 에서 Web 페이지 입력 상자에 복사 붙여넣기하면, emoji 가 표시되지 않거나 문자 깨짐이 발생할 수 있습니다. native 와 Web 브라우저가 지원하는 emoji 규범 버전 또는 구현 정도에 차이가 있기 때문입니다. 더욱이, Unicode 신규범은 이미 정의된 emoji 규범과 충돌할 가능성이 있으며, 이 경우 당연히 emoji 규범이 양보해야 합니다:

Unicode 12.0 is the new version of the Unicode Standard planned for release in March 2019. See Emoji 12.0 for a more complete list of potential emojis for 2019.

Note: All emojis listed throughout 2018 are candidates only, and subject to change before a final release.

emoji 가 직면하는 환경은 얼마나 엄격할까요? 그림과 같습니다:

[caption id="attachment_1802" align="alignnone" width="625"]emoji-unicode-platform emoji-unicode-platform[/caption]

emoji 규범 자체로 돌아가면, 이렇습니다:

1F600 ;	emoji ;	L1 ;	secondary ;	x	# V6.1 () GRINNING FACE
1F48F ;	emoji ;	L1 ;	none ;	j	# V6.0 () KISS

왼쪽 끝은 Unicode 코드 포인트 값으로, Unicode 규범에 정상적으로 등록되면, U+1F48FKISS 이모티콘에 대응합니다:

> '\u{1F48F}'
""

이렇게 Unicode 와 일대일로 대응하는 emoji 외에, Unicode 대가족에加入하는 것 외에도, 몇 가지 특수한 emoji 가 있습니다:

  • variation selector-16:불가시 문자( U+FE0F)로, 그 앞의 문자를 emoji 로 표시해야 함을 나타냅니다

  • zero width joiner:제로 폭 접속 부호로, 제로 폭 스페이스(U+200D)이며, 여러 emoji 를 1 개의 emoji 로 결합하는 데 사용됩니다

  • tone modifier:피부색 수식으로, 일종의 구문으로, 앞의 emoji 의 피부색을 변경할 수 있습니다. 구문 형식은 <emoji>\ud83c[\udffb-\udfff] 로, U+D83C 뒤에 다른 값을 붙여 다른 피부색 제어를 나타냅니다

  • keycap:키캡 기호로, 키캡 스타일의 0-9, #* 로, U+20E3 로 끝납니다

  • unofficial emoji flag:매우 비규칙적인 국기 emoji 가 존재하며, 검은 깃발(U+1F3F4)로 시작하여, 취소 기호U+E007F)로 끝납니다

예를 들어:

// \ufe0f 让黑心字符显示成 emoji,连续两个也没关系
'???' === '\u2764\ufe0f\ufe0f'
'\u2764\ufe0f' === '??'
'\u2764' === '?'
// 零宽连接符\u200d 合成复杂表情, + ?? +  +  = ?????
'?????' == '\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69'
// 肤色修饰,黑 baby、白 baby
'' === '\ud83d\udc76\ud83c\udfff'
'' === '\ud83d\udc76\ud83c\udffb'
// 键帽样式
'#??' === '\u0023\ufe0f\u20e3'
// 非官方国旗
'' ==='\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f'

四.JavaScript 里的 emoji

그렇다면JS 중에서, 1 개의 emoji 는 도대체 몇 개의 Unicode 문자를 포함하고 있는가?

> '?'.length
1
> ''.length
2
> '1??'.length
3
> ''.length
4
> '???'.length
11
> ''.length
14

1 개의 emoji 리터럴의 길이는 1 에서 14(더 긴 것이 존재할 가능성도 있습니다)까지 다양합니다……따라서, 이러한 상황이 발생합니다:

> '???我们是一家人'.slice(0, 1)
"?"
> '???我们是一家人'.substr(0, 2)
""

slice(0, 1) 로 첫 번째 emoji 를 잘라내려고 기대했지만, 표시할 수 없는 문자를 얻어버렸고, 심지어 substr(0, 2) 로一家 4 구에서 Man()을 분해해 버렸습니다……이것은 어떻게 하면 좋은가?

일부 emoji 에 대해서는, 매우 간단한 처리 방법이 있습니다. Array.from 입니다:

> Array.from('').length
1

문자열을 배열로 변환할 때 서로게이트 페어를 함께 유지하므로, length 는 정확해지지만, 이 방법은 만능이 아닙니다:

> Array.from('???').length
7

P.S. 유사하게, Unicode 부호화 변환을 지원하는 bestiejs/punycode.js 에도 유사한 문제가 있습니다:

> punycode.ucs2.decode(' ').length
1
> punycode.ucs2.decode('???').length
7

즉, JS 의 Unicode 에 대한 네이티브 지원만으로는, emoji 를 포함한 문자열을 올바르게 처리할 수 없습니다. 따라서, 일부 장면에서 문제에 직면합니다:

  • 폼 검증의 글자 수 제한

  • 기사 요약 잘라내기

  • 문자열 반전

  • 문자 단위 처리

  • 정규 표현식 매치

  • ……기타 emoji 를 포함한 텍스트 처리 장면

예를 들어:

> '???一个打十个'.length >= 10 === true
true
> '你好 hi233..。'.substr(0, 10)
"你好 hi233?"
> Array.from('1??23').reverse().join('')
"32??1"
> '开心'[0] === ''
false
> /a.b/.test('ab')
false

P.S. JavaScript 중의 Unicode 의 더 많은 문제에 대해서는, JavaScript has a Unicode problem 참조

五.해결 방안:emoutils.js

위에 열거한 일련의 문제를 해결하려면, emoji 를 식별하는 방법을 생각할 수밖에 없습니다. 현재(2018/09/15)그런 도구 라이브러리는 존재하지 않는 것 같습니다

손으로 만듭니다. 詞法分析 과 유사하게, 문자 단위로 매치하여, emoji 부호화 규칙에 부합하는 Unicode 조합을 골라냅니다. 상세한 내용은 아래 소스 코드 참조

Github 주소:https://github.com/ayqy/emoji-utils

온라인 Demo(테스트 case):https://ayqy.github.io/emoji/index.html

API

6 개의 심플한 API 를 제공합니다:

// 是不是一个 emoji
isEmoji(str)
// 是否包含 emoji
containsEmoji(str)
// 字符串转 Unicode 数组
str2unicodeArray(str)
// 计算长度
length(str)
// 子串截取
substr(str = '', start = 0, len = Infinity)
// 字符串转数组,相当于 split('')
toArray(str)

내부에서 공개되지 않은 방법:

// 尝试匹配开头的 emoji,失败返回''
matchOneEmoji(str, matched = '')

欠陥

하지만, 이러한 도구 함수들은100% 신뢰할 수 없습니다. 왜냐하면:

Not all browsers, UIs, etc even render ????? as a single symbol. The code assumes the joiners are used between characters appropriately which could be very problematic.

따라서, emoutils.js구현은 3 가지 가정에 기반하고 있습니다:

  • 모든 서로게이트 페어는 emoji 이다(실제로는, 일부 서로게이트 페어는 emoji 가 아니다)

  • 피부색 제어는 모든 emoji 에 유효하며, emoji 에만 유효하다(일반 텍스트 기호에는 무효)

  • joiner 로 연결된 emoji 는 모두 1 개로 간주한다. 표시상에서 1 개의 emoji 로 합성될 수 있는지 여부는 묻지 않는다

첫 번째 가정에 대해, 서로게이트 페어 형태는 반드시 emoji 라고 할 수 없으며, 순수한 텍스트일 수도 있습니다. 예를 들어:

'\ud835\udc00' === ''

뒤의 두 가정도 몇 가지 badcase 를 일으킵니다. 예를 들어(Chrome Console 환경):

// 尝试制造黑色笑脸,未遂
'\ud83d\ude0a\ud83c\udfff' === ''
// 尝试人工合成新物种,失败
'\u0023\ufe0f\u20e3\u200d\ud83d\ude0a' === '#???'
'\ud83d\ude0a\u200d\ud83d\ude0a' === '?'

이러한 case 들은 모두 1 개의 emoji 로 식별되지만, Chrome Console 환경에서의 표시는 2 개입니다. 왜냐하면:

  • emoji 부호화의 구문 규칙에 부합

  • 하지만 합법적인 emoji 라고 할 수 없다

  • 합법적이라도, 현재 플랫폼이 지원한다고 할 수 없다

emoutils.js 는 첫 번째 점을 만족하는 것을 독립 표시의 합법적인 emoji 로 가정하며, emoji 규범 버전 및 플랫폼 지원성을 고려하지 않았기 때문에, 이러한 badcase 가 존재합니다. badcase 가 가져올 수 있는 영향은:

  • isEmoji/containsEmoji() 가''와 유사한 텍스트 문자를 오판단

  • length() 가 실제 표시 문자 길이보다 작다

  • substr()/toArray() 가 실제 예상과 일치하지 않는다

따라서 이 도구 라이브러리가 식별할 수 있는 문자 세트는 emoji 의 초집합으로, 추가된 일부는 서로게이트 페어 형태의 텍스트, 및 emoji 부호화 규칙에 부합하지만 emoji 규범 중에서 정의되지 않은 문자 시퀀스입니다. 그럼에도 불구하고, 실제 응용에서는 대다수의 장면에 대응하기에 충분합니다

P.S. emoji 를 정확하게 처리해야 하는 장면에 대해서는, emoji-regex 의 검토를 권장합니다

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성