정규식 테스터 사용법과 실무에서 자주 막히는 지점들
정규식은 짧게 쓸 수 있어서 강력하지만, 짧기 때문에 틀렸을 때 어디가 틀렸는지 알기 어렵습니다. 테스터의 가치는 문법을 알려 주는 데 있지 않고, 내가 의도한 패턴이 실제 데이터에 어떻게 걸리는지 즉시 확인해 준다는 데 있습니다. 아래는 자주 쓰는 문법 정리와, 동작은 하지만 잘못된 패턴을 만들게 되는 함정들입니다.
핵심 문법 요약
| 패턴 | 의미 | 예 |
|---|---|---|
| . | 줄바꿈을 제외한 모든 문자 | a.c → abc, a1c |
| \d \w \s | 숫자 / 단어문자 / 공백 | \d{3} → 123 |
| \D \W \S | 위의 부정 | \D → 숫자가 아닌 문자 |
| [abc] [^abc] | 문자 집합 / 부정 집합 | [aeiou] → 모음 하나 |
| * + ? | 0회 이상 / 1회 이상 / 0 또는 1회 | ab+ → ab, abb |
| {n} {n,} {n,m} | 정확히 n회 / n회 이상 / n~m회 | \d{2,4} → 12, 1234 |
| ^ $ | 문자열(또는 줄)의 시작 / 끝 | ^abc$ → abc만 |
| \b | 단어 경계 | \bcat\b → cat, ≠ category |
| (...) | 캡처 그룹 | (\d+)-(\d+) |
| (?:...) | 캡처하지 않는 그룹 | (?:ab)+ |
| (?<name>...) | 이름 있는 그룹 | (?<year>\d{4}) |
| a|b | 선택 | cat|dog |
| (?=...) (?!...) | 전방 탐색 / 부정 전방 탐색 | \d(?=원) → '1000원'의 0 |
| (?<=...) (?<!...) | 후방 탐색 / 부정 후방 탐색 | (?<=\$)\d+ → '$100'의 100 |
플래그의 의미
- g (global): 첫 번째 일치에서 멈추지 않고 전부 찾습니다. 이 플래그가 없으면 replace는 첫 하나만 바꿉니다.
- i (ignoreCase): 대소문자를 구분하지 않습니다.
- m (multiline): ^와 $가 문자열 전체가 아니라 각 줄의 시작·끝에 맞습니다.
- s (dotAll): .이 줄바꿈까지 포함하게 합니다. 여러 줄에 걸친 블록을 잡을 때 필요합니다.
- u (unicode): 코드포인트 단위로 처리하며 \p{...} 유니코드 속성을 쓸 수 있게 합니다.
사용 방법
- 정규식 패턴을 입력합니다. 슬래시로 감싸지 않고 패턴 본문만 넣으세요.
- 필요한 플래그를 선택합니다. 여러 줄 텍스트를 다룬다면 m과 s를 확인해 보세요.
- 테스트할 문자열을 붙여넣으면 일치하는 부분이 강조되어 표시됩니다.
- 캡처 그룹이 있으면 그룹별 값을 확인해 원하는 부분이 정확히 잡혔는지 봅니다.
- 의도한 것보다 많이 잡히면 탐욕 수량자 항목을, 하나만 잡히면 g 플래그를 확인하세요.
탐욕 수량자와 게으른 수량자
* 와 + 는 기본적으로 탐욕적입니다. 가능한 한 길게 먹으려 하고, 그 상태에서 나머지 패턴이 맞지 않으면 한 글자씩 물러나며 재시도합니다. HTML 태그를 잡으려고 <.+> 라고 쓰면 <b>hello</b> 전체가 하나로 잡히는 이유가 이것입니다.
뒤에 ?를 붙이면 게으른(최소 일치) 수량자가 되어 가능한 한 짧게 먹습니다. <.+?> 는 <b> 와 </b> 를 각각 잡습니다. 다만 더 정확한 접근은 '금지 문자 집합'을 쓰는 것입니다. <[^>]+> 는 백트래킹 없이 한 번에 결정되므로 더 빠르고 예측 가능합니다.
const s = '<b>hello</b>'; s.match(/<.+>/)[0] // "<b>hello</b>" ← 탐욕적 s.match(/<.+?>/)[0] // "<b>" ← 게으름 s.match(/<[^>]+>/)[0] // "<b>" ← 권장
성능 함정: 파국적 백트래킹
정규식이 무한 루프처럼 멈추는 현상이 있습니다. 중첩된 수량자, 특히 (a+)+ 나 (\s*,\s*)* 같은 형태에서 발생합니다. 매칭이 실패할 때 엔진이 가능한 분할 조합을 모두 시도하는데, 그 수가 입력 길이에 따라 지수적으로 증가합니다. 입력이 30자만 되어도 사실상 끝나지 않습니다.
이것이 사용자 입력을 정규식으로 검증하는 서버에서 ReDoS(정규식 서비스 거부) 취약점이 되는 경로입니다. 공격자가 매칭에 실패하도록 만든 문자열 하나를 보내면 CPU가 점유됩니다. 방어책은 중첩 수량자를 피하고, 가능하면 부정 문자 집합으로 경계를 명확히 하고, 복잡한 검증은 정규식 대신 파싱 코드로 옮기는 것입니다.
// 위험: 중첩 수량자
/^(\w+\s?)+$/.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaa!") // 매우 느림
// 안전: 경계를 명확히
/^\w+(?:\s\w+)*$/.test("...")정규식으로 하지 말아야 할 것
- 이메일 완전 검증: RFC 5322를 정확히 구현한 정규식은 수백 자에 달하고 실용성이 없습니다. /^[^@\s]+@[^@\s]+\.[^@\s]+$/ 정도로 형태만 걸러내고, 실제 유효성은 확인 메일 발송으로 검증하는 것이 표준적인 접근입니다.
- HTML 파싱: 중첩 구조는 정규식으로 안정적으로 다룰 수 없습니다. DOMParser나 전용 파서를 쓰세요.
- JSON 파싱: JSON.parse가 있습니다.
- URL 검증: new URL()로 파싱해 보고 예외 여부와 스킴을 확인하는 편이 정확합니다.
- 비밀번호 정책 검사: 전방 탐색을 여러 개 쌓기보다 조건별로 나눠 검사하면 사용자에게 무엇이 부족한지 알려 줄 수 있습니다.
한글을 다룰 때
완성형 한글 음절은 U+AC00부터 U+D7A3까지이므로 [가-힣] 로 잡을 수 있습니다. 자모까지 포함하려면 [ㄱ-ㅎㅏ-ㅣ가-힣] 처럼 범위를 추가합니다. u 플래그를 쓰면 유니코드 속성으로 \p{Script=Hangul} 이라고 쓸 수도 있어 의도가 더 분명해집니다.
주의할 점은 \w 가 ASCII 영숫자와 밑줄만 의미한다는 것입니다. 한글은 \w 에 포함되지 않으므로 \w+ 로 단어를 잡으려 하면 한글이 걸리지 않습니다. 같은 이유로 \b 단어 경계도 한글에서는 기대와 다르게 동작합니다.
자주 묻는 질문
- 테스터에서는 되는데 코드에서는 안 됩니다.
- 가장 흔한 원인은 문자열 리터럴에서의 이스케이프입니다. new RegExp("\d") 는 \d 가 아니라 d 를 의미하므로 "\\d" 로 써야 합니다. 리터럴 /\d/ 를 쓰면 이 문제가 없습니다. 두 번째 원인은 언어별 문법 차이입니다. 후방 탐색이나 이름 있는 그룹은 언어와 버전에 따라 지원 여부가 다릅니다.
- replace가 첫 번째만 바꿉니다.
- g 플래그가 없기 때문입니다. /a/ 대신 /a/g 를 쓰거나, 전체 치환이 목적이라면 문자열 메서드 replaceAll을 쓰는 편이 의도가 분명합니다. 참고로 replaceAll에 정규식을 넘길 때 g 플래그가 없으면 오류가 발생합니다.
- lastIndex 때문에 결과가 이상합니다.
- g 또는 y 플래그가 붙은 정규식 객체는 lastIndex 상태를 유지합니다. 같은 객체로 test를 반복 호출하면 검색 시작 위치가 계속 이동해 true와 false가 번갈아 나오는 현상이 생깁니다. 반복 사용할 때는 매번 새 정규식을 만들거나, 호출 전에 lastIndex를 0으로 초기화하세요.
- 캡처 그룹과 비캡처 그룹은 언제 구분하나요?
- 그룹으로 묶는 목적이 값을 꺼내는 것이 아니라 단지 수량자를 적용하거나 선택 범위를 한정하는 것이라면 (?:...)를 쓰세요. 불필요한 캡처는 그룹 번호를 밀어 놓아 코드를 깨지기 쉽게 만들고 약간의 성능 비용도 있습니다. 값을 꺼낸다면 번호보다 이름 있는 그룹 (?<name>...)이 읽기 쉽습니다.
- 여러 줄 텍스트에서 ^가 각 줄에 맞지 않습니다.
- m 플래그를 켜야 합니다. 기본 상태에서 ^와 $는 문자열 전체의 시작과 끝만 의미합니다. 그리고 로그처럼 여러 줄에 걸친 블록을 하나로 잡아야 한다면 s 플래그도 함께 필요합니다.
- 정규식이 대소문자를 구분하지 않게 하려면?
- i 플래그를 쓰면 됩니다. 다만 문자 집합에 [a-zA-Z] 처럼 직접 두 범위를 적어 두고 i 플래그까지 켜는 것은 중복입니다. 그리고 튀르키예어의 i 처럼 언어별 대소문자 규칙이 다른 문자가 있으므로, 로케일이 중요한 비교라면 toLocaleLowerCase를 거친 뒤 비교하는 편이 안전합니다.
💡 참고: 복잡한 패턴은 한 번에 완성하려 하지 말고, 짧게 만들어 테스트하고 조금씩 붙여 가는 편이 훨씬 빠릅니다. 완성된 패턴에는 반드시 주석이나 테스트 케이스를 함께 남겨 두세요. 석 달 뒤의 자신도 남입니다.