← 블로그

정규표현식 입문: 패턴 매칭의 모든 것

2026년 1월 19일 · 10분 읽기

솔직히 말할게요. 정규표현식(regex)은 처음 보면 외계어 같습니다. ^[\w.-]+@[\w.-]+\.\w{2,}$ 이런 거 보면 "이게 대체 뭐야" 싶죠. 저도 그랬습니다.

그런데 한 번 익숙해지면, 왜 다들 정규표현식 정규표현식 하는지 알게 됩니다. 텍스트 처리에서 이것만큼 강력한 도구가 없거든요.

정규표현식이 뭔데?

한 문장으로: 텍스트에서 특정 패턴을 찾는 언어입니다.

"이메일 주소만 찾고 싶어", "전화번호 형식이 맞는지 확인하고 싶어", "URL에서 도메인만 추출하고 싶어"—이런 작업을 정규표현식으로 해결합니다.

기본부터 차근차근

1. 그냥 문자 매칭

가장 간단한 정규표현식은 그냥 찾고 싶은 문자 그대로입니다.

패턴: hello

텍스트: "say hello to the world"

2. 메타문자: 특별한 의미를 가진 문자들

여기서부터 정규표현식의 진짜 힘이 나옵니다.

메타문자 의미 예시
. 아무 문자 하나 h.t → hat, hit, hot
^ 문자열 시작 ^Hello → Hello로 시작하는 문자열
$ 문자열 끝 world$ → world로 끝나는 문자열
* 앞 문자 0번 이상 ab*c → ac, abc, abbc, abbbc...
+ 앞 문자 1번 이상 ab+c → abc, abbc, abbbc... (ac는 X)
? 앞 문자 0번 또는 1번 colou?r → color, colour

3. 문자 클래스: 여러 문자 중 하나

대괄호 [] 안에 있는 문자 중 하나와 매칭됩니다.

[aeiou] → 모음 하나

[0-9] → 숫자 하나

[a-zA-Z] → 영문자 하나

[^0-9] → 숫자가 아닌 문자 (^가 안에 있으면 "제외")

4. 미리 정의된 문자 클래스

자주 쓰는 패턴은 단축 표현이 있습니다.

단축 의미 동일 표현
\d 숫자 [0-9]
\w 단어 문자 [a-zA-Z0-9_]
\s 공백 문자 스페이스, 탭, 줄바꿈
\D, \W, \S 대문자 = 반대 숫자 아닌 것, 단어 문자 아닌 것...

실전 예제

이메일 검증 (간단 버전)

[\w.-]+@[\w.-]+\.\w{2,}

분해해보면:

  • [\w.-]+ : 단어문자, 점, 하이픈이 1개 이상 (아이디 부분)
  • @ : 골뱅이
  • [\w.-]+ : 도메인 이름
  • \. : 점 (\.로 이스케이프 해야 진짜 점)
  • \w{2,} : 2자 이상의 단어 문자 (com, net, co.kr 등)

한국 휴대폰 번호

01[016789]-?\d{3,4}-?\d{4}

010, 011, 016, 017, 018, 019로 시작하고, 중간에 하이픈은 있어도 되고 없어도 됩니다.

HTML 태그 제거

<[^>]+>

<로 시작하고, >가 아닌 문자가 1개 이상, >로 끝나는 패턴. 모든 HTML 태그를 찾습니다.

자주 하는 실수

1. 이스케이프 깜빡

점(.), 별표(*), 괄호 등은 특별한 의미가 있어서, 그 문자 자체를 찾으려면 앞에 \를 붙여야 합니다. 1+1=2를 찾으려면 1\+1=2

2. 탐욕적 매칭

.*는 가능한 많이 매칭합니다. <.*><b>hi</b>를 매칭하면 <b>만 아니라 전체가 매칭됩니다. <.*?>처럼 ?를 붙이면 최소 매칭(게으른 매칭)이 됩니다.

3. 앵커 빠뜨리기

\d{3}은 "숫자 3개"를 찾는데, "12345"에서도 매칭됩니다. 정확히 3개만 원하면 ^\d{3}$처럼 앵커를 써야 합니다.

수량자 심화: 그리디 vs 레이지

정규표현식에서 가장 혼동되는 개념 중 하나가 그리디(greedy)와 레이지(lazy) 매칭입니다. 기본적으로 모든 수량자(*, +, ?)는 그리디합니다. 가능한 한 많이 매칭하려고 합니다.

텍스트: "<b>hello</b> world <b>foo</b>"

그리디: <.*>  → "<b>hello</b> world <b>foo</b>" (전체!)
레이지: <.*?> → "<b>" (첫 번째 태그만)

수량자 뒤에 ?를 붙이면 레이지 모드가 됩니다. 가능한 한 적게 매칭합니다.

그리디 레이지 의미
* *? 0번 이상 (최소/최대)
+ +? 1번 이상 (최소/최대)
{n,m} {n,m}? n~m번 (최소/최대)

캡처 그룹과 후방 참조

괄호 ()는 단순히 그룹을 만드는 것이 아니라, 매칭된 내용을 "캡처"합니다. 캡처된 내용은 나중에 참조하거나 치환에서 사용할 수 있습니다.

// 날짜 형식 변환: 2024-01-15 → 01/15/2024
패턴: (\d{4})-(\d{2})-(\d{2})
치환: $2/$3/$1

$1 = "2024" (첫 번째 괄호)
$2 = "01"   (두 번째 괄호)
$3 = "15"   (세 번째 괄호)

후방 참조(backreference)는 같은 패턴 안에서 이전에 캡처한 그룹을 다시 참조하는 기능입니다. \1, \2 같은 형태로 사용합니다.

// 연속 중복 단어 찾기
패턴: \b(\w+)\s+\1\b

"the the quick brown fox" → "the the" 매칭
\1은 첫 번째 그룹에서 캡처한 "the"를 참조

명명 그룹 (Named Groups)

그룹에 이름을 붙일 수도 있습니다. 숫자보다 의미가 명확해서 복잡한 패턴에서 유용합니다.

// JavaScript
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = regex.exec("2024-01-15");
console.log(match.groups.year);  // "2024"
console.log(match.groups.month); // "01"

룩어헤드와 룩비하인드

룩어헤드(lookahead)와 룩비하인드(lookbehind)는 "주변을 확인하지만 매칭에 포함하지 않는" 패턴입니다. 특정 조건이 앞이나 뒤에 있는지 확인만 하고 싶을 때 사용합니다.

종류 문법 의미
긍정 룩어헤드 (?=...) 뒤에 ...이 있으면 매칭
부정 룩어헤드 (?!...) 뒤에 ...이 없으면 매칭
긍정 룩비하인드 (?<=...) 앞에 ...이 있으면 매칭
부정 룩비하인드 (?<!...) 앞에 ...이 없으면 매칭
// 가격에서 숫자만 추출 ($ 뒤의 숫자)
패턴: (?<=\$)\d+
텍스트: "Price: $100" → "100" 매칭 ($는 포함 안 됨)

// "ing"로 끝나지 않는 단어 찾기
패턴: \b\w+(?!ing)\b
// 주의: 이건 의도대로 안 됨! 단어 경계와 조합이 까다로움

룩어헤드/룩비하인드는 강력하지만, 과도하게 사용하면 패턴이 읽기 어려워집니다. 간단한 문자열 처리는 정규표현식 대신 프로그래밍 언어의 문자열 메서드로 처리하는 것이 나을 때도 많습니다.

실전 예제 모음

URL 매칭

https?:\/\/[\w\-.]+(:\d+)?(\/[\w\-._~:/?#\[\]@!$&'()*+,;%=]*)?

분해:
  https?          → http 또는 https
  :\/\/           → ://
  [\w\-.]+(:\d+)? → 도메인(+ 선택적 포트)
  (\/...)?        → 나머지 경로 (선택적)

IP 주소 (IPv4)

// 간단 버전 (0-999 허용)
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}

// 정확한 버전 (0-255만 허용)
(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}

정확한 IP 검증을 정규표현식으로 하려면 이렇게 복잡해집니다. 이런 경우 정규표현식으로 대략적인 형식만 확인하고, 실제 범위 검증은 코드로 하는 것이 실용적입니다.

날짜 형식 (YYYY-MM-DD)

\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])

분해:
  \d{4}             → 연도 4자리
  (0[1-9]|1[0-2])   → 월 (01-12)
  (0[1-9]|[12]\d|3[01]) → 일 (01-31)

이 패턴은 2월 30일 같은 잘못된 날짜는 거르지 못합니다. 날짜 검증은 정규표현식보다 Date 객체나 라이브러리를 사용하는 것이 정확합니다.

정규표현식의 역사

정규표현식의 기원은 1950년대 수학자 Stephen Kleene에게 거슬러 올라갑니다. 그가 "정규 집합(regular sets)"이라는 수학적 개념을 정의했고, 이것이 정규표현식의 이론적 토대가 되었습니다. 이후 1968년 Ken Thompson이 이 개념을 Unix의 텍스트 편집기 ed에 구현했고, 이것이 grep의 탄생으로 이어졌습니다.

현대 정규표현식은 Larry Wall이 Perl에서 크게 확장했습니다. 비캡처 그룹, 룩어헤드, 유니코드 지원 등 오늘날 우리가 사용하는 많은 기능이 Perl에서 시작되었습니다. 그래서 많은 정규표현식 구현이 "Perl Compatible Regular Expressions(PCRE)"라는 이름을 사용합니다.

성능 팁과 주의사항

정규표현식은 잘못 쓰면 성능 문제를 일으킬 수 있습니다. 몇 가지 중요한 팁을 소개합니다.

1. 카타스트로픽 백트래킹 (Catastrophic Backtracking)

중첩된 수량자가 있으면 엔진이 기하급수적으로 많은 경우의 수를 시도합니다. 대표적인 예가 (a+)+ 패턴입니다. 입력이 "aaaaaaaaaaaaaab"처럼 마지막에 매칭이 실패하면, 엔진이 모든 가능한 분할을 시도하느라 몇 분 이상 걸릴 수 있습니다.

2. 구체적인 패턴을 사용하세요

.* 대신 [^"]*처럼 필요한 문자만 매칭하면 불필요한 백트래킹을 줄일 수 있습니다. "아무거나 매칭"보다 "이것을 제외한 나머지"가 훨씬 빠릅니다.

3. 컴파일된 정규표현식 재사용

반복문 안에서 매번 정규표현식을 생성하지 마세요. 루프 밖에서 한 번 생성하고 재사용하는 것이 효율적입니다.

// 나쁜 예
for (const line of lines) {
  if (line.match(/^\d{4}-\d{2}-\d{2}/)) { ... }
}

// 좋은 예
const datePattern = /^\d{4}-\d{2}-\d{2}/;
for (const line of lines) {
  if (datePattern.test(line)) { ... }
}

4. 정규표현식이 필요 없는 경우도 있습니다

단순히 문자열에 특정 단어가 포함되어 있는지 확인하는 것이라면, includes()startsWith()가 정규표현식보다 빠르고 읽기 쉽습니다. 정규표현식은 패턴 매칭이 필요할 때만 사용하세요.

연습이 답이다

정규표현식은 읽는 것보다 직접 써보는 게 훨씬 빨리 익숙해집니다. 처음에는 기본 메타문자와 수량자부터 시작하고, 캡처 그룹과 룩어헤드는 필요할 때 하나씩 배워가면 됩니다. 모든 문법을 한 번에 외울 필요는 없습니다. 필요할 때 찾아보면서 쓰다 보면 자연스럽게 손에 익습니다. 아래 도구에서 직접 테스트해보세요. 실시간으로 매칭 결과를 보면서 연습하면 금방 감이 옵니다.

자주 묻는 질문

정규표현식을 배우기 가장 좋은 방법은?

실제 문제를 풀며 배우는 것이 가장 효과적입니다. 이메일 검증, 전화번호 추출 같은 간단한 패턴부터 시작하세요. 온라인 테스터에서 실시간으로 결과를 확인하며 연습하면 빠르게 익힐 수 있습니다. 모든 문법을 외우려 하지 말고 필요할 때 찾아보세요.

정규표현식이 느린 경우는 언제인가요?

중첩된 수량자(예: (a+)+)는 카타스트로픽 백트래킹을 일으켜 성능이 급격히 저하됩니다. 큰 입력에 복잡한 패턴을 적용할 때도 느려질 수 있습니다. 가능하면 구체적인 패턴을 사용하고, .* 같은 탐욕적 수량자는 최소화하세요.

JavaScript와 Python의 정규표현식은 같나요?

기본 문법은 유사하지만 차이점이 있습니다. Python은 명명 그룹에 (?P)을, JavaScript는 (?)을 사용합니다. Python은 후방 참조(lookbehind)에 가변 길이를 지원하지 않지만, JavaScript ES2018부터는 지원합니다. 각 언어의 문서를 확인하세요.

정규표현식으로 HTML을 파싱할 수 있나요?

단순한 패턴 추출은 가능하지만, HTML의 중첩 구조를 정규표현식으로 완벽하게 파싱하는 것은 불가능합니다. HTML은 문맥 자유 문법이므로 정규 문법으로는 표현할 수 없습니다. DOM 파서를 사용하는 것이 올바른 접근입니다.

자주 쓰는 정규표현식 패턴은?

이메일: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}, 한국 전화번호: 01[016789]-?\d{3,4}-?\d{4}, URL: https?://[\S]+, 숫자만: ^\d+$, 한글만: ^[가-힣]+$ 등이 있습니다.