정규식을 처음 보면 키보드를 고양이가 밟고 지나간 것 같죠. 저도 한동안은 검색해서 나온 패턴을 복사해 쓰고, 왜 되는지는 모르는 채로 넘어갔어요. 그러다 남이 짠 패턴을 고쳐야 하는 상황이 와서 제대로 읽는 법을 익혔는데, 막상 해 보니 자주 쓰는 문법은 열 가지 남짓이고, 패턴을 "글자 고르기 → 반복 횟수 → 위치 → 묶기" 덩어리로 끊어 읽으면 대부분 해석돼요.
이 글은 그 열 가지를 예제와 함께 정리한 치트시트예요. 여기에 실무에서 자주 걸리는 부분 — 한글 매칭, 같은 \d 가 언어마다 다르게 동작하는 문제, 자주 보는 에러 메시지 — 를 Node.js 22와 Python 3.9에서 직접 돌려 본 결과로 덧붙였어요. 정규식 테스터를 옆에 띄워 두고 하나씩 쳐 보면서 읽으면 훨씬 빨리 익숙해져요.
핵심 요약
정규식은 글자 고르기(. [ ] \d \w \s), 반복(? + * {n,m}), 위치(^ $ \b), 묶기(그룹, 선택), 플래그 다섯 덩어리로 읽어요.
수량자는 바로 앞 한 요소에만 붙고, 입력 검증엔 ^ 와 $ 앵커가 필수예요.
같은 \d라도 JavaScript는 0~9 열 글자, Python 3는 유니코드 숫자 650글자를 매치해요.
한글은 [가-힣]으로 고르고, JS의 \b는 한글 단어 경계로 동작하지 않아요.
정규식 읽는 법: 덩어리로 끊어 읽기
긴 패턴을 만나면 앞에서부터 한 글자씩 읽지 말고, 의미 단위로 끊는 것부터 해요. 이메일 형식을 대충 검사하는 흔한 패턴으로 연습해 볼게요.
^[\w.+-]+@[\w-]+\.[a-z]{2,}$
시작 → 영문·숫자·밑줄·점·플러스·하이픈이 1개 이상 → @ → 영문·숫자·밑줄·하이픈 1개 이상 → 진짜 점 → 소문자 2개 이상 → 끝. 이렇게 읽고 나면 이 패턴의 한계도 보여요. 도메인 부분에 점을 허용하지 않으니 smith@mail.co.kr 은 거부되고, 대문자 최상위 도메인 a@b.COM 도 거부돼요. 반대로 ..@x.io 는 통과하고요. 직접 확인한 결과예요.
const re = /^[\w.+-]+@[\w-]+\.[a-z]{2,}$/;
['smith.dev+tag@example.com', 'smith@mail.co.kr', 'a@b.COM', '..@x.io']
.map(s => [s, re.test(s)]);
// true, false, false, true
이메일·전화번호 같은 검증 패턴이 왜 생각보다 어려운지는 정규식 입력 검증 함정 글에서 따로 다뤘어요.
1~4. 글자를 고르는 문법: 점, 문자 클래스, 축약 클래스
- 그냥 글자 —
cat은 c, a, t가 연속된 부분을 찾아요. 정규식의 기본은 결국 문자열 검색이에요. - 점
.— 줄바꿈을 뺀 아무 글자 하나.c.t는 cat, cut, c9t 전부 맞아요. 진짜 점을 찾으려면\.처럼 이스케이프해야 해요. 이걸 빼먹어서example.com패턴이exampleXcom까지 통과시키는 일이 흔해요. - 문자 클래스
[ ]— 대괄호 안의 글자 중 하나.[aeiou]는 모음 하나,[0-9]는 숫자 하나, 맨 앞에^를 쓴[^0-9]는 "숫자만 빼고"예요. - 축약 클래스 —
\d숫자,\w영문자·숫자·밑줄,\s공백류(스페이스, 탭, 줄바꿈). 대문자로 쓰면 반대예요.\D는 숫자가 아닌 것.
| 문법 | 뜻 | 예시 | 매치 |
|---|---|---|---|
. | 줄바꿈 제외 아무 글자 1개 | c.t | cat, c9t |
\. | 진짜 점 | a\.b | a.b |
[abc] | a, b, c 중 하나 | gr[ae]y | gray, grey |
[^abc] | a, b, c 제외 아무거나 | [^0-9] | 숫자 아닌 글자 |
[a-z] | 범위 | [A-Za-z] | 영문 1자 |
\d / \D | 숫자 / 숫자 아님 | \d\d | 42 |
\w / \W | 단어 문자 / 아님 | \w+ | user_01 |
\s / \S | 공백류 / 아님 | \s+ | 스페이스·탭·줄바꿈 |
문자 클래스 안에서는 대부분의 특수문자가 평범한 글자가 돼요. [.+] 의 점과 플러스는 이스케이프 없이도 진짜 점과 플러스예요. 예외는 ], \, 맨 앞의 ^, 가운데의 - 네 개예요. 하이픈을 글자로 쓰고 싶으면 위 이메일 패턴처럼 맨 끝([\w.+-])에 두면 안전해요.
5. 몇 번 반복할지: 수량자
수량자는 바로 앞 요소의 반복 횟수를 정해요.
? 0번 또는 1번 colou?r → color, colour
+ 1번 이상 \d+ → 7, 42, 2026
* 0번 이상 ab*c → ac, abc, abbbc
{3} 정확히 3번 \d{3} → 010
{2,4} 2번 이상 4번 이하 \d{2,4} → 12, 123, 1234
{2,} 2번 이상 [a-z]{2,}
수량자는 바로 앞 한 요소에만 붙어요. ab+ 는 "ab가 여러 번"이 아니라 "a 다음에 b가 여러 번"이에요. 묶어서 반복하려면 그룹이 필요해요. 그리고 기본 수량자는 가능한 한 많이 먹는 탐욕적(greedy) 방식이라, <.+> 가 태그 하나가 아니라 줄 전체를 먹어 버리는 일이 생겨요. 이건 뒤에 ? 를 붙인 게으른 수량자(+?)로 해결하는데, 자세한 동작과 서버를 멈추게 하는 ReDoS 문제는 탐욕적·게으른 수량자와 ReDoS 글에 정리했어요.
6~7. 위치를 정하는 문법: 앵커와 단어 경계
- 앵커
^와$— 문자열의 시작과 끝이에요. 글자를 소비하지 않고 위치만 확인해요. 입력 검증에서 이걸 빼먹으면 "어딘가에 숫자 6개가 들어 있기만 하면" 통과돼요.\d{6}은abc123456xyz안에서도 매치되지만,^\d{6}$은 정확히 숫자 6개일 때만 맞아요. - 단어 경계
\b— 단어 문자(\w)와 아닌 것 사이의 위치.\bcat\b은 cat은 찾지만 category 안의 cat은 안 찾아요.
여기서 한글 함정이 하나 있어요. JS의 \w 는 ASCII 영숫자와 밑줄뿐이라 한글 앞뒤에는 단어 경계가 생기지 않아요.
'고양이가 고양이'.match(/\b고양이\b/g); // null
'고양이가 고양이'.match(/(?<![가-힣])고양이(?![가-힣])/g); // ['고양이']
import re
re.findall(r'\b고양이\b', '고양이가 고양이') # ['고양이'] ← 두 번째만
파이썬 3는 \w 가 유니코드라서 한글도 단어 문자로 봐요. 그래서 '고양이가'의 고양이는 뒤에 '가'가 붙어 경계가 없고, 단독 '고양이'만 잡혀요. JS에서 같은 효과를 내려면 위처럼 전후방 탐색(lookaround)으로 "앞뒤가 한글이 아님"을 직접 써야 해요.
8~9. 묶고 고르는 문법: 그룹, 캡처, 선택
- 그룹
( )— 여러 글자를 하나로 묶어요.(ab)+는 ab, abab, ababab. 묶은 부분은 따로 꺼내 쓸 수 있어요(캡처). - 선택 — 세로 막대(파이프)는 "또는"이에요. 범위를 제한하려면 그룹과 함께 써요.
\.(jpg|png|gif)$ 확장자가 jpg, png, gif 중 하나로 끝남
(?:jpg|png) 캡처하지 않는 그룹 (번호를 안 매김)
(?<year>\d{4}) 이름 붙은 캡처 그룹 (JS 문법, 파이썬은 (?P<year>...))
이름 붙은 그룹은 번호보다 읽기 쉬워서 날짜 파싱 같은 곳에 좋아요.
const m = '2026-10-05'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/);
m.groups; // { y: '2026', m: '10', d: '05' }
'2026-10-05'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1년 $2월 $3일'); // '2026년 10월 05일'
m = re.search(r'(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})', '2026-10-05')
m.group('y') # '2026'
re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\1년 \2월 \3일', '2026-10-05')
같은 기능인데 문법이 달라요. JS는 (?<이름>) 과 치환에서 $1, 파이썬은 (?P<이름>) 과 \1 이에요. 파이썬에 JS식 (?<y>...) 를 쓰면 unknown extension ?<y 에러가 나요.
10. 동작을 바꾸는 플래그
플래그는 패턴 바깥에서 동작을 바꿔요.
| 의미 | JavaScript | Python | 주의할 점 |
|---|---|---|---|
| 전부 찾기 | g | re.findall, re.finditer | JS는 g 없으면 첫 번째만 |
| 대소문자 무시 | i | re.I | |
여러 줄 (^ $ 가 줄마다) | m | re.M | 없으면 전체 문자열 기준 |
| 점이 줄바꿈도 매치 | s | re.S | |
| 유니코드 | u / v | 기본값 | JS는 u 없으면 이모지가 2글자 |
JS의 g 플래그에는 두 가지 함정이 있어요. 첫째, replaceAll 에 g 없는 정규식을 넘기면 에러가 나요. 둘째, g가 붙은 정규식 객체를 test() 로 재사용하면 lastIndex 가 남아서 결과가 번갈아 바뀌어요.
'a-b'.replaceAll(/-/, '+');
// TypeError: String.prototype.replaceAll called with a non-global RegExp argument
const re = /a/g;
re.test('a'); re.test('a'); re.test('a'); // true, false, true ← lastIndex 때문
검증 용도로 재사용하는 정규식에는 g를 붙이지 마세요. u 플래그는 이모지 같은 보조 평면 문자를 한 글자로 다루게 해요. '😀'.match(/./g) 는 길이 2, /./gu 는 1이에요.
같은 \d, 언어마다 다른 결과
"\d 는 숫자"라고 외우지만, 정확히 어떤 숫자인지는 언어마다 달라요. 유니코드 전체(약 111만 코드 포인트)를 하나씩 넣어서 \d 가 몇 글자에 매치되는지 직접 세어 봤어요.
re.findall(r'\d+', '3월 ٣ 3') # ['3', '٣', '3']
'3월 ٣ 3'.match(/\d+/g); // ['3']
파이썬 서버에서 ^\d+$ 로 검증한 전각 숫자 123 이 통과한 뒤 int() 에선 123으로 변환되지만, 다른 시스템으로 넘어가 깨지는 일이 생길 수 있어요. ASCII 숫자만 받으려면 파이썬은 [0-9] 나 re.ASCII 플래그를 쓰세요. \w 차이는 더 커서 JS는 63글자, 파이썬은 13만 글자 이상을 단어 문자로 봐요.
한글 정규식과 자주 보는 에러 메시지
한글 완성형 음절은 유니코드 U+AC00(가)부터 U+D7A3(힣)까지 연속이라 [가-힣] 으로 고를 수 있어요. 이 범위엔 자음·모음 단독 글자(ㅋ, ㅏ)는 없어요.
'안녕 ㅋㅋ hello 뷁'.match(/[가-힣]+/g); // ['안녕', '뷁']
자음·모음까지 포함하려면 [가-힣ㄱ-ㅎㅏ-ㅣ] 로 넓히면 돼요. 패턴을 잘못 썼을 때 나오는 에러 메시지도 정리해 둘게요. 검색할 때 그대로 붙여 넣으면 돼요.
| 잘못된 패턴 | JavaScript (Node 22) | Python 3.9 |
|---|---|---|
( | Invalid regular expression: /(/: Unterminated group | missing ), unterminated subpattern at position 0 |
*a | Nothing to repeat | nothing to repeat at position 0 |
[a- | Unterminated character class | unterminated character set at position 0 |
a{2,1} | numbers out of order in {} quantifier | min repeat greater than max repeat at position 2 |
Nothing to repeat 은 수량자 앞에 반복할 대상이 없다는 뜻이에요. 사용자 입력을 그대로 new RegExp(input) 에 넣다가 +, *, ?, ( 같은 글자 때문에 자주 터져요. 사용자 입력은 반드시 이스케이프하세요. 파이썬은 re.escape(), JS는 아직 표준 함수가 널리 쓰이기 전이라 s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 를 많이 써요.
자주 묻는 질문
정규식에서 한글만 찾으려면 어떻게 하나요?
완성형 한글은 [가-힣] 으로 찾을 수 있어요. 자음·모음 단독 글자까지 포함하려면 [가-힣ㄱ-ㅎㅏ-ㅣ] 를 쓰세요. JS에서 한글 단어 경계가 필요하면 \b 대신 (?<![가-힣]) 와 (?![가-힣]) 를 조합해야 해요.
Python re.match와 re.search의 차이는 무엇인가요?
re.match 는 문자열 맨 앞에서만 매치를 시도하고, re.search 는 문자열 어디서든 찾아요. 전체가 패턴과 일치하는지 검사하려면 re.fullmatch 를 쓰는 게 가장 명확해요. re.match 를 검증에 쓰면 뒤에 붙은 쓰레기 문자를 놓칠 수 있어요.
정규식 Nothing to repeat 에러는 왜 나나요?
*, +, ?, {n} 같은 수량자 앞에 반복할 대상이 없을 때 나요. 패턴이 수량자로 시작하거나, 사용자 입력의 특수문자를 이스케이프 없이 정규식에 넣은 경우가 대부분이에요. 글자 그대로 찾으려면 \+ 처럼 이스케이프하세요.
정규식 g 플래그를 붙였더니 test 결과가 번갈아 바뀌어요.
g 플래그가 붙은 정규식 객체는 마지막 매치 위치(lastIndex)를 기억해서, 다음 test() 가 그 위치부터 검색해요. 검증용 정규식에는 g를 빼거나, 매번 re.lastIndex = 0 으로 초기화하세요.
\d와 [0-9]는 같은 건가요?
JavaScript와 Java 기본 설정에서는 같아요. 하지만 Python 3의 \d 는 전각 숫자, 아랍 숫자 등 유니코드 숫자 650자를 모두 매치해요. ASCII 숫자만 허용하려면 [0-9] 를 쓰는 게 언어와 무관하게 안전해요.
정리
정규식은 글자 고르기, 반복, 위치, 묶기, 플래그 다섯 덩어리로 끊어 읽으면 외계어가 문장이 돼요. 여기에 "수량자는 앞 한 요소에만", "검증엔 앵커 필수", "JS와 Python의 \d, \w, \b 는 다르다" 세 가지를 기억해 두면 남이 짠 패턴을 고칠 때 실수가 확 줄어요.
처음엔 한 글자씩 끊어 읽는 게 느려도 열 번쯤 하면 덩어리로 보이기 시작해요. 정규식 테스터에서 패턴을 조금씩 바꿔 가며 하이라이트가 어떻게 변하는지 보는 게 제일 빠른 연습이에요.