← 모두의 툴

정규식 문자 클래스의 함정, 왜 이모지가 절반만 지워지는가

가이드 · 2026.08.25 최종 확인

"특정 문자를 지우는" 도구는 내부적으로 거의 항상 정규식 문자 클래스 [...]를 씁니다. 그런데 이 문자 클래스가 u(유니코드) 플래그 없이 만들어지면, 이모지처럼 UTF-16에서 두 코드 유닛으로 표현되는 문자를 다룰 때 예상과 다른 결과가 나올 수 있습니다. 특수 문자 제거기의 실제 코드를 그대로 Node.js에서 실행해 이 문제가 정확히 어떤 형태로 나타나는지 검증했습니다.

1. 서로게이트 쌍이란 무엇인가

유니코드는 U+0000부터 U+10FFFF까지 백만 개가 넘는 문자를 정의하지만, JavaScript 문자열의 내부 인코딩인 UTF-16은 하나의 코드 유닛(16비트)으로 U+0000~U+FFFF까지만 직접 표현할 수 있습니다. 이 범위를 기본 다국어 평면(BMP)이라 부르며, 대부분의 이모지(U+1F300 이상)는 이 범위 밖에 있습니다. BMP를 벗어나는 문자는 "서로게이트 쌍"이라는 방식으로, 상위 서로게이트와 하위 서로게이트 두 개의 코드 유닛을 이어붙여 하나의 문자를 표현합니다. 예를 들어 😀(U+1F600)은 내부적으로 \ud83d\ude00 두 코드 유닛이 순서대로 붙어 있는 형태입니다.

2. u 플래그 없는 정규식이 서로게이트를 다루는 방식

문제는 JS 정규식 엔진이 u 플래그 없이 동작할 때, 문자열을 유니코드 코드포인트가 아니라 UTF-16 코드 유닛 단위로 읽는다는 데 있습니다. 즉 문자 클래스 [😀]u 플래그 없이 만들면, 이 클래스는 "😀라는 한 글자"가 아니라 "\ud83d 또는 \ude00 코드 유닛 중 하나"를 의미하는 두 개의 개별 원소로 해석됩니다. 특수 문자 제거기의 실제 소스(remove-special-characters.html)를 보면 문자 클래스를 이렇게 만듭니다.

실제 코드: if(re)txt=txt.replace(new RegExp('['+re+']','g'),'');
RegExp 생성자에 u 플래그가 전달되지 않습니다. 사용자가 추가 제거 필드에 이모지를 입력하면, 그 이모지의 두 서로게이트 코드 유닛이 그대로 문자 클래스 문자열 안에 들어갑니다.

3. 실제로 실행해서 검증한 결과

이 도구가 만드는 정규식을 Node.js에서 그대로 재현해 두 가지 케이스를 테스트했습니다. 먼저 사용자가 지우고 싶은 이모지 😀(U+1F600)를 추가 제거 필드에 입력하고, 본문에도 같은 😀가 있는 경우입니다.

테스트정규식대상 텍스트실행 결과
같은 이모지 제거/[😀]/g (u 플래그 없음)"hello 😀 world""hello world" — 😀가 온전히 지워짐
서로게이트가 겹치는 다른 이모지/[😀]/g (u 플래그 없음)"hello 😁 world" (😁=U+1F601, 상위 서로게이트가 😀와 동일)"hello \ude01 world" — 상위 서로게이트만 지워지고 하위 서로게이트가 고아로 남아 깨진 문자로 표시됨

흥미로운 점은, 지우려던 이모지 자체는 우연히 "완전히" 지워진다는 것입니다. 상위·하위 서로게이트 코드 유닛이 둘 다 클래스 안에 개별 원소로 들어 있으므로 replace()가 두 번 매칭되어 결과적으로 통째로 사라지기 때문입니다. 진짜 문제는 다른 곳에서 터집니다. 같은 상위 서로게이트(\ud83d)를 공유하는 U+1F300~U+1F5FF, U+1F600~U+1F64F 대역의 다른 이모지(예: 😁, 😂, 🙂 등 매우 많은 이모지)가 본문에 있으면, 그 이모지의 상위 서로게이트만 개별적으로 매칭되어 지워지고 하위 서로게이트만 외로이 남습니다. 그 결과 브라우저는 유효하지 않은 단독 서로게이트를 렌더링하지 못해 대체 문자(□, U+FFFD 등)로 표시하게 됩니다. 즉 "지운 이모지는 사라지지만, 지우려 하지 않은 다른 이모지가 깨진다"는 부작용입니다.

4. u 플래그를 붙이면 왜 해결되는가

u 플래그가 있는 정규식은 문자열을 코드 유닛이 아니라 유니코드 코드포인트 단위로 해석합니다. 같은 [😀] 문자 클래스라도 u 플래그를 붙이면 "서로게이트 쌍 전체가 하나의 코드포인트 😀"로만 매칭되고, 상위·하위 서로게이트를 개별 원소로 쪼개 취급하지 않습니다. 그래서 다른 이모지가 우연히 서로게이트 절반을 공유하더라도 잘못 매칭될 일이 없습니다. 유니코드 인식 문자 처리가 필요한 경우라면 유니코드 변환기처럼 codePointAt()이나 스프레드 연산자([...text])로 코드포인트 단위 순회를 쓰는 방식도 대안입니다.

5. 실무에서 안전하게 쓰는 방법

자주 묻는 질문

Q. 특수 문자 제거기의 정규식에는 실제로 u 플래그가 없나요?

네. remove-special-characters.html 소스의 new RegExp('['+re+']','g') 호출을 확인하면 플래그 인자가 'g' 하나뿐이고 u는 전달되지 않습니다.

Q. 그럼 이모지를 추가 제거 필드에 넣으면 아예 못 지우나요?

지정한 이모지 자체는 대부분 지워집니다(상위·하위 서로게이트가 둘 다 개별 매칭되어 사라지므로). 문제는 그 이모지와 상위 서로게이트를 공유하는 다른 이모지가 본문에 있을 때, 그쪽이 반쪽만 지워져 깨진 문자로 남는다는 점입니다.

Q. 사전 정의된 체크박스(구두점, 특수기호 등)를 쓸 때도 이 문제가 생기나요?

아니요. 체크박스가 제공하는 문자들은 모두 BMP 안의 단일 코드 유닛 문자라 서로게이트 쌍 문제와 무관합니다. 이슈는 사용자가 추가 제거 필드에 직접 이모지 같은 BMP 밖 문자를 입력했을 때만 발생합니다.