Skip to content
Published on

정규표현식 완전 정복 2025: 기초부터 고급 패턴, 실전 활용까지 — 더 이상 두렵지 않다

Authors

1. 왜 정규표현식은 여전히 중요한가

2025년에도 정규표현식(Regular Expression, regex)은 개발자의 핵심 도구입니다.

입력 검증 — 이메일, URL, 전화번호, 신용카드 번호의 형식을 검증하는 가장 간결한 방법입니다.

로그 분석 — 수 GB의 로그 파일에서 특정 패턴(에러 코드, IP 주소, 타임스탬프)을 추출합니다.

텍스트 변환 — 코드 리팩토링, 데이터 클렌징, 파일 포맷 변환에 활용됩니다.

AI 프롬프트 전처리 — LLM에 입력하기 전에 불필요한 특수문자, HTML 태그 제거에 사용됩니다.

모든 언어에서 지원 — JavaScript, Python, Java, Go, Rust 등 거의 모든 프로그래밍 언어에 내장되어 있습니다.


2. 기초 문법: 리터럴과 메타문자

리터럴 매칭

정규표현식의 가장 기본은 리터럴 매칭입니다. 문자를 그대로 매칭합니다.

import re

# 리터럴 매칭
print(re.findall(r'hello', 'hello world hello'))
# ['hello', 'hello']

핵심 메타문자

메타문자의미예시매칭 결과
.아무 문자 1개 (개행 제외)h.that, hit, hot
^문자열 시작^Hello"Hello world"의 Hello
$문자열 끝world$"Hello world"의 world
*0회 이상 반복ab*cac, abc, abbc
+1회 이상 반복ab+cabc, abbc (ac는 X)
?0회 또는 1회colou?rcolor, colour
|OR (또는)cat|dogcat 또는 dog
[]문자 클래스[aeiou]모음 1개
()그룹화 / 캡처(ab)+ab, abab
\이스케이프\.실제 마침표

이스케이프가 필요한 문자

다음 문자들은 메타문자이므로 리터럴로 사용하려면 \를 앞에 붙여야 합니다:

. * + ? ^ $ | [ ] ( ) { } \
# 마침표를 리터럴로 매칭
print(re.findall(r'www\.example\.com', 'visit www.example.com'))
# ['www.example.com']

3. 문자 클래스 (Character Classes)

기본 문자 클래스

# [abc] — a, b, c 중 하나
re.findall(r'[aeiou]', 'hello world')
# ['e', 'o', 'o']

# [^abc] — a, b, c가 아닌 문자
re.findall(r'[^aeiou ]', 'hello world')
# ['h', 'l', 'l', 'w', 'r', 'l', 'd']

# [a-z] — 범위 지정
re.findall(r'[A-Z]', 'Hello World')
# ['H', 'W']

# [0-9a-fA-F] — 16진수 문자
re.findall(r'[0-9a-fA-F]+', 'color: #FF00AB')
# ['FF00AB']

축약 문자 클래스

축약동등한 표현의미
\d[0-9]숫자
\D[^0-9]숫자가 아닌 것
\w[a-zA-Z0-9_]단어 문자
\W[^a-zA-Z0-9_]단어 문자가 아닌 것
\s[ \t\n\r\f\v]공백 문자
\S[^ \t\n\r\f\v]공백이 아닌 것
# 전화번호 추출
text = "전화: 010-1234-5678, 팩스: 02-123-4567"
print(re.findall(r'\d{2,3}-\d{3,4}-\d{4}', text))
# ['010-1234-5678', '02-123-4567']

POSIX 문자 클래스 (일부 언어)

[:alpha:]  — 알파벳 문자
[:digit:]  — 숫자
[:alnum:]  — 알파벳 + 숫자
[:space:]  — 공백 문자
[:upper:]  — 대문자
[:lower:]  — 소문자
[:punct:]  — 구두점

POSIX 클래스는 grep, sed 등 UNIX 도구에서 주로 사용되며, Python/JavaScript에서는 \d, \w 등을 사용합니다.


4. 수량자 (Quantifiers)

기본 수량자

# * (0회 이상)
re.findall(r'go*d', 'gd god good goood')
# ['gd', 'god', 'good', 'goood']

# + (1회 이상)
re.findall(r'go+d', 'gd god good goood')
# ['god', 'good', 'goood']

# ? (0 또는 1회)
re.findall(r'colou?r', 'color colour')
# ['color', 'colour']

정확한 횟수 지정

# {n} — 정확히 n회
re.findall(r'\d{4}', '2025 is the year 12345')
# ['2025', '1234']

# {n,m} — n회 이상 m회 이하
re.findall(r'\d{2,4}', '1 12 123 1234 12345')
# ['12', '123', '1234', '1234']

# {n,} — n회 이상
re.findall(r'\d{3,}', '1 12 123 1234')
# ['123', '1234']

탐욕적(Greedy) vs 게으른(Lazy) 매칭

기본적으로 수량자는 탐욕적입니다 — 가능한 한 많이 매칭합니다. ?를 뒤에 붙이면 게으른 모드가 됩니다.

text = '<b>bold</b> and <i>italic</i>'

# 탐욕적 (기본)
print(re.findall(r'<.*>', text))
# ['<b>bold</b> and <i>italic</i>']  — 전체를 하나로 매칭!

# 게으른 (?를 추가)
print(re.findall(r'<.*?>', text))
# ['<b>', '</b>', '<i>', '</i>']  — 각 태그를 개별 매칭

소유적(Possessive) 수량자

소유적 수량자(*+, ++, ?+)는 백트래킹을 하지 않습니다. Java, PCRE에서 지원합니다 (Python 표준 re에서는 미지원, regex 모듈에서 지원).

// Java 예시
// 소유적 수량자 — 백트래킹 없음
String pattern = "a++b";  // 'a'를 최대한 소비 후 돌아가지 않음

5. 그룹 (Groups)

캡처 그룹

# 기본 캡처 그룹
text = "2025-03-23"
match = re.match(r'(\d{4})-(\d{2})-(\d{2})', text)
if match:
    print(match.group(0))  # '2025-03-23' (전체 매치)
    print(match.group(1))  # '2025' (년)
    print(match.group(2))  # '03' (월)
    print(match.group(3))  # '23' (일)

비캡처 그룹

(?:...) — 그룹화하되 캡처하지 않습니다. 성능상 이점이 있습니다.

# 비캡처 그룹
text = "http://example.com https://secure.com"
print(re.findall(r'(?:https?://)(\S+)', text))
# ['example.com', 'secure.com']  — 도메인만 캡처

명명된 그룹 (Named Groups)

# 명명된 그룹
text = "2025-03-23"
match = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', text)
if match:
    print(match.group('year'))   # '2025'
    print(match.group('month'))  # '03'
    print(match.group('day'))    # '23'

역참조 (Backreferences)

# 중복 단어 찾기
text = "the the quick brown fox fox"
print(re.findall(r'\b(\w+)\s+\1\b', text))
# ['the', 'fox']

# HTML 태그 매칭 (같은 태그 이름)
html = "<div>content</div> <span>text</span>"
print(re.findall(r'<(\w+)>.*?</\1>', html))
# ['div', 'span']

6. 앵커와 경계 (Anchors and Boundaries)

기본 앵커

# ^ — 문자열 시작
print(re.findall(r'^Hello', 'Hello World\nHello Again'))
# ['Hello']  — 첫 번째만

# $ — 문자열 끝
print(re.findall(r'end$', 'start to end'))
# ['end']

# 멀티라인 모드
print(re.findall(r'^Hello', 'Hello World\nHello Again', re.MULTILINE))
# ['Hello', 'Hello']  — 각 줄의 시작

단어 경계 (\b)

\b는 단어 문자와 비단어 문자 사이의 위치를 매칭합니다. 제로폭이라 문자를 소비하지 않습니다.

text = "cat catfish concatenate scattered"

# \b 없이
print(re.findall(r'cat', text))
# ['cat', 'cat', 'cat', 'cat']

# \b로 정확한 단어만
print(re.findall(r'\bcat\b', text))
# ['cat']

# 단어 시작에서만
print(re.findall(r'\bcat\w*', text))
# ['cat', 'catfish', 'concatenate']

7. 전방탐색과 후방탐색 (Lookahead and Lookbehind)

전방탐색과 후방탐색은 제로폭 단언(Zero-width Assertions) 입니다. 문자를 소비하지 않고 조건만 확인합니다.

4가지 유형

유형문법의미
긍정 전방탐색(?=...)뒤에 ...이 있는 위치
부정 전방탐색(?!...)뒤에 ...이 없는 위치
긍정 후방탐색(?<=...)앞에 ...이 있는 위치
부정 후방탐색(?<!...)앞에 ...이 없는 위치

실용 예시: 비밀번호 검증

def validate_password(password):
    """
    비밀번호 규칙:
    - 8자 이상
    - 대문자 최소 1개
    - 소문자 최소 1개
    - 숫자 최소 1개
    - 특수문자 최소 1개
    """
    pattern = r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$'
    return bool(re.match(pattern, password))

print(validate_password("Abc123!@"))   # True
print(validate_password("abc123"))     # False (대문자, 특수문자 없음)
print(validate_password("Short1!"))    # False (8자 미만)

금액 포맷팅 (후방탐색 활용)

# 숫자에 쉼표 추가 (천 단위 구분)
def format_number(n):
    return re.sub(r'(?<=\d)(?=(\d{3})+(?!\d))', ',', str(n))

print(format_number(1234567890))
# '1,234,567,890'

부정 전방탐색 활용

# .js 파일이지만 .min.js가 아닌 것 찾기
files = ["app.js", "app.min.js", "utils.js", "vendor.min.js"]
pattern = r'^(?!.*\.min\.js$).*\.js$'
for f in files:
    if re.match(pattern, f):
        print(f)
# app.js
# utils.js

8. 실전 패턴 30선

이메일 관련

# 1. 기본 이메일 검증
email_basic = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

# 2. 이메일에서 도메인 추출
email_domain = r'@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})'

# 3. Gmail 주소만 매칭
gmail_only = r'^[a-zA-Z0-9._%+-]+@gmail\.com$'

URL 관련

# 4. 기본 URL 매칭
url_basic = r'https?://[a-zA-Z0-9.-]+(?:/[^\s]*)?'

# 5. URL에서 도메인 추출
url_domain = r'https?://([^/\s]+)'

# 6. 쿼리 파라미터 추출
query_param = r'[?&]([^=&]+)=([^&]*)'

IP 주소

# 7. IPv4 주소
ipv4 = r'\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b'

# 8. 사설 IP 주소
private_ip = r'\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b'

전화번호

# 9. 한국 휴대전화
kr_mobile = r'01[016789]-?\d{3,4}-?\d{4}'

# 10. 한국 일반전화
kr_phone = r'0\d{1,2}-?\d{3,4}-?\d{4}'

# 11. 국제 전화번호 (E.164)
intl_phone = r'\+\d{1,3}\d{4,14}'

날짜/시간

# 12. YYYY-MM-DD
date_iso = r'\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])'

# 13. 다양한 날짜 형식
date_multi = r'\d{4}[/.-]\d{1,2}[/.-]\d{1,2}'

# 14. 24시간 시간
time_24h = r'(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?'

# 15. ISO 8601 날짜시간
iso_datetime = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})'

개발 관련

# 16. HTML 태그
html_tag = r'<\/?[a-zA-Z][a-zA-Z0-9]*(?:\s[^>]*)?\/?>'

# 17. HEX 색상 코드
hex_color = r'#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b'

# 18. 시맨틱 버전 (SemVer)
semver = r'\bv?(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?(?:\+([\w.]+))?\b'

# 19. JSON 키
json_key = r'"([^"\\]*)"\s*:'

# 20. CSS 클래스 셀렉터
css_class = r'\.[a-zA-Z_][\w-]*'

한국어/일본어

# 21. 한글만 (자모 포함)
hangul = r'[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F]+'

# 22. 한글 이름 (2-5자)
kr_name = r'[\uAC00-\uD7AF]{2,5}'

# 23. 일본어 히라가나
hiragana = r'[\u3040-\u309F]+'

# 24. 일본어 가타카나
katakana = r'[\u30A0-\u30FF]+'

# 25. 한자 (CJK 통합)
kanji = r'[\u4E00-\u9FFF]+'

보안/검증

# 26. 강한 비밀번호
strong_password = r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$'

# 27. SQL 인젝션 의심 패턴
sql_injection = r"(?i)(?:union\s+select|or\s+1\s*=\s*1|drop\s+table|insert\s+into)"

# 28. XSS 의심 패턴
xss_pattern = r'(?i)<script[^>]*>.*?</script>'

기타 유용한 패턴

# 29. 공백 정규화 (연속 공백을 하나로)
text = "too    many   spaces"
print(re.sub(r'\s+', ' ', text))
# 'too many spaces'

# 30. CSV 필드 추출 (쉼표 구분, 따옴표 지원)
csv_field = r'(?:"([^"]*)"|([^,]*))'

9. 언어별 정규표현식 차이

JavaScript

// 리터럴 방식
const re1 = /hello/gi;

// 생성자 방식
const re2 = new RegExp('hello', 'gi');

// 주요 메서드
'hello world'.match(/hello/);       // ['hello']
'hello world'.replace(/hello/, 'hi'); // 'hi world'
/hello/.test('hello world');         // true

// Named Groups (ES2018+)
const match = '2025-03-23'.match(
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
);
console.log(match.groups.year);  // '2025'

// matchAll (ES2020+)
const text = 'cat bat sat';
for (const m of text.matchAll(/[a-z]at/g)) {
  console.log(m[0], m.index);
}

// Lookbehind (ES2018+)
'$100 200 $300'.match(/(?<=\$)\d+/g);  // ['100', '300']

Python

import re

# 기본 메서드
re.match(r'^hello', 'hello world')   # 시작에서만 매칭
re.search(r'hello', 'say hello')     # 어디서든 매칭
re.findall(r'\d+', 'a1 b2 c3')      # 모든 매칭 리스트
re.sub(r'\d+', 'N', 'a1 b2')        # 치환: 'aN bN'

# 컴파일 (반복 사용 시 성능 향상)
pattern = re.compile(r'\d{3}-\d{4}')
pattern.findall('010-1234-5678')

# 플래그
re.IGNORECASE  # 대소문자 무시
re.MULTILINE   # ^ $를 각 줄에 적용
re.DOTALL      # .이 개행도 매칭
re.VERBOSE     # 주석과 공백 허용

# VERBOSE 예시
phone_re = re.compile(r'''
    (\d{3})     # 지역번호
    [-.\s]?     # 구분자
    (\d{3,4})   # 중간 번호
    [-.\s]?     # 구분자
    (\d{4})     # 마지막 번호
''', re.VERBOSE)

Java

import java.util.regex.*;

// 기본 사용
Pattern pattern = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
Matcher matcher = pattern.matcher("Date: 2025-03-23");
if (matcher.find()) {
    System.out.println(matcher.group());  // "2025-03-23"
}

// Named Groups
Pattern datePattern = Pattern.compile(
    "(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})"
);
Matcher m = datePattern.matcher("2025-03-23");
if (m.find()) {
    System.out.println(m.group("year"));  // "2025"
}

// String 메서드
"hello world".matches("hello.*");   // true
"a1b2c3".replaceAll("\\d", "N");   // "aNbNcN"
"a,b,,c".split(",", -1);           // ["a", "b", "", "c"]

Go

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // 컴파일
    re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)

    // 매칭 확인
    fmt.Println(re.MatchString("2025-03-23"))  // true

    // 찾기
    fmt.Println(re.FindString("Date: 2025-03-23"))  // "2025-03-23"

    // 모두 찾기
    fmt.Println(re.FindAllString("2025-03-23 and 2025-12-31", -1))

    // Named Groups
    re2 := regexp.MustCompile(
        `(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`,
    )
    match := re2.FindStringSubmatch("2025-03-23")
    for i, name := range re2.SubexpNames() {
        if name != "" {
            fmt.Printf("%s: %s\n", name, match[i])
        }
    }
}

언어별 기능 비교

기능JavaScriptPythonJavaGo
LookaheadO (ES2018+)OOX
LookbehindO (ES2018+)OOX
Named GroupsO (ES2018+)OOO
PossessiveXX (regex 모듈)OX
Atomic GroupsXX (regex 모듈)OO
Unicode 카테고리OOOO
재귀 패턴XX (regex 모듈)XX
VERBOSE 모드XOO (COMMENTS)X

10. 성능 최적화와 ReDoS 방지

백트래킹의 이해

정규표현식 엔진(NFA)은 매칭 실패 시 백트래킹으로 다른 경로를 시도합니다.

# 정상적인 백트래킹
# 패턴: a*b
# 입력: "aaac"
# 시도: "aaa" + b? 실패 → "aa" + b? 실패 → "a" + b? 실패 → "" + b? 실패
# 총 4번 백트래킹 — 문제 없음

재앙적 백트래킹 (Catastrophic Backtracking)

import time

# 위험한 패턴!
dangerous_pattern = r'(a+)+b'
safe_pattern = r'a+b'

# 짧은 입력 — 둘 다 빠름
text_short = 'a' * 10 + 'c'

# 긴 입력 — 위험한 패턴은 지수적 시간 소요
text_long = 'a' * 25 + 'c'

start = time.time()
re.match(safe_pattern, text_long)
print(f"안전한 패턴: {time.time() - start:.4f}s")

# 주의: 아래 코드는 매우 오래 걸릴 수 있음!
# start = time.time()
# re.match(dangerous_pattern, text_long)
# print(f"위험한 패턴: {time.time() - start:.4f}s")

ReDoS 취약 패턴 목록

# 1. 중첩된 수량자
r'(a+)+'      # 위험!
r'(a*)*'      # 위험!
r'(a+)*'      # 위험!

# 2. 겹치는 대안
r'(a|a)+'     # 위험!
r'(\d+|\d+\.)+' # 위험!

# 3. 안전한 대체 패턴
r'a+'         # (a+)+ 대신
r'a*'         # (a*)* 대신
r'\d+\.?'     # (\d+|\d+\.)+ 대신

ReDoS 방지 가이드라인

  1. 중첩 수량자 금지(a+)+, (a*)* 패턴을 피합니다
  2. 대안의 겹침 제거 — OR 대안이 같은 문자를 매칭하지 않도록 합니다
  3. 원자 그룹 사용 — 지원하는 엔진에서 (?>...) 사용
  4. 입력 길이 제한 — 정규표현식 적용 전에 입력 길이를 검증합니다
  5. 타임아웃 설정 — 정규표현식 실행에 시간 제한을 둡니다
# Python에서 타임아웃 설정
import signal

def timeout_handler(signum, frame):
    raise TimeoutError("Regex timeout!")

signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(1)  # 1초 타임아웃

try:
    re.match(r'(a+)+b', 'a' * 30 + 'c')
except TimeoutError:
    print("정규표현식 실행이 시간 초과되었습니다.")
finally:
    signal.alarm(0)

11. 도구 활용

regex101.com

온라인 정규표현식 테스터로 가장 인기 있는 도구입니다.

주요 기능:

  • 실시간 매칭 하이라이팅
  • 패턴 설명 자동 생성
  • JavaScript, Python, Go, Java 등 다양한 엔진 지원
  • 매칭 과정 단계별 디버거
  • 패턴 저장 및 공유

grep과 ripgrep

# grep 기본 사용
grep -E 'error|warning' /var/log/syslog
grep -P '(?<=user:)\w+' access.log  # Perl 호환 정규식

# ripgrep (rg) — 더 빠른 대안
rg 'TODO|FIXME' --type py
rg '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b' access.log
rg -e 'pattern1' -e 'pattern2' .  # 여러 패턴

sed와 awk

# sed — 스트림 편집기
# 날짜 형식 변환: 2025/03/23 → 2025-03-23
echo "2025/03/23" | sed 's/\//-/g'

# 이메일 주소 마스킹
echo "user@example.com" | sed 's/\(.\).*@/\1***@/'
# u***@example.com

# awk — 패턴 매칭 + 처리
# 200 상태코드인 로그만 출력
awk '/HTTP\/[0-9.]+" 200/' access.log

# IP별 요청 수 집계
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head

IDE 활용

대부분의 IDE(VS Code, IntelliJ, Vim 등)에서 정규표현식 검색/치환을 지원합니다.

VS Code 팁:

  • Ctrl+H (또는 Cmd+H)로 치환 모드
  • 정규표현식 아이콘(.*) 클릭하여 활성화
  • 캡처 그룹은 $1, $2로 역참조

12. 면접 질문 10선

Q1. .*.*?의 차이는?

.*은 탐욕적(greedy) — 가능한 한 많은 문자를 매칭합니다. .*?은 게으른(lazy) — 가능한 한 적은 문자를 매칭합니다.

Q2. ^$는 멀티라인 모드에서 어떻게 달라지나요?

기본 모드에서 ^는 문자열의 시작, $는 문자열의 끝만 매칭합니다. 멀티라인 모드(re.MULTILINE)에서는 각 줄의 시작과 끝을 매칭합니다.

Q3. 전방탐색(Lookahead)과 후방탐색(Lookbehind)의 차이는?

전방탐색 (?=...) 은 현재 위치 뒤의 패턴을 확인하고, 후방탐색 (?<=...) 은 현재 위치 앞의 패턴을 확인합니다. 둘 다 제로폭 단언으로 문자를 소비하지 않습니다.

Q4. ReDoS란 무엇이고 어떻게 방지하나요?

ReDoS는 비효율적 정규표현식이 지수적 백트래킹을 일으켜 CPU를 과도하게 사용하는 공격입니다. 중첩 수량자를 피하고, 입력 길이를 제한하며, 타임아웃을 설정하여 방지합니다.

Q5. 캡처 그룹과 비캡처 그룹의 차이는?

캡처 그룹 (...) 은 매칭된 문자열을 저장하여 후에 참조할 수 있습니다. 비캡처 그룹 (?:...) 은 그룹화만 하고 저장하지 않아 성능이 약간 더 좋습니다.

Q6. \b의 역할은?

단어 경계(word boundary)를 매칭합니다. 단어 문자(\w)와 비단어 문자 사이의 위치, 또는 문자열의 시작/끝과 단어 문자 사이를 매칭합니다.

Q7. 이메일 검증에 정규표현식을 사용할 때의 한계는?

RFC 5322 표준을 완벽히 구현하려면 매우 복잡한 패턴이 필요합니다. 실무에서는 기본적인 형식 검증만 정규표현식으로 하고, 실제 존재 여부는 인증 이메일 전송으로 확인합니다.

Q8. Python의 re.matchre.search의 차이는?

re.match는 문자열의 시작에서만 매칭을 시도하고, re.search는 문자열 전체에서 첫 번째 매칭을 찾습니다.

Q9. 정규표현식에서 백슬래시를 매칭하려면?

패턴에서 \\\\를 사용합니다. 문자열 이스케이프와 정규표현식 이스케이프가 각각 필요합니다. Python에서는 raw string(r'\\')을 사용하면 \\만으로 충분합니다.

Q10. 정규표현식으로 HTML을 파싱하면 안 되는 이유는?

HTML은 정규 언어가 아닌 문맥 자유 언어입니다. 중첩된 태그, 주석, CDATA 섹션 등을 정규표현식으로 올바르게 처리할 수 없습니다. HTML 파서(BeautifulSoup, Cheerio 등)를 사용해야 합니다.


13. 퀴즈

Q1. 패턴 \d{3}-\d{4}는 "010-1234-5678"에서 무엇을 매칭하나요?

"010-1234"와 "234-5678" 중 첫 번째인 "010-1234"를 매칭합니다. re.findall을 사용하면 두 개 모두 찾을 수 있습니다. 전체 전화번호를 매칭하려면 \d{3}-\d{4}-\d{4} 패턴을 사용해야 합니다.

Q2. 왜 (a+)+b는 위험한 패턴인가요?

입력이 "aaaaac"처럼 b로 끝나지 않으면, 엔진이 내부 그룹과 외부 그룹의 모든 조합을 시도하며 지수적 백트래킹이 발생합니다. a가 25개만 되어도 수천만 번의 시도가 필요합니다. 단순히 a+b로 대체하면 해결됩니다.

Q3. (?:...)(?=...)의 차이는?

(?:...)는 비캡처 그룹으로, 문자를 소비하면서 그룹화합니다 (캡처만 하지 않음). (?=...)는 전방탐색으로, 문자를 소비하지 않고 조건만 확인하는 제로폭 단언입니다.

Q4. Go 언어에서 Lookahead를 사용할 수 없는 이유는?

Go의 regexp 패키지는 RE2 엔진을 사용하는데, RE2는 선형 시간 보장을 위해 백트래킹을 사용하지 않습니다. Lookahead는 백트래킹이 필요한 기능이므로 RE2에서 지원하지 않습니다.

Q5. 다음 중 "abc"에 매칭되는 패턴은? a) [abc]+ b) [^abc]+ c) a.c d) a\bc

a) [abc]+ — "abc" 전체에 매칭됩니다. b) [^abc]+ — a, b, c가 아닌 문자만 매칭하므로 실패. c) a.c.b를 매칭하므로 "abc"에 매칭. d) a\bc\b는 단어 경계인데, a와 b 사이에는 단어 경계가 없으므로 실패. 정답은 a와 c입니다.


14. 참고 자료

  1. Mastering Regular Expressions (Jeffrey Friedl) — 정규표현식의 바이블
  2. Regular-Expressions.infohttps://www.regular-expressions.info/
  3. regex101https://regex101.com/ — 온라인 정규표현식 테스터
  4. Regexrhttps://regexr.com/ — 시각적 정규표현식 학습
  5. MDN Web Docs: Regular Expressions — JavaScript 정규표현식 레퍼런스
  6. Python re module docshttps://docs.python.org/3/library/re.html
  7. RE2 Syntaxhttps://github.com/google/re2/wiki/Syntax
  8. ReDoS Prevention — OWASP 가이드
  9. ripgrep (rg)https://github.com/BurntSushi/ripgrep
  10. Debuggexhttps://www.debuggex.com/ — 정규표현식 시각화
  11. RegExp Playgroundhttps://regexper.com/ — 철도 다이어그램
  12. Automata Theory — 정규 언어와 유한 오토마타 이론