← 모두의 툴

텍스트를 뒤집으면 왜 이모지가 깨지는가 — 서로게이트 쌍의 함정

가이드 · 2026-08-19 최종 확인

"Hi😀!"를 뒤집었더니 이모지 대신 깨진 네모(□)나 물음표가 나온 경험이 있다면, 그건 우연한 버그가 아니라 자바스크립트 문자열이 내부적으로 어떻게 저장되는지에서 비롯된 예정된 함정입니다. 이 가이드는 왜 순진하게 짠 문자열 반전 코드가 이모지를 깨뜨리는지, 그리고 어떻게 고치는지 설명합니다.

1. 자바스크립트 문자열의 진짜 단위: UTF-16 코드유닛

자바스크립트 문자열은 사람이 생각하는 "글자" 단위가 아니라 UTF-16 코드유닛(code unit) 단위로 저장됩니다. 알파벳·한글 대부분·기본 특수문자는 코드유닛 1개로 표현되지만, 대부분의 이모지와 일부 희귀 한자는 유니코드 코드포인트가 U+FFFF를 넘는 영역(Supplementary Plane)에 있어서 코드유닛 2개가 한 쌍으로 묶여야만 의미를 가집니다. 이 쌍을 서로게이트 쌍(surrogate pair)이라 부릅니다. 하이 서로게이트(High Surrogate)와 로우 서로게이트(Low Surrogate)가 정해진 순서로 붙어 있을 때만 브라우저가 이를 하나의 이모지로 렌더링합니다.

2. 문제가 터지는 지점: split('')과 .length

많은 문자열 반전 코드가 str.split('').reverse().join('') 형태로 짜여 있습니다. 여기서 split('')은 문자열을 "코드유닛" 단위로 쪼갭니다 — 사람이 보는 "글자" 단위가 아닙니다. 이모지 하나가 코드유닛 2개(서로게이트 쌍)로 이루어져 있다면, split('')은 이 쌍을 강제로 둘로 쪼개버립니다. 그 상태에서 .reverse()를 하면 원래 붙어있던 하이/로우 서로게이트가 서로 떨어지거나 순서가 뒤바뀌어, 어느 쪽이든 "쌍이 안 맞는" 상태가 됩니다. 쌍이 안 맞는 서로게이트는 유효한 문자를 가리키지 않으므로 브라우저는 이를 깨진 문자(□, replacement character 등)로 표시합니다.

개념적 예시: "Hi😀!"는 코드유닛 기준으로 ['H','i','😀'의 하이서로게이트,'😀'의 로우서로게이트,'!'] 5개로 쪼개집니다. 이걸 그대로 .reverse()하면 ['!', 로우서로게이트, 하이서로게이트, 'i', 'H']가 되는데, 로우서로게이트가 하이서로게이트보다 에 오는 순서는 유효하지 않은 서로게이트 배열이라 이모지가 깨집니다.

3. 올바른 해법: 코드포인트 단위 반전

ECMAScript(자바스크립트 표준) 스펙은 [...str] 스프레드 문법과 Array.from(str)을 문자열의 코드포인트(code point) 단위로 순회하도록 정의합니다. 즉 서로게이트 쌍을 자동으로 인식해서 하나의 단위로 묶어줍니다. [...str].reverse().join('')으로 바꾸면, 서로게이트 쌍이 통째로 이동하기 때문에 하이/로우 순서가 유지되고 이모지가 깨지지 않습니다.

방식분해 단위이모지 안전성
str.split('')UTF-16 코드유닛❌ 서로게이트 쌍 분리 위험
[...str] / Array.from(str)유니코드 코드포인트✅ 서로게이트 쌍 보존
Intl.Segmenter그래프임 클러스터(사람이 보는 "글자")✅✅ ZWJ 합성 이모지까지 보존

4. 코드포인트 반전으로도 못 막는 것: 그래프임 클러스터

다만 [...str]이 만능은 아닙니다. 국기 이모지(🇰🇷)는 지역 표시 기호(Regional Indicator) 코드포인트 2개가 나란히 붙어 하나의 국기로 렌더링되는데, 이 둘은 서로게이트 쌍이 아니라 서로 다른 두 개의 코드포인트라서 코드포인트 단위 반전에서도 따로따로 취급됩니다. 가족 이모지(👨‍👩‍👧‍👦)처럼 ZWJ(zero-width joiner)로 여러 이모지를 하나로 합친 경우도 마찬가지입니다. 이런 "사람이 보는 진짜 한 글자(그래프임 클러스터)"까지 정확히 보존하려면 더 최신 API인 Intl.Segmenter가 필요합니다 — 코드포인트보다 한 단계 더 높은 수준에서 텍스트를 분해해줍니다.

5. 이 도구는 어떻게 처리하는가

modoohub의 텍스트 뒤집기 도구는 실제 소스 코드에서 [...input].reverse().join('') 방식(텍스트 전체 뒤집기)과 각 단어에 대해서도 [...seg].reverse().join('') 방식(단어별 뒤집기)을 사용합니다. 즉 처음부터 split('')이 아니라 스프레드 연산자로 코드포인트 단위 반전을 적용하고 있어, 일반적인 이모지(서로게이트 쌍으로 구성된 것)는 깨지지 않고 그대로 유지됩니다. 다만 위에서 설명한 국기·ZWJ 합성 이모지처럼 여러 코드포인트로 이루어진 그래프임 클러스터는 Intl.Segmenter 없이는 순서가 흐트러질 수 있다는 점은 이 도구를 포함한 코드포인트 단위 반전 방식 전반의 공통적인 한계입니다.

자주 묻는 질문

Q. 왜 하필 이모지에서만 이 문제가 생기나요?

A. 영어·한글 대부분의 글자는 유니코드 코드포인트가 U+FFFF 이하라서 UTF-16 코드유닛 1개로 표현됩니다. 반면 대부분의 이모지는 코드포인트가 U+FFFF를 넘는 영역(예: 😀는 U+1F600)에 있어서 코드유닛 2개(서로게이트 쌍)가 필요합니다. 그래서 코드유닛 단위로 자르는 연산에서만 이 문제가 터집니다.

Q. .length로 이모지 개수를 세면 왜 틀리나요?

A. .length는 코드유닛 개수를 반환하므로, 서로게이트 쌍으로 이루어진 이모지 하나는 길이 2로 계산됩니다. "😀".length는 1이 아니라 2입니다. 정확한 "글자 수"를 세려면 [...str].length나 Intl.Segmenter를 써야 합니다.

Q. modoohub 텍스트 뒤집기 도구는 이 버그가 있나요?

A. 없습니다. 이 도구는 처음부터 [...text] 스프레드 연산자로 코드포인트 단위 반전을 구현하고 있어 일반적인 이모지는 깨지지 않습니다. 다만 국기 이모지나 ZWJ 합성 이모지처럼 여러 코드포인트가 결합된 경우는 코드포인트 단위 반전의 공통적인 한계로 순서가 흐트러질 수 있습니다.

Q. 팰린드롬(회문) 검사도 같은 문제를 겪나요?

A. 네. split('')과 reverse()로 팰린드롬을 검사하는 코드는 이모지가 포함된 문자열에서 잘못된 결과를 낼 수 있습니다. 코드포인트 단위 비교를 쓰거나 전용 팰린드롬 체커를 사용하는 것이 안전합니다.