실무에서 쓰는 정규표현식
정규표현식은 '쓰기 전용'이라는 평판이 있습니다. 만들기는 쉽고, 다시 읽기는 어렵고, 실제 데이터에서만 조용히 틀리죠. 그 대부분은 엔진이 무엇을 하는지에 대한 몇 가지 오해에서 나옵니다. 이 가이드는 그 오해들을 하나씩 짚고, 마지막에는 패턴을 내려놓는 편이 맞는 경우를 다룹니다.
엔진은 실제로 무엇을 하는가
정규표현식은 원하는 문자열에 대한 '설명'이 아니라, 엔진이 입력의 매 위치에서 왼쪽부터 실행하는 작은 프로그램입니다. 인덱스 0에서 매칭을 시도하고, 실패하면 인덱스 1로 옮겨 다시 시도하며, 무언가 매칭되거나 입력이 끝날 때까지 반복합니다. 놀라운 결과의 거의 전부가 이 스캔에서 비롯됩니다.
그래서 `\d{3}`은 "abc1234def"에서 태연히 매칭에 성공합니다. 가운데에서 "123"을 찾았고, 패턴 어디에도 그러지 말라는 말이 없었으니까요. '숫자 세 개를 포함한다'와 '정확히 숫자 세 개다'는 다른 질문이고, 패턴은 당신이 물어본 쪽에만 답합니다.
앵커와 단어 경계
`^`와 `$`는 패턴을 입력의 처음과 끝에 묶습니다 — 멀티라인 플래그가 켜져 있으면 각 '줄'의 처음과 끝에 묶이는데, 이게 검증기를 조용히 헐겁게 만드는 원인입니다. `\b`는 단어 문자와 비단어 문자 사이의 폭 없는 위치라서, `\bcat\b`는 "the cat sat"의 고양이는 찾지만 "concatenate" 속 글자는 찾지 않습니다.
검증용이라면 패턴 전체를 앵커로 묶고, 통과시키고 싶은 값뿐 아니라 '거부하고 싶은' 값으로도 시험하세요. 좋은 값을 다 통과시키면서 쓰레기도 통과시키는 검증기는 없느니만 못합니다. 실패 지점을 훨씬 추적하기 어려운 하류로 밀어낼 뿐이니까요.
탐욕적, 게으른, 그리고 파국적 백트래킹
수량자는 기본이 탐욕적입니다. `.*`는 가능한 만큼 삼키고, 패턴의 나머지가 실패할 때만 글자를 되돌려 줍니다. `?`를 붙이면 게을러집니다. `<b>one</b> and <b>two</b>`에 대해 `<.*>`는 줄 전체를 삼키지만 `<.*?>`는 첫 `>`에서 멈춥니다. 의도는 같은데 결과는 완전히 다릅니다.
그 '되돌려 주기'가 성능이 무너지는 지점이기도 합니다. 겹치는 문자 집합 위에 수량자를 중첩하면 — 교과서적인 예가 `(a+)+b` — 엔진이 매칭 없음을 인정하기까지 지수적으로 많은 분할을 시도할 수 있습니다. 공격자가 넣은 텍스트에서는 이게 서비스 거부 취약점(ReDoS)입니다. 수량자가 붙은 그룹끼리 겹치지 않게 하고, 가능하면 `.` 대신 구체적인 문자 클래스를 쓰세요.
문자 클래스, 점, 그리고 유니코드
`.`은 dotall 플래그를 켜지 않는 한 '개행을 제외한 아무 문자'이고, `\d`·`\w`·`\s`는 대부분의 구현에서 ASCII 중심입니다. `\w`는 악센트 붙은 글자를 제외하고, `\d`가 다른 문자 체계의 숫자를 포함하는지는 엔진마다 다릅니다. 순수 ASCII가 아닌 텍스트라면 유니코드 모드를 켜고 `\p{L}`(모든 글자)이나 `\p{N}`(모든 숫자) 같은 속성 이스케이프를 쓰세요.
클래스 안의 범위는 코드포인트 범위라서, `[A-z]`는 `Z`와 `a` 사이에 있는 구두점 여섯 개를 조용히 포함합니다. 의도한 클래스를 그대로 쓰세요. 그리고 똑같아 보이는 텍스트가 다른 시퀀스일 수 있다는 점도요. 하나의 코드포인트인 "é"는 "e" + 결합 악센트를 상정한 패턴에 매칭되지 않으므로, 비교 전에 정규화하세요.
정규식이 틀린 도구일 때
중첩과 인용이 있는 형식 — HTML, JSON, CSV, 소스 코드 — 은 정규 언어가 아닙니다. 패턴으로 조금씩 갉아먹을 수는 있지만, 인용된 구분자 하나와 중첩 한 단계마다 예외가 늘어나 결국 읽을 수 없으면서도 아직 못 본 입력에서는 여전히 틀린 식이 됩니다. 진짜 파서를 쓰세요. CSV라면 인용을 지키는 파서, HTML이라면 DOM입니다.
이메일 주소가 또 하나의 고전적인 함정입니다. 명세의 문법은 인터넷에서 복사해 온 패턴보다 훨씬 크기 때문에, 엄격해 보이는 정규식이 멀쩡한 주소를 일상적으로 거부합니다 — 플러스 기호가 들어간 주소나 긴 신규 최상위 도메인처럼 지극히 평범한 것들까지요. `@` 양쪽에 그럴듯한 무언가가 있는지만 확인하고, 그다음엔 유일하게 통하는 방법으로 증명하세요. 그 주소로 메일을 보내는 겁니다.
- 의도한 위치에서 매칭하세요. 검증이면 `^`…`$`로 묶고, 검색이면 묶지 마세요.
- 구분자를 안다면 `.*?`보다 `[^"]*`가 낫습니다. 더 명확하고 백트래킹이 없습니다.
- 통과시킬 입력만이 아니라 거부할 입력으로 시험하세요.
- 형식에 중첩이나 인용이 있으면 파서를 쓰세요.