정규표현식이란?
정규표현식(Regular Expression, Regex)은 문자열에서 특정 패턴을 찾거나 치환하는 도구입니다. 텍스트 속에서 금속 탐지기로 원하는 물건을 찾는 것과 같습니다. 한 번 익히면 어떤 프로그래밍 언어에서든 동일하게 사용할 수 있어, 투자 대비 효과가 매우 높은 기술입니다.
기본 문법
메타문자
| 문자 | 의미 | 예시 | 매칭 |
|---|---|---|---|
. | 임의의 한 문자 | a.c | abc, a1c, a-c |
\d | 숫자 [0-9] | \d{3} | 123, 456 |
\w | 영숫자+밑줄 [a-zA-Z0-9_] | \w+ | hello, user_1 |
\s | 공백 (스페이스, 탭, 줄바꿈) | a\sb | a b |
\D | 숫자가 아닌 문자 | \D+ | hello, --- |
\W | 영숫자가 아닌 문자 | \W | @, #, ! |
^ | 문자열 시작 | ^Hello | Hello… |
$ | 문자열 끝 | world$ | …world |
\b | 단어 경계 | \bcat\b | cat (category는 불일치) |
수량자
| 수량자 | 의미 | 예시 | 매칭 |
|---|---|---|---|
* | 0회 이상 | ab*c | ac, abc, abbc |
+ | 1회 이상 | ab+c | abc, abbc (ac 불일치) |
? | 0 또는 1회 | colou?r | color, colour |
{n} | 정확히 n회 | \d{4} | 2026 |
{n,} | n회 이상 | \d{2,} | 12, 123, 1234 |
{n,m} | n회 이상 m회 이하 | \d{2,4} | 12, 123, 1234 |
문자 클래스
import re
# 문자 클래스 — 대괄호 안의 문자 중 하나와 매칭
pattern_vowel = r"[aeiou]" # 모음
pattern_hex = r"[0-9a-fA-F]" # 16진수 문자
pattern_not_digit = r"[^0-9]" # 숫자가 아닌 문자 (^ = 부정)
text = "Hello World 123"
vowels = re.findall(pattern_vowel, text)
print(vowels) # 출력: ['e', 'o', 'o']
non_digits = re.findall(pattern_not_digit, text)
print(non_digits) # 출력: ['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', ' ']
그룹과 캡처
괄호 ()로 패턴을 그룹화하면, 매칭된 부분을 따로 추출할 수 있습니다.
import re
# 날짜에서 연, 월, 일 추출
date_pattern = r"(\d{4})-(\d{2})-(\d{2})"
text = "오늘 날짜는 2026-04-07입니다."
match = re.search(date_pattern, text)
if match:
print(f"전체: {match.group(0)}") # 출력: 전체: 2026-04-07
print(f"연도: {match.group(1)}") # 출력: 연도: 2026
print(f"월: {match.group(2)}") # 출력: 월: 04
print(f"일: {match.group(3)}") # 출력: 일: 07
# 이름 붙은 그룹 — 가독성 향상
named_pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
match = re.search(named_pattern, text)
if match:
print(f"연도: {match.group('year')}") # 출력: 연도: 2026
print(f"월: {match.group('month')}") # 출력: 월: 04
# 비캡처 그룹 — 그룹화만 하고 캡처하지 않음
non_capture = r"(?:http|https)://(\S+)"
url_text = "방문: https://example.com/path"
match = re.search(non_capture, url_text)
if match:
print(f"도메인+경로: {match.group(1)}") # 출력: 도메인+경로: example.com/path
# group(1)이 프로토콜이 아닌 도메인+경로 (비캡처 그룹 덕분)
전방/후방 탐색 (Lookahead/Lookbehind)
패턴의 앞이나 뒤에 특정 조건이 있는지 확인하되, 매칭 결과에는 포함하지 않습니다.
import re
# 전방 탐색 (Lookahead) — 뒤에 특정 패턴이 오는 경우만 매칭
# 가격 뒤에 "원"이 오는 숫자만 찾기
text = "사과 3000원, 배 5000원, 수량 3개"
prices = re.findall(r"\d+(?=원)", text)
print(prices) # 출력: ['3000', '5000'] ("3"은 불포함 — 뒤에 "원"이 아님)
# 부정 전방 탐색 — 뒤에 특정 패턴이 오지 않는 경우
not_price = re.findall(r"\d+(?!원)", text)
print(not_price) # 출력: ['300', '500', '3']
# 후방 탐색 (Lookbehind) — 앞에 특정 패턴이 있는 경우만 매칭
# "$" 뒤의 숫자만 추출
text2 = "가격: $100, 수량: 50개, 할인: $30"
dollar_amounts = re.findall(r"(?<=\$)\d+", text2)
print(dollar_amounts) # 출력: ['100', '30'] ("50"은 불포함)
# 조합 — 특정 태그 안의 내용만 추출
html = "이름: <b>김철수</b>, 나이: <b>30</b>"
bold_contents = re.findall(r"(?<=<b>).+?(?=</b>)", html)
print(bold_contents) # 출력: ['김철수', '30']
탐욕적 vs 게으른 매칭
import re
text = '<div>첫 번째</div><div>두 번째</div>'
# 탐욕적 (Greedy) — 가능한 한 많이 매칭 (기본)
greedy = re.findall(r"<div>.*</div>", text)
print(greedy) # 출력: ['<div>첫 번째</div><div>두 번째</div>'] (전체를 하나로)
# 게으른 (Lazy) — 가능한 한 적게 매칭 (?를 추가)
lazy = re.findall(r"<div>.*?</div>", text)
print(lazy) # 출력: ['<div>첫 번째</div>', '<div>두 번째</div>'] (각각 분리)
실전 패턴 모음
import re
# 1. 이메일 주소 검증
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
emails = ["user@example.com", "invalid@", "test@co.kr"]
for email in emails:
valid = bool(re.match(email_pattern, email))
print(f"{email}: {'유효' if valid else '무효'}")
# 출력: user@example.com: 유효
# invalid@: 무효
# test@co.kr: 유효
# 2. 한국 전화번호 (다양한 형식 허용)
phone_pattern = r"0\d{1,2}[- ]?\d{3,4}[- ]?\d{4}"
phones = ["010-1234-5678", "02 1234 5678", "0311234567"]
for phone in phones:
match = re.search(phone_pattern, phone)
print(f"{phone}: {'매칭' if match else '불일치'}")
# 출력: 010-1234-5678: 매칭
# 02 1234 5678: 매칭
# 0311234567: 매칭
# 3. 비밀번호 강도 검증 (8자 이상, 영문+숫자+특수문자)
def validate_password(pw):
"""비밀번호 강도를 검증합니다."""
checks = {
"8자 이상": r".{8,}",
"영문 포함": r"[a-zA-Z]",
"숫자 포함": r"\d",
"특수문자 포함": r"[!@#$%^&*(),.?\":{}|]"
}
results = {}
for name, pattern in checks.items():
results[name] = bool(re.search(pattern, pw))
return results
print(validate_password("Abc123!@"))
# 출력: {'8자 이상': True, '영문 포함': True, '숫자 포함': True, '특수문자 포함': True}
# 4. 문자열 치환 — 개인 정보 마스킹
def mask_personal_info(text):
"""전화번호와 이메일을 마스킹합니다."""
# 전화번호 마스킹
text = re.sub(
r"(\d{3})-(\d{3,4})-(\d{4})",
r"\1-****-\3", # 중간 번호를 ****로
text
)
# 이메일 마스킹
text = re.sub(
r"([a-zA-Z0-9._%+-]{2})([a-zA-Z0-9._%+-]*)(@\S+)",
r"\1***\3", # 이메일 앞 2글자만 남기고 ***
text
)
return text
sample = "연락처: 010-1234-5678, 메일: hong@example.com"
print(mask_personal_info(sample))
# 출력: 연락처: 010-****-5678, 메일: ho***@example.com
# 5. 로그에서 IP 주소 추출
log_text = """
[2026-04-07 10:00:01] 192.168.1.100 GET /api/users 200
[2026-04-07 10:00:02] 10.0.0.55 POST /api/login 401
[2026-04-07 10:00:03] 172.16.0.1 GET /api/health 200
"""
ip_pattern = r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"
ips = re.findall(ip_pattern, log_text)
print(f"발견된 IP: {ips}")
# 출력: 발견된 IP: ['192.168.1.100', '10.0.0.55', '172.16.0.1']
JavaScript에서의 정규표현식
// JavaScript 정규표현식 기본
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(emailRegex.test("user@example.com")); // true
console.log(emailRegex.test("invalid")); // false
// 이름 붙은 그룹 (ES2018+)
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = "2026-04-07".match(dateRegex);
console.log(match.groups.year); // "2026"
console.log(match.groups.month); // "04"
// replaceAll + 캡처 그룹
const text = "2026-04-07 and 2026-12-25";
const formatted = text.replaceAll(
/(\d{4})-(\d{2})-(\d{2})/g,
"$1년 $2월 $3일"
);
console.log(formatted);
// 출력: "2026년 04월 07일 and 2026년 12월 25일"
정리
| 상황 | 추천 패턴 |
|---|---|
| 특정 문자열 포함 여부 | str.includes() (정규식 불필요) |
| 단순 형식 검증 | 기본 메타문자 + 수량자 |
| 데이터 추출 | 캡처 그룹 () |
| 조건부 매칭 | 전방/후방 탐색 |
| 문자열 치환 | re.sub() + 역참조 \1 |
| 복잡한 파싱 | 정규식 대신 전용 파서 사용 |
- 정규식은 만능이 아닙니다: HTML 파싱, JSON 파싱처럼 중첩 구조가 있는 경우 전용 파서를 사용하세요.
re.compile()로 재사용하세요: 같은 패턴을 반복 사용하면 컴파일된 객체가 더 빠릅니다.re.VERBOSE플래그를 활용하세요: 복잡한 정규식에 주석과 공백을 넣어 가독성을 높일 수 있습니다.- regex101.com에서 테스트하세요: 실시간으로 매칭 결과를 확인하고, 각 토큰의 설명을 볼 수 있습니다.