정규표현식(Regular Expression, Regex)은 처음 보면 암호 같지만, 익히고 나면 텍스트 처리의 강력한 무기가 됩니다. 이메일 유효성 검사, 전화번호 파싱, 로그 분석, 코드 리팩토링 등 수많은 곳에서 매일 쓰입니다.

기본 문법

// 리터럴 표기법
const pattern = /hello/;

// 생성자 표기법 (동적 패턴)
const pattern2 = new RegExp('hello');

// 플래그
/hello/i   // i: 대소문자 무시
/hello/g   // g: 전체 검색 (모든 매칭)
/hello/m   // m: 여러 줄 모드 (^, $가 각 줄에 적용)
/hello/s   // s: . 이 줄바꿈도 포함
/hello/gi  // 여러 플래그 조합

문자 클래스와 수량자

// 문자 클래스
/[abc]/    // a, b, c 중 하나
/[^abc]/   // a, b, c 를 제외한 문자
/[a-z]/    // 소문자 알파벳
/[0-9]/    // 숫자 (= \d)
/\d/       // 숫자
/\D/       // 숫자가 아닌 문자
/\w/       // 단어 문자 [a-zA-Z0-9_]
/\W/       // 단어 문자가 아닌 것
/\s/       // 공백 (스페이스, 탭, 줄바꿈)
/\S/       // 공백이 아닌 문자
/./        // 줄바꿈 제외 모든 문자

// 수량자
/a?/       // 0 또는 1개
/a*/       // 0개 이상
/a+/       // 1개 이상
/a{3}/     // 정확히 3개
/a{2,4}/   // 2~4개
/a{2,}/    // 2개 이상
/a+?/      // 최소 매칭 (게으른 수량자)

앵커와 그룹

// 앵커
/^hello/   // 문자열 시작
/hello$/   // 문자열 끝
/\bhello\b/ // 단어 경계

// 그룹
/(ab)+/    // 캡처 그룹: ab 반복
/(?:ab)+/  // 비캡처 그룹: 추출 불필요할 때
/(?<year>\d{4})/  // 명명된 캡처 그룹

// 예시: 날짜 파싱
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2026-04-07'.match(dateRe);
console.log(match.groups); // { year: '2026', month: '04', day: '07' }

JavaScript에서 사용하기

const str = 'Hello, World! Hello, Regex!';

// test — 매칭 여부 (boolean)
/hello/i.test(str);  // true

// match — 매칭 결과 배열
str.match(/hello/i);   // ['Hello', index: 0, ...]
str.match(/hello/gi);  // ['Hello', 'Hello']

// matchAll — 모든 매칭 반복자
for (const m of str.matchAll(/hello/gi)) {
  console.log(m[0], m.index);
}

// replace — 치환
str.replace(/hello/i, 'Hi');      // 'Hi, World! Hello, Regex!'
str.replace(/hello/gi, 'Hi');     // 'Hi, World! Hi, Regex!'

// replace with 함수
str.replace(/hello/gi, (match) => match.toUpperCase());

// split
'a,b,,c'.split(/,+/);  // ['a', 'b', 'c']

실무 패턴 모음

// 이메일 검증
const emailRe = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
emailRe.test('user@devlog.kr');  // true

// 한국 전화번호
const phoneRe = /^(010|011|016|017|018|019)-?\d{3,4}-?\d{4}$/;
phoneRe.test('010-1234-5678');  // true

// URL 파싱
const urlRe = /^(https?):\/\/([^/\s]+)(\/[^\s]*)?(\?[^\s#]*)?(#\S*)?$/;
const [, protocol, host, path] = 'https://devlog.kr/posts/regex'.match(urlRe);

// 비밀번호 검증 (8자 이상, 대/소문자, 숫자, 특수문자 포함)
const pwRe = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%]).{8,}$/;

// 16진수 색상 코드
const hexRe = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
hexRe.test('#6891f8');  // true

// 마크다운 제목 파싱
const headingRe = /^(#{1,6})\s+(.+)$/m;
const [, level, title] = '## 정규표현식이란'.match(headingRe);
// level: '##', title: '정규표현식이란'

Lookahead & Lookbehind

// Positive Lookahead (?=...) — 뒤에 이게 있을 때
/\d+(?=원)/  // '1000원'에서 '1000' 매칭

// Negative Lookahead (?!...) — 뒤에 이게 없을 때
/\d+(?!원)/  // '1000$'에서 매칭

// Positive Lookbehind (?<=...) — 앞에 이게 있을 때
/(?<=\$)\d+/  // '$100'에서 '100' 매칭

// 숫자 세 자리마다 콤마 삽입
function addCommas(n) {
  return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
addCommas(1234567);  // '1,234,567'

성능과 보안 — 주의해야 할 패턴

정규표현식은 잘못 작성하면 심각한 성능 문제를 일으킬 수 있습니다. 특히 ReDoS(Regular Expression Denial of Service)라고 불리는 취약점은 특정 입력값에 대해 처리 시간이 기하급수적으로 늘어나는 패턴에서 발생합니다.

  • 위험한 패턴(a+)+, ([a-zA-Z]+)*처럼 중첩된 수량자가 있는 패턴은 입력 길이에 따라 처리 시간이 폭발적으로 증가할 수 있습니다.
  • 입력 길이 제한 — 사용자 입력을 정규표현식으로 검증할 때는 미리 입력 길이를 제한하세요. if (input.length > 100) return false;처럼 간단한 검사만으로도 대부분의 ReDoS를 예방할 수 있습니다.
  • 라이브러리 활용 — 이메일, URL, 신용카드 번호처럼 표준이 복잡한 형식은 직접 정규표현식을 작성하지 말고 validator.js 같은 검증된 라이브러리를 사용하는 것이 안전합니다.

정규표현식이 어려운 이유와 대안

정규표현식은 강력하지만 코드 가독성을 크게 해칩니다. 팀원이 수정해야 할 때마다 처음부터 다시 해석해야 하는 경우가 많습니다. 특히 복잡한 패턴은 6개월 후 본인도 이해하기 어렵습니다. 이런 경우에는 단계별로 문자열 메서드(split, includes, startsWith)를 조합하거나, 정규표현식에 주석을 붙이는 verbose 모드를 지원하는 언어(Python의 re.VERBOSE)를 활용할 수 있습니다. JavaScript에서는 긴 정규표현식을 변수로 분리하고 의미를 설명하는 주석을 달아두는 것이 최선입니다.

실전 팁
regex101.com 또는 regexr.com에서 실시간으로 패턴을 테스트하세요. 복잡한 정규표현식은 주석과 함께 명명된 그룹을 활용해 가독성을 높이세요. 이메일이나 URL 검증처럼 복잡한 케이스는 battle-tested 라이브러리(validator.js 등)를 사용하는 것이 안전하고, 중첩 수량자 패턴은 ReDoS 취약점을 유발할 수 있으므로 반드시 피하세요.

명명된 캡처 그룹으로 가독성 높이기

숫자 인덱스(match[1], match[2]) 대신 이름으로 그룹을 참조하면 코드 가독성이 크게 향상됩니다.

// ❌ 숫자 인덱스 참조 — 의미 파악이 어려움
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const m = '2026-01-15'.match(dateRegex);
console.log(m[1], m[2], m[3]); // 4개 중 어떤 게 뭔지 불명확

// ✅ 명명된 그룹 — 의미가 명확
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { groups: { year, month, day } } = '2026-01-15'.match(dateRegex);
console.log(year, month, day); // 2026 01 15

// URL 파싱 예시
const urlPattern = /(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?/;
const { groups } = 'https://example.com/blog/post'.match(urlPattern);
// groups.protocol = 'https'
// groups.domain = 'example.com'
// groups.path = '/blog/post'

자주 쓰는 실전 정규표현식 패턴

// 한국 휴대폰 번호 (010-1234-5678 또는 01012345678)
const phoneRegex = /^01[016789]-?\d{3,4}-?\d{4}$/;

// 한국 주민등록번호 형식 검사 (실제 유효성은 추가 로직 필요)
const rrnRegex = /^\d{6}-[1-4]\d{6}$/;

// URL 슬러그 (소문자, 숫자, 하이픈만 허용)
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

// 비밀번호 강도 (8자 이상, 대소문자+숫자+특수문자 각 1개 이상)
const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;

// HTML 태그 제거
const stripHtmlRegex = /<[^>]*>/g;
const plainText = htmlString.replace(stripHtmlRegex, '');

String.matchAll — 여러 매치 처리

정규표현식의 모든 매치를 처리할 때 matchAllexec보다 훨씬 편리합니다.

const text = '가격: ₩10,000 / ₩25,000 / ₩50,000';
const pricePattern = /₩([\d,]+)/g;

// matchAll — 모든 매치를 한 번에
const matches = [...text.matchAll(pricePattern)];
const prices = matches.map(m => parseInt(m[1].replace(',', '')));
console.log(prices); // [10000, 25000, 50000]

// 명명된 그룹과 함께 사용
const logPattern = /\[(?<level>INFO|WARN|ERROR)\] (?<message>.+)/g;
for (const match of logText.matchAll(logPattern)) {
  const { level, message } = match.groups;
  if (level === 'ERROR') alertTeam(message);
}

정규식을 언제 쓰고 언제 쓰지 말아야 하는가

정규식은 강력한 도구이지만, 모든 문자열 처리에 적합한 것은 아닙니다. 정규식을 쓰기 좋은 경우는 패턴이 명확하고 반복적으로 나타나는 문자열을 검색하거나 치환할 때입니다. 로그 파일에서 에러 패턴 추출, 폼 입력값 형식 검증, 코드에서 특정 함수 호출 패턴 검색 같은 작업이 여기에 해당합니다. 반면 정규식을 피해야 하는 경우도 있습니다. HTML 파싱이 대표적입니다. HTML의 중첩 구조와 다양한 예외 케이스를 정규식으로 다루려 하면 복잡성이 폭발적으로 늘어나고 예상치 못한 오류가 생깁니다. HTML 파싱에는 DOMParser나 Cheerio 같은 전용 파서를 사용하는 것이 훨씬 안전합니다. JSON, XML도 마찬가지입니다. 전용 파싱 라이브러리가 있다면 정규식보다 그것을 쓰는 것이 올바른 접근입니다.

가독성도 고려해야 합니다. 정규식은 짧지만 읽기 어렵습니다. 복잡한 정규식에는 반드시 설명 주석을 달고, 여러 부분으로 나눠 명명된 상수로 관리하는 것이 유지보수에 유리합니다. 팀 내에서 정규식을 잘 모르는 팀원도 이해할 수 있도록 간단한 언어로 무엇을 검증하는 패턴인지 설명을 남겨두는 습관이 중요합니다. 그리고 작성한 정규식은 반드시 엣지 케이스를 포함한 단위 테스트로 검증해야 합니다. 특정 입력에만 통과하고 실제 다양한 입력에서 오작동하는 정규식이 프로덕션에 숨어있는 경우가 생각보다 많습니다.