
"이모지가 두 글자처럼 보이는 이상한 현상의 정체"
여러분이 한 번쯤 마주쳤을 만한 버그가 있어요. 어떤 사용자가 이름란에 이모지를 넣어서 가입했는데, DB에 저장하려고 보니까 글자 수가 이상하게 카운트되거나, 절반만 잘려서 들어가거나, 아예 에러가 터지는 그런 상황이요.
George Mandis라는 개발자가 자기 블로그에 "내가 가장 좋아하는 버그: 잘못된 서러게이트 쌍(Invalid Surrogate Pairs)"이라는 글을 올렸는데요. 단순한 버그 후일담을 넘어서, 우리가 매일 다루는 문자열에 숨어 있는 함정을 깔끔하게 설명해줘요.
서러게이트 쌍이라는 게 뭐냐면
이야기를 이해하려면 유니코드 역사를 잠깐 알아야 해요. 처음 유니코드가 설계됐을 때는 "전 세계 모든 문자를 65,536개 안에 다 담을 수 있다"고 생각했거든요. 그래서 2바이트(16비트)면 충분하다고 봤어요. 이게 UCS-2라는 옛 인코딩이고, 자바와 자바스크립트, 윈도우 내부 문자열이 다 이걸 기반으로 만들어졌어요.
그런데 시간이 지나면서 65,536개로는 부족해졌어요. 이모지, 옛 한자, 고대 문자 같은 게 계속 추가되면서 유니코드 코드포인트는 110만 개 이상으로 늘어났거든요. 문제는 자바스크립트 같은 언어가 여전히 16비트 단위로 문자열을 다룬다는 거예요. 그래서 16비트로 표현 못 하는 문자(예를 들어 😀 같은 이모지)는 두 개의 16비트 값으로 쪼개서 표현해요. 이걸 서러게이트 쌍(surrogate pair)이라고 불러요.
쉽게 비유하면, "한 글자"가 사실은 "두 개의 코드 유닛"으로 표현되는 거예요. 그래서 자바스크립트에서 '😀'.length를 찍으면 2가 나와요. 사람이 보기엔 한 글자인데, 컴퓨터에게는 두 단위인 거죠. 처음 마주치면 "왜 한 글자가 길이 2지?" 하고 당황스러워요.
그럼 "잘못된" 서러게이트 쌍은 뭘까
서러게이트 쌍은 항상 상위 서러게이트(high surrogate, 0xD800–0xDBFF)와 하위 서러게이트(low surrogate, 0xDC00–0xDFFF)가 짝을 이뤄야 해요. 그런데 문자열을 자르거나 합치거나 인코딩 변환을 하다 보면 짝이 깨질 수 있거든요. 가령 이모지를 포함한 문자열을 5글자에서 잘라버리면, 운 나쁘게 서러게이트 쌍의 중간에서 잘려서 절반만 남을 수 있어요. 이런 외톨이 서러게이트(lone surrogate)가 바로 "잘못된 서러게이트 쌍"이에요.
이게 왜 문제가 되냐면, UTF-8로 인코딩이 안 돼요. UTF-8 규격에 따르면 서러게이트 영역은 짝을 이루어 다른 문자를 표현하는 보조 코드이지, 그 자체로 유효한 문자가 아니거든요. 그래서 외톨이 서러게이트가 들어 있는 자바스크립트 문자열을 JSON으로 직렬화하거나 HTTP로 전송하려고 하면 에러가 나는 거예요.
저자가 마주친 실제 버그도 비슷한 흐름이었어요. 사용자 입력을 받아서 일정 길이로 자르고, 그걸 다시 JSON으로 직렬화해서 API에 보내는 코드였는데, 서러게이트 중간에서 잘려서 JSON.stringify가 실패하거나 백엔드에서 디코딩 에러가 터지는 거였죠. 일반 텍스트로 테스트할 때는 멀쩡한데, 이모지를 쓴 어떤 사용자에게서만 터지니까 재현이 정말 어려워요.
어떻게 해결할까
가장 안전한 해결책은 문자열을 자를 때 단순 인덱스 기반이 아니라 코드포인트 기반으로 자르는 거예요. 자바스크립트에서는 Array.from('😀😎')을 쓰면 코드포인트 단위로 잘 쪼개져요. 그러면 길이가 2가 나와요. ES2015 이후의 for...of 루프나 spread 연산자도 코드포인트 기반으로 동작해요.
또 외톨이 서러게이트를 미리 걸러내거나 대체 문자(\uFFFD)로 바꿔주는 방법도 있어요. 노드에서는 string.normalize()나 Buffer의 인코딩 옵션을 활용할 수 있고, 라이브러리로는 stringz 같은 게 도움이 돼요.
데이터베이스 단에서는 utf8mb4 같은 4바이트 UTF-8을 지원하는 인코딩을 쓰는 게 필수예요. MySQL의 기본 utf8은 사실 3바이트까지만 지원해서 이모지를 못 담거든요. 이것 때문에 한국 서비스에서도 "이모지 닉네임 가입 안 됨" 같은 이슈가 종종 터져요. 마이그레이션 한 번씩 해본 분들 있으실 거예요.
비슷한 함정들
서러게이트 쌍 외에도 유니코드에는 함정이 많아요. 결합 문자(combining characters) 때문에 "한 글자"가 여러 코드포인트로 표현되기도 해요. 가령 한글 "각"은 한 글자로 보이지만 "ㄱ + ㅏ + ㄱ"으로 분해되어 저장될 수도 있어요. 이걸 NFC/NFD 정규화 문제라고 하고, 맥에서 만든 파일명이 윈도우에서 깨져 보이는 대표적인 원인이에요.
또 방향성 문자(RTL, LTR), 제로 폭 결합자(ZWJ), 변형 선택자(variation selector) 같은 것들도 있어요. 가족 이모지 👨👩👧👦 하나가 실제로는 7개 코드포인트로 이뤄져 있다는 사실, 아마 모르셨을 거예요. 사람 이모지 + ZWJ + 사람 이모지 + ZWJ ... 식으로 조합되어서 한 글자처럼 보이게 만든 거죠.
한국 개발자에게 주는 교훈
한국어는 한글 자모, 한자, 이모지가 다 섞이는 언어라서 유니코드 이슈를 더 자주 마주쳐요. 특히 다음 세 가지는 꼭 체크해보세요. 첫째, MySQL을 쓴다면 utf8mb4 사용하기. 둘째, 사용자 입력 자르기는 코드포인트 기반으로 하기. 셋째, 외부 API로 데이터를 보낼 때는 항상 정규화 + 검증을 거치기.
이런 버그가 골치 아픈 또 다른 이유는, 재현하기 정말 어렵다는 거예요. 일반 사용자에게는 멀쩡하게 동작하다가, 어느 특정 이모지를 쓰는 사용자에게서만 터지거든요. 로그에 찍힌 깨진 문자열만 보고 원인을 찾으려면 유니코드 지식이 필수예요. 평소에 이런 배경지식을 조금이라도 챙겨두면, 진짜 터졌을 때 디버깅 시간이 몇 시간씩 단축돼요.
정리하면
서러게이트 쌍 문제는 작아 보이지만, 글로벌 서비스를 운영한다면 한 번은 반드시 마주칠 함정이에요. 이모지 닉네임, 한자 이름, 옛 한글 입력 같은 케이스에서 자주 터지죠. 여러분은 유니코드 때문에 곤란했던 버그, 한 번쯤 있으셨나요?
🔗 출처: Hacker News
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공