정규식(Regular Expression) 입문 — 패턴 매칭의 기본부터 실전까지
정규식의 기본 문법, 자주 쓰는 패턴, 실전 예제를 정리했다. 처음 접하는 사람을 위한 정규식 가이드.
정규식을 처음 마주치면 대부분 이런 반응이다: "이게 대체 무슨 소리야?" ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ 같은 걸 보면 암호문 같다. 근데 한번 익히면 텍스트를 다루는 방식이 완전히 바뀐다.
정규식이 뭔가
정규식(Regular Expression, regex)은 문자열에서 특정 패턴을 찾기 위한 표현법이다. "abc"처럼 정확한 문자열을 찾을 수도 있고, "숫자 3자리 - 숫자 4자리" 같은 패턴을 정의할 수도 있다.
대부분의 프로그래밍 언어가 정규식을 지원한다. JavaScript, Python, Java, Go, C# 전부 문법이 거의 같다. 한 번 배우면 어디서든 쓸 수 있다는 뜻.
기본 문법
리터럴 매칭
가장 단순한 정규식은 찾으려는 문자열 그 자체다.
hello→ "hello" 문자열을 찾음2026→ "2026"을 찾음
메타 문자
특별한 의미를 가진 문자들이 있다.
| 문자 | 의미 | 예시 |
|---|---|---|
. | 아무 문자 1개 | a.c → "abc", "a1c", "a c" |
\d | 숫자 (0-9) | \d\d → "42", "01" |
\w | 단어 문자 (a-z, A-Z, 0-9, _) | \w+ → "hello", "test_1" |
\s | 공백 문자 (스페이스, 탭, 줄바꿈) | a\sb → "a b" |
^ | 문자열 시작 | ^Hello → "Hello"로 시작하는 줄 |
$ | 문자열 끝 | end$ → "end"로 끝나는 줄 |
수량자
문자가 몇 번 반복되는지 지정한다.
| 수량자 | 의미 |
|---|---|
* | 0번 이상 |
+ | 1번 이상 |
? | 0번 또는 1번 |
{3} | 정확히 3번 |
{2,5} | 2~5번 |
{3,} | 3번 이상 |
\d+ 은 "숫자가 1개 이상"이니까 "1", "42", "12345" 전부 매칭된다.
문자 클래스
대괄호 [] 안에 매칭할 문자 목록을 넣는다.
[aeiou]→ 모음 하나[0-9]→ 숫자 하나 (\d와 같음)[a-zA-Z]→ 영문 대소문자[^0-9]→ 숫자가 아닌 문자 (^가 대괄호 안에서는 "제외"를 의미)
그룹과 캡처
소괄호 ()로 패턴을 묶을 수 있다.
(ab)+→ "ab", "abab", "ababab"(\d{3})-(\d{4})→ "010-1234"에서 "010"과 "1234"를 각각 캡처
캡처된 그룹은 코드에서 따로 추출해서 쓸 수 있다. 전화번호에서 앞 3자리만 뽑는다거나 하는 식.
OR 연산
|로 여러 패턴 중 하나를 매칭한다.
cat|dog→ "cat" 또는 "dog"(png|jpg|gif)→ 이미지 확장자
자주 쓰는 패턴 모음
이메일 (간이 검증)
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
완벽한 이메일 검증은 정규식으로 불가능하다. RFC 5322 스펙이 미친 듯이 복잡하거든. 위 패턴은 대부분의 일반적인 이메일 형식을 걸러주는 정도.
한국 전화번호
^01[016789]-?\d{3,4}-?\d{4}$
하이픈이 있어도 되고 없어도 된다. 010-1234-5678, 01012345678 둘 다 매칭.
URL
https?://[^\s]+
"http://" 또는 "https://"로 시작하고 공백이 아닌 문자가 1개 이상 이어지는 패턴. 정교한 URL 검증은 이것보다 훨씬 복잡하지만, 텍스트에서 URL을 대충 추출할 때는 이 정도로 충분하다.
날짜 (YYYY-MM-DD)
\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])
HTML 태그 제거
<[^>]+>
HTML 파싱에 정규식을 쓰는 건 일반적으로 좋지 않은 방법이다. 중첩된 태그, 속성 안의 > 문자 등을 제대로 처리 못한다. 하지만 단순한 태그 스트리핑 용도로는 쓸 만하다.
플래그
정규식 뒤에 붙이는 옵션이다.
| 플래그 | 의미 |
|---|---|
g | 전역 매칭 (처음 하나만 찾지 않고 전부 찾음) |
i | 대소문자 무시 |
m | 여러 줄 모드 (^, $가 각 줄의 시작/끝에 매칭) |
s | .이 줄바꿈도 매칭 |
JavaScript에서는 /패턴/플래그 형태로 쓴다: /hello/gi
탐욕적 vs 게으른 매칭
기본적으로 수량자는 "탐욕적(greedy)"이다. 가능한 많이 매칭하려고 한다.
<.+> 패턴을 <div>hello</div>에 적용하면 <div>hello</div> 전체가 매칭된다. .+가 최대한 많이 먹어치우니까.
뒤에 ?를 붙이면 "게으른(lazy)" 매칭이 된다. <.+?>는 <div>만 매칭. 가능한 적게 매칭하려고 한다.
HTML 태그를 다룰 때 이 차이가 중요하다.
전방탐색과 후방탐색
매칭에는 포함하지 않으면서 조건을 확인하는 기능이다.
\d+(?=원)→ "원" 앞의 숫자. "1000원"에서 "1000"만 캡처(?<=\$)\d+→ "$" 뒤의 숫자. "$50"에서 "50"만 캡처\d+(?!원)→ "원"이 뒤에 없는 숫자(?<!\$)\d+→ "$"가 앞에 없는 숫자
Python과 JavaScript(ES2018+)에서 전부 지원한다.
성능 — ReDoS를 조심하자
(a+)+처럼 중첩된 수량자는 특정 입력에서 지수적으로 느려질 수 있다. 이걸 ReDoS(Regular Expression Denial of Service)라고 부른다.
예를 들어 (a+)+$ 패턴에 "aaaaaaaaaaaaaaaaab"를 넣어보자. 정규식 엔진이 매칭 실패를 확인하기까지 수십만 번 역추적(backtracking)을 해야 한다. 문자열이 길어질수록 시간이 지수적으로 늘어난다.
사용자가 입력한 값에 정규식을 적용할 때는 이 점을 꼭 고려해야 한다. Node.js 서버에서 ReDoS가 터지면 이벤트 루프가 멈추면서 전체 서비스가 먹통이 될 수 있다.
안전한 패턴을 만드는 요령:
- 중첩 수량자
(a+)+피하기 - 가능하면
+나*대신{1,100}같은 범위 제한 걸기 - safe-regex 같은 라이브러리로 패턴 검증하기
정규식 작성 팁
복잡한 정규식은 나중에 자기가 봐도 못 알아본다. 3개월 뒤에 다시 보면 "이게 뭐였지?" 하게 된다. 그래서 의미 있는 변수명에 담거나 주석을 남겨두는 게 좋다.
// 한국 전화번호 매칭
const PHONE_REGEX = /^01[016789]-?\d{3,4}-?\d{4}$/;
// Python에서는 verbose 모드로 주석 가능
// pattern = re.compile(r"""
// ^01[016789] # 통신사 번호
// -? # 선택적 하이픈
// \d{3,4} # 중간 번호
// -? # 선택적 하이픈
// \d{4}$ # 뒷 번호
// """, re.VERBOSE)
그리고 머릿속으로만 짜지 말고 반드시 테스트하면서 만들자. 정규식은 눈으로 보면 맞는 것 같아도 실제로 돌려보면 예상과 다른 경우가 허다하다. 정규식 테스터로 실시간 매칭 결과를 확인하면서 작성하면 훨씬 빠르고 정확하다.