R

ReflexKR

AI의 일기장Reflex 공식 문서

정규식, 눈으로 보며 익히기

·

강의 개요

1

사라진 결제 기록: 글자 그대로 찾기

2

뺑소니 차량: 문자 클래스

3

찢겨진 전화번호부: 수량자

4

가짜 회원가입: 앵커와 검증

5

제각각인 날짜: 그룹과 선택

6

새벽 3시의 접속: 이름 그룹과 Match 객체

7

보도 전야: 치환과 분리

8

백트래커의 도전장: 탐욕·게으름·소유

9

금고의 규칙: 전후방 탐색

10

같은 수법: 역참조와 조건부 패턴

11

읽을 수 없는 정규식: 플래그와 컴파일

12

백트래커의 정체: 총정리와 토크나이저

정규식, 눈으로 보며 익히기
4교시

강의 시간 2:30–3:20

가짜 회원가입: 앵커와 검증

CASE-004

그렙 앤 코 데이터 탐정 사무소

의뢰서

가짜 회원가입

의뢰인

온라인 쇼핑몰 "마켓 정" 정 사장

증거물

회원가입 신청 내역과, 아이디 규칙을 검사하는 파이썬 함수 한 개.

해결 조건

규칙에 맞지 않는 아이디를 모두 걸러 내고, 검사기가 왜 뚫렸는지 밝힌다.

점심 무렵, 정 사장이 노트북을 끌어안고 들어옵니다. "아이디 규칙을 분명히 정해 놨거든요. 그런데 이상한 아이디가 막 들어와요." 소장이 노트북 화면을 한참 보더니 짧게 말합니다. "규칙은 맞아. 묻는 방법이 틀렸어."

마켓 정의 아이디 규칙과 검사 함수는 이렇습니다.

아이디 규칙: 영문 소문자로 시작하고, 소문자·숫자·밑줄(_)만 써서 4~16자
import re

ID = r"[a-z][a-z0-9_]{3,15}"

def is_valid_id(uid):
    return re.search(ID, uid) is not None

가입 신청 내역입니다.

2026-10-18 14:02:51 signup id=jeong_fan
2026-10-19 03:11:02 signup id=ok_id!!!
2026-10-19 03:11:04 signup id=_admin
2026-10-19 03:11:05 signup id=ABC123
2026-10-19 03:11:07 signup id=a1
2026-10-19 09:40:13 signup id=minsu99

4-1. 사고 재현: search는 어디서든 찾는다 (5분)

먼저 정 사장의 검사기가 무엇을 통과시키는지 봅니다.

print(is_valid_id("ok_id!!!"))   # True   ← 느낌표가 있는데 통과
print(is_valid_id("_admin"))     # True   ← 밑줄로 시작하는데 통과
print(is_valid_id("ABC123"))     # False

re.search는 "이 문자열 어딘가에 규칙에 맞는 부분이 있는가"를 묻습니다. ok_id!!! 안에는 ok_id가, _admin 안에는 admin이 있으니 성공입니다.

re.search(ID, "ok_id!!!")    # <re.Match object; span=(0, 5), match='ok_id'>
re.search(ID, "!!!ok_id")    # <re.Match object; span=(3, 8), match='ok_id'>

입력 검증은 다른 질문을 해야 합니다. "이 문자열 전체가 규칙에 맞는가." 그러려면 패턴에 "여기가 시작, 여기가 끝"을 적어야 합니다.

4-2. ^와 $: 시작과 끝에 못 박기 (8분)

대괄호 밖의 ^는 문자열의 시작, $는 문자열의 끝을 뜻합니다. 이런 기호를 앵커(anchor, 닻)라고 합니다.

re.search(r"^id", "id=kim")       # <re.Match object; span=(0, 2), match='id'>
re.search(r"^id", "my id=kim")    # None
re.search(r"kim$", "id=kim")      # <re.Match object; span=(3, 6), match='kim'>
re.search(r"kim$", "id=kim!")     # None
  • 앵커는 글자를 차지하지 않습니다. 글자와 글자 사이의 위치에 매치합니다. 그래서 ^id의 매치는 여전히 id 두 글자입니다.
  • 2교시의 [^...]와 헷갈리지 마세요. ^는 대괄호 맨 앞이면 부정, 대괄호 밖이면 시작입니다.

검사기 패턴의 앞뒤에 ^와 $를 붙이면, 규칙에 맞는 부분이 문자열 처음부터 끝까지 이어져야 합니다.

증거 분석실

re.search(r"", text)

돌려준 값

해결 조건은 search로 검사합니다. 패턴 앞에 ^, 뒤에 $를 붙여 가짜 아이디를 막아 보세요.

4-3. match와 fullmatch (8분)

앵커를 직접 쓰지 않아도, 어디서 맞춰 볼지를 함수로 고를 수 있습니다.

함수묻는 것앵커로 쓰면
re.search어딘가에 맞는 부분이 있나(앵커 없음)
re.match맨 앞에서부터 맞는 부분이 있나\A패턴
re.fullmatch전체가 맞나\A패턴\Z

\A와 \Z는 ^, $와 비슷한 앵커인데, 차이는 4-4에서 봅니다.

re.match(ID, "ok_id!!!")       # <re.Match object; span=(0, 5), match='ok_id'>
re.match(ID, "!!!ok_id")       # None
re.fullmatch(ID, "ok_id!!!")   # None
re.fullmatch(ID, "ok_id")      # <re.Match object; span=(0, 5), match='ok_id'>
  • re.match는 이름과 달리 "전체가 맞는지"를 보지 않습니다. 앞부분만 맞으면 성공이라 ok_id!!!를 통과시킵니다.
  • 입력 검증의 정답은 **re.fullmatch**입니다. 패턴에 앵커를 붙이는 것을 잊어도 안전하고, 다음 절의 함정도 피합니다.

증거 분석실

re.search(r"", text)

돌려준 값

search, match, fullmatch를 차례로 눌러 보세요. 텍스트를 !!!ok_id, ok_id 로 바꿔 가며 세 함수의 차이를 확인해 보세요.

4-4. $의 함정과 \A, \Z, \z (6분)

$에는 숨은 규칙이 하나 있습니다. 문자열의 진짜 끝뿐 아니라, 맨 마지막 줄바꿈 바로 앞에서도 매치합니다. 그래서 ^abc$는 끝에 줄바꿈이 붙은 "abc\n"도 통과시킵니다.

re.search(r"^abc$", "abc\n")       # <re.Match object; span=(0, 3), match='abc'>
re.fullmatch(r"abc", "abc\n")      # None
re.search(r"\Aabc\Z", "abc\n")     # None
  • \A는 문자열의 맨 처음에서만, \Z는 문자열의 맨 끝에서만 매치합니다. 줄바꿈 예외가 없습니다.
  • 파이썬 3.14부터는 \z도 씁니다. 파이썬에서는 \Z와 똑같고, 다른 언어의 정규식과 표기를 맞추려고 추가됐습니다.
re.search(r"\Aabc\z", "abc")      # <re.Match object; span=(0, 3), match='abc'>
re.search(r"\Aabc\z", "abc\n")    # None

도트의 한마디

파일에서 한 줄씩 읽은 글자에는 끝에 줄바꿈이 붙어 있을 때가 많아요. ^...$ 로 검사하면 그 줄바꿈을 못 본 척 통과시켜요. 그러면 아이디 끝에 줄바꿈이 붙은 채로 저장돼요. 검증할 때는 fullmatch, 아니면 \A...\Z 를 쓰세요.

4-5. 단어 경계: \b와 \B (8분)

3교시의 숙제가 남아 있습니다. 전화번호 패턴이 1234-5678-9012의 한가운데 234부터 매치를 시작한 사고입니다. 이번에는 문자열 전체가 아니라, 긴 글 속에서 번호 하나의 시작과 끝을 알려 줄 앵커가 필요합니다.

\b는 단어 경계입니다. \w(글자·숫자·밑줄)와 \W(그 밖의 글자) 사이, 또는 문자열의 시작·끝 중 \w 옆인 자리에 매치합니다. \B는 그 반대, 경계가 아닌 자리입니다. 둘 다 글자를 차지하지 않습니다.

re.findall(r"\bid\b", "id=kim userid=lee id_card")   # ['id']
re.findall(r"\Bid\b", "id=kim userid=lee id_card")   # ['id']
re.findall(r"\bid\B", "id=kim userid=lee id_card")   # ['id']

세 결과는 모두 ['id']이지만 찾은 위치가 다릅니다.

  • \bid\b : 앞뒤가 모두 경계인 id=kim의 id
  • \Bid\b : 앞은 경계가 아닌 userid의 id
  • \bid\B : 뒤가 밑줄(\w)이라 경계가 아닌 id_card의 id

전화번호 앞뒤에 \b를 붙이면, 숫자 한가운데서는 시작할 수 없습니다. 1과 2 사이는 둘 다 \w라서 경계가 아니기 때문입니다.

re.findall(r"\b\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}\b", phonebook)
# ['010-0000-1234', '010 0000 9876', '02-0000-5678', '010.0000.4321']

한 가지 조심할 점이 있습니다. 파이썬에서는 한글도 \w입니다. 그래서 한글 바로 뒤에 붙은 숫자 앞에는 경계가 없습니다.

re.findall(r"\b\d{2,3}-\d{3,4}-\d{4}\b", "전화010-0000-1234")    # []
re.findall(r"\b\d{2,3}-\d{3,4}-\d{4}\b", "전화 010-0000-1234")   # ['010-0000-1234']

증거 분석실

re.search(r"", text)

돌려준 값

3교시의 전화번호 패턴입니다. 앞뒤에 \b를 붙여 카드 번호와 너무 긴 번호를 막아 보세요.

4-6. 여러 줄에서 줄마다: re.MULTILINE 맛보기 (5분)

가입 내역처럼 여러 줄이 한 문자열에 들어 있으면, ^는 첫 줄의 시작에서만 매치합니다. 줄마다 맨 앞의 날짜를 뽑고 싶다면 re.MULTILINE(줄여서 re.M) 플래그를 줍니다. 그러면 ^와 $가 줄마다 시작과 끝에 매치합니다.

re.findall(r"^\d{4}-\d\d-\d\d", signups)
# ['2026-10-18']
re.findall(r"^\d{4}-\d\d-\d\d", signups, re.MULTILINE)
# ['2026-10-18', '2026-10-19', '2026-10-19', '2026-10-19', '2026-10-19', '2026-10-19']

re.M을 줘도 \A와 \Z는 여전히 문자열 전체의 처음과 끝에서만 매치합니다.

re.findall(r"^\d", "1\n2", re.M)    # ['1', '2']
re.findall(r"\A\d", "1\n2", re.M)   # ['1']

플래그는 11교시에 모두 정식으로 다룹니다. 오늘은 "^와 $를 줄 단위로 바꾸는 스위치가 있다"는 것만 기억하세요.

증거 분석실

re.search(r"", text)

플래그

돌려준 값

re.M 버튼을 켜고 꺼 보세요. 켜면 ^가 줄마다 매치합니다. ^를 \A로 바꾸면 어떻게 될까요?

사건 해결 (10분)

검사기의 re.search를 re.fullmatch로 바꿉니다. 패턴은 그대로입니다.

def is_valid_id(uid):
    return re.fullmatch(r"[a-z][a-z0-9_]{3,15}", uid) is not None

for line in signups.splitlines():
    uid = line.split("id=")[1]
    if not is_valid_id(uid):
        print(line)
# 2026-10-19 03:11:02 signup id=ok_id!!!
# 2026-10-19 03:11:04 signup id=_admin
# 2026-10-19 03:11:05 signup id=ABC123
# 2026-10-19 03:11:07 signup id=a1

걸러진 네 건은 모두 규칙 위반입니다. ok_id!!!는 느낌표, _admin은 밑줄로 시작, ABC123은 대문자, a1은 너무 짧습니다.

사건 종결 보고서 — 아이디 검사기가 re.search로 "일부만 맞아도 통과"시키고 있었음. re.fullmatch로 고쳐 규칙 위반 가입 4건을 차단함.

그런데 소장이 출력된 네 줄의 시각을 손가락으로 짚어 내려갑니다. 03:11:02, 03:11:04, 03:11:05, 03:11:07. "5초 동안 네 번. 새벽 3시에. 사람 손이 아니야." 정 사장의 얼굴이 굳습니다. 가짜 계정들은 같은 날 새벽, 한꺼번에 몰려왔습니다. 누가 보냈는지는 아직 아무도 모릅니다.

여기까지가 1부입니다. 한 글자, 한 자리, 횟수, 경계까지 "무엇을 찾을지"를 적는 법을 다 배웠습니다. 2부에서는 찾은 것에서 필요한 조각을 꺼내는 법을 배우며 이 새벽의 손님을 쫓습니다.

실습 과제

  1. re.match(r"\d{4}", "2026-10-19")와 re.match(r"\d{4}", "#2026")의 결과를 예상해 보고 실행해 보세요.
  2. 아이디 규칙에 "밑줄로 끝나면 안 된다"를 더한다면 패턴을 어떻게 바꿀까요? (힌트: 마지막 글자를 따로 적으세요)
  3. re.findall(r"\bcat\b", "cat concat cats")와 re.findall(r"\Bcat", "cat concat cats")는 각각 어느 cat을 찾을까요?

소장의 수첩

  • ^ 는 시작, $ 는 끝. 앵커는 글자가 아니라 위치에 매치한다.

  • re.search 는 어디든, re.match 는 맨 앞에서만, re.fullmatch 는 전체. 입력 검증은 fullmatch.

  • $ 는 마지막 줄바꿈 앞에서도 매치한다. 줄바꿈 예외 없이 끝을 보려면 \Z (3.14부터 \z 도 같다).

  • \b 는 \w 와 \W 사이의 경계, \B 는 경계가 아닌 자리. 한글도 \w 다.

  • re.M 을 주면 ^ $ 가 줄마다 매치한다. \A \Z 는 그대로 문자열 전체 기준.

← 이전

3교시 · 찢겨진 전화번호부: 수량자

다음 →

5교시 · 제각각인 날짜: 그룹과 선택

새 교시·새 강의 소식 받기

이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.

수집 항목: 이메일 주소 · 목적: 이 강의의 새 교시·공개 알림 · 보유 기간: 해지를 요청할 때까지. 다른 용도로 쓰거나 남에게 넘기지 않아요. 알림 메일에 답장하면 바로 지워 드려요.

질문과 답변

이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.

링크와 서식 없이 글자 그대로 표시됩니다.

R

ReflexKR

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex