정규식 기초 10패턴과 역추적 폭주(ReDoS) 피하기
2026.10.03 업데이트 · 5분 읽기
자주 쓰는 정규식 패턴 10가지를 실행해 본 예시와 함께 정리하고, (a+)+ 같은 중첩 반복이 왜 역추적 폭주(ReDoS)를 일으키는지, 어떻게 고쳐 쓰는지 설명합니다.
정규식 테스터 바로 사용하기일상적인 정규식은 점, 줄임 클래스 세 가지, 문자 집합, 앵커, 수량자, 그룹, 게으른 일치까지 열 가지로 대부분 해결됩니다. 피해야 할 습관은 하나입니다. (a+)+처럼 반복 안에 반복을 넣으면, 거의 맞지만 끝내 맞지 않는 글에서 실행 시간이 지수적으로 늘어날 수 있습니다. 이 글의 예시는 모두 정규식 테스터에서 바로 실행해 볼 수 있고, 문법은 JavaScript(ECMAScript) 기준입니다.
기초 패턴 10가지
예시는 모두 일치하는 곳을 전부 찾는 g 플래그로 실행한 결과입니다.
| # | 패턴 | 뜻 | 예시 | 일치 |
|---|---|---|---|---|
| 1 | . |
줄바꿈을 뺀 아무 글자 하나 | c.t → cat cot c-t ct |
cat, cot, c-t |
| 2 | \d |
숫자 0부터 9 | \d+ → v2.10 build 345 |
2, 10, 345 |
| 3 | \w |
영문자·숫자·밑줄 | \w+ → user_id=42; name=kim |
user_id, 42, name, kim |
| 4 | \s |
공백·탭·줄바꿈 | a\s+b → a b, a b, ab |
a b, a b |
| 5 | [abc] [^abc] |
나열한 글자 중 하나, 또는 그 밖 | [aeiou] → zekilo |
e, i, o |
| 6 | ^ $ |
글의 시작과 끝 | \.json$ → data.json |
.json |
| 7 | * + ? |
0번 이상, 1번 이상, 0 또는 1번 | colou?r → color colour colr |
color, colour |
| 8 | {n} {n,m} |
정확한 횟수, 횟수 범위 | a{2,3} → a aa aaa aaaa |
aa, aaa, aaa |
| 9 | (…) | |
그룹, 여러 후보 중 하나 | cat|dog → cat, dog, cow |
cat, dog |
| 10 | *? +? |
게으른 일치(되도록 짧게) | <.+?> → <b>bold</b> |
<b>, </b> |
처음에 알아 두면 좋은 점입니다.
- 특수 문자는 이스케이프합니다. 점은 거의 모든 글자와 일치하므로 진짜 점은 6번처럼
\.으로 씁니다. - 수량자는 기본이 탐욕적입니다. 10번에서
?를 빼면<.+>는<b>bold</b>전체를 한 번에 잡습니다. \w는 한글과 일치하지 않습니다.ZEKILO 제킬로 dev에서\w+는ZEKILO와dev만 찾습니다. 한글은[가-힣]+으로 찾을 수 있고, 이 글에서는제킬로가 일치합니다.- 플래그가 규칙을 바꿉니다.
i는 대소문자를 무시하고,m은^·$가 줄마다 동작하게 하며,s는 점이 줄바꿈과도 일치하게 합니다.
그룹과 바꾸기
괄호는 일치한 부분을 기억하므로 바꾸기에서 다시 쓸 수 있습니다.
패턴 (\d{3})-(\d{4})-(\d{4}) 플래그 g
대상 연락처: 010-1234-5678, 02-123-4567, 010-9876-5432
일치 1 010-1234-5678 인덱스 5 그룹: 010, 1234, 5678
일치 2 010-9876-5432 인덱스 33 그룹: 010, 9876, 5432
가운데 번호는 첫 부분이 두 자리라 일치하지 않습니다. 바꾸기에서 $1·$3은 그룹을 가리키므로, 바꿀 내용을 $1-****-$3으로 쓰면 가운데 자리를 가릴 수 있습니다.
연락처: 010-****-5678, 02-123-4567, 010-****-5432
그룹이 많아지면 번호 대신 이름을 붙입니다. (?<year>\d{4})-(?<m>\d{2})를 2026-10에 적용하면 year는 2026, m은 10이고, 바꾸기에서는 $<year>처럼 씁니다.
역추적 폭주란
역추적 방식 엔진은 한 가지 방법으로 맞춰 보다가 실패하면 되돌아가 다음 방법을 시도합니다. 보통은 방법이 몇 개뿐이지만 (a+)+$는 다릅니다. 안쪽 a+와 바깥 +가 연속된 a를 나누는 경우의 수가 매우 많고, 글 끝에 일치를 깨는 글자가 있으면 엔진은 그 조합을 모두 확인한 뒤에야 「일치 없음」을 알립니다.
OWASP는 ^(a+)+$를 예로 듭니다. 입력 aaaaX에는 가능한 경로가 16개, a 16개 뒤에 X가 오면 65,536개이고, a가 하나 늘 때마다 두 배가 됩니다. 실제 시간도 같은 모양입니다. 연속된 a 뒤에 b를 붙인 글에 (a+)+$를 실행한 결과입니다(Node.js 24.14, 데스크톱 PC 한 대, 3회 중 중앙값).
a의 개수 |
걸린 시간 |
|---|---|
| 20 | 20 ms |
| 22 | 85 ms |
| 24 | 300 ms |
| 26 | 1,311 ms |
수치는 기기와 엔진에 따라 다르지만, 두 글자 늘 때마다 약 네 배가 되는 흐름은 같습니다. 같은 26글자에서 중첩을 없앤 a+$는 0.1 ms도 걸리지 않았습니다.
이 문제를 ReDoS(정규식 서비스 거부)라고 합니다. 방문자가 보낸 글에 이런 패턴을 적용하는 서버는 요청 하나로 CPU가 오래 묶일 수 있습니다. OWASP는 위험한 패턴의 조건으로 반복이 붙은 그룹과, 그 그룹 안의 또 다른 반복이나 서로 겹치는 선택지를 꼽고 (a+)+$, ([a-zA-Z]+)*$, (a|aa)+$, (a|a?)+$를 예로 듭니다.
피하는 방법
- 수량자를 중첩하지 않습니다.
(a+)+는a+와,(\d+)*는\d*와 같은 글에 일치합니다. - 선택지가 겹치지 않게 합니다.
(a|aa)+는 두 후보가 같은 자리에서 시작할 수 있습니다. 한 자리를 한 가지로만 읽도록a+로 고칩니다. .*대신 좁은 문자 집합을 씁니다. 따옴표 문자열은"[^"]*"로 쓰면"a" and "b"에서"a"와"b"를 따로 찾습니다.".*"는 첫 따옴표부터 마지막 따옴표까지 한 번에 잡습니다.- 입력 길이를 제한하고 앵커를 붙입니다.
a+$도 매우 긴 글에서는 느립니다. 시작 앵커가 없으면 자리마다 다시 찾기 때문에,a10만 개 뒤에b가 오는 글에서 약 7초가 걸렸고^a+$는 1 ms도 걸리지 않았습니다(같은 환경). - 시간 제한을 둡니다. 정규식 테스터는 패턴을 항상 별도 Worker에서 실행하고 1,000ms가 지나면 멈춥니다.
(a+)+$에a30개와b를 넣으면 화면이 멈추는 대신 「패턴이 너무 오래 걸립니다」가 표시됩니다. - 사용자 입력으로 패턴을 만들지 않습니다. 사용자가 패턴을 직접 넣어야 한다면 RE2처럼 입력 길이에 비례하는 시간을 보장하는 엔진을 검토하세요. 대신 RE2는 역참조와 전후방 탐색을 지원하지 않습니다.
PCRE나 Java 자료에서 권하는 원자 그룹 (?>…)과 소유 수량자 a++는 2026년 10월 기준 ECMAScript 정규식 문법에 없고, Node.js 24에서는 둘 다 문법 오류입니다. JavaScript에서는 패턴을 고쳐 쓰는 것이 해결책입니다.
ZEKILO Dev로 확인하기
- 정규식 테스터에 패턴을 입력하고 대상 글을 붙여 넣습니다.
- 플래그(
gimsuyd, 지원하는 브라우저에서는v)와 모드(찾기·바꾸기·나누기)를 고릅니다. - 일치 수와 일치별 위치·그룹 표를 확인합니다. 일치한 곳은 대상 글에도 강조됩니다.
배포하기 전에 「거의 맞는」 글, 예를 들어 올바른 긴 값의 끝에 틀린 글자 하나를 붙인 입력을 넣어 보세요. 시간 초과가 나오면 패턴을 고쳐야 합니다. 패턴과 대상 글은 브라우저 안에서만 처리합니다.
정리
- 점,
\d·\w·\s, 문자 집합, 앵커, 수량자, 횟수, 그룹과 선택, 게으른 일치를 알면 대부분의 패턴을 읽을 수 있습니다. (a+)+같은 중첩·겹치는 반복은 거의 맞는 글에서 시간이 지수적으로 늘어납니다.- 패턴을 고쳐 쓰고, 입력 길이를 제한하고, 시간 제한을 두고 실행하세요.