원문: Regexes Got Good: The History And Future Of Regular Expressions In JavaScript
세 줄 요약: 자바스크립트 정규 표현식(이하 정규식)은 다른 최신 언어에 비해 기능이 부족했습니다. 하지만 최근 몇 년간 수많은 개선이 이루어진 덕분에 과거의 허물을 벗었습니다. 이 글에서 Steven Levithan은 자바스크립트 정규식의 역사와 현재 상태를 평가하며 정규식의 가독성, 유지보수성, 탄력성을 높이기 위한 팁을 제공합니다.
최신 자바스크립트 정규식은 여러분이 익히 알고 있는 것보다 더 많은 발전을 이루었습니다. 정규식은 텍스트를 검색하고 변경하는 멋진 도구이기도 하지만, (어쩌면 오래된 버전을 사용해서) 사용하고 이해하기 어렵다는 평가를 받아 왔습니다.
특히 자바스크립트는 수년 동안 PCRE, Perl, .NET, Java, Ruby, C++, Python 등 최신 언어들에 비해 상대적으로 정규식의 진전이 더뎠습니다. 하지만 이제 그런 시절은 지나갔습니다.
이 글에서는 자바스크립트 정규식 개선의 역사(스포하자면 특히 ES2018과 ES2024가 판도를 바꿨습니다!)를 살펴봅니다. 더불어 최신 정규식 기능의 예시, 다른 최신 정규식과 나란히 또는 이를 능가하게 하는 경량 자바스크립트 라이브러리도 소개합니다. 마지막으로 향후 버전의 자바스크립트에서 정규식을 지속적으로 개선할 제안을 미리 살펴봅니다. 이 중 일부는 이미 최신 브라우저에서 작동하고 있습니다.
1999년, 표준이 된 ECMAScript 3은 자바스크립트에 Perl에서 영감을 받은 정규식을 도입했습니다. 이 표준은 정규식을 꽤 유용하고 Perl에서 영감을 받은 다른 언어와 대부분 호환되도록 만들었지만, 중요한 부분들이 누락되었습니다. 자바스크립트의 다음 표준화 버전인 ES5까지 10년을 기다려야 했고, 그 사이에 다른 프로그래밍 언어와 정규식 구현체들은 더 강력하고 읽기 좋은 새 기능들을 추가하였습니다.
하지만 그건 옛날 얘기입니다.
대부분 새 버전의 자바스크립트에서 정규 표현식을 적게나마 개선해 왔다는 사실, 알고 계셨나요?
이를 좀 더 살펴보겠습니다.
다음 기능 중 이해하기 어려운 부분이 있더라도 걱정하지 마세요. 나중에 일부 주요 기능에 대해 자세히 살펴볼 것입니다.
(/]/\/)
를 허용함으로써 정규식을 더욱 직관적으로 만들었습니다.y
, u
)를 추가했습니다. y
(sticky
) 플래그는 파서에서 정규식을 쉽게 사용할 수 있게 만들고, u
(unicode
) 플래그는 엄격한 오류와 함께 유니코드와 관련된 중요한 개선 사항을 추가했습니다. 또한 RegExp.prototype.flags
접근자(getter), RegExp
서브클래스 지원, 플래그를 변경하면서 정규식을 복사하는 기능도 추가했습니다.u
가 필요한 \p{...}
및 \P{...}
를 통해 s
(dotAll
) 플래그, 후방탐색, 명명된 캡처, 유니코드 속성이 추가되었습니다. 앞으로 살펴보겠지만, 이 기능들은 매우 유용합니다.matchAll
이 추가되었는데, 이 메서드도 아래에서 더 살펴보겠습니다.u
를 개선하여 플래그 v
(unicodeSets
)를 추가했습니다. v
플래그는 \p{...}
에 다중 문자 "문자열 프로퍼티" 집합, \p{...}
및 \q{...}
를 통한 문자 클래스 내의 다중 문자 요소, 중첩 문자 클래스, 차집합[A--B]
및 교집합[A&&B]
, 문자 클래스 내의 다양한 이스케이프 규칙을 추가합니다. 또한 부정 집합[^...]
내의 유니코드 속성의 대소문자를 구분하지 않는 매칭을 수정했습니다.현재 이 기능들을 안전하게 사용할 수 있는지 궁금하신가요? 답은 'YES'입니다! 최신 기능 중 플래그 v
기능만 Node.js 20 및 2023 버전 브라우저에서 지원됩니다. 이 외 기능들은 2021 이전 브라우저에서 지원됩니다.
ES2019 버전부터 ES2023 버전까지
\p{...}
와\P{...}
로 사용할 수 있는 유니코드 속성도 추가되었습니다. 또한 완성도를 높이고자 ES2021 버전에서 문자열 메서드replaceAll
이 추가되었는데, 기존 ES3의replace
메서드는g
플래그를 사용하지 않으면 두 번째 일치 항목부터 무시된다는 점에서 차이가 있습니다.
위의 변화들로 인해 개선된 자바스크립트 정규 표현식은 다른 언어와 어떻게 비교해 볼 수 있을까요? 여러 가지 방법이 있겠지만, 몇 가지 핵심적인 요소는 다음과 같습니다.
x
("확장") 플래그가 없어 정규식 관점에서 최악의 언어로 하나로 인식됐습니다. 또한 (PCRE 및 Perl의) 합성을 통해 복잡한 패턴을 구축하는 문법 정규식을 작성할 수 있는 강력한 기능인 정규식 서브루틴과 서브루틴 정의 그룹이 부족합니다.두 얼굴을 가진 동전과 같은 셈입니다.
자바스크립트 정규식은 매우 강력해졌지만, 여전히 더 안전하고 읽기 쉽고 유지보수하기 쉽게 만드는 핵심 기능이 부족합니다(일부 사람들이 이 강력한 기능을 사용하지 못하게 하는 원인이기도 합니다).
좋은 소식은 아래에서 살펴볼 자바스크립트 라이브러리로 이러한 한계를 극복할 수 있다는 것입니다.
어쩌면 여러분들에게 익숙지 않을 유용한 최신 기능들에 대해 더 살펴보겠습니다. 그전에, 이 글은 중간 정도 수준에 해당하는 가이드임을 말씀드립니다. 만약 정규식을 처음 접하신다면 좋은 튜토리얼 몇 개를 소개할테니 아래 목록을 참고하세요.
단순히 정규식의 일치 여부를 확인하는 것을 넘어, 일치한 부분의 문자열을 추출하여 코드에서 처리해야 할 때가 있습니다. 무언가를 수행하고자 하는 경우가 많습니다. 명명된 캡처 그룹을 사용함으로써 정규식과 코드의 가독성을 높이고 자체적인 문서화를 수행할 수 있습니다.
다음 예제는 두 날짜 필드를 갖는 기록을 매칭하고 날짜 값을 캡처합니다.
const record = "Admitted: 2024-01-01\nReleased: 2024-01-03";
const re =
/^Admitted: (?<admitted>\d{4}-\d{2}-\d{2})\nReleased: (?<released>\d{4}-\d{2}-\d{2})$/;
const match = record.match(re);
console.log(match.groups);
/* → {
admitted: '2024-01-01',
released: '2024-01-03'
} */
걱정하지 마세요. 이 정규식이 지금은 이해하기 어렵더라도 더 가독성을 높이는 방법을 앞으로 알게 됩니다. 여기서 핵심은 명명된 캡처 그룹은 (?<name>...)
와 같은 문법을 사용하며 결과값들은 매칭된 객체의 groups
에 저장된다는 것입니다.
또한 명명된 후참조(named backreference) 기능이 있어 \k<name>
와 같은 문법으로 명명된 캡처 그룹과 일치하는 모든 값을 다시 일치시킬 수 있습니다. 따라서 다음과 같이 검색하고 대체하는 용도로 값을 다시 사용할 수 있습니다.
// 'FirstName LastName'을 'LastName, FirstName'로 변경하기
const name = "Shaquille Oatmeal";
name.replace(/(?<first>\w+) (?<last>\w+)/, "$<last>, $<first>");
// → 'Oatmeal, Shaquille'
마지막 인수로 그룹 객체가 제공되므로 대체 콜백 함수 내에서 명명된 역참조를 사용하는 고급 정규식을 작성할 수 있습니다. 아래는 이를 보여주는 멋진 예제입니다.
function fahrenheitToCelsius(str) {
const re = /(?<degrees>-?\d+(\.\d+)?)F\b/g;
return str.replace(re, (...args) => {
const groups = args.at(-1);
return Math.round(((groups.degrees - 32) * 5) / 9) + "C";
});
}
fahrenheitToCelsius("98.6F");
// → '37C'
fahrenheitToCelsius("May 9 high is 40F and low is 21F");
// → 'May 9 high is 4C and low is -6C'
ES2018에서 도입된 후방탐색은 자바스크립트 정규식에서 항상 지원되던 전방탐색(Lookahead)를 보완합니다. 전방탐색과 후방탐색은 문자열의 시작을 나타내는 ^
나 단어 경계를 나타내는 \b
와 유사하게 일치하는 문자들을 사용하지 않는 어설션입니다. 후방탐색은 현재 일치하는 위치 바로 앞에 지정된 패턴이 존재하는지에 따라 성공하거나 실패합니다.
예를 들어, 다음 정규식은 후방탐색(?<=...)
를 사용함으로써 "cat" 단어 앞에 "fat " 단어가 오면 "cat" 단어만 일치시킵니다.
const re = /(?<=fat )cat/g;
"cat, fat cat, brat cat".replace(re, "pigeon");
// → 'cat, fat pigeon, brat cat'
또한 (?<!...)
와 같이 부정 후방탐색을 사용하여 어설션을 반전시킬 수도 있습니다. 이는 정규식이 "fat"가 앞에 오지 않는 "cat"의 모든 인스턴스를 매치합니다.
const re = /(?<!fat )cat/g;
"cat, fat cat, brat cat".replace(re, "pigeon");
// → 'pigeon, fat cat, brat pigeon'
자바스크립트의 후방탐색 구현은 매우 뛰어나며 .NET과 거의 동등한 수준입니다. 다른 언어에서는 후방탐색 내 가변 길이의 패턴을 허용하는 조건에 대해 일관성 없고 복잡한 규칙을 가지지만, 자바스크립트는 모든 하위 패턴을 자유롭게 후방탐색으로 확인할 수 있습니다.
matchAll
메서드ES2020 버전에 추가된 String.prototype.matchAll
을 사용하면, 반복문에서 결과에서 확장된 세부 정보를 더 쉽게 사용할 수 있습니다. 이전에도 다른 해결책은 있었지만, matchAll
은 더 쉬울 뿐 아니라 길이가 0인 일치 항목을 반환할 수 있는 정규식 결과를 반복할 때 무한 루프와 같은 문제를 방지할 수 있습니다.
matchAll
은 배열이 아닌 반복자(iterator)를 반환하므로 for...of
루프에서 쉽게 사용할 수 있습니다.
const re = /(?<char1>\w)(?<char2>\w)/g;
for (const match of str.matchAll(re)) {
const { char1, char2 } = match.groups;
// 각 일치 항목과 일치된 서브 패턴을 출력
console.log(`Matched "${match[0]}" with "${char1}" and "${char2}"`);
}
참고: matchAll
메서드는 해당 정규식에 g
(global
) 플래그를 사용해야 합니다. 또한 다른 반복자와 마찬가지로, Array.from
또는 배열 스프레드를 사용하여 결괏값을 배열로 가져올 수 있습니다.
const matches = [...str.matchAll(/./g)];
ES2018 버전에 추가된 유니코드 속성은 \p{...}
구문과 그 부정형인 버전 \P{...}
을 사용하여 다국어 텍스트를 강력하게 제어할 수 있습니다. 더욱 포괄적인 유니코드 범주, 스크립트, 스크립트 확장 및 바이너리 속성을 포함하는 수백 가지의 다양한 속성을 일치시킬 수 있습니다.
참고: 더욱 자세한 내용은 MDN 문서에서 확인해 보세요.
유니코드 속성은 u
(unicode
) 플래그 또는 v
(unicodeSets
) 플래그를 사용해야 합니다.
v
(unicodeSets
) 플래그는 ES2024 버전에서 추가되었습니다. 이는 u
플래그에서 더욱 진화된 기능으로 두 플래그를 동시에 사용할 순 없습니다. 유니코드를 인식하지 않는 기본 모드로 인해 발생할 수 있는 버그를 방지하려면 이러한 플래그 중 하나를 사용하는 것이 가장 좋습니다. 어떤 플래그를 사용할지 결정하는 것은 비교적 간단합니다. 만약 v
플래그(Node.js 20 및 2023년 버전 브라우저)를 사용할 수 있는 환경만 지원해도 된다면 v
플래그를 사용하고, 그렇지 않다면 u
플래그를 사용하세요.
v
플래그는 일부 새로운 정규식 기능을 지원하는데, 아마 가장 멋진 기능은 차집합과 교집합 기능일 것입니다. 예를 들어, 문자 클래스 내에서 A--B
를 사용하여 A에는 있지만 B에는 없는 문자열을 일치시키거나, A&&B
를 사용하여 A와 B 모두 존재하는 문자열을 일치시킬 수 있습니다.
// 'π' 문자를 제외한 모든 그리스어 일치
/[\p{Script_Extensions=Greek}--π]/v
// 그리스어 문자만 일치
/[\p{Script_Extensions=Greek}&&\p{Letter}]/v
다른 새로운 기능을 포함하여 v
플래그의 더 자세한 내용은 구글 크롬 팀에서 작성한 설명을 참고하시길 바랍니다.
이모지는 🤩🔥😎👌와 같이 멋지지만, 텍스트에서 이모지를 인코딩하는 건 꽤 복잡한 일입니다. 정규식으로 이모지를 일치시킬 때, 하나의 이모지는 하나 이상의 유니코드 코드 포인트로 이루어져 있다는 걸 알아야 합니다. 직접 이모지 정규식을 구현하는 많은 사람들(과 라이브러리!)이 이 점을 놓쳐서(또는 잘못된 구현으로) 버그가 발생하기도 합니다.
예를 들어 “👩🏻🏫”(여성 교사: 밝은 피부색) 이모지는 다음과 같이 아주 복잡하게 이루어져 있습니다.
// 코드 유닛 길이
"👩🏻🏫".length;
// → 7
// 유니코드 \uFFFF 이상의 각 천상(astral) 코드 포인트는 고차 서로게이트와 저차 서로게이트로 나뉩니다.
// 코드 포인트 길이
[..."👩🏻🏫"].length;
// → 4
// 네 개의 코드 포인트: \u{1F469} \u{1F3FB} \u{200D} \u{1F3EB}
// \u{1F469}와 \u{1F3FB}가 결합되어 '👩🏻'
// \u{200D}는 너비 0의 연결자
// \u{1F3EB}는 '🏫'
// 글자(Grapheme) 클러스터 길이(사용자가 인식할 수 있는 문자)
[...new Intl.Segmenter().segment("👩🏻🏫")].length;
// → 1
다행히도 자바스크립트에 \p{RGI_Emoji}
표현을 통해 개별의 완전한 이모티콘을 쉽게 일치시키는 방법이 추가되었습니다. 이는 한 번에 둘 이상의 코드 포인트를 일치시킬 수 있는 멋진 "문자열 속성"이므로 ES2024의 v
플래그가 필요합니다.
v
플래그를 지원하지 않는 환경에서 이모지를 일치시켜야 한다면, 훌륭한 라이브러리인 emoji-regex 또는 emoji-regex-xs를 참고해 보세요.
여러 해에 걸쳐 정규식 기능이 개선되었지만, 복잡한 네이티브 자바스크립트 정규식은 여전히 읽고 유지보수 하는 것이 아주 어려울 수 있습니다.
출처: 트위터
ES2018 버전의 네임드 캡처는 정규식 자체 문서화를 강화시켜주는 훌륭한 기능이었습니다. 또한 ES6 버전의 String.raw
태그를 사용하면 RegExp
생성자를 사용할 때 모든 백슬래시를 이스케이프하지 않아도 됩니다. 하지만 가독성 관점에서 할 수 있는 건 이 정도가 전부입니다.
하지만 정규식의 가독성을 높이며 가볍고 성능이 뛰어난 자바스크립트 라이브러리인 regex
(제가 직접 만듦)가 있습니다. 이 라이브러리는 Perl 호환 정규 표현식(PCRE)에서 누락된 주요 기능을 추가하고 네이티브 자바스크립트 정규식을 출력합니다. 또한 바벨 플러그인으로 사용할 수도 있어 정규식 호출이 빌드 시점에 트랜스파일링 되므로, 사용자가 런타임 비용을 지불하지 않으면서도 더 나은 개발자 경험을 얻을 수 있습니다.
PCRE는 PHP에서 정규식 지원을 위해 사용되는 인기 있는 C 라이브러리이며, 여러 프로그래밍 언어와 도구에서도 사용 가능합니다.
이제 regex
라이브러리에서 제공하는 템플릿 태그인 regex
가 복잡한 정규식을 이해할 수 있고 유지보수 가능한 형태로 작성하는 데 어떻게 도움을 줄 수 있는지 간략히 살펴보겠습니다. 아래에서 설명하는 새로운 구문은 PCRE에서도 동일하게 작동합니다.
regex
를 사용하면 기본적으로 가독성의 향상을 위해 정규식에 공백과 주석(#으로 시작)을 자유롭게 추가할 수 있습니다.
import { regex } from "regex";
const date = regex`
# YYYY-MM-DD 형태로 날짜를 매칭
(?<year> \d{4}) - # 연도 부분
(?<month> \d{2}) - # 월 부분
(?<day> \d{2}) # 일 부분
`;
이는 PCRE의 xx
플래그와 동일합니다.
서브루틴은 \g<name>
(여기서 name은 명명된 그룹을 의미)으로 작성되며, 참조된 그룹을 현재 위치에서 일치시키려는 독립적인 서브 패턴으로 취급합니다. 이는 하위 패턴의 구성과 재사용을 용이하게 하여 가독성과 유지보수성이 향상됩니다.
예를 들어, 아래 정규식은 "192.168.12.123"과 같은 IPv4 주소와 매치합니다.
import { regex } from "regex";
const ipv4 = regex`\b
(?<byte> 25[0-5] | 2[0-4]\d | 1\d\d | [1-9]?\d)
# Match the remaining 3 dot-separated bytes
(\. \g<byte>){3}
\b`;
서브루틴 정의 그룹을 통해 참조용으로만 사용할 서브 패턴을 정의하여 이를 더욱 개선할 수 있습니다. 다음은 글의 앞부분에서 살펴본 정규식을 개선하는 예제입니다.
const record = "Admitted: 2024-01-01\nReleased: 2024-01-03";
const re = regex`
^ Admitted:\ (?<admitted> \g<date>) \n
Released:\ (?<released> \g<date>) $
(?(DEFINE)
(?<date> \g<year>-\g<month>-\g<day>)
(?<year> \d{4})
(?<month> \d{2})
(?<day> \d{2})
)
`;
const match = record.match(re);
console.log(match.groups);
/* → {
admitted: '2024-01-01',
released: '2024-01-03'
} */
regex
라이브러리 기본 설정regex
는 기본적으로 v
플래그를 포함하므로 코드를 작성할 때 해당 플래그를 켜는 것을 잊지 않도록 합니다. 또한 v
플래그를 지원하지 않는 환경에서는 v
플래그의 이스케이프 규칙을 적용하면서 u
플래그로 자동 전환되므로 정규식을 양방향으로 호환할 수 있습니다.
기본적으로 x
(중요하지 않은 공백과 주석) 플래그와 n
("명명된 캡처 전용" 모드) 플래그가 암시적으로 활성화되므로 상위 모드를 계속 선택하지 않아도 됩니다. 또한 원시 문자열 템플릿 태그의 형식이므로 RegExp
생성자처럼 백슬래시(\\\\)
를 이스케이프할 필요가 없습니다.
원자 그룹과 소유격 한정자는 regex
라이브러리의 또 다른 강력한 기능입니다. 이 기능은 주로 성능과 위험한 역추적(ReDoS, "정규식 서비스 거부"라고도 하며, 특정 정규식이 일치하지 않는 특정 문자열을 검색할 때 시간이 무한정 소요되는 심각한 문제)을 방지하고자 추가되었지만, 패턴을 더 간단히 작성할 수 있어 가독성을 높이는 데 도움이 됩니다.
참고: regex
공식 문서에 더 많은 내용이 있습니다.
자바스크립트의 정규식을 개선하기 위한 여러 제안이 활발히 이루어지고 있습니다. 아래에서는 향후 버전에 포함될 가능성이 높은 세 가지 제안을 살펴보겠습니다.
이 제안은 스테이지 3(거의 마지막)에 도달했습니다. 더욱 희망적인 건 최근의 모든 주요 브라우저에서 동작한다는 점입니다.
명명된 캡처가 처음 도입되었을 때는 (?<name>...)
와 같은 모든 캡처에 고유한 name을 사용해야 했습니다. 그러나 정규식에 여러 대체 경로가 있는 경우 각 경로에서 동일한 그룹 이름을 재사용하면 코드가 단순해집니다.
예를 들면 다음과 같습니다.
/(?<year>\d{4})-\d\d|\d\d-(?<year>\d{4})/;
이 제안은 위 정규식에서 "중복된 캡처 그룹 이름" 오류를 방지합니다. 단, 이름은 여전히 각 대체 경로 내에서 고유해야 한다는 점에 유의하세요.
이 제안 또한 스테이지 3에 해당합니다. 크롬/엣지 125와 오페라 111에서 이미 지원되며 파이어폭스에서도 지원될 예정입니다. 사파리는 아직 지원 소식이 없습니다.
패턴 변경자는 (?ims:...)
, (?-ims:...)
또는 (?im-s:...)
를 사용하여 정규식의 특정 부분에 대해서만 i
, m
, s
플래그를 켜거나 끕니다.
예를 들면 다음과 같습니다.
/hello-(?i:world)/;
// 'hello-WORLD'는 일치하지만 'HELLO-WORLD'는 일치하지 않음
RegExp.escape
로 특수 문자 이스케이프이 제안은 오랜 기간을 거쳐 최근 스테이지 3에 도달했습니다. 아직 주요 브라우저에서 지원되지 않습니다. RegExp.escape(str)
함수를 제공함으로써, 모든 정규식 특수 문자를 이스케이프 처리해 문자 그대로 일치시킬 수 있도록 합니다.
이 기능이 당장 필요하다면, 가장 널리 사용되는 패키지인 escape-string-regexp를 고려해보세요. 초경량의 단일 목적 유틸리티로 월 5억 건 이상의 npm 다운로드 횟수를 기록하고 있습니다. 이 패키지는 최소한의 이스케이프 처리를 수행하며 대부분의 경우를 처리합니다. 하지만 이스케이프된 문자열이 정규식 내 임의의 위치에서도 안전하게 사용되어야 한다면, escape-string-regexp
는 위에서 언급한 regex
라이브러리를 권장합니다. regex
라이브러리는 문자열을 컨텍스트에 따라 이스케이프 처리하기 위해 보간법을 사용합니다.
지금까지 자바스크립트 정규식의 과거, 현재, 미래를 톺아보았습니다.
정규식의 세계에 더 깊이 탐험하고 싶으시다면 최고의 정규식 테스터, 튜토리얼, 라이브러리 및 기타 자료를 제공하는 Awesome Regex를 확인해 보세요. 재미있는 정규식 십자말풀이인 regexle에 도전해 보는 건 어떨까요?
여러분의 정규식이 술술 분석되고 술술 읽히길 바랍니다!
Experience the best of both worlds with Tez888, India’s premier destination for thrilling online experiences. From cricket and football to dynamic games and interactive challenges, Tez888 provides a secure, exhilarating, and immersive environment. Play smart, make confident choices, and elevate your experience with our diverse range of gaming options! Discover more at: https://tez888in.in
While JavaScript's regex capabilities have made great strides, do you think the complexity of regex patterns ultimately hinders their widespread adoption? https://www.my-fordbenefits.com Are there better alternatives or abstractions that could simplify text manipulation for developers, especially those who might find regex daunting?