본문으로 건너뛰기
Toova
모든 도구

모든 개발자가 알아야 할 정규표현식 트릭 10가지

Toova

정규표현식은 처음에는 난해해 보이다가 어느 순간 갑자기 이해되는 도구 중 하나입니다. 한번 이해하면 입력 검증, 로그 파싱, 검색 및 치환 파이프라인, URL 라우팅 등 모든 곳에서 보이기 시작합니다. 하지만 대부분의 개발자는 문자 클래스, 수량자, 앵커 같은 소수의 기능만 사용하고 나머지 명세는 손대지 않은 채로 둡니다.

이 가이드는 기본을 넘어선 열 가지 정규표현식 기능을 다룹니다. 각각은 더 단순한 패턴으로는 깔끔하게 처리할 수 없는 실제 문제를 해결합니다. 모든 예제는 JavaScript 문법을 사용하며, 이는 ECMAScript 호환 환경 어디서나 유효합니다.

이 문서의 모든 패턴은 설정 코드 한 줄도 작성하지 않고 Toova Regex Tester에서 테스트할 수 있습니다.

1. 룩어헤드: 소비 없이 매칭하기

룩어헤드는 매치 엔진이 해당 문자를 지나가게 하지 않으면서 현재 위치 뒤에 패턴이 따라와야 함(또는 따라오지 않아야 함)을 단언합니다. 매칭된 텍스트에는 룩어헤드가 검사하는 내용이 포함되지 않습니다.

긍정형 룩어헤드 문법: (?=...)

// 긍정형 룩어헤드: "bar"가 뒤에 올 때만 "foo" 매칭
const re1 = /foo(?=bar)/;
re1.test('foobar'); // true
re1.test('foobaz'); // false

부정형 룩어헤드 문법: (?!...)

// 부정형 룩어헤드: "bar"가 뒤에 오지 않는 "foo" 매칭
const re2 = /foo(?!bar)/;
re2.test('foobaz'); // true
re2.test('foobar'); // false

실용적인 사용 예: 통화 기호가 뒤에 올 때만 가격 숫자를 매칭하되 캡처된 값에는 기호를 포함하지 않습니다. 또는 (?=.*\d)를 사용하여 숫자가 어디에 있어야 하는지 지정하지 않고 비밀번호에 최소 하나의 숫자가 포함되어 있는지 검증합니다.

룩어헤드는 너비가 0입니다 — 문자를 소비하지 않습니다. 동일한 위치에 여러 룩어헤드를 쌓아서 여러 독립적인 조건을 동시에 적용할 수 있습니다.

2. 룩비하인드: 앞에 온 것 확인하기

룩비하인드는 룩어헤드의 거울입니다: 현재 위치 앞에 오는 텍스트를 매칭에 포함하지 않고 검사합니다.

긍정형 룩비하인드 문법: (?<=...)

// 긍정형 룩비하인드: "foo"가 앞에 올 때만 "bar" 매칭
const re3 = /(?<=foo)bar/;
re3.test('foobar'); // true
re3.test('bazbar'); // false

부정형 룩비하인드 문법: (?<!...)

// 부정형 룩비하인드: "foo"가 앞에 오지 않는 "bar" 매칭
const re4 = /(?<!foo)bar/;
re4.test('bazbar'); // true
re4.test('foobar'); // false

룩비하인드는 ECMAScript 2018에 도입되었으며 모든 현대 브라우저와 Node.js 10+에서 지원됩니다. 일반적인 사용 사례: name=Alice 같은 키-값 쌍에서 값 부분을 추출할 때, 키를 매치에 포함하지 않고 name= 뒤의 모든 것을 매칭합니다.

참고: 룩어헤드와 달리 JavaScript의 룩비하인드 표현식은 가변 길이 패턴을 포함할 수 없습니다 — 룩비하인드 표현식은 고정 또는 제한된 최대 길이를 가져야 합니다.

3. 명명된 캡처 그룹: 스스로 문서화하는 패턴

표준 캡처 그룹은 숫자로 참조됩니다: $1, $2 등. 그룹을 추가하거나 제거하면 모든 다운스트림 참조가 깨집니다. 명명된 그룹은 각 그룹에 레이블을 붙일 수 있게 하여 이 문제를 해결합니다.

const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = '2026-05-10'.match(dateRe);
console.log(m.groups.year);  // "2026"
console.log(m.groups.month); // "05"
console.log(m.groups.day);   // "10"

명명된 그룹은 $<name>을 통해 치환 문자열에서도 사용할 수 있습니다:

// 패턴 내에서 명명된 그룹을 사용한 역참조
const quoteRe = /(?<q>['"]).*?\k<q>/;
quoteRe.test('"hello"'); // true
quoteRe.test('"hello''); // false

그룹 이름은 유효한 JavaScript 식별자여야 합니다. 그룹이 캡처하는 내용을 반영하는 설명적 이름을 사용하세요 — year, port, protocol — 그러면 패턴이 거의 스스로 문서화됩니다. 위에서 보여준 것처럼 패턴 내에서 명명된 그룹을 역참조하기 위해 \k<name>도 사용할 수 있습니다.

4. 논그리디 수량자: 최소한 매칭하기

기본적으로 수량자(*, +, ?)는 그리디입니다 — 가능한 한 많은 문자를 매칭합니다. 수량자 뒤에 ?를 추가하면 논그리디(lazy 또는 reluctant라고도 함)가 되어 가능한 한 적은 문자를 매칭합니다.

const html = '<a>클릭</a>';

// 그리디(기본값) — 가능한 가장 긴 문자열 매칭
/<.+>/.exec(html)?.[0]; // '<a>클릭</a>'

// 논그리디 — 가능한 가장 짧은 문자열 매칭
/<.+?>/.exec(html)?.[0]; // '<a>'

이는 HTML, XML 또는 동일한 구분자가 여러 번 나타날 수 있는 형식을 파싱할 때 중요합니다. 그리디 버전은 전체 문자열에서 첫 번째 여는 태그부터 마지막 닫는 태그까지 모든 것을 삼킵니다. 논그리디 버전은 첫 번째 유효한 닫는 매치에서 멈춥니다.

동일한 것이 +?(하나 이상, lazy)와 ??(0개 또는 1개, lazy)에도 적용됩니다. 논그리디 수량자는 무엇을 매칭할 수 있는지를 바꾸지 않습니다 — 여러 옵션이 존재할 때 어떤 유효한 매치가 선택되는지를 바꿉니다.

5. 재앙적 백트래킹 피하기

백트래킹은 정규표현식 엔진이 실패한 매치 시도에서 복구하는 방식입니다 — 패턴을 통한 다른 경로를 시도합니다. 대부분의 경우 이는 보이지 않고 빠릅니다. 하지만 특정 패턴은 엔진이 기하급수적으로 증가하는 경로 수를 탐색하게 하여 적당한 크기의 입력 문자열에 대해서도 Node.js 프로세스를 무릎 꿇게 만들 수 있습니다.

전형적인 위험 패턴은 aaaaab 같은 문자열에 적용된 (a+)+처럼 중첩된 수량자입니다. 엔진은 매치가 없다고 결론짓기 전에 내부 그룹과 외부 그룹 사이에서 a 문자를 나누는 모든 가능한 방법을 시도합니다.

원자 그룹((?>...))은 그룹이 매칭된 후 엔진에 백트래킹하지 말라고 지시하여 이를 방지합니다. JavaScript는 원자 그룹을 기본 지원하지 않지만 룩어헤드로 소유적 동작을 에뮬레이션할 수 있습니다:

// 원자 그룹 없이 — 엔진이 (\d+)에서 백트래킹
// 원자 그룹 사용 — (\d+)가 매칭되면 백트래킹 불가
// JavaScript는 원자 그룹을 기본 지원하지 않지만,
// 룩어헤드 트릭으로 에뮬레이션 가능:
const re5 = /(?=(\d+))\1(?!\d)/; // \d++ 소유 수량자 에뮬레이션

더 안전한 경험 법칙: 특별한 이유가 없는 한 다른 수량자 안에 직접 중첩된 수량자를 피하세요. 매칭하는 것에 대해 더 정확하도록 패턴을 다시 작성하세요. 또한 리팩토링할 때 두 등가 패턴의 출력을 나란히 비교하기 위해 Text Diff 도구를 사용할 수도 있습니다.

6. 문자 클래스 집합 연산(유니코드 v 플래그)

ECMAScript 2024는 문자 클래스 내 집합 연산을 활성화하는 v 플래그를 도입했습니다. 이를 통해 다루기 힘든 교대 대신 깔끔한 클래스 정의로 "모음을 제외한 모든 글자" 또는 "ASCII이기도 한 대문자"를 표현할 수 있습니다.

// POSIX 문자 클래스 빼기는 JS에 없지만,
// Unicode sets 모드(`v` 플래그)는 집합 연산을 추가합니다:
const lettersNoVowels = /[a-z--[aeiou]]/v;
lettersNoVowels.test('b'); // true
lettersNoVowels.test('e'); // false

v 플래그는 문자 클래스 내에서 세 가지 연산을 지원합니다:

  • 빼기: [A--B] — A에 있지만 B에 없는 문자
  • 교집합: [A&&B] — A와 B 모두에 있는 문자
  • 합집합: [AB] — A 또는 B에 있는 문자(표준 문자 클래스와 동일)

Node.js 20+와 모든 에버그린 브라우저는 v 플래그를 지원합니다. 이는 u 플래그의 상위 집합입니다 — 둘을 결합하지 말고 그 기능이 필요할 때 v만 사용하세요.

7. 단어 경계: 전체 단어 매칭

\b 앵커는 단어 문자(\w)와 비단어 문자 사이의 위치를 매칭합니다. 문자를 소비하지 않습니다 — 위치만 단언합니다. 그 역인 \B는 단어 경계가 아닌 모든 위치를 매칭합니다.

const sentence = 'cat concatenate';

// \b 없이 — "concatenate" 안의 "cat"도 발견됨
/cat/g.exec(sentence); // "cat"과 "concatenate" 안의 "cat" 모두 매칭

// \b 사용 — 단독 단어 "cat"만
/\bcat\b/g.exec(sentence); // 독립된 "cat"만 매칭
// \B는 반대: 단어 경계가 아닌 단어 내부 매칭
/\Bcat\B/.test('concatenate'); // true — "cat"이 단어 안에 있음

단어 경계는 코드나 산문에서 식별자를 검색할 때 필수적입니다. 이것 없이는 id라는 변수를 검색하면 indexOf, invalid, grid도 적중합니다. \bterm\b를 사용하여 매치를 독립된 발생으로 제한하세요.

중요한 주의 사항: \b는 JavaScript의 단어 문자 정의([a-zA-Z0-9_])를 사용합니다. 악센트 문자와 비라틴 문자는 비단어 문자로 취급됩니다. 유니코드 인식 단어 경계를 위해서는 v 플래그를 유니코드 속성 클래스와 결합하세요.

8. 멀티라인 모드: 라인별 앵커

기본적으로 ^는 문자열의 맨 시작만 매칭하고 $는 맨 끝만 매칭합니다. m(멀티라인) 플래그는 이를 변경합니다: ^는 각 라인의 시작을 매칭하고 $는 각 라인의 끝을 매칭합니다.

const text = '라인 하나\n라인 둘\n라인 셋';

// m 플래그 없이 — ^는 전체 문자열의 시작만 매칭
/^라인/.test(text); // true (첫 줄만)

// m 플래그 사용 — ^는 각 라인의 시작 매칭
const matches = text.match(/^라인/gm);
console.log(matches); // ['라인', '라인', '라인']

이는 로그 파일, 설정 파일 또는 코드 같은 멀티라인 텍스트를 처리할 때 필수적입니다. 일반적인 용도로는 키워드로 시작하는 라인 추출, 줄 끝 토큰 치환, 블록의 각 라인이 패턴과 일치하는지 검증 등이 있습니다.

m 플래그를 s 플래그(dotAll)와 혼동하지 마세요. s 플래그는 .이 줄바꿈 문자도 매칭하게 만듭니다. m 플래그는 .에 전혀 영향을 주지 않습니다 — ^$의 동작에만 영향을 줍니다.

9. 유니코드 속성 이스케이프: 국제 문자 매칭

u 플래그는 유니코드 카테고리, 스크립트 또는 기타 속성을 기준으로 문자를 매칭할 수 있게 하는 유니코드 속성 이스케이프를 활성화합니다. 이는 ASCII뿐만 아니라 모든 인간 표기 체계에서 글자, 숫자 또는 구두점을 매칭하는 올바른 방법입니다.

// u 플래그는 유니코드 속성 이스케이프 활성화
const letters = /\p{L}+/u;
letters.test('Héllo');   // true
letters.test('你好');    // true
letters.test('12345');   // false

// 모든 스크립트의 대문자만 매칭
const upper = /\p{Lu}+/u;
upper.test('ABC');  // true
upper.test('abc');  // false
// 이모지 매칭(유니코드 일반 카테고리: Symbol, Other)
const emoji = /\p{So}/u;
emoji.test('🚀'); // true

가장 일반적으로 사용되는 유니코드 속성은 다음과 같습니다:

  • \p{L} — 모든 글자(모든 스크립트)
  • \p{Lu} — 대문자
  • \p{Ll} — 소문자
  • \p{N} — 모든 숫자
  • \p{Nd} — 십진 숫자
  • \p{P} — 구두점
  • \p{Script=Latin} — 라틴 스크립트 문자
  • \p{Emoji} — 이모지 문자

\P{...}(대문자 P)를 사용하여 부정하세요 — 지정된 속성을 갖지 않은 모든 것을 매칭합니다. 지원되는 유니코드 속성과 그 값의 전체 목록은 MDN 문서에서 관리됩니다.

10. 문자열 조립을 통한 자세한 패턴

많은 정규표현식 변형(Python, Ruby, .NET, PCRE)은 패턴 내부에 공백과 주석을 허용하는 자세한 또는 확장 모드(x 플래그)를 지원합니다. JavaScript에는 이 플래그가 없습니다 — x 플래그는 ECMAScript에서 유효하지 않습니다.

표준 우회 방법은 명명된 문자열 상수에서 패턴을 조립하고 new RegExp()로 결합하는 것입니다:

// JavaScript에는 기본 x 플래그가 없지만,
// 명명된 문자열 상수로 가독성 있는 패턴을 구성할 수 있습니다:
const YEAR  = '(?<year>\\d{4})';
const SEP   = '-';
const MONTH = '(?<month>\\d{2})';
const DAY   = '(?<day>\\d{2})';
const datePattern = new RegExp(YEAR + SEP + MONTH + SEP + DAY);

각 상수는 매칭하는 내용을 설명하며 최종 패턴은 문장처럼 읽힙니다. 이 접근 방식은 코드베이스의 여러 정규표현식 정의에 걸쳐 공유 하위 패턴을 구성하는 것과 각 컴포넌트를 독립적으로 단위 테스트하는 것도 쉽게 만듭니다.

덜 복잡한 패턴의 경우 전체 정규표현식을 한 줄에 유지하고 근처 블록 주석에 인라인 주석을 다는 것으로 충분합니다. 목표는 다음 개발자(미래의 당신 포함)가 디코더를 통해 실행하지 않고도 패턴을 이해할 수 있도록 보장하는 것입니다.

모두 종합하기

이 열 가지 기법은 "왜 이 정규표현식이 엣지 케이스에서 실패할까?" 영역의 상당 부분을 다룹니다. 룩어헤드와 룩비하인드는 소비 없이 컨텍스트를 단언할 수 있게 합니다. 명명된 그룹은 리팩토링 전반에 걸쳐 패턴을 가독성 있게 유지합니다. 논그리디 수량자는 우연한 과도한 매칭을 방지합니다. 유니코드 속성 이스케이프는 ASCII를 넘어선 입력을 처리합니다.

유창성을 기르는 가장 좋은 방법은 실제 데이터에 대해 실제 패턴을 실험하는 것입니다. 빠르게 반복하기 위해 Regex Tester를 사용하세요. 패턴이 차이 비교나 정리가 필요한 출력을 생성할 때 Text Diff 도구는 실행 간에 무엇이 변경되었는지 정확히 보여줍니다. 정규표현식이 JSON을 파싱하는 경우 JSON Formatter를 사용하면 브라우저를 벗어나지 않고 구조화된 결과를 검사할 수 있습니다.

모든 JavaScript 정규표현식 문법과 플래그의 포괄적인 참조를 위해 MDN Regex Cheatsheet가 북마크할 가장 좋은 단일 페이지입니다. 전체 매치 시각화로 대화식 패턴 디버깅을 위해 regex101.com은 패턴의 모든 구성 요소에 대한 내장 설명이 있는 JavaScript 모드를 지원합니다.