정규표현식(정규식)의 기본 기호는 언어가 달라도 거의 같습니다. 차이는 한글을 다룰 때 드러납니다. [가-힣]은 자음만 입력한 ㅋㅋ를 놓치고, macOS에서 온 파일 이름처럼 자모가 분리된 한글도 놓칩니다. \w와 \b는 언어에 따라 한글을 단어 문자로 보기도 하고 아니기도 해서, 조사가 붙는 한국어 문장에서 예상과 다른 결과를 냅니다. 이 글은 기본 문법 표와 함께 이런 차이를 실제 실행 결과로 정리합니다.
표의 결과는 모두 2026-10-02에 직접 실행한 것입니다. JavaScript는 Node.js 22와 24, Python은 3.12의 re, PCRE2는 10.49(PHP의 preg_*, grep -P, 엑셀 정규식 함수가 쓰는 계열), Go는 1.27의 regexp입니다. “오류”는 해당 엔진이 패턴을 컴파일하지 못한다는 뜻입니다. 플래그가 g·i·m·s뿐인 예제는 이 사이트의 정규표현식 테스터에 붙여 넣으면 같은 결과가 나옵니다. 이 도구는 브라우저의 RegExp를 그대로 쓰며 u·v 플래그 체크박스가 없으므로, 유니코드 속성 예제는 브라우저 콘솔이나 Node.js에서 확인하세요.
정규표현식 기본 문법 표
| 구분 | 문법 | 의미 | 예 |
|---|---|---|---|
| 문자 | . | 줄바꿈을 뺀 아무 문자 1개 | 서.시는 서울시에 일치 |
| 문자 클래스 | [abc] / [^abc] | 그중 하나 / 그 외 | [^,]+로 쉼표 사이 값 추출 |
| 범위 | [가-힣] | 코드 포인트 범위 | 완성형 한글 11,172자 |
| 축약 | \d \w \s | 숫자, 단어 문자, 공백 | 언어마다 의미가 다름 |
| 축약 부정 | \D \W \S | 각각의 반대 | \S+로 공백 아닌 덩어리 |
| 위치 | ^ / $ | 시작 / 끝(m이면 각 줄) | ^#으로 제목 줄 |
| 위치 | \b / \B | 단어 경계 / 경계 아님 | 한글 단어에는 기대대로 동작하지 않음 |
| 수량자 | * + ? | 0회 이상, 1회 이상, 0 또는 1회 | -?로 하이픈 선택 |
| 수량자 | {n} {n,} {n,m} | 횟수 지정 | \d{5}로 다섯 자리 |
| 게으른 수량자 | *? +? | 가능한 한 짧게 | \(.+?\)로 괄호 하나씩 |
| 그룹 | ( ) / (?: ) | 캡처 / 캡처 안 함 | (\d{4})년으로 연도만 |
| 선택 | | | 또는 | 주식회사|㈜ |
| 역참조 | \1 | 1번 그룹과 같은 문자열 | (.)\1은 ㅋㅋ에 일치 |
| 전방 탐색 | (?=…) / (?!…) | 뒤에 온다 / 오지 않는다 | \d+(?=원) |
| 후방 탐색 | (?<=…) / (?<!…) | 앞에 있다 / 없다 | (?<=₩)\d+ |
게으른 수량자는 괄호로 확인하면 쉽습니다. \(.+?\)는 (주) 대표 (2026)에서 (주)와 (2026)을 따로 찾고, ?를 빼면 처음 여는 괄호부터 마지막 닫는 괄호까지 한 덩어리가 됩니다.
한글 범위: [가-힣]이 놓치는 글자
[가-힣]은 유니코드의 완성형 한글 음절 U+AC00부터 U+D7A3까지, 11,172자를 가리킵니다. 회원 가입 폼의 이름 검증이나 “한글만 입력” 검사에 가장 많이 쓰이는 범위입니다. 하지만 세 가지를 놓칩니다.
| 놓치는 것 | 예 | 코드 포인트 | 해결 |
|---|---|---|---|
| 호환용 자모 | ㅋㅋㅋ, ㅠㅠ | U+3131–U+3163 | [ㄱ-ㅎㅏ-ㅣ가-힣] |
| 분해된 한글(NFD) | 자모 6개로 저장된 한글 | U+1112 U+1161 U+11AB … | 검사 전에 NFC로 정규화 |
| 옛한글·반각 자모 | ᄒ(초성 자모) 등 | U+1100–U+11FF 등 | \p{sc=Hangul}(u 플래그) |
첫째, 채팅이나 댓글에 자주 나오는 ㅋㅋㅋ, ㅠㅠ 같은 자음·모음 단독 입력은 호환용 자모(U+3131–U+318E) 영역이라 [가-힣]에 들어가지 않습니다. 단독 자모까지 허용하려면 [ㄱ-ㅎㅏ-ㅣ가-힣]처럼 범위를 더합니다.
둘째, 같은 한글이라도 완성형 2글자가 아니라 초성·중성·종성 자모 6개로 저장된 문자열이 있습니다. 유니코드 정규화 형식 NFD(분해형)입니다. Apple의 HFS Plus 파일 시스템은 파일 이름을 분해형으로 저장한다고 문서에 적혀 있고(Apple Technical Q&A QA1173), 그런 파일 이름을 읽어 오면 화면에는 똑같이 한글로 보여도 ^[가-힣]+$가 일치하지 않습니다. 검사 전에 JavaScript는 str.normalize('NFC'), Python은 unicodedata.normalize('NFC', s)로 완성형으로 바꾸면 해결됩니다.
셋째, 유니코드에는 완성형 음절 외에도 한글 자모 블록(U+1100–U+11FF)과 반각 자모 등이 있습니다. 문자 종류로 판정하려면 범위 대신 유니코드 스크립트 속성 \p{sc=Hangul}을 쓰는 편이 확실합니다. JavaScript에서는 u나 v 플래그가 필요하고, v 플래그를 쓰면 집합 연산도 할 수 있습니다. [\p{sc=Hangul}--[가-힣]]은 “한글이지만 완성형 음절은 아닌 글자”라서, 한글ㅋᄒ에서 ㅋ와 ᄒ를 찾아냅니다. 분해형 입력이나 단독 자모가 섞였는지 경고할 때 유용합니다.
Python 표준 re에는 \p{…}가 없어서 bad escape \p 오류가 납니다. Python에서는 서드파티 regex 모듈을 쓰거나 범위로 적어야 합니다. Go는 \p{Hangul}처럼 스크립트 이름만 쓰는 형식을 지원합니다.
const nfd = '한글'.normalize('NFD');
console.log(nfd.length, /^[가-힣]+$/.test(nfd));
const nfc = nfd.normalize('NFC');
console.log(nfc.length, /^[가-힣]+$/.test(nfc));
console.log('한글ㅋᄒ'.match(/[\p{sc=Hangul}--[가-힣]]/gv).join(','));
\w와 \b: 조사가 붙는 한국어의 단어 경계
\b는 단어 문자(\w)와 그 밖의 문자 사이에 일치합니다. 영어에서는 \bcat\b가 concat 속의 cat을 빼고 찾아 주지만, 한국어에서는 언어마다 결과가 갈립니다.
| 패턴 | 대상 | JavaScript | Python | PCRE2 | Go |
|---|---|---|---|---|---|
\w+ | 서울에서 만나요 | 일치 없음 | 서울에서, 만나요 | 일치 없음 | 일치 없음 |
\b서울\b | 서울 시내 | 일치 없음 | 서울 | 일치 없음 | 일치 없음 |
\b서울\b | 서울에서 | 일치 없음 | 일치 없음 | 일치 없음 | 일치 없음 |
JavaScript·PCRE2·Go의 \w는 [A-Za-z0-9_]뿐이라 한글이 아예 단어 문자가 아닙니다. 그래서 \b서울\b는 어디에도 경계가 없어 일치하지 않습니다. Python은 str 패턴에서 \w가 유니코드 문자 전체를 포함하므로(Python 문서) 띄어쓰기 단위인 어절을 하나의 단어로 봅니다. 서울 시내에서는 서울을 찾지만, 조사가 붙은 서울에서는 어절 전체가 하나의 단어라 서울 뒤에 경계가 없습니다.
결국 어느 언어에서도 \b로 “조사를 뗀 명사”를 찾을 수는 없습니다. 단순히 서울이라는 글자가 들어 있는지만 보려면 \b 없이 서울이라고 쓰면 되고, 명사만 뽑아야 한다면 형태소 분석기로 먼저 나눈 뒤에 정규식을 적용합니다.
우편번호·휴대전화·금액·이름 패턴
입력 검증에서 자주 쓰는 패턴을 테스트 입력과 함께 정리했습니다. 여러 줄 입력은 m 플래그로 한 줄씩 판정했습니다.
| 용도 | 패턴 | 통과 | 거부 |
|---|---|---|---|
| 우편번호(5자리) | ^\d{5}$ | 03488, 06234 | 122-090(옛 6자리), 1234 |
| 휴대전화 | ^010-?\d{4}-?\d{4}$ | 010-1234-5678, 01012345678 | 011-123-4567, 010-123-4567 |
| 금액(원 앞의 숫자) | \d[\d,]*(?=원) | 12,900원의 12,900, 3,000원의 3,000 | |
| 금액(₩ 뒤의 숫자) | (?<=[₩₩])\d[\d,]* | ₩12,900, ₩3,000 | |
| 한글 이름 2–4자 | ^[가-힣]{2,4}$ | 홍길동, 남궁민수 | 김, John, 선우 용녀 |
우편번호는 2015년 8월 1일부터 국가기초구역번호 5자리를 씁니다(우정사업본부 우편번호 개요). 그 전의 6자리 122-090 형식은 이 패턴에서 거부됩니다. 03488처럼 0으로 시작하는 번호가 있으므로, 우편번호를 숫자형 열에 저장하면 앞의 0이 사라집니다. 정규식 검사를 통과한 값은 문자열로 저장하세요.
휴대전화는 010만 받습니다. 방송통신위원회는 이동전화 식별번호를 “모든 이동통신사가 2G 서비스를 종료하는 때” 010으로 통합하기로 결정했고, 010 통합 후에는 뒤 8자리만으로 통화할 수 있다고 설명했습니다(방송통신위원회 보도자료, 2010-09-15). 그래서 새 검증은 010 뒤에 8자리를 요구하면 됩니다. 다만 오래된 회원 데이터에는 011이나 016~019로 시작하는 옛 번호가 남아 있을 수 있으니, 기존 데이터를 일괄 검사할 때는 거부된 행을 따로 모아 확인하는 편이 안전합니다.
금액의 원화 기호는 반각 ₩(U+20A9)와 전각 ₩(U+FFE6)이 섞이기 쉬워서 문자 클래스에 둘 다 넣었습니다. 이름 패턴 ^[가-힣]{2,4}$는 흔히 쓰이지만 외자 이름 김, 띄어 쓴 복성 이름, 외국인 이름을 모두 거부합니다. 이름은 길이와 공백 정도만 검사하고 형식은 제한하지 않는 쪽이 사용자를 덜 막습니다.
그룹과 전후방 탐색: 언어별 문법 차이
같은 뜻의 정규식이라도 이름 있는 그룹과 후방 탐색은 언어마다 쓰는 법이 다릅니다.
| 기능 | JavaScript | Python | PCRE2 | Go |
|---|---|---|---|---|
| 이름 있는 그룹 | (?<y>…) | (?P<y>…) | 둘 다 | 둘 다 |
| 치환에서 참조 | $<y> | \g<y> | 호출하는 언어에 따름 | ${y} |
| 후방 탐색 | 길이 제한 없음 | 고정 길이만 | 최대 길이가 정해진 가변 길이 | 지원 안 함 |
역참조 \1 | 지원 | 지원 | 지원 | 지원 안 함 |
(?<y>\d{4})년 (?<m>\d{1,2})월 (?<d>\d{1,2})일은 JavaScript·PCRE2·Go에서 2026년 10월 2일에 일치하지만 Python에서는 unknown extension ?<y 오류가 납니다. 반대로 Python식 (?P<y>\d{4})년 (?P<m>\d{1,2})월은 JavaScript에서 오류입니다. 세 언어 이상에서 같은 패턴을 써야 한다면 PCRE2와 Go가 모두 받는 (?P<…>) 쪽이 그나마 범용적이지만, JavaScript만은 예외입니다.
후방 탐색은 길이가 다른 선택지에서 차이가 납니다. (?<=부가세|VAT )\d[\d,]*는 부가세(3자)와 VAT (4자)의 길이가 달라서, Python은 look-behind requires fixed-width pattern으로 거부합니다. JavaScript는 길이 제한이 없고, PCRE2는 10.43부터 최대 길이가 정해진 가변 길이 후방 탐색을 허용합니다(pcre2pattern). Go는 전방·후방 탐색이 모두 없습니다. 어느 언어에서나 돌아가게 하려면 탐색 대신 (?:부가세|VAT )(\d[\d,]*)처럼 그룹으로 캡처한 뒤 1번 그룹을 읽습니다.
import re, unicodedata
m = re.fullmatch(r'(?P<y>\d{4})년 (?P<m>\d{1,2})월 (?P<d>\d{1,2})일', '2026년 10월 2일')
print(m['y'], m['m'], m['d'])
s = unicodedata.normalize('NFC', unicodedata.normalize('NFD', '한글'))
print(s, len(s))
# 후방 탐색 대신 그룹으로 캡처
print(re.findall(r'(?:부가세|VAT )(\d[\d,]*)', '부가세1,290 VAT 1,290'))
플래그: u·v·i·m·s·y·d
| JavaScript | Python | 의미 | 정규표현식 테스터 |
|---|---|---|---|
g | findall / finditer | 모든 일치 찾기 | 있음(항상 전체 검색) |
i | re.I | 대소문자 무시 | 있음 |
m | re.M | ^ $가 줄마다 일치 | 있음 |
s | re.S | .이 줄바꿈에도 일치 | 있음 |
u | (str에서는 항상 켜짐) | 코드 포인트 단위, \p{…} 사용 | 없음 |
v | 없음 | u + 문자 클래스 집합 연산 | 없음 |
y | pattern.match(s, pos) | 지정 위치에서만 일치 | 없음 |
d | match.span() | 일치 위치 기록 | 없음 |
u 플래그가 없으면 JavaScript는 문자열을 UTF-16 코드 단위로 다룹니다. 이모지처럼 U+FFFF를 넘는 문자는 2단위라서 /^.$/가 😀에 일치하지 않고, /^.$/u는 일치합니다. 완성형 한글은 모두 U+FFFF 이하라 u가 없어도 한 글자로 처리되지만, \p{sc=Hangul} 같은 속성은 u 없이는 p{sc=Hangul}이라는 문자열로 해석됩니다. v 플래그는 Chrome 112, Firefox 116, Safari 17부터 지원됩니다(MDN).
패턴 일부에만 플래그를 거는 (?i:…)는 ES2025에서 JavaScript에 들어왔고 Chrome 125, Firefox 132, Safari 26, Node.js 23부터 쓸 수 있습니다(MDN 호환성 데이터). Node.js 22에서는 Invalid group 오류가 납니다. Python, PCRE2, Go는 오래전부터 인라인 플래그를 지원합니다.
엑셀과 구글 시트의 정규식
스프레드시트에서도 정규식을 쓸 수 있지만, 엔진이 달라서 쓸 수 있는 문법이 다릅니다.
- Microsoft 365 엑셀의
REGEXTEST,REGEXEXTRACT,REGEXREPLACE는 PCRE2 계열 문법을 씁니다(REGEXTEST 함수). - 구글 시트의
REGEXMATCH,REGEXEXTRACT,REGEXREPLACE는 RE2를 쓰며, 유니코드 문자 클래스 일치는 지원하지 않습니다(REGEXMATCH 도움말). Go의regexp도 RE2 문법입니다.
RE2는 입력 길이에 비례하는 시간 안에 반드시 끝나는 대신 전후방 탐색, 역참조, 소유 수량자(\d++)를 지원하지 않습니다. 이 글의 표에서 Go가 “오류”인 패턴은 구글 시트에서도 그대로는 쓸 수 없다고 보면 됩니다. 예를 들어 문장 속 다섯 자리 숫자만 찾는 (?<!\d)\d{5}(?!\d)는 우편번호 03488, 전화 0212345678에서 03488만 찾지만 RE2에서는 오류입니다. 구글 시트에서는 (^|[^0-9])([0-9]{5})([^0-9]|$)처럼 앞뒤 문자까지 포함해 일치시키고 가운데 그룹을 꺼내는 식으로 바꿔 씁니다.
반대로 JavaScript·Python·PCRE2는 되추적(backtracking) 방식이라, ^(a+)+$처럼 수량자를 중첩한 패턴은 일치하지 않는 긴 입력에서 처리 시간이 급격히 늘어납니다. 사용자가 입력한 텍스트에 정규식을 적용할 때는 입력 길이에 상한을 두세요.
정규표현식 테스터로 확인하기
패턴을 코드에 넣기 전에 통과해야 할 입력, 거부해야 할 입력, 애매한 입력(빈 문자열, 전각 숫자, 끝의 줄바꿈, 이모지)을 나란히 놓고 시험해 보는 것이 가장 빠릅니다. 이 사이트의 정규표현식 테스터에서는 이렇게 확인합니다.
- 패턴 칸에
^010-?\d{4}-?\d{4}$를 넣고m에 체크합니다. - 테스트 문자열에
010-1234-5678,01012345678,011-123-4567,010-1234-5678을 한 줄씩 붙여 넣습니다. - 상태 표시가 “2개 일치 항목을 찾았습니다.”가 되고, 강조 표시에서 앞의 두 줄만 일치하는지 확인합니다. 전각 숫자 줄이 일치하지 않는 이유는 브라우저 JavaScript의
\d가 ASCII 숫자만 받기 때문입니다. 이 패턴은 앞의010을 글자 그대로 적었으므로 Python에서도 전각 줄은 거부됩니다. 그러나^\d{3}-?\d{4}-?\d{4}$처럼\d만 쓰면 Python은 전각 줄도 통과시키므로, 서버(Python)와 화면(JavaScript)의 검사 결과가 달라집니다. 양쪽에서[0-9]로 쓰거나, 검사 전에 NFKC로 정규화해 전각 숫자를 반각으로 바꾸세요.
목록에 표시되는 위치(index)는 JavaScript와 같은 UTF-16 기준이라 앞에 이모지가 있으면 2씩 늘어납니다. 이름 있는 그룹도 “그룹 1”, “그룹 2”처럼 번호로 표시됩니다. u·v 플래그가 필요한 패턴은 이 도구 대신 브라우저 콘솔이나 Node.js에서 확인하세요.