~ / posts / developer

정규식 기초 문법 치트시트 — 10가지로 읽는 법과 JS·Python 차이

정규식은 자주 쓰는 문법 10가지(문자 클래스, 수량자, 앵커, 그룹, 플래그 등)만 알면 대부분 읽혀요. 끊어 읽는 법, 한글 매칭, JS와 Python에서 결과가 달라지는 지점, 실제 에러 메시지까지 예제로 정리했습니다.

정규식을 처음 보면 키보드를 고양이가 밟고 지나간 것 같죠. 저도 한동안은 검색해서 나온 패턴을 복사해 쓰고, 왜 되는지는 모르는 채로 넘어갔어요. 그러다 남이 짠 패턴을 고쳐야 하는 상황이 와서 제대로 읽는 법을 익혔는데, 막상 해 보니 자주 쓰는 문법은 열 가지 남짓이고, 패턴을 "글자 고르기 → 반복 횟수 → 위치 → 묶기" 덩어리로 끊어 읽으면 대부분 해석돼요.

이 글은 그 열 가지를 예제와 함께 정리한 치트시트예요. 여기에 실무에서 자주 걸리는 부분 — 한글 매칭, 같은 \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. 글자를 고르는 문법: 점, 문자 클래스, 축약 클래스

  1. 그냥 글자 — cat 은 c, a, t가 연속된 부분을 찾아요. 정규식의 기본은 결국 문자열 검색이에요.
  2. 점 . — 줄바꿈을 뺀 아무 글자 하나. c.t 는 cat, cut, c9t 전부 맞아요. 진짜 점을 찾으려면 \. 처럼 이스케이프해야 해요. 이걸 빼먹어서 example.com 패턴이 exampleXcom 까지 통과시키는 일이 흔해요.
  3. 문자 클래스 [ ] — 대괄호 안의 글자 중 하나. [aeiou] 는 모음 하나, [0-9] 는 숫자 하나, 맨 앞에 ^ 를 쓴 [^0-9] 는 "숫자만 빼고"예요.
  4. 축약 클래스 — \d 숫자, \w 영문자·숫자·밑줄, \s 공백류(스페이스, 탭, 줄바꿈). 대문자로 쓰면 반대예요. \D 는 숫자가 아닌 것.
문법뜻예시매치
.줄바꿈 제외 아무 글자 1개c.tcat, c9t
\.진짜 점a\.ba.b
[abc]a, b, c 중 하나gr[ae]ygray, grey
[^abc]a, b, c 제외 아무거나[^0-9]숫자 아닌 글자
[a-z]범위[A-Za-z]영문 1자
\d / \D숫자 / 숫자 아님\d\d42
\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. 위치를 정하는 문법: 앵커와 단어 경계

  1. 앵커 ^ 와 $ — 문자열의 시작과 끝이에요. 글자를 소비하지 않고 위치만 확인해요. 입력 검증에서 이걸 빼먹으면 "어딘가에 숫자 6개가 들어 있기만 하면" 통과돼요. \d{6} 은 abc123456xyz 안에서도 매치되지만, ^\d{6}$ 은 정확히 숫자 6개일 때만 맞아요.
  2. 단어 경계 \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. 묶고 고르는 문법: 그룹, 캡처, 선택

  1. 그룹 ( ) — 여러 글자를 하나로 묶어요. (ab)+ 는 ab, abab, ababab. 묶은 부분은 따로 꺼내 쓸 수 있어요(캡처).
  2. 선택 — 세로 막대(파이프)는 "또는"이에요. 범위를 제한하려면 그룹과 함께 써요.
\.(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. 동작을 바꾸는 플래그

플래그는 패턴 바깥에서 동작을 바꿔요.

의미JavaScriptPython주의할 점
전부 찾기gre.findall, re.finditerJS는 g 없으면 첫 번째만
대소문자 무시ire.I
여러 줄 (^ $ 가 줄마다)mre.M없으면 전체 문자열 기준
점이 줄바꿈도 매치sre.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 가 몇 글자에 매치되는지 직접 세어 봤어요.

\d 한 글자가 매치하는 문자 수 (유니코드 전체 대상)
JavaScript (u 플래그 포함)JavaScript (u 플래그 포함) · 10자10자Java 기본Java 기본 · 10자10자Python re.ASCIIPython re.ASCII · 10자10자Java UNICODE_CHARACTER_CLASSJava UNICODE_CHARACTER_CLASS · 650자650자Python 3 기본Python 3 기본 · 650자650자

Python 3의 \d는 전각 숫자(3)와 아랍 숫자(٣) 등 유니코드 Nd 범주를 모두 포함

단위: 자 · 자료: Node.js 22.18 · OpenJDK 17 · Python 3.9.6 에서 U+0000~U+10FFFF(서로게이트 제외)를 전수 검사해 직접 집계
표로 보기
구분매치 문자 수
JavaScript (u 플래그 포함)10
Java 기본10
Python re.ASCII10
Java UNICODE_CHARACTER_CLASS650
Python 3 기본650
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 groupmissing ), unterminated subpattern at position 0
*aNothing to repeatnothing to repeat at position 0
[a-Unterminated character classunterminated character set at position 0
a{2,1}numbers out of order in {} quantifiermin 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 는 다르다" 세 가지를 기억해 두면 남이 짠 패턴을 고칠 때 실수가 확 줄어요.

처음엔 한 글자씩 끊어 읽는 게 느려도 열 번쯤 하면 덩어리로 보이기 시작해요. 정규식 테스터에서 패턴을 조금씩 바꿔 가며 하이라이트가 어떻게 변하는지 보는 게 제일 빠른 연습이에요.

#정규식 기초#정규표현식 문법#regex 치트시트#정규식 한글#자바스크립트 정규식#파이썬 re#정규식 플래그
← 이전 글Base64는 암호화가 아니다 — 인코딩·암호화·해시 차이와 올바른 사용법다음 글 →Unix 타임스탬프란? 2038년 문제가 생기는 이유와 점검할 곳