정규표현식 완벽 가이드 — 패턴, 그룹, 실전 예제

정규표현식이란?

정규표현식(Regular Expression, Regex)은 문자열에서 특정 패턴을 찾거나 치환하는 도구입니다. 텍스트 속에서 금속 탐지기로 원하는 물건을 찾는 것과 같습니다. 한 번 익히면 어떤 프로그래밍 언어에서든 동일하게 사용할 수 있어, 투자 대비 효과가 매우 높은 기술입니다.

기본 문법

메타문자

문자의미예시매칭
.임의의 한 문자a.cabc, a1c, a-c
\d숫자 [0-9]\d{3}123, 456
\w영숫자+밑줄 [a-zA-Z0-9_]\w+hello, user_1
\s공백 (스페이스, 탭, 줄바꿈)a\sba b
\D숫자가 아닌 문자\D+hello, ---
\W영숫자가 아닌 문자\W@, #, !
^문자열 시작^HelloHello…
$문자열 끝world$…world
\b단어 경계\bcat\bcat (category는 불일치)

수량자

수량자의미예시매칭
*0회 이상ab*cac, abc, abbc
+1회 이상ab+cabc, abbc (ac 불일치)
?0 또는 1회colou?rcolor, 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에서 테스트하세요: 실시간으로 매칭 결과를 확인하고, 각 토큰의 설명을 볼 수 있습니다.

이 글이 도움이 되었나요?