Javascript

RegExp.escape() 마스터하기: 정규표현식 이스케이프 처리 완벽 가이드

드리프트2 2025. 2. 13. 22:33

RegExp.escape() 마스터하기: 정규표현식 이스케이프 처리 완벽 가이드

오늘은 ECMAScript의 새로운 제안인 "RegExp escaping"에 대해 자세히 알아보겠습니다.

 

Jordan Harband와 Kevin Gibbons가 제안한 이 기능은 현재 stage 3 단계에 있는데요.

 

RegExp.escape() 함수를 사용하면 주어진 문자열을 정규표현식에서 안전하게 사용할 수 있도록 이스케이프 처리할 수 있습니다.

 

RegExp.escape()는 어떻게 동작하는가?

RegExp.escape(text)는 주어진 text 문자열과 정확히 일치하는 정규표현식 패턴을 생성합니다.

 

정규표현식에서 특별한 의미를 가지는 문자들은 그대로 사용할 수 없어서 이스케이프 처리가 필요한데요.

 

예를 들어보겠습니다:

> RegExp.escape('(*)')
'\\(\\*\\)'

 

여기서 각각의 정규표현식 백슬래시가 두 번 나타나는 것을 볼 수 있습니다.

 

하나는 실제 백슬래시이고, 다른 하나는 문자열 리터럴 내에서 이를 이스케이프 처리하는 용도입니다:

> '\\(\\*\\)' === String.raw`\(\*\)`
true

 

특별한 의미가 없는 일반 문자들은 이스케이프 처리가 필요 없습니다:

> RegExp.escape('_abc123')
'_abc123'

 

RegExp.escape()의 활용 사례

 

예제 1: 모든 텍스트 발생 교체하기

 

텍스트 검색과 교체는 이스케이프의 고전적인 사용 사례입니다:

function replacePlainText(str, searchText, replace) {
  const searchRegExp = new RegExp(
    RegExp.escape(searchText),
    'gu'
  );
  return str.replace(searchRegExp, replace)
}

assert.equal(
  replacePlainText('(a) and (a)', '(a)', '@'),
  '@ and @'
);

 

다만 ES2021부터는 .replaceAll()을 사용할 수 있게 되었는데요:

assert.equal(
  '(a) and (a)'.replaceAll('(a)', '@'),
  '@ and @'
);

 

 

예제 2: 정규표현식의 일부가 주어진 텍스트와 일치해야 하는 경우

function removeUnquotedText(str, text) {
  const regExp = new RegExp(
    `(?<!")${RegExp.escape(text)}(?!")`,
    'gu'
  );
  return str.replaceAll(regExp, '•');
}

assert.equal(
  removeUnquotedText('"yes" and yes and "yes"', 'yes'),
  '"yes" and • and "yes"'
);

 

이러한 방식은 따옴표로 둘러싸이지 않은 텍스트를 찾거나 개수를 세는 데도 활용할 수 있습니다.

 

이스케이프 처리 시 고려사항

RegExp.escape()가 반환하는 패턴은 오랫동안 유지될 수 있습니다.

 

따라서 미래의 정규표현식 기능들이 이 패턴의 작동을 방해하지 않도록 하는 것이 중요한데요.

 

이런 이유로 RegExp.escape()는 현재 특별한 문법으로 사용되는 문장부호만 이스케이프 처리하는 것이 아니라, 미래에 문법으로 사용될 수 있는 문자들도 이스케이프 처리합니다.

 

 

모든 플래그에서 작동하는 이스케이프

 

앞으로 도입될 /x 플래그는 이스케이프되지 않은 공백을 무시하게 되는데요.

 

따라서 공백도 이스케이프 처리가 필요합니다:

> RegExp.escape(' \t')
'\\x20\\t'

 

이스케이프된 문자는 최대한 짧게 표현하고 싶지만, /u와 /v 플래그로 활성화되는 기능은 사용할 수 없습니다.

 

따라서 다음과 같은 방식을 사용합니다:

  • 일부 공백과 줄 종결자 문자는 다음과 같이 처리됩니다: \t \n \v \f \r
  • 0xFF까지의 유니코드 코드 포인트는 ASCII 16진수 이스케이프를 사용합니다 (예: \x41은 A와 매칭)
  • BMP(Basic Multilingual Plane)의 코드 포인트(0xFFFF까지)는 유니코드 코드 유닛 이스케이프를 사용합니다 (예: \u2028은 LINE SEPARATOR 문자와 매칭)
  • 더 높은 코드 포인트는 두 개의 코드 유닛 이스케이프를 사용해야 합니다

 

모든 구문 컨텍스트에서 작동하는 이스케이프

 

정규표현식에는 여러 "컨텍스트"(중첩된 범위라고 생각하면 됩니다)가 있습니다:

  1. 최상위 레벨에서는 *, $ 같은 구문 문자를 이스케이프 해야 합니다.
  2. 문자 클래스([abc] 같은)에서는:
    • 대부분의 최상위 레벨 구문은 이스케이프가 필요 없습니다 (*, $ 등)
    • 다른 구문은 이스케이프가 필요합니다 (예: -(하이픈))
    • /v 플래그에서는 &&, -- 같은 이중 문장부호도 이스케이프 해야 합니다
> RegExp.escape('$')
'\\$'

> RegExp.escape('=>')
'\\x3d\\x3e'

 

이스케이프된 텍스트 앞뒤의 구문 고려사항

 

정규표현식을 다음과 같이 구성하는 경우가 있습니다:

new RegExp('<regex pattern>' + RegExp.escape(text))

 

이때 RegExp.escape()의 결과가 앞에 오는 정규표현식 패턴에 영향을 주지 않도록 해야 하는데요. 몇 가지 중요한 고려사항이 있습니다:

  1. \0은 NULL 문자(U+0000)를 나타내며 십진수 숫자가 뒤따라오면 안 됩니다. 그래서 시작하는 십진수 숫자는 이스케이프 처리가 필요합니다:
> RegExp.escape('123')
'\\x3123'
  1. \1, \2 등은 번호가 매겨진 캡처 그룹에 대한 역참조입니다. 이스케이프된 텍스트가 이러한 역참조에 십진수를 추가하지 않도록 시작 숫자를 이스케이프 처리합니다.
  2. 제어 문자는 \cA (Ctrl-A)에서 \cZ (Ctrl-Z)까지 이런 방식으로 표현할 수 있습니다. 하지만 \c는 단독으로도 사용될 수 있어서 그대로 해석됩니다. 따라서 이스케이프된 텍스트는 ASCII 문자로 시작하면 안 됩니다:
> RegExp.escape('abc')
'\\x61bc'
  1. /u와 /v 플래그는 서로게이트 쌍을 하나의 단위로 처리합니다. 따라서 단독 서로게이트가 정규표현식 패턴의 앞뒤에 있는 단독 서로게이트와 결합되지 않도록 이스케이프 처리해야 합니다.

 

RegExp.escape() 구현체

  • 다양한 JavaScript 플랫폼의 지원 현황은 MDN에서 확인할 수 있습니다
  • Jordan Harband가 만든 npm의 폴리필을 사용할 수 있습니다
  • 교육 목적으로 작성된 구현체도 있는데, 실용성보다는 가독성에 중점을 둔 버전입니다

이렇게 RegExp.escape()의 모든 측면을 자세히 살펴보았는데요.

 

정규표현식에서 특수 문자를 안전하게 처리하는 것이 얼마나 복잡한 작업인지 이해하실 수 있었을 것 같습니다.

 

이 기능은 아직 제안 단계에 있지만, 정규표현식을 더욱 안전하고 예측 가능하게 만들어줄 것으로 기대됩니다.