👽
정규표현식 완전정복

JavaScript를 활용하여 정규표현식의 기초부터 심화까지의 개념을 짚어봅니다.

July 19, 2020


JavaScript


정규표현식을 처음 마주하는 사람들의 반응은 아마 거의 비슷할 것입니다.

“…? 대체 이게 무슨 뜻이야?”

고양이가 쳤어요 판사님 이거 고양이가 친 거 같아요

영어에 숫자에, 거기에 특수문자도 섞인 게 마치 외계어 같기도 하고… 고양이가 타이핑한 것처럼 생기기도 했습니다. 그만큼 난해하게 생겼고, 그래서 읽기도 어렵습니다.

하지만 정규표현식은 필요합니다. 사실 예전에 대학 수업에서 오토마타 수업을 들으면서 정규표현식을 처음 알게 되었습니다. 뭐 촘스키 위계라느니, 오토마타같은 걸 배우는 이론 위주의 수업이었지만, 대체 이런 걸 어디에서 써먹을 일이나 있을까… 라고 생각한 적이 몇 번 있었거든요.

하지만 현업에서 제품을 만들다보니, 방대한 데이터들 속에서 특정 규칙을 만족하는 문자열을 찾아야 하는 경우가 생각보다 많았습니다. 일반적으로 그 과정에서 언어에서 지원하는 정규표현식을 마주하고 사용하게 되지요.

하지만 따로 공부나 정리를 한 적은 없었고, 필요할 때마다 검색해서 찾는 경우가 많았습니다. 거의 Stack Overflow의 예제 코드를 복붙해서 사용하는 경우가 대부분이었고(…), 자주 쓰이는 패턴들은 직관을 통해서만 어렴풋이 기억하고 있었죠. 저만 그런 거 아니죠? ㅠ

사실 시간 내서 공부 겸 정리 해봐야지 라는 마음가짐이 있긴 했지만, 실행에 옮기지는 못해서 항상 마음에 담고 있었습니다. 그래서 오늘은 마음먹고 정규표현식의 개념과 심화 패턴들을 다시 짚어보고 정리하는 시간을 가져보고자 합니다. 이 포스트를 통해 정규표현식을 학습하고 싶은 분들께 도움이 되기를 바랍니다.

정규표현식을 해석하는 정규표현식 엔진이 언어 별로 다르게 구현되어 있기 때문에 일부 표현이나 문법이 다를 수 있습니다. 본 포스트에서는 JavaScript를 기준으로 작성합니다.

정규표현식이란

정규표현식(Regular Expression, 이하 정규식)의 정의를 간단하게 설명하자면, 주어진 문자열에서 발견할 수 있는 글자 패턴을 표현한 식입니다.

그럼 이 정규식을 왜 사용할까요? 정규식을 사용하면 유용한 몇 가지 예시들을 소개해봅니다.

  • 각각 다른 포맷으로 저장된 엄청나게 많은 전화번호 데이터를 추출해야 할 때
  • 사용자가 입력한 이메일, 휴대폰 번호, IP 주소 등이 올바른지 검증하고 싶을 때
  • 코드에서 특정 변수의 이름을 치환하고 싶지만, 해당 변수의 이름을 포함하고 있는 함수는 제외하고 싶을 때
  • 특정 조건과 위치에 따라서 문자열에 포함된 공백이나 특수문자를 제거하고 싶을 때

위의 시나리오들을 살펴보면 결국 검색치환이라는 두 개의 카테고리로 나누어져 있는 것을 확인할 수 있습니다. 즉, 정규식이 제공하는 강력하고 유연하며 효율적인 문자열 처리 방법을 통해 우리의 아까운 시간과 노력을 아낄 수 있습니다.

생성하기

JavaScript에서는 정규식을 리터럴로 선언하는 방법과 정규식 생성자 RegExp를 이용해 생성하는 두 가지 방법을 제공하고 있습니다.

const regex = /abc/;

정규식 리터럴 방식은 문자열 양쪽을 슬래시(/)로 감싸는 방식입니다. 스크립트가 로드될 때 컴파일되므로, 정규식 문자열이 변하지 않는다면 리터럴 방식으로 선언하는 것이 성능 상 조금 더 이점이 있습니다.

const regex = new RegExp("abc");

정규식 생성자를 이용하는 방법은 RegExp 생성자에 매개변수로 패턴을 선언하는 방식입니다. 정규식이 실행 시점에 컴파일되므로, 정규식을 동적으로 변화시켜야 할 때 유용합니다.

특수문자 이스케이프(Escape)

정규식 문자열은 특수문자를 포함할 수 있습니다. 이 때 몇몇 특수문자들은 아래에서 알아볼 바와 같이 특수한 용도로 사용되는 예약어이기 때문에, 백슬래시(\)를 문자 앞에 붙여줌으로써 이용해 문자 그대로 해석되어야 함을 알려주어야 합니다.

// 특수문자를 문자 그대로 검색하고 싶을 때에는 백슬래시로 이스케이프 시켜줍니다
const regex1 = /\*/;
const regex2 = /\?/;
const regex3 = /\./;
const regex3 = /\\/;

플래그(Flags)

JavaScript에서는 정규식을 생성할 때 고급 검색을 위한 옵션을 설정할 수 있도록 플래그를 지원합니다. 플래그는 리터럴 방식인지 생성자 방식인지에 따라 설정하는 방식이 조금 다릅니다.

// flags 에 플래그 문자열이 들어갑니다
const regex1 = /abc/flags;
const regex2 = new Regex(/abc/, flags);

이 때 자주 사용되는 대표적인 세 가지 플래그에 대해 알아보도록 하겠습니다.

  • g: 전역 검색(Global)

전역 검색 플래그가 없는 경우에는 최초 검색 결과만 반환하는 반면, 전역 검색 플래그가 있는 경우에는 모든 검색 결과를 배열로 반환합니다.

// `a`가 두 개 포함된 문자열을 만들었습니다
const str = "abcabc";

// `g` 플래그 없이는 최초에 발견된 문자만 반환됩니다
str.match(/a/);
// ["a", index: 0, input: "abcabc", groups: undefined]

// `g` 플래그와 함께라면 모든 결과가 배열로 반환됩니다
str.match(/a/g);
// (2) ["a", "a"]
  • m: 줄바꿈 검색(Multiline)

여러 줄의 정규식 문자열이 실제 여러 줄로써 다루어져야 할 때 사용되며, 아래에서 알아볼 입력 시작(^) 앵커와 입력 종료($) 앵커가 전체 문자열이 아닌 각 줄 별로 대응됩니다.

// 줄바꿈이 포함된 문자열을 만들었습니다
const str = `abc
abc`;

// 줄바꿈을 별도로 확인할 수 없습니다
str.match(/$/g);
// [""]

// 각 줄 별로 시작과 끝을 확인할 수 있습니다
str.match(/$/gm);
// (2) ["", ""]
  • i: 대소문자 구분 없음(Case Insensitive)

정규식은 기본적으로 대소문자를 구분합니다(Case sensitive). i 플래그를 통해 대소문자 구분을 사용하지 않을 수 있습니다.

const str = "abcABC";

str.match(/a/gi);
// (2) ["a", "A"]

이 외에도 y, u, s 등의 옵션이 있지만, 자주 사용하지 않아서 생략하였습니다. 관심이 있으신 분들은 이 곳에서 전체 플래그를 확인하실 수 있습니다.

메타 문자(Meta Character)

지금까지는 정규식 문자열로 입력받은 단순한 패턴을 그대로 검색하는 경우만 알아보았지만, 이제 본격적인 암기(…)의 시간이 왔습니다.

지금부터 알아볼 메타 문자정규식 엔진에게 어떤 단일 문자를 매칭할 지 알려주는 역할의 특수문자입니다. 뜻 자체는 크게 어렵지 않지만, 함축적인 단어들이 많아서 외우지 않고 정규식을 보게 되면 문맥을 파악하기가 어렵습니다. 그래서 사실 어느 정도 자주 사용되는 패턴은 외우는 것이 가장 좋습니다.

문자 그룹(Character Set, [], [^])

// gray와 grey를 모두 검색할 수 있는 문자열을 만들어봅니다
const str = "gray and grey";

str.match(/gr[ae]y/g);
// (2) ["gray", "grey"]

중괄호([])로 묶인 긍정 문자 그룹중괄호 내부의 문자열 중 하나라도 일치하는 경우를 의미합니다.

이 때 중괄호 내부에 들어가는 특수문자는 메타 문자로 취급되지 않기 때문에, 별도로 이스케이프 하지 않아도 된다는 특징이 있습니다.

// 특수문자 문자열을 만들어봅니다
const str = "!?@#$%^&";

// [] 안에 있는 특수문자는 리터럴 특수문자로 취급됩니다
str.match(/[?!.]/g);
// (2) ["!", "?"]

문자 그룹에서 연속된 문자열을 나타내고자 하는 경우, 범위 지정 구문(dash, -) 를 이용해 보다 간략하게 표기를 할 수 있습니다.

// 아래의 두 정규식은 같습니다
const regex1 = /[A-Z]/;
const regex2 = /[ABCDEFGHIJKLMNOPQRSTUVWXYZ]/;

// 아래의 두 정규식도 같습니다
const regex3 = /[0-9]/;
const regex4 = /[0123456789]/;

// 이를 이용해 한글도 나타낼 수 있습니다
const regex5 = /[가-힣]/;

문자 그룹이 중괄호 안에 있는 문자들 중에서 일치하는 경우를 나타냈다면, 이의 역집합에 해당하는 문법도 있습니다. 부정 문자 그룹(Negative Character Set, [^]) 입니다.

중괄호의 시작이 캐럿(^)일 때에는 중괄호에 해당하지 않는 문자열만 매칭합니다.

// 숫자와 문자가 섞인 문자열을 만들었습니다
const str = "2020 07 18 Saturday";

// 숫자만 찾아보았습니다
str.match(/[0-9]/g);
// (8) ["2", "0", "2", "0", "0", "7", "1", "8"]

// 숫자가 아닌 것만 찾아보았습니다
str.match(/[^0-9]/g);
// (11) [" ", " ", " ", "S", "a", "t", "u", "r", "d", "a", "y"]

10진수 문자(Digit Character, \d, \D)

10진수 문자 \d는 0에서의 9까지의 문자 그룹([0-9])와 동일한 역할을 하는 단축어입니다. 대문자 \D를 사용하면 그 역집합을 나타낼 수 있습니다.

// 위의 예시를 가져와봤습니다
const str = "2020 07 18 Saturday";

// 숫자만 찾아보았습니다
str.match(/\d/g);
// (8) ["2", "0", "2", "0", "0", "7", "1", "8"]

// 숫자가 아닌 것만 찾아보았습니다
str.match(/\D/g);
// (11) [" ", " ", " ", "S", "a", "t", "u", "r", "d", "a", "y"]

단어 문자(Word Character, \w, \W)

단어 문자 \w는 영어 대소문자와 숫자, 그리고 언더스코어(_)를 포함하는 그룹([0-9a-zA-Z_])과 동일한 역할을 하는 단축어입니다. 대문자 \W를 사용하면 그 역집합을 나타낼 수 있습니다.

// 위의 예시를 또 가져와봤습니다
const str = "2020 07 18 Saturday";

// 단어만 찾아보았습니다
str.match(/\w/g);
// (16) ["2", "0", "2", "0", "0", "7", "1", "8", "S", "a", "t", "u", "r", "d", "a", "y"]

// 단어가 아닌 것만 찾아보았습니다
str.match(/\W/g);
// (3) [" ", " ", " "]

공백 문자(Whitespace Character, \s, \S)

공백 문자 \s는 스페이스, 탭, 폼피드, 줄 바꿈 문자 등을 포함한 하나의 공백 문자에 해당합니다. 마찬가지로 대문자 \S 를 사용해 역집합을 나타냅니다.

// 공백만 찾아보았습니다
str.match(/\s/g);
// (3) [" ", " ", " "]

// 공백이 아닌 것만 찾아보았습니다
str.match(/\S/g);
// (16) ["2", "0", "2", "0", "0", "7", "1", "8", "S", "a", "t", "u", "r", "d", "a", "y"]

임의의 문자(Any Character, .)

임의의 문자를 나타내는 .은 개행 문자를 제외한 모든 단일 문자에 해당합니다.

// 임의의 단일 문자를 모두 가져옵니다
str.match(/./g);
// (19) ["2", "0", "2", "0", " ", "0", "7", " ", "1", "8", " ", "S", "a", "t", "u", "r", "d", "a", "y"]

앵커(Anchor, ^, $)

위에서 설명한 메타 문자들이 어떤 문자열을 일치시킬 것인지에 관한 것이었다면, 앵커는 입력된 정규식이 어떤 특정 위치에서 동작할지를 제한하는 역할의 문자입니다. 즉, 위치만 제한할 뿐 검색 결과에는 포함되지 않습니다.

앵커의 종류로는 크게 패턴 시작 앵커패턴 종료 앵커가 있습니다.

// 여러 `w` 중에서 문자열의 시작 부분과 일치하는지를 확인합니다
const str = "www";

// 가장 첫 번째 `w` 만 반환됩니다
str.match(/^w/);
// ["w", index: 0, input: "www", groups: undefined]

// 가장 마지막 `w` 만 반환됩니다
str.match(/w$/);
// ["w", index: 2, input: "www", groups: undefined]

패턴 시작 앵커(^)는 해당 정규식이 줄의 시작 부분인지를 확인하는 역할을 합니다. 보통 정규식의 가장 앞에 붙여서 사용합니다.

패턴 종료 앵커($)는 해당 정규식이 줄의 마지막 부분인지를 확인하는 역할을 합니다. 보통 정규식의 가장 마지막에 붙여서 사용합니다.

이 때 위에서 본 문자 그룹([])에서 부정 그룹을 나타내기 위한 캐럿(^) 와는 다른 용법임을 주의해야 합니다. 둘의 차이점은 중괄호에 포함되어 있는지 아닌지를 확인하면 됩니다.

단어 경계(Word Boundaries, \b, \B)

단어 경계는 다른 단어 문자(\w)가 앞이나 뒤에 등장하지 않는 위치임을 나타내는 문자입니다.

const str = "create or ate";

// `ate`의 좌우로 단어 문자가 없는 것만 찾아봅니다
str.match(/\bate\b/g);
// ["ate", index: 10, input: "create or ate", groups: undefined]

// `ate` 좌측에만 단어가 포함되어 있는 것만 찾아봅니다
str.match(/\Bate\b/);
// ["ate", index: 3, input: "create or ate", groups: undefined]

교체 구문(Alternation, |)

교체 구문은 OR 연산자와 동일한 역할을 하며, 연산자 우선 순위가 가장 낮습니다. 따라서 여러 정규식을 붙여 사용할 수 있습니다. 기호로는 파이프(|)를 사용합니다.

const str = "red or green or blue";

str.match(/red|blue/g);
// (2) ["red", "blue"]

수량자(Quantifier)

수량자는 메타 문자들이 N회 반복됨을 나타내기 위한 문자입니다. 적용하고자 하는 문자의 오른쪽에 수량자를 붙입니다.

기본적으로 정규식은 탐욕적이기 때문에, 수량자에 다양한 경우의 패턴이 매칭될 때에는 가능한 긴 패턴을 반환합니다.

게으른 수량자(Lazy Quantifier)

먼저 수량자에 대해 알아보기 전에, 정규식은 탐욕적이라는 것에 대해 짚고 넘어가볼게요. 아래 예시를 살펴봅시다.

const str = "abc";

// 아래 예시는 `ab` 또는 `abc` 가 있는지 검색하는 정규식입니다
str.match(/abc?/);
// ["abc", index: 0, input: "abc", groups: undefined]

위 정규식은 c라는 단어 문자에 옵셔널(?)한 플래그를 줆으로써 ab, abc 라는 두 가지 경우에 일치하는지를 확인하는 정규식입니다. 그 결과는 ab도 일치함과 동시에 abc도 일치하지만, 정규식은 탐욕적(Greedy)이기 때문에 기본적으로 더 길고 많은 값을 반환합니다.

그렇다면 탐욕적이지 않게 만드는 방법은 없을까요? 바로 수량자 뒤에 ?를 다시 붙이는 방법입니다.

str.match(/abc??/);
// ["ab", index: 0, input: "abc", groups: undefined]

위의 경우 역시 ababc 두 경우를 모두 검색하는 것이지만, 게으르게 만듦(Lazy)으로써 가능한 한 더 적고 짧은 값을 반환받게 되었습니다.

위에서 메타 문자 \w, \d의 역집합이 \W, \D 이었던 것이 기억 나시나요? 마찬가지로 수량자에서는 ? 플래그를 마지막에 붙여줆으로써 반환값을 다르게 받을 수 있습니다.

그럼 다시 본론으로 돌아와서, 우선 범위수량자라고도 불리는 중괄호 패턴에 대해 알아봅시다.

정확히 n번 일치({n})

{n}에 들어간 숫자만큼 반복된 경우 일치한다고 판단하는 수량자입니다.

const str = "abcaabc";

str.match(/a{1}b/);
// ["ab", index: 0, input: "abcaabc", groups: undefined]

str.match(/a{2}b/);
// ["aab", index: 3, input: "abcaabc", groups: undefined]

n번 이상 m번 이하 일치({n, m}, {n, m}?)

{n, m}에 들어간 숫자 중 n번 이상이면서 m번 이하일 때 일치한다고 판단하는 수량자입니다.

const str = "aaaaa";

str.match(/a{1,3}/);
// ["aaa", index: 0, input: "aaaaa", groups: undefined]

str.match(/a{1,3}?/);
// ["a", index: 0, input: "aaaaa", groups: undefined]

적어도 n번 일치({n,}, {n,}?)

{n}에 들어간 숫자 중 적어도 n번 이상인 경우를 일치한다고 판단하는 수량자입니다.

const str = "aaaaa";

str.match(/a{1,}/);
// ["aaaaa", index: 0, input: "aaaaa", groups: undefined]

str.match(/a{1,}?/);
// ["a", index: 0, input: "aaaaa", groups: undefined]

0번 또는 1번 일치(?, ??)

물음표, 흔히 옵셔널(Optional, ?)이라고도 불리는 문자로, 0번 또는 1번 반복되는 경우 일치한다고 판단하는 수량자입니다.

const str = "aaaaa";

str.match(/aa?/);
// ["aa", index: 0, input: "aaaaa", groups: undefined]

str.match(/aa??/);
// ["a", index: 0, input: "aaaaa", groups: undefined]

1번 이상 일치(+)

더하기 기호(Plus, +)는 1번 이상 반복되는 경우 일치한다고 판단하는 수량자입니다.

예전에 강의에서 형식언어 배울 때는 대거(dagger)라고 배웠었는데 그렇게 많이 부르는 이름은 아닌 것 같네요.

const str = "aaaaa";

str.match(/a+/);
// ["aaaaa", index: 0, input: "aaaaa", groups: undefined]

str.match(/a+?/);
// ["a", index: 0, input: "aaaaa", groups: undefined]

0번 이상 일치(*)

별표, 아스테리스크 또는 와일드카드(Asterisk, Wildcard *)라고 불리며 0번 이상 반복되는 경우 일치한다고 판단하는 수량자입니다.

const str = "aaaaa";

str.match(/a*/);
// ["aaaaa", index: 0, input: "aaaaa", groups: undefined]

// 아스테리스크는 0번 반복도 포함하기 때문에 0번 반복,
// 즉 `str`의 시작 문자열에서도 일치한다고 판단되었기 때문에
// 빈 문자열이 옵니다
str.match(/a*?/);
// ["", index: 0, input: "aaaaa", groups: undefined]

그룹화(Grouping)와 캡처화(Capturing)

그룹화는 정규표현식 내의 특정 부분을 단일 표현식(single entitiy)로 구분짓기 위해 사용됩니다. 그룹화에는 기본적으로 캡처화가 동반되기 때문에 조금 헷갈릴 수 있는 개념입니다.

우선 그룹화는 말 그대로 표현식의 일부를 그룹화 할 수 있습니다. 아래 예시를 볼까요?

const str = "aaabbbabab";

// 이 표현식은 [ab, abb, abbb, ...] 를 검색합니다.
str.match(/ab+/g);
// (3) ["abbb", "ab", "ab"]

// 이 표현식은 [ab, abab, abab, ...] 를 검색합니다.
str.match(/(ab)+/g);
// (2) ["ab", "abab"]

괄호로 묶여진 그룹을 하나의 단일 표현식으로 정의하고, 이를 수량자를 이용해 다시 반복하고 있는 모습입니다.

캡처화는 괄호로 묶인 단일 표현식이 정규식 내부에서 \1, \2, \3 과 같은 이름의 임시 변수에 저장되게 되어 참조할 수 있게 됨을 말하고, 이를 정규식 내에서 다시 호출하는 것을 역참조(Back References) 라고 부릅니다. 이 때 저장되는 순서는 정규식이 실행되는 순서에 기반하며, 밖에서부터 안으로, 왼쪽에서 오른쪽 순으로 저장되게 됩니다.

const str = "aaabbbabab";

// 아래 표현식은 순서대로 `(a)`, `(b)`가 캡처되어
// `\1`, `\2`로 대치되었기 때문에 `abab`와 동일합니다
str.match(/(a)(b)\1\2/);
// (3) ["abab", "a", "b", index: 6, input: "aaabbbabab", groups: undefined]

// `str.match`에 캡쳐 그룹이 포함된 경우, 각 캡쳐 그룹을 실행한 결과를 배열에 포함해서 반환합니다.
// 즉 3개의 배열이 오는 이유는
// - 1. `str.match`의 기본 결과값,
// - 2. `str.match`의 1번 캡쳐 그룹의 결과값,
// - 3. `str.match`의 2번 캡쳐 그룹의 결과값
// 을 모두 포함하고 있기 때문입니다

캡처링 그룹화((x), \n)

일반 괄호를 사용해서 정규식을 묶을 경우, 그룹화가 되며 캡쳐화가 됩니다. 즉 \n을 이용해 그룹의 역참조가 가능합니다.

const str = "aaabbbabab";

str.match(/(ab)\1/);
// (2) ["abab", "ab", index: 6, input: "aaabbbabab", groups: undefined]

비 캡처링 그룹화((?:x))

그룹화가 필요하지만 캡처화는 필요하지는 않은 경우에는 ?:로 괄호를 시작함으로써 불필요한 캡쳐화를 막을 수 있습니다.

const str = "aaabbbabab";

str.match(/(?:ab)+/g);
// (2) ["ab", "abab"]

명명된 캡처링 그룹화(?<name>x), \k<name>)

ES2018에서 캡처링 그룹에 명시적으로 이름을 명명할 수 있는 기능이 추가되었습니다. 아래와 같은 문법으로 사용합니다.

const str = "aaabbbabab";

// 아래 문법은 `ab`를 `foo`라는 이름으로 캡처하고,
// `\k<foo>`라는 곳에서 해당 이름을 호출하여 전체 정규식이 `abab`가 된 모습입니다
str.match(/(?<foo>ab)\k<foo>/);
// (2) ["abab", "ab", index: 6, input: "aaabbbabab", groups: {…}]

전후방탐색(Lookaround)

전후방탐색은 주어진 패턴보다 좌측 혹은 우측에 있는 문자열이 일치하는지를 판별하는 패턴입니다. 여기에서 괄호에 포획된 식은 결과값에 포함되지 않는 다는 것에 주의해주세요.

전방 탐색(x(?=y))

y를 만족하면서 왼쪽(전방)에 x가 있는 경우를 나타냅니다.

const str = "abc";

str.match(/b(?=c)/);
// ["b", index: 1, input: "abc", groups: undefined]

str.match(/a(?=c)/);
// null

부정 전방 탐색(x(?!y))

y를 만족하면서 왼쪽(전방)에 x가 없는 경우를 나타냅니다.

const str = "abc";

str.match(/b(?!c)/);
// null

str.match(/a(?!c)/);
// ["a", index: 0, input: "abc", groups: undefined]

후방 탐색((?<=y)x)

ES2018에 도입된 문법으로, y를 만족하면서 오른쪽(후방)에 x가 있는 경우를 나타냅니다.

const str = "abc";

str.match(/(?<=a)b/);
// ["b", index: 1, input: "abc", groups: undefined]

str.match(/(?<=a)c/);
// null

부정 후방 탐색((?<!y)x)

ES2018에 도입된 문법으로, y를 만족하면서 오른쪽(후방)에 x가 없는 경우를 나타냅니다.

const str = "abc";

str.match(/(?<!a)b/);
// null

str.match(/(?<!a)c/);
// ["c", index: 2, input: "abc", groups: undefined]

결론

혹시 2019년 7월 2일, Cloudflare의 웹 방화벽이 적용된 모든 서비스가 27분간 마비된 사건이 있었는데, 그 원인이 무엇이었을까요? 첨부된 링크를 보면 아시겠지만, 바로 단 한 줄의 정규표현식 때문었다고 합니다.

그 이유는 정규식은 기본적으로 백트래킹 방식을 사용하기 때문인데, 백트래킹에서는 가능한 모든 경우의 수를 탐색하려 하고, 최초로 발견한 패턴을 반환합니다. 이러한 속성 때문에, 비효율적으로 작성된 정규식은 성능에 나쁜 영향을 미칠 수도 있습니다. 심지어는 Cloudflare의 사례처럼 서비스를 먹통으로 만들기도 하죠.

개발 과정에서 마주칠 수 밖에 없는 정규식, 두려워하지 말고 부딪혀봅시다. 언제까지나 검색에 의존할 수는 없으니까요.

참조