- Published on
ES2024 RegExp v 플래그로 유니코드 매칭 버그 잡기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/클라이언트에서 유니코드 텍스트를 다루다 보면, 정규식이 “대부분은 맞는데 특정 문자에서만 틀리는” 상황을 종종 만납니다. 대표적으로 이모지, 결합 문자(예: 악센트), 한글/일본어/중국어처럼 넓은 문자 집합, 그리고 유니코드 속성(스크립트, 범주)을 섞어 쓸 때 문제가 커집니다.
ES2015의 u 플래그는 코드 포인트 단위 처리를 도입해 많은 문제를 해결했지만, 문자 클래스 내부에서의 표현력은 여전히 제한적이었습니다. ES2024의 RegExp v 플래그는 이 지점을 크게 개선합니다. 핵심은 다음 두 가지입니다.
- 유니코드 문자 클래스 집합 연산(intersection, subtraction 등)을 표준 문법으로 제공
- 유니코드 속성 기반 매칭을 문자 클래스에서 더 안전하고 조합 가능하게 제공
이 글에서는 u에서 흔히 발생하는 유니코드 매칭 버그를 짚고, v 플래그로 고치는 패턴을 실전 위주로 정리합니다.
왜 기존 정규식이 유니코드에서 깨지나
1) 코드 유닛 vs 코드 포인트 vs 그래페म(사용자 인지 문자)
자바스크립트 문자열은 UTF-16 기반이라, BMP(기본 다국어 평면) 밖의 문자는 2개의 코드 유닛(서로게이트 페어)로 표현됩니다. u 플래그는 정규식 엔진이 이를 코드 포인트로 해석하도록 돕지만, 사용자가 “문자 하나”로 인지하는 단위(그래페म 클러스터)와는 또 다릅니다.
예를 들어 "🇰🇷" 같은 국기 이모지는 실제로는 여러 코드 포인트 조합일 수 있고, "é"는 "e" + 결합 악센트로 구성될 수 있습니다. 이런 문자들을 단순히 . 또는 \w 같은 것으로 처리하면 기대와 달라집니다.
2) 문자 클래스에서의 유니코드 속성 조합 한계
u 플래그와 \p{...}(Unicode property escapes)를 쓰면 다음 같은 매칭은 가능합니다.
\p{Script=Hangul}: 한글 스크립트\p{Letter}: 문자(letters)
하지만 문제는 “한글 글자에서 자모는 빼고 싶다”, “라틴 문자에서 결합 마크는 제외하고 싶다”, “숫자 중 ASCII만 허용하고 싶다” 같은 현실적인 요구에서 발생합니다.
u에서는 이런 요구를 충족하기 위해 부정 클래스나 복잡한 대체식을 섞어야 했고, 그 과정에서 범위가 어긋나거나(특히 - 처리), 유지보수가 어려워지거나, 엔진/브라우저별 미묘한 차이로 버그가 생기곤 했습니다.
ES2024 RegExp v 플래그란
v 플래그는 흔히 “Unicode sets”라고 부르는 기능을 자바스크립트 정규식에 도입합니다. 요약하면 문자 클래스가 단순 나열이 아니라 ‘집합’이 되고, 그 집합을 연산으로 조합할 수 있습니다.
- 교집합(intersection):
&& - 차집합(subtraction):
--
또한 \p{...} 같은 유니코드 속성도 집합으로 취급되어, 문자 클래스 안에서 더 자연스럽게 결합됩니다.
주의할 점:
v는u를 “대체”한다기보다, 더 강한 유니코드 모드라고 보는 편이 이해가 쉽습니다.- 런타임/브라우저 지원 범위를 확인해야 합니다. Node.js와 최신 브라우저는 점진적으로 지원이 확대되고 있으니, 서비스 환경에서는 폴리필/대체 로직 또는 점진적 적용 전략이 필요합니다.
흔한 버그 1: “문자/숫자만” 검증이 유니코드에서 새는 문제
예를 들어 사용자 ID를 “문자와 숫자만” 허용한다고 할 때, 많은 코드가 이렇게 작성됩니다.
// 흔한 패턴: ASCII만 의도했지만, 환경/요구에 따라 문제 발생
const re = /^[A-Za-z0-9_]+$/;
console.log(re.test("cafe")); // true
console.log(re.test("카페")); // false (원하면 false일 수도)
여기서 요구가 바뀌어 “전 세계 문자(letters) + 숫자”를 허용하되, 결합 마크나 제어문자는 제외하고 싶다고 해봅시다. u로는 대충 이렇게 갑니다.
// u 플래그 + 유니코드 속성
const re = /^[\p{Letter}\p{Number}_]+$/u;
console.log(re.test("카페123")); // true
console.log(re.test("cafe01")); // true/false 기대가 갈릴 수 있음 (결합 마크 포함 여부)
문제는 “결합 마크(\p{Mark})는 빼고 싶다” 같은 조건이 추가되면, u에서 깔끔하게 표현하기가 어려워집니다.
v로 해결: 집합 차집합으로 제외 조건을 명확히
// ES2024: v 플래그 (Unicode sets)
// 문자/숫자/언더스코어는 허용하되, Mark(결합 문자)는 제외
const re = /^[([\p{Letter}\p{Number}_]--\p{Mark})]+$/v;
console.log(re.test("카페123")); // true
console.log(re.test("cafe01")); // false (결합 마크를 제외했기 때문)
포인트는 “허용 집합”과 “제외 집합”을 문법적으로 분리할 수 있다는 점입니다. 예전에는 부정 클래스나 전방탐색으로 우회하며 실수하기 쉬웠지만, v에서는 의도가 패턴에 그대로 드러납니다.
흔한 버그 2: 스크립트 기반 필터링에서 ‘원치 않는 문자’가 섞이는 문제
예를 들어 “한글만 허용”을 구현할 때, \p{Script=Hangul}을 쓰면 한글 스크립트 전반(자모 포함)이 들어옵니다. 서비스 정책이 “완성형 한글 음절만”이라면 자모가 섞이는 것을 버그로 볼 수 있습니다.
// u 플래그로 스크립트만 체크
const re = /^[\p{Script=Hangul}]+$/u;
console.log(re.test("가나다")); // true
console.log(re.test("ㄱㄴㄷ")); // true (원치 않으면 버그)
v로 해결: 범위/속성 집합을 조합해 ‘완성형만’ 표현
완성형 한글 음절은 유니코드 블록 Hangul Syllables(U+AC00..U+D7A3)에 해당합니다. 이를 코드 포인트 범위로 표현할 수도 있지만, 유지보수 관점에선 속성/블록 기반이 더 읽기 좋을 때가 많습니다.
v에서는 “한글 스크립트에서 자모 블록을 빼기” 같은 접근이 가능합니다.
// 예시: 한글 스크립트에서 자모 계열을 제외하는 식으로 완성형에 가깝게 제한
// 실제 정책에 따라 제외 집합(자모/호환자모/확장자모 등)을 조정하세요.
const re = /^[([\p{Script=Hangul}]--[\p{Block=Hangul_Jamo}\p{Block=Hangul_Compatibility_Jamo}\p{Block=Hangul_Jamo_Extended-A}\p{Block=Hangul_Jamo_Extended-B])]+$/v;
console.log(re.test("가나다")); // true
console.log(re.test("ㄱㄴㄷ")); // false
이 패턴은 “한글 허용” 같은 요구가 시간이 지나 “자모는 금지”, “공백은 허용”, “중간점은 금지”처럼 복잡해질 때 특히 유용합니다. 허용/제외 집합을 늘리거나 줄이기 쉬워서, 운영 중 정책 변경에도 안전합니다.
흔한 버그 3: 이모지/기호를 빼려다 범위가 터지는 문제
유니코드에서 이모지와 다양한 기호는 여러 블록/범주에 흩어져 있습니다. 단순히 [^\p{Letter}\p{Number}] 같은 부정 패턴으로 필터링하면, 생각보다 많은 문자가 걸러지거나(예: 한글/일본어/중국어를 letters로 보지 않는 실수), 반대로 통과해버릴 수 있습니다.
v의 장점은 “허용 집합을 명확히 정의하고, 제외 집합을 명시적으로 빼는” 방식으로 정책을 구현할 수 있다는 점입니다.
// 예시: 사용자 닉네임 정책
// - 문자/숫자/공백/언더스코어/하이픈 허용
// - 제어문자, Mark(결합), Symbol(기호), Emoji 성격의 문자(대개 Symbol/Other에 걸림)를 제외
const nicknameRe = /^[([\p{Letter}\p{Number} _\-]--[\p{Mark}\p{Control}\p{Symbol}])]+$/v;
console.log(nicknameRe.test("홍길동")); // true
console.log(nicknameRe.test("John Doe")); // true
console.log(nicknameRe.test("cafe01")); // false
console.log(nicknameRe.test("홍길동★")); // false
현실에서는 “기호는 일부만 허용” 같은 예외가 생깁니다. 그때는 제외 집합에서 특정 문자를 다시 더하는 방식이 아니라, 허용 집합을 더 정확히 설계하는 쪽이 예측 가능성이 높습니다.
u에서 v로 마이그레이션 전략
1) 정규식이 ‘검증(validation)’인지 ‘검색(search)’인지 분리
- 검증용 정규식: 허용 집합을 좁게 잡고, 예외를 차집합으로 빼는
v가 특히 잘 맞습니다. - 검색용 정규식: 기존
u패턴을 유지하되, 특정 버그 케이스(결합 문자, 스크립트 혼합)만v로 보강하는 방식이 안전합니다.
2) 테스트 케이스를 코드 포인트/그래페म 기준으로 준비
정규식은 “눈에 보이는 문자” 기준으로 테스트하면 놓치는 케이스가 많습니다. 최소한 아래 샘플은 포함하는 것을 권합니다.
- 결합 문자:
"e\u0301"(분해형) - 사전 조합:
"é" - 서로게이트 페어 문자: 예를 들어 특정 이모지 1개
- 스크립트 혼합: 라틴+키릴, 한글+자모
const samples = [
"é", // NFC
"e\u0301", // NFD
"가", // Hangul syllable
"ㄱ", // Hangul jamo
"A", // ASCII
];
for (const s of samples) {
console.log(s, [...s].length); // 코드 포인트 단위 길이 감각 잡기
}
3) 런타임 지원 체크 및 폴백
서비스가 Node.js 기반이라면, 배포 환경의 Node 버전이 v를 지원하는지 확인해야 합니다. 프런트엔드라면 타깃 브라우저 매트릭스가 더 중요합니다.
- 지원이 불확실하면:
v정규식을 “옵션 기능”로 두고, 미지원 환경에서는 보수적인u정규식 또는 별도 검증 로직으로 폴백
function supportsVFlag() {
try {
new RegExp("[a]", "v");
return true;
} catch {
return false;
}
}
const re = supportsVFlag()
? /^[([\p{Letter}\p{Number}_]--\p{Mark})]+$/v
: /^[\p{Letter}\p{Number}_]+$/u; // 폴백은 정책에 맞게 조정
console.log(re.test("cafe\u0301"));
디버깅 팁: “정규식이 아니라 유니코드 정규화 문제”일 수도
정책이 “사용자 눈에는 같은 문자면 같은 것으로 취급”이라면, 정규식 이전에 유니코드 정규화를 적용하는 편이 더 정확할 때가 많습니다.
- NFC로 정규화하면
"e\u0301"가"é"로 합쳐질 수 있어, 결합 마크를 제외하는 정책과 충돌을 줄일 수 있습니다.
const input = "e\u0301";
console.log(input, input.normalize("NFC"));
다만 정규화는 보안/동등성 정책과 직결됩니다. 예를 들어 동형이의어(비슷하게 생긴 다른 문자) 문제까지 고려해야 하는 경우, 정규식만으로 해결하려 하지 말고 별도의 정책(스크립트 제한, confusable 검사 등)을 함께 설계하는 것이 좋습니다.
운영 관점: 정규식 정책 변경은 ‘장애’로 이어질 수 있다
입력 검증 정규식은 작은 변경이 대규모 장애로 번질 수 있습니다. 예를 들어 갑자기 특정 국가 사용자 닉네임이 가입/수정 불가가 되거나, 반대로 필터가 풀려 데이터 정합성이 깨질 수 있습니다. 이런 류의 “정책 변경이 곧 장애”라는 성격은 인프라/플랫폼에서 설정 한 줄로 장애가 나는 것과 유사합니다. 운영 리스크를 줄이는 접근은 다른 트러블슈팅 글에서도 동일하게 적용됩니다.
- 설정/정책은 점진 롤아웃 + 모니터링이 핵심이라는 점에서, AWS STS 토큰 만료로 403? IRSA·AssumeRole 점검 같은 체크리스트형 접근이 도움이 됩니다.
- 재현 가능한 최소 케이스를 만들고 원인-해결을 문서화하는 습관은, MySQL InnoDB 데드락 1213 재현·원인·해결 같은 글의 방식과도 통합니다.
정리
u플래그는 유니코드 코드 포인트 처리를 가능하게 했지만, 문자 클래스 조합/제외 같은 “정책형 정규식”에는 한계가 있었습니다.- ES2024의 RegExp
v플래그는 유니코드 문자 클래스를 집합으로 다루고,&&,--같은 연산으로 허용/제외 조건을 명확하게 표현하게 해줍니다. - 특히 결합 문자, 스크립트 기반 필터링, 기호/이모지 제외 같은 케이스에서 “의도는 단순한데 구현이 복잡해지던 문제”를 크게 줄입니다.
- 적용 시에는 런타임 지원 여부 확인, 폴백, 테스트 케이스(결합/정규화/서로게이트)를 반드시 함께 준비하는 것이 안전합니다.
실무에서 v 플래그는 “정규식 문법이 늘었다” 수준이 아니라, 유니코드 입력 정책을 코드로 안전하게 고정하는 도구에 가깝습니다. 기존에 유니코드 때문에 간헐적으로 터지던 검증/필터링 버그가 있다면, 가장 먼저 v로 “허용 집합을 선언하고, 제외 집합을 차집합으로 빼는” 형태로 리라이트해보는 것을 권합니다.