regex test debug developer-tools regexp

정규식 테스터: 정규 표현식 디버깅 필수 가이드

실시간 정규식 테스터로 정규 표현식 개발을 간소화하세요. 라이브 매칭, 그룹화 및 일반적인 정규식 스니펫을 지원합니다.

소개

정규 표현식(Regular Expression, 줄여서 regex 또는 regexp)은 검색 패턴을 정의하는 문자열의 시퀀스입니다. 정규 표현식은 개발자 도구함에서 가장 강력한 도구 중 하나로, 단 하나의 간결한 표현식으로 텍스트를 검색, 검증, 추출, 변환할 수 있습니다.

웹 폼에서 이메일 주소를 검증하거나, 로그에서 데이터를 추출하거나, 수천 개의 파일에서 복잡한 찾기-바꾸기를 수행할 때 정규 표현식은 거의 모든 프로그래밍 언어와 대부분의 텍스트 편집기가 이해할 수 있는 간결한 표기법으로 원하는 것을 정확하게 표현할 수 있게 해줍니다.


정규 표현식의 역사

정규 표현식의 역사는 이론 컴퓨터 과학의 기초로 거슬러 올라갑니다:

  • 1951년 — 수학자 Stephen Kleene이 오토마타 이론 연구의 일환으로 정규 언어의 개념을 형식화하고 Kleene 스타(*) 표기법을 도입.
  • 1968년Ken Thompson이 QED 텍스트 편집기에 정규 표현식을 구현하고, 이후 grep, sed, awk 등의 Unix 도구에 도입하여 개발자의 일상 작업에 정규 표현식을 가져옴.
  • 1986년POSIXBRE(기본 정규 표현식)와 ERE(확장 정규 표현식) 두 가지 방언을 표준화하여 Unix 시스템 간 상호 운용성을 보장.
  • 1997년Philip Hazel이 lookahead, lookbehind, 명명된 캡처 등 강력한 기능을 포함한 PCRE(Perl Compatible Regular Expressions) 라이브러리를 생성.
  • 1999년ECMAScript 3이 JavaScript의 RegExp 객체를 표준화.
  • 2015년ES6u(Unicode)와 y(sticky) 플래그를 추가.
  • 2018년ES2018이 명명된 캡처 그룹((?<name>...))과 lookbehind 어서션을 추가.

POSIX vs PCRE vs JavaScript RegExp 비교

기능 BRE/ERE (POSIX) PCRE JavaScript RegExp
Lookahead
Lookbehind ✓ (ES2018+)
명명된 그룹 ✓ (ES2018+)
비탐욕 매칭
Unicode 제한적 ✓ (u 플래그 사용 시)
역참조

핵심 문법 참조

빠른 참조 표

패턴 의미
. 개행 문자를 제외한 임의의 문자
^ 문자열 시작 / 줄 시작 (m 플래그 사용 시)
$ 문자열 끝 / 줄 끝 (m 플래그 사용 시)
\d 임의의 숫자 [0-9]
\D 임의의 비숫자
\w 단어 문자 [a-zA-Z0-9_]
\W 비단어 문자
\s 공백 문자 (스페이스, 탭, 개행 등)
\S 비공백 문자
[abc] 문자 클래스 — a, b, 또는 c 중 하나와 일치
[^abc] 부정 문자 클래스 — a, b, c를 제외한 모든 문자와 일치
[a-z] 문자 범위
* 0회 이상 (탐욕적)
+ 1회 이상 (탐욕적)
? 0회 또는 1회 (탐욕적)
{n} 정확히 n회
{n,m} n회 이상 m회 이하 (탐욕적)
*? +? ?? 게으른 (비탐욕적) 동등 형태
(abc) 캡처 그룹
(?:abc) 비캡처 그룹
(?<name>abc) 명명된 캡처 그룹
| 선택 — 왼쪽 또는 오른쪽과 일치
(?=...) 긍정적 lookahead
(?!...) 부정적 lookahead
(?<=...) 긍정적 lookbehind
(?<!...) 부정적 lookbehind

문자 클래스

문자 클래스를 사용하면 문자 집합에서 하나의 문자를 매칭할 수 있습니다. [aeiou]는 단일 모음에, [a-zA-Z]는 임의의 알파벳에, [^0-9]는 숫자가 아닌 임의의 문자에 매칭됩니다.

자주 사용되는 단축 표기:

  • \d[0-9]와 동일
  • \w[a-zA-Z0-9_]와 동일
  • \s는 스페이스, 탭(\t), 개행(\n), 캐리지 리턴(\r) 등의 공백 문자에 매칭

수량자: 탐욕적 vs 게으른

기본적으로 수량자는 탐욕적이며 가능한 한 많은 문자를 매칭하려 합니다. HTML 문자열 <b>굵게</b>와 <i>기울임</i>를 예로 들면:

<.*>    → <b>에서 </i>까지 전체 문자열에 매칭 (탐욕적)
<.*?>   → <b>, </b>, <i>, </i>에 각각 매칭 (게으른)

수량자 뒤에 ?를 추가하면 게으른(비탐욕적)이 되어, 전체 패턴이 성공하는 범위 내에서 가능한 한 적게 매칭합니다.

앵커

  • ^는 문자열의 시작에 매칭 (m 플래그 사용 시 줄 시작).
  • $는 문자열의 에 매칭 (m 플래그 사용 시 줄 끝).
  • \b단어 경계에 매칭 — 단어 문자와 비단어 문자 사이의 위치.
  • \B비단어 경계에 매칭.

그룹과 역참조

캡처 그룹 (...)은 매칭된 텍스트를 캡처하며, \1, \2 등의 역참조로 후속 패턴에서 참조하거나 매칭 결과 배열에서 접근할 수 있습니다.

비캡처 그룹 (?:...)은 캡처를 생성하지 않고 하위 패턴을 그룹화합니다. 캡처 값이 필요 없을 때 더 효율적입니다.

명명된 캡처 그룹 (?<year>\d{4})은 이름(JavaScript에서는 match.groups.year)으로 캡처를 참조할 수 있게 하여 패턴의 가독성을 크게 향상시킵니다.

Lookahead와 Lookbehind

이러한 제로 너비 어서션은 문자가 아닌 위치에 매칭됩니다:

\d+(?=원)      → 뒤에 "원"이 올 때만 숫자에 매칭
\d+(?!원)      → 뒤에 "원"이 오지 않을 때만 숫자에 매칭
(?<=\$)\d+     → 앞에 "$"가 있을 때만 숫자에 매칭
(?<!\$)\d+     → 앞에 "$"가 없을 때만 숫자에 매칭

Lookahead와 lookbehind는 문자를 소비하지 않으므로 매칭 결과에 해당 부분이 포함되지 않습니다.


플래그

플래그 이름 효과
i 대소문자 무시 [a-z][A-Z]에도 매칭
g 전역 첫 번째 매칭만이 아닌 모든 매칭을 검색
m 멀티라인 ^$가 줄 경계에 매칭
s dotAll .이 개행 문자에도 매칭
u Unicode 전체 Unicode 매칭 활성화; \p{}에 필요
y Sticky lastIndex 위치에서만 매칭
x 상세/확장 공백과 주석 허용 (PCRE/Python 전용)

x(상세) 플래그는 복잡한 패턴을 문서화하는 데 특히 유용합니다:

import re
pattern = re.compile(r"""
    ^                        # 문자열 시작
    (?P<year>\d{4})          # 4자리 연도
    -
    (?P<month>0[1-9]|1[0-2]) # 월 01~12
    -
    (?P<day>0[1-9]|[12]\d|3[01])  # 일 01~31
    $
""", re.VERBOSE)

자주 사용되는 패턴과 실제 예제

이메일 주소 검증

^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

분석:

  • ^[a-zA-Z0-9._%+-]+ — 로컬 부분 (문자, 숫자, 특수 문자 일부)
  • @ — 리터럴 @ 기호
  • [a-zA-Z0-9.-]+ — 도메인 이름
  • \.[a-zA-Z]{2,}$ — 2자 이상의 TLD

URL 매칭

https?://(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z]{2,})+(?:/[^\s]*)?

ISO 날짜 (YYYY-MM-DD)

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

0112와 일 0131을 검증합니다. 월별 최대 일수(예: 2월 30일)는 검증하지 않습니다.

IPv4 주소

\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b

분석:

  • 25[0-5] — 250~255
  • 2[0-4]\d — 200~249
  • [01]?\d\d? — 0~199

미국 전화번호

\+?1?\s?[\(]?\d{3}[\)]?[-.\s]?\d{3}[-.\s]?\d{4}

(555) 123-4567, 555-123-4567, +1 555 123 4567 등의 형식에 매칭.

16진수 색상 코드

#(?:[0-9A-Fa-f]{3}){1,2}\b

3자리(#F00)와 6자리(#FF0000) 16진수 색상 모두에 매칭.


다양한 프로그래밍 언어에서의 정규 표현식

JavaScript

// 리터럴 문법
const regex = /^hello\s+world$/im;
const match = "Hello World".match(regex);

// 생성자 문법 (동적 패턴에 유용)
const term = "world";
const dynamic = new RegExp(`hello\\s+${term}`, "im");

// 전체 치환
const result = "foo bar foo".replaceAll(/foo/g, "baz");

// 명명된 캡처 (ES2018+)
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { year, month, day } = "2024-03-15".match(dateRegex).groups;

Python

import re

# 재사용을 위한 컴파일
pattern = re.compile(r'^hello\s+world$', re.IGNORECASE | re.MULTILINE)
match = pattern.match("Hello World")

# 모든 매칭 찾기
dates = re.findall(r'\d{4}-\d{2}-\d{2}', text)

# 함수를 사용한 치환
result = re.sub(r'\b\d+\b', lambda m: str(int(m.group()) * 2), "1 더하기 2는 3")

# 명명된 그룹
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', "2024-03")
print(m.group('year'))  # 2024

Java

import java.util.regex.*;

Pattern p = Pattern.compile("^hello\\s+world$",
    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher("Hello World");
boolean found = m.matches();

// 그룹 추출
Pattern datePattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher dm = datePattern.matcher("오늘은 2024-03-15입니다");
if (dm.find()) {
    String year = dm.group(1);
}

Go

import "regexp"

re := regexp.MustCompile(`(?im)^hello\s+world$`)
match := re.FindString("Hello World")

// 모든 서브매칭 찾기
dateRe := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
all := dateRe.FindAllStringSubmatch(text, -1)
for _, m := range all {
    year, month, day := m[1], m[2], m[3]
    _ = year; _ = month; _ = day
}

// 명명된 그룹
namedRe := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})`)
match2 := namedRe.FindStringSubmatch("2024-03")
yearIdx := namedRe.SubexpIndex("year")
fmt.Println(match2[yearIdx]) // 2024

성능과 파국적 백트래킹

백트래킹의 작동 방식

대부분의 정규 표현식 엔진은 NFA(비결정적 유한 오토마톤) 기반 매칭을 사용합니다. 이는 한 경로가 실패하면 엔진이 패턴의 여러 경로를 시도할 수 있음을 의미합니다. 이 백트래킹 덕분에 lookahead와 역참조 같은 기능이 가능하지만, 성능 함정이 될 수도 있습니다.

파국적 사례

패턴 (a+)+를 문자열 "aaaaaX"에 적용하는 경우를 생각해보세요:

  1. 외부 +는 가능한 한 많은 그룹을 매칭하려 합니다.
  2. 엔진이 X에 도달해 실패하면 백트래킹하여 그룹의 반복 사이에 a 문자를 다르게 분배하려 합니다.
  3. 길이 n의 문자열에는 *2^(n-1)*개의 가능한 분배 방식이 있어 지수 시간 복잡도로 이어집니다.
(a+)+  에 "aaaaaaaaaaaaaaaaaX" 적용  →  몇 초 또는 몇 분이 걸릴 수 있습니다!

다른 위험한 패턴으로 (a|aa)+, (\w+\s*)+ 및 겹치는 문자 클래스에 대한 중첩 수량자가 있습니다.

회피 방법

  1. 같은 문자 집합에 중첩 수량자를 사용하지 않기: (a+)+a+ 사용.
  2. 원자 그룹 (?>...) 또는 소유 수량자 a++(PCRE)를 사용하여 이미 매칭된 그룹으로의 백트래킹 방지.
  3. 구체성 높이기: 구분자를 제외하는 문자 클래스로 .*를 대체 (예: 인용된 문자열 내에서 [^"]*).
  4. 가능한 한 앵커 사용하여 엔진이 빠르게 실패할 수 있도록.
  5. 프로덕션 코드에서 타임아웃 설정 (특히 신뢰할 수 없는 입력 처리 시).

복잡한 정규 표현식 읽는 방법

^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$를 분해합니다:

^                    → 앵커: 문자열 시작
[a-zA-Z0-9._%+-]+   → 로컬 부분에 허용된 1개 이상의 문자
@                    → 리터럴 @
[a-zA-Z0-9.-]+      → 1개 이상의 도메인 문자
\.                   → 리터럴 점 (이스케이프됨)
[a-zA-Z]{2,}        → TLD: 2자 이상의 알파벳
$                    → 앵커: 문자열 끝

팁: 이 사이트의 정규 표현식 테스터를 사용하여 각 캡처 그룹을 하이라이트하고 각 토큰이 입력의 어느 부분과 매칭되는지 실시간으로 확인하세요. 가장 단순한 유효한 조각부터 시작하여 점진적으로 패턴을 구축하고, 검증 후 확장하세요.


모범 사례

  1. 한 번 컴파일하고 여러 번 사용하기. 패턴을 미리 컴파일하는 것(Python의 re.compile(), Java의 Pattern.compile())이 매번 재파싱하는 것보다 훨씬 효율적입니다.
  2. 캡처 값이 필요 없을 때는 비캡처 그룹 (?:...) 선호. 의도를 명확히 하고 불필요한 메모리 할당을 피할 수 있습니다.
  3. 패턴에 원시 문자열 사용. Python에서는 '\\d+' 대신 r'\d+'를 사용하여 이중 이스케이프를 방지. JavaScript의 리터럴 문법 /\d+/는 이를 자동으로 처리합니다.
  4. 캡처 그룹에 이름 붙이기. (?<year>\d{4})\1 같은 인덱스 참조보다 훨씬 유지보수하기 쉽습니다.
  5. 엣지 케이스로 테스트하기: 빈 문자열, 거의 매칭되지만 완전히는 아닌 문자열, Unicode 문자, 매우 긴 입력.
  6. 복잡한 패턴 문서화: Python/PCRE에서는 x(상세) 플래그를 사용하고, 코드에서는 인라인 주석 사용.
  7. 정규 표현식으로 전체 HTML이나 XML을 파싱하지 않기. 적절한 파서 라이브러리를 사용하세요.
  8. 서버 측에서 입력 검증하기. 클라이언트 측 정규 표현식 검증은 UX를 향상시키지만 유일한 방어선이 되어서는 안 됩니다.

자주 묻는 질문 (FAQ)

Q: Python에서 match()search()의 차이는 무엇인가요?
A: re.match()는 문자열의 처음에서만 매칭합니다. re.search()는 전체 문자열을 스캔하여 매칭을 찾습니다. 패턴이 전체 문자열과 매칭되도록 하려면 re.fullmatch()를 사용하세요.

Q: 문자 클래스 [^abc] 내의 ^는 왜 의미가 다른가요?
A: 문자 클래스 내에서 ^첫 번째 문자로 나타나면 클래스를 부정합니다 — 해당 집합에 없는 임의의 문자와 매칭됩니다. 문자 클래스 밖에서 ^는 문자열 시작의 앵커입니다.

Q: 정규 표현식으로 HTML을 파싱할 수 있나요?
A: 알려진 HTML 구조에서의 단순하고 명확한 추출이라면 정규 표현식으로 작동할 수 있습니다. 그러나 HTML은 정규 언어가 아니며 임의의 중첩과 선택적 닫는 태그를 허용합니다. 강건한 파싱을 위해 적절한 HTML 파서(Python의 BeautifulSoup, JS의 DOMParser)를 사용하세요.

Q: 탐욕적 수량자와 소유 수량자의 차이는 무엇인가요?
A: 탐욕적 수량자는 백트래킹합니다 — 최대 매칭을 시도하고 필요시 문자를 반환합니다. 소유 수량자(예: PCRE의 a++)는 반환하지 않습니다 — 한번 매칭되면 잠금됩니다. 이는 파국적 백트래킹을 방지하지만 탐욕적 수량자였다면 성공했을 매칭이 실패할 수도 있습니다.

Q: 리터럴 점 .이나 괄호 (에 매칭하려면 어떻게 하나요?
A: 백슬래시로 이스케이프합니다: \.는 리터럴 점에, \(는 리터럴 왼쪽 괄호에 매칭됩니다.

Q: 정규 표현식은 기본적으로 대소문자를 구분하나요?
A: 네. i 플래그(JavaScript의 /pattern/i, Python의 re.IGNORECASE)를 사용하면 대소문자를 구분하지 않는 매칭이 활성화됩니다.

Q: \b는 무엇에 매칭되나요?
A: \b는 제로 너비 단어 경계 어서션입니다. 단어 문자(\w)와 비단어 문자(\W) 사이의 위치에 매칭됩니다.

Q: 전체 문자열이 패턴과 매칭되는지 테스트하려면 어떻게 하나요?
A: ^$로 앵커를 사용합니다: ^pattern$. Python에서는 re.fullmatch()도 사용할 수 있습니다. JavaScript에서는 앵커를 사용한 .test()를 사용하거나 match()[0].length === input.length를 확인합니다.


이 사이트의 정규 표현식 테스터를 사용하여 이 가이드의 모든 패턴을 실험해보세요. 패턴을 붙여넣고 테스트 문자열을 입력하면 매칭 결과가 실시간으로 하이라이트됩니다.