상세 컨텐츠

본문 제목

[이모지와 유니코드]"👫" 얘랑 "👩👨" 얘가 같은 문자로 보이나요?

소프트웨어

by moonionn 2023. 11. 1. 23:59

본문

(공부하기 싫어서 잼는 글 써보기)

 

며칠 전 친구와 대화 도중, 맥(Mac)환경에서 하얀피부의 사람신체 이모지를 보내면 윈도우 환경을 쓰는 친구에게는 사람신체 + 웬 사각형 블락이 표기된다는 사실을 알게 되었습니다.

 

관련해서 또 재밌는 사실 하나 알려드리자면.. 사람 여러명 붙어있는 이모지에다 백스페이스를 사용해 지우려 하면 사람이 하나씩 없어진다는 사실을 아시나요?

 

오늘은 이와 관련해서 유니코드에 대한 이야기를 조금 해볼까 합니다.

 

유니코드란?

우리가 컴퓨터로 보는 이모지 글자 형태는 유니코드(Unicode)로 구현됩니다. 유니코드는 현존하는 대부분의 문자를 표현할 수 있죠. 이모지도 여기 포함됩니다. 그렇다면 유니코드는 어떻게 이 세상에 존재하는 그 많은 글자들을 표현할 수 있는걸까요?


컴퓨터는 0과 1로만 동작한다는 이야기는 이 분야 전문가가 아니더라도 한 번쯤은 들어봤을법할 정도로 유명합니다. 여기서 0,1이 들어가는 자리의 단위를 비트(bit)라고 부르죠. 그리고 유니코드는 32비트를 활용할 수 있습니다. 그럼 한 비트 당 두 종류의 표현법(0,1)을 쓸 수 있는 거니까, 유니코드는 2의 32제곱, 즉 4,294,967,296개의 문자를 구현할 여유가 있습니다. (참고로 사실 정말 42억대의 글자를 표현할 수 있는 건 아닙니다만 이 이야기는 여기서 서술하지 않도록 하겠습니다.) 

 

유니코드 표현식

유니코드는 코드 포인트(code point)라는 이름의 각자 고유한 코드를 가지고 있습니다. 이는 U+ (혹은 \u) 뒤에 16진수 넘버링 규칙을 준수하며, 범위는 U+0000 부터 U+10FFFF 까지입니다.

U+270B  > '✋'
U+0041  > 'A'
U+0023  > '#'

 

JAVA

String str="\u270B"
str ==> "✋"

String str="\u0041"
str ==> "A"

String str="\u0023"
str ==> "#"

 

Javascript

str='\u{270B}'
'✋'

str='\u{0041}'
'A'

str='\u{0023}'
'#'

 

유니코드 조합

재밌는 사실은, 어떤 문자(character)들은 여러 코드 포인트들의 조합으로 구성되었다는 점입니다. 👨🏻‍🌾 여기 농부 아저씨 이모지를 예시로 들어보겠습니다. 이 농부를 표현하기 위해 필요한 유니코드 코드 포인트는 총 몇개일까요? 

 

Javascript

farmer='\u{1F468}\u{1F3FB}\u{200D}\u{1F33E}'
'👨🏻‍🌾'

 

 

 

답은 총 네 개 입니다. 코드 포인트를 하나하나 살펴보면 아래와 같습니다. 성인 남성 + 밝은 피부 톤 + 빈 공백 + 수확물 이모지의 조합으로 구성되었다는 것이 파악됩니다. 하나하나 살펴보면 밝은 피부의 성인 남성이 수확물을 들고 있는 걸 표현했구나... 라는걸 가늠할 수 있습니다만, 중간에 껴있는 +U200D 이건 뭘까요? 

 

+U200D 코드는 흔히 Zero Width Joiner, 줄여서 ZWJ라 불립니다. (즈윋ㅈ..? 라고 발음하는듯...?) 이는 화면에 실제로 표현되는 문자는 아니며, 어떤 문자 뒤에 ZWJ가 붙으면 또 다른 문자와 조합해 새로운 문자를 표현한다고 볼 수 있습니다. 그래서 농부 = 사람 + 수확물 이기 때문에 ZWJ가 붙은 것이죠. 또 재밌는 사실을 말씀드리자면 국기 이모지도 사실 ZWJ를 사용한 여러 이모지의 조합입니다. 네모박스 K와 네모박스 R을 조합하면 태극기가 나온다는 사실 알고 계셨나요?

 

이 외에도 ZWJ는 다른 언어에도 활용되곤 합니다. 아랍어, 말레이시아어 등 여러 국가들의 언어를 표현할 때도 필요한 요즘 시대 필수 유니코드라고 할 수 있죠.

 

여기서 재밌는 사실은 저 농부 이모지(👨🏻‍🌾)를 보면 피부색 조합에는 ZWJ가 사용되지 않음을 확인할 수 있습니다. 이는 피부색 조합의 경우 Fitzpatrick Skin Modifier 라는 유니코드가 붙으면 자동으로 피부색 결합이 되기 때문입니다. FitzPatrick Skin Modifier 유니코드 코드 포인트 범위는 +U1F3FB 부터 +U1F3FF 로 구성되어 있습니다. 

 

그렇다고 아무 이모지나 스킨 모디파이어와 조합할 순 없겠죠? (ex: 🎄 + white skin tone ????) 스킨 모디파이어와 조합할 수 있는 이모지인지를 판별할 수 있는 기능을, 자바스크립트는 내장 지원한다는 사실을 아시나요?

 

> /\p{Emoji_Modifier_Base}/u.test('🙋')
true

> /\p{Emoji_Modifier_Base}/u.test('🎄')
false

 

 

자바스크립트 같은 경우 위처럼 Emoji_Modifier_Base 라는 regexp를 별도 선언 없이 사용하면 스킨톤 변경이 가능한 이모지인지 아닌지를 판별해줍니다. 정말 편리한 기능이지 않습니까!!

 

실생활 적용

특정 텍스트를 20바이트까지만 잘라서 가공해줘~ 라는 요청이 들어왔다고 칩시다. 해당 문자는 이모지를 포함한 UTF-8 인코딩 텍스트라 가정합시다. (유니코드와 UTF-8에 대해서는 나중에 2탄 쓸 일 있으면 그때 서술하겠습니다 😅. UTF-8에 대해 잘 모르신다면 지금은 그냥 그렇다 치고 이해 바랍니다.)

 

예시 텍스트는 아래와 같습니다.

const text = '놓칠 수 없는 가족 세일!👨‍👩‍👧‍👦 오늘부터 10일간 진행됩니다';

 

만약 이런 이모지 특성을 고려하지 않고 텍스트를 트림(trim)해버린다면 무슨 일이 발생할까요?

function trimTextToBytes(text) {
  const MAX_BYTES = 50;

  let trimmedText = text;
  while (Buffer.byteLength(trimmedText, 'utf8') > MAX_BYTES) {
    trimmedText = trimmedText.slice(0, -1);
  }
  return trimmedText;
}

const text = '놓칠 수 없는 가족 세일!👨‍👩‍👧‍👦 오늘부터 10일간 진행됩니다';
const trimmedText = trimTextToBytes(text);

 

 

결과는 위와 같습니다. 이모지 결합을 고려하지 않아 아들래미가 빠진 이모지가 표현되게 됩니다. 아들램을 제거함으로써 50 바이트 리밋을 충족하기는 하나, 기존 텍스트와는 좀 다른 형상이 표현되게 되는 것이죠. 앞서 말한 내용들을 고려해 코드를 수정하면 이런 상황을 피할 수 있습니다.

 

function trimTextToBytes(text) {
  const MAX_BYTES = 50;
  const EMOJI_REGEXP = /([\uD800-\uDBFF][\uDC00-\uDFFF])/;
  const EMOJI_ZERO_WIDTH_JOINER = '\u200D';

  let trimmedText = '';
  let detectedEmoji = '';
  const splitTextByEmoji = text.split(EMOJI_REGEXP);

  splitTextByEmoji.forEach((text) => {
    if (
      Buffer.byteLength(trimmedText + text + detectedEmoji, 'utf8') < MAX_BYTES
    ) {
      const isPlainText =
        !EMOJI_REGEXP.test(text) && text !== EMOJI_ZERO_WIDTH_JOINER;

      if (isPlainText) {
        if (detectedEmoji) {
          trimmedText += detectedEmoji;
          detectedEmoji = '';
        }
        trimmedText += text;
      } else {
        detectedEmoji += text;
      }
    }
  });
  return trimmedText;
}

const text = '놓칠 수 없는 가족 세일!👨‍👩‍👧‍👦 오늘부터 10일간 진행됩니다';
const trimmedText = trimTextToBytes(text);
console.log(trimmedText);

 

 

관련글 더보기

댓글 영역