문자 클래스와 이스케이프
문자 클래스는 집합 중 한 글자와 맞습니다. 단축 이스케이프는 가장 자주 쓰게 되는데, 다른 언어에서 넘어오면 세 가지가 낯섭니다. \d와 \w는 u 플래그를 켜도 ASCII만 대상으로 하고, \s는 스페이스보다 훨씬 넓습니다.
\w는 [A-Za-z0-9_]만 잡으므로 /\w+/g를 café naïve 안녕 x_1에 적용하면 caf, na, ve, x_1이 나옵니다. 악센트 글자와 한글은 건너뜁니다. \d도 u 여부와 관계없이 ٣ 같은 아랍 인도 숫자와 맞지 않습니다. 언어에 관계없이 글자를 잡으려면 u 플래그와 \p{L}을 씁니다. 같은 문장에 /\p{L}+/gu를 적용하면 café, naïve, 안녕이 나옵니다.
| 토큰 | 일치 대상 | 비고 |
|---|---|---|
\d \D | 숫자 [0-9] / 숫자 아님 | ASCII만 |
\w \W | [A-Za-z0-9_] / 그 외 | ASCII만, 악센트 글자·한글은 제외 |
\s \S | 공백 / 공백 아님 | 탭, 줄바꿈, 줄바꿈 없는 공백 U+00A0, 전각 공백 U+3000 포함 |
. | \n, \r, U+2028, U+2029를 뺀 모든 글자 | s 플래그를 켜면 예외가 사라짐 |
[abc] [^abc] | a, b, c 중 하나 / 그 외 한 글자 | [^...]는 줄바꿈도 잡음 |
[a-z0-9] | 클래스 안의 범위 | -는 클래스의 맨 앞이나 맨 뒤에서 문자 그대로의 뜻 |
\p{L} \p{Script=Hangul} | 유니코드 속성 | u 또는 v 플래그 필요, 없으면 \p{L}은 그냥 p{L}이라는 글자와 맞음 |
수량자: 탐욕적 대 게으른
수량자는 바로 앞 항목이 몇 번 반복되는지를 정합니다. 기본은 탐욕적(greedy)이어서 가능한 한 많이 가져가고, 나머지 패턴이 실패할 때만 되돌려 줍니다. 수량자 뒤에 ?를 붙이면 게으른(lazy) 수량자가 되어 최소한만 가져갑니다.
<a><b>에서 탐욕적인 /<.+>/는 .+가 마지막 >까지 삼키므로 문자열 전체 <a><b>를 돌려주고, 게으른 /<.+?>/g는 <a>와 <b>를 따로 돌려줍니다. 횟수를 지정한 수량자도 같습니다. aaaaaaa에 /a{2,3}/g를 쓰면 aaa, aaa(a 하나가 남음)이고 /a{2,3}?/g는 aa, aa, aa입니다.
| 탐욕적 | 게으른 | 반복 횟수 |
|---|---|---|
* | *? | 0회 이상 |
+ | +? | 1회 이상 |
? | ?? | 0회 또는 1회 |
{n} | 해당 없음 | 정확히 n회 |
{n,} | {n,}? | n회 이상 |
{n,m} | {n,m}? | n회 이상 m회 이하 |
앵커, 단어 경계, m 플래그
^와 $는 글자가 아니라 위치와 맞습니다. 플래그가 없으면 입력 전체의 시작과 끝입니다. \b는 \w 글자와 \w가 아닌 글자 사이의 경계라서, cat concat cat.에 /\bcat\b/g를 쓰면 concat 안의 cat은 건너뛰고 두 개만 찾습니다.
m 플래그를 켜면 ^와 $가 모든 줄의 시작과 끝에서 맞습니다. one, 줄바꿈, two에 /^\w+$/g를 쓰면 텍스트 전체가 한 단어가 아니므로 null이지만 m을 붙이면 one과 two가 나옵니다. 자바스크립트는 \n, \r, U+2028, U+2029를 줄 끝으로 보므로 윈도우의 \r\n 파일도 m으로 처리됩니다.
하나 더 있습니다. m이 없을 때 $는 입력의 맨 끝에서만 맞습니다. /^\d+$/.test('12\n')은 false이고 m을 붙이면 true입니다. 끝에 줄바꿈이 붙어 올 수 있는 입력을 검증할 때 중요합니다.
그룹, 이름 있는 그룹, 역참조, 룩어라운드
괄호는 묶고 캡처합니다. (?:...)는 캡처 없이 묶고, (?<name>...)은 이름을 붙여 캡처하며 match.groups.name으로 읽습니다. 역참조는 같은 텍스트에 다시 맞습니다. 번호 그룹은 \1, 이름 그룹은 \k<name>입니다. hello aabb에 /(\w)\1/g를 쓰면 ll, aa, bb가 나오고, say "hi" and 'yo'에 /(?<q>['"]).*?\k<q>/g를 쓰면 여는 따옴표와 같은 종류의 닫는 따옴표를 짝지어 "hi"와 'yo'가 나옵니다. 최신 JavaScript는 (?<x>a)|(?<x>b)처럼 둘 중 하나만 참여할 수 있는 별도 대안에서는 같은 이름을 허용하지만, (?<x>a)(?<x>b)는 여전히 문법 오류입니다.
룩어라운드는 주변을 확인하되 소비하지 않습니다. 룩어헤드는 (?=...)와 (?!...), 룩비하인드는 (?<=...)와 (?<!...)입니다. 10 dollars 20 euros에 /\d+(?= dollars)/g를 쓰면 10이 나오고, cost $30 or €40에 /(?<=\$)\d+/g를 쓰면 30이 나옵니다.
부정 룩어헤드에는 함정이 있습니다. 10 dollars 20 euros에 /\d+(?! dollars)/g를 쓰면 20만이 아니라 1과 20이 나옵니다. 엔진이 10을 1로 줄여서 되돌아가고, 1 뒤에는 dollars가 오지 않기 때문입니다. \d+\b(?! dollars)처럼 경계를 넣어 숫자가 줄어드는 것을 막으세요. 다른 엔진과 달리 자바스크립트는 룩비하인드에 가변 길이를 허용합니다. cost $12.50에 /(?<=\$\d+\.)\d+/를 쓰면 50이 나옵니다.
const m = /(?<y>\d{4})-(?<m>\d{2})/.exec("on 2026-10-01");
m[0] // "2026-10"
m.groups.y // "2026"
m.groups.m // "10"플래그: g i m s u v y d
플래그는 닫는 슬래시 뒤의 한 글자, 또는 new RegExp의 두 번째 인자이며 패턴 전체의 동작을 바꿉니다. new RegExp('a', 'dgimsuy').flags는 알파벳 순으로 보고합니다. v와 u는 함께 쓸 수 없고, 쓰면 엔진이 Invalid flags supplied to RegExp constructor 'uv'를 던집니다.
g 플래그에는 알아 둘 부작용이 있습니다. g나 y가 있는 정규식은 lastIndex에 위치를 기억하므로, 공유한 정규식에 test를 거듭 부르면 답이 번갈아 나옵니다. const r = /a/g; r.test('aa')는 true(lastIndex 1), 다시 true(lastIndex 2), 그다음 false(lastIndex가 0으로 초기화)입니다. 함수 안에서 정규식을 새로 만들거나, 예·아니오만 필요하면 g를 빼세요.
| 플래그 | 이름 | 바뀌는 것 |
|---|---|---|
g | global | 모든 일치를 찾음, replace가 전부 바꿈, test·exec가 상태를 가짐 |
i | ignoreCase | 대소문자 무시 |
m | multiline | ^와 $가 모든 줄 경계에서 맞음 |
s | dotAll | .이 줄바꿈 문자도 잡음 |
u | unicode | 코드 포인트 단위로 동작. /^.$/u.test('😀')는 true, u 없이는 이모지가 UTF-16 두 단위라서 false. \p{...}와 \u{1F600}를 켬 |
v | unicodeSets | u의 상위 기능으로 집합 연산 지원. /[\p{L}--[a-z]]/v는 é와 맞고 a와는 맞지 않음. /[\p{L}&&\p{ASCII}]/v는 a와 맞고 é와는 맞지 않음 |
y | sticky | lastIndex 위치에서만 맞춤. /a/y.test('ba')는 false, lastIndex가 1이면 true |
d | hasIndices | match.indices를 추가. /(?<x>a)/d.exec('ba').indices.groups.x는 [1, 2] |
replace와 달러 토큰
치환 문자열에는 몇 가지 특수 표기가 있습니다. g 플래그가 없으면 첫 일치만 바뀌는데, 파이썬 re.sub와 반대입니다. 없는 번호를 가리키는 참조는 글자 그대로 남습니다. 'x'.replace(/(x)/, '$2')는 $2를 돌려주고 $0도 마찬가지입니다.
날짜 순서 바꾸기로 번호와 이름 방식을 나란히 볼 수 있습니다. '2026-10-01'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1')은 01/10/2026, '2026-10-01'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, '$<d>.$<m>.$<y>')는 01.10.2026입니다. 템플릿으로 표현할 수 없는 로직은 함수를 넘기면 되고, 마지막 인자가 groups 객체입니다.
| 토큰 | 삽입되는 것 |
|---|---|
$1 ~ $99 | 해당 번호 캡처 그룹의 텍스트 |
$<name> | 해당 이름 그룹의 텍스트 |
$& | 일치한 전체. 'John Smith'.replace(/(\w+) (\w+)/, '$2, $1 [$&]')는 Smith, John [John Smith] |
$$ | 달러 기호 자체 |
| 달러와 백틱, 달러와 작은따옴표 | 일치 앞의 텍스트, 일치 뒤의 텍스트 |
"2026-10-01".replace(
/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
(...args) => {
const g = args.at(-1); // 마지막 인자가 groups 객체
return g.d + "/" + g.m + "/" + g.y;
}
); // "01/10/2026"실무에서 쓸 만한 패턴
정규식은 모양을 검사하는 데는 좋지만 의미를 검사하는 데는 약합니다. 아래 ISO 날짜 패턴은 2026-10-01을 받고 2026-13-01과 2026-1-1을 거부하지만, 2026-02-31도 받아 줍니다. 달마다 며칠인지는 정규식이 풀 문제가 아니기 때문입니다. 모양 검사 뒤에 Date나 날짜 라이브러리로 파싱하세요.
16진 색상 패턴은 #fff와 #FFAA00을 받고 #ffaa0, #ggg, # 없는 값을 거부합니다. 유의적 버전(semver) 패턴은 semver.org에 게시된 것으로, 1.2.3, 1.0.0-rc.1, 1.2.3-beta.1+build.5를 받고 01.2.3, 1.2, v1.2.3을 거부합니다. 검증이 아니라 글 속에서 버전을 찾으려면 release v2.14.0 shipped에 /\bv?(\d+)\.(\d+)\.(\d+)\b/를 써서 2, 14, 0을 캡처합니다.
공백 제거는 내장 trim()이 낫습니다. 정규식은 줄 단위 작업에서 값을 합니다. 'a\n b\n'.replace(/^[ \t]+|[ \t]+$/gm, '')는 모든 줄의 앞뒤 공백을 지우고 줄바꿈은 남깁니다.
// ISO 8601 날짜, 모양만 검사
/^(?<y>\d{4})-(?<m>0[1-9]|1[0-2])-(?<d>0[1-9]|[12]\d|3[01])$/
// 16진 색상: #rgb 또는 #rrggbb (알파가 있는 4자리·8자리 형태 포함 버전도 아래에)
/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i
/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i
// 유의적 버전 (semver.org)
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
// 양끝 공백 제거
/^\s+|\s+$/g한국 휴대전화 번호와 한글 검사
휴대전화 번호 모양은 /^01[016789]-\d{3,4}-\d{4}$/로 검사합니다. 010-1234-5678, 011-123-4567, 010-123-4567은 통과하고 02-123-4567, 하이픈이 없는 01012345678, 010-12345-678은 거부합니다. 하이픈 유무를 모두 허용하되 한 번호 안에서는 일관되게 하려면 역참조를 씁니다. /^01[016789]([-\s]?)\d{3,4}\1\d{4}$/는 010-1234-5678, 010 1234 5678, 01012345678을 받고 010-12345678을 거부합니다. 번호가 실제로 존재하는지가 아니라 모양만 보는 검사입니다.
붙여 쓴 번호에 하이픈을 넣을 때는 '01012345678'.replace(/^(01[016789])(\d{3,4})(\d{4})$/, '$1-$2-$3')이 010-1234-5678을 돌려줍니다. 0111234567은 탐욕적인 \d{3,4}가 한 자리를 되돌려 주면서 011-123-4567이 됩니다. 가운데 네 자리를 가릴 때는 '010-1234-5678'.replace(/^(\d{3})-(\d{4})-(\d{4})$/, '$1-****-$3')으로 010-****-5678을 얻습니다.
한글만 있는지 검사할 때는 u 플래그와 \p{Script=Hangul}이 가장 정확합니다. /^\p{Script=Hangul}+$/u는 한글을 받고 한글a와 한 글(공백 포함)을 거부합니다. 공백을 허용하려면 /^[\p{Script=Hangul}\s]+$/u입니다. [가-힣]은 완성형 음절 U+AC00부터 U+D7A3까지만이어서 결과가 다릅니다. 자모 ㅎㅏㄴ은 [가-힣]과 맞지 않지만 \p{Script=Hangul}과는 맞습니다. ㅋㅋㅋ도 맞습니다. 반대로 한을 NFD로 분해한 3글자는 \p{Script=Hangul}과는 맞고 [가-힣]과는 맞지 않습니다. 완성형 음절만 허용할 것인지, 자모까지 한글로 볼 것인지를 먼저 정하고 패턴을 고르세요. 글 속에서 한글 덩어리를 뽑을 때는 '안녕하세요 hello'.match(/\p{Script=Hangul}+/gu)가 ['안녕하세요']를 돌려줍니다.
치명적 백트래킹: 실측 예제
백트래킹 엔진은 한 가지 방법으로 맞춰 보다가 나머지가 실패하면 되돌아가 다른 방법을 시도합니다. 같은 텍스트를 나누는 방법이 둘 이상이면 시도 횟수가 곱으로 늘어납니다. 대표적인 경우가 수량자 안의 수량자입니다. /^(a+)+$/를 aaaa...ab에 적용하면, 마지막 b 때문에 모든 시도가 실패하고 엔진은 a를 안쪽 반복과 바깥 반복에 나누는 모든 방법을 탐색합니다.
Node 24를 쓴 한 대의 컴퓨터에서 측정한 결과, test 호출 한 번은 a 22개에서 19ms, 24개에서 82ms, 26개에서 327ms, 28개에서 1,424ms, 30개에서 5,467ms가 걸렸습니다. 두 글자가 늘 때마다 4배가 되니 글자 하나마다 두 배입니다. 고친 /^a+$/는 같은 문자열을 몇 천분의 1ms 만에 거부했습니다. 절대 시간은 기기마다 다르지만 두 배씩 느는 성질은 같습니다.
조심할 모양은 안쪽이 같은 텍스트를 여러 방식으로 맞출 수 있는, 수량자가 붙은 그룹입니다. (a+)+, (\w+\s?)*, 겹치는 선택지 (a|a)+(28글자에서 2.3초)가 그렇습니다. 해법은 텍스트의 각 부분이 한 가지로만 맞게 만드는 것입니다. (a+)+ 대신 a+, (\w+\s?)* 대신 \w+(?:\s\w+)*\s?로 바꾸면, 글자 25개 뒤에 !가 오는 입력에서 0.05ms에 끝났습니다.
- 중첩된 수량자와 겹치는 선택지를 없애고, 입력 전체를 뜻한다면 앵커를 붙이세요.
- 매칭 전에 입력 길이를 제한하고, 사용자가 준 정규식을 서버의 요청 스레드에서 실행하지 마세요.
- 룩어헤드와 역참조를 합친
(?=(a+))\1은 원자 그룹처럼 동작합니다. 룩어헤드가 한번 성공하면 엔진이 그 안으로 되돌아갈 수 없기 때문입니다. - 이 사이트의 정규식 테스터는 매칭을 Web Worker에서 돌리고 1.5초 뒤에 멈추므로, 폭주하는 패턴이 화면을 얼리는 대신 오류로 표시됩니다.
PCRE·파이썬과 다른 점
PCRE(PHP, Perl, grep -P)나 파이썬 답변에서 복사한 패턴은 자바스크립트에서 자주 실패합니다. 아래 차이는 자바스크립트 열을 모두 Node 24에서 확인했습니다. PCRE와 파이썬 열은 그 엔진들의 오래된 문서화된 동작입니다.
| 항목 | 자바스크립트 | PCRE / 파이썬 re |
|---|---|---|
| 인라인 플래그 | (?i)는 문법 오류, 범위 지정 (?i:...)는 최신 엔진에서만 동작 | 둘 다 (?i) 지원 |
소유 수량자 a++, 원자 그룹 (?>...) | 문법 오류(룩어헤드와 역참조로 흉내 냄) | PCRE는 지원, 파이썬은 3.11부터 |
| 이름 그룹 문법 | (?<n>...), \k<n>, $<n> | 파이썬은 (?P<n>...), (?P=n), \g<n>, PCRE는 두 방식 모두 허용 |
| 입력의 시작과 끝 | \A, \Z 없음, m 없는 ^와 $ 사용 | 둘 다 \A, \Z 있음 |
끝 줄바꿈 앞의 $ | 맞지 않음(/^\d+$/.test('12\n')은 false) | 기본적으로 마지막 줄바꿈 앞에서 맞음 |
\d, \w | 항상 ASCII | 파이썬 3의 str 패턴은 기본이 유니코드 |
| 유니코드 속성 | u 또는 v와 \p{...} | PCRE는 지원, 파이썬 re에는 \p 없음 |
| 룩비하인드 길이 | 가변 길이 허용 | 파이썬 re는 고정 폭 필요 |
| 기본 치환 범위 | 첫 번째만, 전부 바꾸려면 g 필요 | 파이썬 re.sub는 전부 치환, 참조는 \1이나 \g<n> |
| Verbose, 재귀 | x 플래그와 (?R) 없음 | PCRE는 둘 다 지원, 파이썬은 re.VERBOSE |