WebPiki
tutorial

정규식(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)

그리고 머릿속으로만 짜지 말고 반드시 테스트하면서 만들자. 정규식은 눈으로 보면 맞는 것 같아도 실제로 돌려보면 예상과 다른 경우가 허다하다. 정규식 테스터로 실시간 매칭 결과를 확인하면서 작성하면 훨씬 빠르고 정확하다.

#정규식#regex#패턴매칭#개발#프로그래밍

관련 글