개요
정규표현식(Regular Expression, RegExp)은 문자열에서 특정 패턴을 찾고, 추출하고, 변환하는 강력한 도구입니다. 이메일 유효성 검사, URL 파싱, 텍스트 검색/치환 등 문자열 처리가 필요한 거의 모든 곳에서 정규표현식이 활용됩니다.
JavaScript는 정규표현식을 1급 객체로 지원하며, 리터럴 문법과 다양한 문자열/정규표현식 메서드를 제공합니다. ES2018 이후로는 명명된 캡처 그룹, 후방탐색, 유니코드 프로퍼티 이스케이프 등 강력한 기능이 추가되어 더욱 정교한 패턴 매칭이 가능해졌습니다.
학습 목표
- 정규표현식의 기본 문법과 생성 방법 이해
- 메타문자, 문자 클래스, 수량자 완벽 마스터
- 그룹, 캡처, 전방탐색/후방탐색 활용법 습득
- 실전 유효성 검사 패턴 구현
- 정규표현식 성능 최적화 기법 학습
사전 지식
정규표현식 생성 방법
JavaScript에서 정규표현식을 생성하는 방법은 두 가지입니다.
리터럴 방식
슬래시(/)로 패턴을 감싸는 방식으로, 가장 일반적으로 사용됩니다.
1
2
3
4
5
6
7
8
| // 기본 형태: /패턴/플래그
const regex1 = /hello/; // 'hello' 문자열 매칭
const regex2 = /hello/i; // 대소문자 무시
const regex3 = /hello/gi; // 전역 검색 + 대소문자 무시
// 리터럴은 스크립트 로드 시 컴파일됨
console.log(regex1.test('hello world')); // true
console.log(regex2.test('Hello World')); // true
|
RegExp 생성자 방식
동적으로 패턴을 생성해야 할 때 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 문자열로 패턴 전달
const pattern = 'hello';
const regex1 = new RegExp(pattern);
const regex2 = new RegExp(pattern, 'gi');
// 동적 패턴 생성
function createSearchRegex(searchTerm) {
// 특수문자 이스케이프 필요
const escaped = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(escaped, 'gi');
}
const userInput = 'hello (world)';
const dynamicRegex = createSearchRegex(userInput);
console.log(dynamicRegex); // /hello \(world\)/gi
|
리터럴 vs 생성자 비교
1
2
3
4
5
6
7
8
9
10
11
| // 리터럴 방식의 이스케이프
const regex1 = /\d+/; // 숫자 매칭
// 생성자 방식의 이스케이프 (역슬래시 두 번)
const regex2 = new RegExp('\\d+');
console.log(regex1.source === regex2.source); // true
// 언제 무엇을 사용할까?
// - 리터럴: 패턴이 고정된 경우 (권장)
// - 생성자: 사용자 입력, 변수 기반 동적 패턴
|
메타문자와 특수문자
메타문자는 정규표현식에서 특별한 의미를 가지는 문자들입니다.
기본 메타문자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // . (점) - 줄바꿈 제외 모든 문자 1개 매칭
console.log(/a.c/.test('abc')); // true
console.log(/a.c/.test('aXc')); // true
console.log(/a.c/.test('ac')); // false (문자 없음)
// ^ - 문자열/줄의 시작
console.log(/^hello/.test('hello world')); // true
console.log(/^hello/.test('say hello')); // false
// $ - 문자열/줄의 끝
console.log(/world$/.test('hello world')); // true
console.log(/world$/.test('world peace')); // false
// | - OR 연산자
console.log(/cat|dog/.test('I have a cat')); // true
console.log(/cat|dog/.test('I have a dog')); // true
console.log(/cat|dog/.test('I have a bird')); // false
|
문자 클래스 []
대괄호 안의 문자 중 하나와 매칭됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 문자 집합
console.log(/[abc]/.test('apple')); // true (a 매칭)
console.log(/[abc]/.test('banana')); // true (a, b 매칭)
console.log(/[abc]/.test('xyz')); // false
// 범위 지정
console.log(/[a-z]/.test('hello')); // true (소문자)
console.log(/[A-Z]/.test('Hello')); // true (대문자)
console.log(/[0-9]/.test('abc123')); // true (숫자)
console.log(/[a-zA-Z0-9]/.test('Test1')); // true (영숫자)
// 부정 문자 클래스 [^...]
console.log(/[^0-9]/.test('123')); // false (숫자만 있음)
console.log(/[^0-9]/.test('12a3')); // true (숫자 아닌 문자 있음)
// 특수문자는 [] 안에서 대부분 리터럴로 취급
console.log(/[.+*?]/.test('3+4')); // true (+ 매칭)
console.log(/[$^]/.test('$100')); // true ($ 매칭)
|
이스케이프 문자
특수문자를 리터럴로 매칭하려면 역슬래시(\)로 이스케이프합니다.
1
2
3
4
5
6
7
8
9
10
| // 메타문자 이스케이프
console.log(/3\.14/.test('3.14')); // true
console.log(/3\.14/.test('3X14')); // false
console.log(/\$100/.test('$100')); // true
console.log(/\(hello\)/.test('(hello)')); // true
// 자주 이스케이프하는 문자: . * + ? ^ $ { } [ ] ( ) | \ /
const priceRegex = /\$\d+\.\d{2}/;
console.log(priceRegex.test('$19.99')); // true
console.log(priceRegex.test('$5.00')); // true
|
문자 클래스 단축형
자주 사용되는 문자 집합에 대한 단축 표기법입니다.
기본 문자 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // \d - 숫자 [0-9]
console.log(/\d/.test('abc123')); // true
console.log(/\d/.test('abcdef')); // false
// \D - 숫자가 아닌 문자 [^0-9]
console.log(/\D/.test('123')); // false
console.log(/\D/.test('12a3')); // true
// \w - 단어 문자 [a-zA-Z0-9_]
console.log(/\w/.test('hello')); // true
console.log(/\w/.test('_var')); // true
console.log(/\w/.test('$$$')); // false
// \W - 단어 문자가 아닌 것 [^a-zA-Z0-9_]
console.log(/\W/.test('hello')); // false
console.log(/\W/.test('hello!')); // true
// \s - 공백 문자 (스페이스, 탭, 줄바꿈 등)
console.log(/\s/.test('hello world')); // true
console.log(/\s/.test('helloworld')); // false
// \S - 공백이 아닌 문자
console.log(/\S/.test(' ')); // false
console.log(/\S/.test(' a ')); // true
|
경계 매칭
1
2
3
4
5
6
7
8
9
10
11
12
13
| // \b - 단어 경계
console.log(/\bcat\b/.test('cat')); // true
console.log(/\bcat\b/.test('the cat sat')); // true
console.log(/\bcat\b/.test('category')); // false (cat이 단어 경계에 없음)
console.log(/\bcat\b/.test('tomcat')); // false
// \B - 단어 경계가 아닌 곳
console.log(/\Bcat/.test('tomcat')); // true (cat 앞이 경계가 아님)
console.log(/cat\B/.test('category')); // true (cat 뒤가 경계가 아님)
// 실용적 예제: 독립된 단어만 찾기
const text = 'I love JavaScript, not Java or JavaBeans';
console.log(text.match(/\bJava\b/g)); // ['Java']
|
수량자 (Quantifiers)
패턴의 반복 횟수를 지정합니다.
기본 수량자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // * - 0회 이상
console.log(/ab*c/.test('ac')); // true (b 0개)
console.log(/ab*c/.test('abc')); // true (b 1개)
console.log(/ab*c/.test('abbbc')); // true (b 3개)
// + - 1회 이상
console.log(/ab+c/.test('ac')); // false (b 0개는 불일치)
console.log(/ab+c/.test('abc')); // true
console.log(/ab+c/.test('abbbc')); // true
// ? - 0회 또는 1회
console.log(/colou?r/.test('color')); // true
console.log(/colou?r/.test('colour')); // true
console.log(/colou?r/.test('colouur')); // false
// 선택적 그룹
console.log(/https?:\/\//.test('http://')); // true
console.log(/https?:\/\//.test('https://')); // true
|
정확한 횟수 지정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // {n} - 정확히 n회
console.log(/\d{4}/.test('2024')); // true
console.log(/\d{4}/.test('123')); // false
// {n,} - n회 이상
console.log(/\d{2,}/.test('1')); // false
console.log(/\d{2,}/.test('12')); // true
console.log(/\d{2,}/.test('12345')); // true
// {n,m} - n회 이상 m회 이하
console.log(/\d{2,4}/.test('1')); // false
console.log(/\d{2,4}/.test('12')); // true
console.log(/\d{2,4}/.test('1234')); // true
console.log(/\d{2,4}/.test('12345')); // true (1234 부분 매칭)
// 실용적 예제: 전화번호 형식
const phoneRegex = /\d{3}-\d{3,4}-\d{4}/;
console.log(phoneRegex.test('010-1234-5678')); // true
console.log(phoneRegex.test('02-123-4567')); // true
|
탐욕적 vs 게으른 수량자
기본적으로 수량자는 탐욕적(greedy)으로 동작하여 가능한 많이 매칭합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 탐욕적 수량자 (기본)
const html = '<div>Hello</div><div>World</div>';
console.log(html.match(/<div>.*<\/div>/));
// ['<div>Hello</div><div>World</div>'] - 전체 매칭
// 게으른 수량자 (? 추가)
console.log(html.match(/<div>.*?<\/div>/));
// ['<div>Hello</div>'] - 최소 매칭
// 게으른 수량자 종류
// *? - 0회 이상 (최소)
// +? - 1회 이상 (최소)
// ?? - 0회 또는 1회 (최소)
// {n,m}? - n~m회 (최소)
// 모든 div 태그 찾기
console.log(html.match(/<div>.*?<\/div>/g));
// ['<div>Hello</div>', '<div>World</div>']
|
그룹과 캡처
괄호를 사용하여 패턴을 그룹화하고 매칭된 부분을 캡처할 수 있습니다.
캡처 그룹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 기본 캡처 그룹
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const match = '2024-12-20'.match(dateRegex);
console.log(match[0]); // '2024-12-20' (전체 매칭)
console.log(match[1]); // '2024' (첫 번째 그룹)
console.log(match[2]); // '12' (두 번째 그룹)
console.log(match[3]); // '20' (세 번째 그룹)
// 역참조 (backreference)
const duplicateRegex = /(\w+)\s+\1/;
console.log(duplicateRegex.test('hello hello')); // true
console.log(duplicateRegex.test('hello world')); // false
// HTML 태그 매칭
const tagRegex = /<(\w+)>.*?<\/\1>/;
console.log(tagRegex.test('<div>content</div>')); // true
console.log(tagRegex.test('<div>content</span>')); // false
|
명명된 캡처 그룹 (ES2018)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // (?<name>pattern) 문법
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2024-12-20'.match(dateRegex);
console.log(match.groups.year); // '2024'
console.log(match.groups.month); // '12'
console.log(match.groups.day); // '20'
// 구조 분해 할당과 함께
const { groups: { year, month, day } } = '2024-12-20'.match(dateRegex);
console.log(`${year}년 ${month}월 ${day}일`); // '2024년 12월 20일'
// 명명된 역참조
const duplicateRegex = /(?<word>\w+)\s+\k<word>/;
console.log(duplicateRegex.test('hello hello')); // true
|
비캡처 그룹
그룹화는 필요하지만 캡처는 필요 없을 때 사용합니다.
1
2
3
4
5
6
7
8
9
10
| // (?:pattern) - 비캡처 그룹
const regex = /(?:http|https):\/\/(\w+)/;
const match = 'https://example'.match(regex);
console.log(match[0]); // 'https://example'
console.log(match[1]); // 'example' (첫 번째 캡처 그룹)
// http|https는 캡처되지 않음
// 성능상 이점: 캡처하지 않으므로 메모리 절약
const emailDomain = /\w+@(?:\w+\.)+\w+/;
|
전방탐색과 후방탐색
특정 패턴 앞이나 뒤에 있는 텍스트를 매칭하되, 탐색 자체는 결과에 포함시키지 않습니다.
전방탐색 (Lookahead)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 긍정 전방탐색 (?=pattern) - 뒤에 패턴이 있어야 매칭
const priceRegex = /\d+(?=원)/g;
console.log('사과 1000원, 바나나 500원'.match(priceRegex));
// ['1000', '500'] - '원'은 결과에 포함되지 않음
// 부정 전방탐색 (?!pattern) - 뒤에 패턴이 없어야 매칭
const notDollar = /\d+(?!\$)/g;
console.log('100$ 200 300$'.match(notDollar));
// ['10', '200', '30'] - $ 앞 숫자 제외
// 비밀번호 강도 검사 (전방탐색 활용)
// 최소 8자, 대문자, 소문자, 숫자 포함
const strongPassword = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$/;
console.log(strongPassword.test('Password1')); // true
console.log(strongPassword.test('password1')); // false (대문자 없음)
console.log(strongPassword.test('PASSWORD1')); // false (소문자 없음)
console.log(strongPassword.test('Password')); // false (숫자 없음)
|
후방탐색 (Lookbehind, ES2018)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 긍정 후방탐색 (?<=pattern) - 앞에 패턴이 있어야 매칭
const dollarAmount = /(?<=\$)\d+/g;
console.log('$100 and $200'.match(dollarAmount));
// ['100', '200'] - '$'는 결과에 포함되지 않음
// 부정 후방탐색 (?<!pattern) - 앞에 패턴이 없어야 매칭
const notWon = /(?<!원)\d+/g;
console.log('1000원 2000 3000원'.match(notWon));
// ['1000', '2000', '3000'] - 복잡한 결과
// 실용적 예제: 통화 기호에 따른 금액 추출
const text = '$100 USD, 50000원, EUR200';
console.log(text.match(/(?<=\$)\d+/g)); // ['100']
console.log(text.match(/\d+(?=원)/g)); // ['50000']
console.log(text.match(/(?<=EUR)\d+/g)); // ['200']
|
플래그 (Flags)
정규표현식의 동작을 제어하는 옵션입니다.
기본 플래그
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // g (global) - 전역 검색
console.log('banana'.match(/a/)); // ['a'] (첫 번째만)
console.log('banana'.match(/a/g)); // ['a', 'a', 'a'] (모두)
// i (ignoreCase) - 대소문자 무시
console.log(/hello/i.test('HELLO')); // true
// m (multiline) - 다중 행 모드
const text = `first line
second line
third line`;
console.log(text.match(/^.+/g)); // ['first line'] (첫 줄만)
console.log(text.match(/^.+/gm)); // ['first line', 'second line', 'third line']
// s (dotAll, ES2018) - .이 줄바꿈도 매칭
const html = `<div>
content
</div>`;
console.log(/<div>.*<\/div>/s.test(html)); // true (줄바꿈 포함)
console.log(/<div>.*<\/div>/.test(html)); // false (s 없으면)
|
고급 플래그
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // u (unicode) - 유니코드 모드
// 이모지, 특수 문자 올바르게 처리
console.log(/^.$/u.test('A')); // true - 단일 문자로 인식
// 유니코드 프로퍼티 이스케이프 (u 플래그 필요)
console.log(/\p{Emoji}/u.test('B')); // false - 이모지로 처리
// y (sticky) - 고정 위치 검색
const regex = /\d+/y;
const str = '123abc456';
regex.lastIndex = 0;
console.log(regex.exec(str)); // ['123']
regex.lastIndex = 3;
console.log(regex.exec(str)); // null (위치 3에서 숫자 시작 안 함)
regex.lastIndex = 6;
console.log(regex.exec(str)); // ['456']
// d (indices, ES2022) - 캡처 그룹 인덱스 정보
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})/d;
const match = '2024-12'.match(dateRegex);
console.log(match.indices.groups.year); // [0, 4]
console.log(match.indices.groups.month); // [5, 7]
|
String 메서드와 정규표현식
문자열 객체에서 정규표현식을 사용하는 메서드들입니다.
match와 matchAll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // match - 매칭 결과 반환
const str = 'Hello World';
console.log(str.match(/o/)); // ['o', index: 4, input: 'Hello World']
console.log(str.match(/o/g)); // ['o', 'o'] (g 플래그시 배열만)
console.log(str.match(/x/)); // null
// matchAll (ES2020) - 이터레이터 반환, 캡처 그룹 포함
const text = '2024-12-20, 2025-01-15';
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/g;
for (const match of text.matchAll(dateRegex)) {
console.log(match[0], '-> Year:', match[1], 'Month:', match[2]);
}
// 2024-12-20 -> Year: 2024 Month: 12
// 2025-01-15 -> Year: 2025 Month: 01
// matchAll을 배열로 변환
const matches = [...text.matchAll(dateRegex)];
console.log(matches.length); // 2
|
replace와 replaceAll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // replace - 첫 번째 매칭만 치환 (g 플래그 없을 때)
console.log('hello world'.replace(/o/, '0')); // 'hell0 world'
console.log('hello world'.replace(/o/g, '0')); // 'hell0 w0rld'
// 캡처 그룹 참조
const dateStr = '2024-12-20';
console.log(dateStr.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1'));
// '12/20/2024'
// 명명된 그룹 참조
console.log(dateStr.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
));
// '12/20/2024'
// 함수를 사용한 치환
const prices = '사과 1000원, 바나나 500원';
const result = prices.replace(/(\d+)원/g, (match, price) => {
return `${(Number(price) * 1.1).toFixed(0)}원 (VAT 포함)`;
});
console.log(result);
// '사과 1100원 (VAT 포함), 바나나 550원 (VAT 포함)'
// replaceAll (ES2021) - 모든 매칭 치환 (문자열도 가능)
console.log('a-b-c'.replaceAll('-', '_')); // 'a_b_c'
console.log('a-b-c'.replaceAll(/-/g, '_')); // 'a_b_c' (정규식도 가능, g 필수)
|
split과 search
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // split - 패턴으로 문자열 분할
console.log('a, b, c, d'.split(/,\s*/));
// ['a', 'b', 'c', 'd'] - 쉼표와 공백 제거
// 캡처 그룹 포함시 구분자도 결과에 포함
console.log('a1b2c3d'.split(/(\d)/));
// ['a', '1', 'b', '2', 'c', '3', 'd']
// search - 매칭 위치 반환
console.log('hello world'.search(/o/)); // 4
console.log('hello world'.search(/world/)); // 6
console.log('hello world'.search(/x/)); // -1 (없음)
// search는 g 플래그 무시
console.log('hello'.search(/l/g)); // 2 (첫 번째 위치만)
|
RegExp 메서드
RegExp 객체의 인스턴스 메서드입니다.
test 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // test - 매칭 여부 반환 (가장 빠름)
const emailRegex = /\S+@\S+\.\S+/;
console.log(emailRegex.test('user@example.com')); // true
console.log(emailRegex.test('invalid-email')); // false
// g 플래그와 lastIndex 주의사항
const regex = /a/g;
const str = 'ababa';
console.log(regex.test(str)); // true, lastIndex: 1
console.log(regex.test(str)); // true, lastIndex: 3
console.log(regex.test(str)); // true, lastIndex: 5
console.log(regex.test(str)); // false, lastIndex: 0 (리셋)
console.log(regex.test(str)); // true, lastIndex: 1
// 매번 새로운 검사가 필요하면 리터럴 사용 권장
const check = str => /a/.test(str);
|
exec 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // exec - 상세한 매칭 정보 반환
const regex = /(\w+)@(\w+)\.(\w+)/;
const result = regex.exec('user@example.com');
console.log(result[0]); // 'user@example.com' (전체)
console.log(result[1]); // 'user'
console.log(result[2]); // 'example'
console.log(result[3]); // 'com'
console.log(result.index); // 0 (매칭 시작 위치)
console.log(result.input); // 'user@example.com' (원본 문자열)
// g 플래그로 반복 실행
const regex2 = /\d+/g;
const str = '12 apples and 34 oranges';
let match;
while ((match = regex2.exec(str)) !== null) {
console.log(`Found ${match[0]} at index ${match.index}`);
}
// Found 12 at index 0
// Found 34 at index 14
|
유니코드와 정규표현식
u 플래그의 중요성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 서로게이트 페어 문제
const emoji = 'A'; // 이모지 예시 문자
// u 플래그 없이는 이모지가 2개 문자로 인식될 수 있음
console.log('Hello'.length); // 5
// 유니코드 코드포인트 이스케이프 (u 플래그 필요)
console.log(/\u{1F600}/u.test('Hello')); // 이모지 테스트
console.log(/\u{AC00}/u.test('가')); // true (한글 '가')
// 유니코드 범위 매칭
const koreanRegex = /[\u{AC00}-\u{D7AF}]+/u;
console.log(koreanRegex.test('안녕하세요')); // true
console.log(koreanRegex.test('Hello')); // false
|
유니코드 프로퍼티 이스케이프 (ES2018)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // \p{Property} - 유니코드 프로퍼티 매칭 (u 플래그 필요)
// \P{Property} - 부정 매칭
// 스크립트 기반 매칭
const koreanRegex = /\p{Script=Hangul}+/u;
console.log(koreanRegex.test('한글')); // true
console.log(koreanRegex.test('English')); // false
const japaneseRegex = /\p{Script=Hiragana}+/u;
console.log(japaneseRegex.test('ひらがな')); // true
// 카테고리 기반 매칭
const letterRegex = /\p{Letter}+/u; // 모든 문자(letter)
console.log(letterRegex.test('Hello')); // true
console.log(letterRegex.test('한글')); // true
console.log(letterRegex.test('123')); // false
const numberRegex = /\p{Number}+/u; // 모든 숫자
console.log(numberRegex.test('123')); // true
console.log(numberRegex.test('abc')); // false
// 실용적 예제: 다국어 텍스트 검증
const multilingualName = /^[\p{Letter}\s]+$/u;
console.log(multilingualName.test('John Smith')); // true
console.log(multilingualName.test('김철수')); // true
console.log(multilingualName.test('田中太郎')); // true
console.log(multilingualName.test('Name123')); // false
|
실전 패턴 예제
이메일 유효성 검사
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 기본 이메일 검증
const basicEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// 더 엄격한 이메일 검증
const strictEmail = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
function validateEmail(email) {
return strictEmail.test(email);
}
console.log(validateEmail('user@example.com')); // true
console.log(validateEmail('user.name@domain.co.kr')); // true
console.log(validateEmail('invalid@')); // false
console.log(validateEmail('@domain.com')); // false
|
전화번호 형식 검증
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 한국 전화번호 (다양한 형식 지원)
const koreanPhone = /^(01[016789]|02|0[3-9]{1}[0-9]{1})-?(\d{3,4})-?(\d{4})$/;
function formatPhoneNumber(phone) {
const cleaned = phone.replace(/\D/g, '');
const match = cleaned.match(/^(01[016789]|02|0[3-9]{1}[0-9]{1})(\d{3,4})(\d{4})$/);
if (match) {
return `${match[1]}-${match[2]}-${match[3]}`;
}
return null;
}
console.log(formatPhoneNumber('01012345678')); // '010-1234-5678'
console.log(formatPhoneNumber('021234567')); // '02-123-4567'
console.log(formatPhoneNumber('031-123-4567')); // '031-123-4567'
|
URL 파싱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // URL 구성 요소 추출
const urlRegex = /^(?<protocol>https?):\/\/(?<host>[^/:]+)(?::(?<port>\d+))?(?<path>\/[^?#]*)?(?:\?(?<query>[^#]*))?(?:#(?<hash>.*))?$/;
function parseUrl(url) {
const match = url.match(urlRegex);
if (!match) return null;
return {
protocol: match.groups.protocol,
host: match.groups.host,
port: match.groups.port || (match.groups.protocol === 'https' ? '443' : '80'),
path: match.groups.path || '/',
query: match.groups.query || '',
hash: match.groups.hash || ''
};
}
console.log(parseUrl('https://example.com:8080/path/to/page?id=123#section'));
// {
// protocol: 'https',
// host: 'example.com',
// port: '8080',
// path: '/path/to/page',
// query: 'id=123',
// hash: 'section'
// }
|
비밀번호 강도 검사
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // 비밀번호 규칙 검사기
const passwordRules = {
minLength: /.{8,}/,
hasUpperCase: /[A-Z]/,
hasLowerCase: /[a-z]/,
hasNumber: /\d/,
hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/,
noSpaces: /^\S+$/
};
function checkPasswordStrength(password) {
const results = {
minLength: passwordRules.minLength.test(password),
hasUpperCase: passwordRules.hasUpperCase.test(password),
hasLowerCase: passwordRules.hasLowerCase.test(password),
hasNumber: passwordRules.hasNumber.test(password),
hasSpecialChar: passwordRules.hasSpecialChar.test(password),
noSpaces: passwordRules.noSpaces.test(password)
};
const passedRules = Object.values(results).filter(Boolean).length;
const strength = passedRules <= 2 ? 'weak' : passedRules <= 4 ? 'medium' : 'strong';
return { results, strength, score: passedRules };
}
console.log(checkPasswordStrength('Password1!'));
// { results: {...}, strength: 'strong', score: 6 }
|
HTML 태그 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // HTML 태그 제거
function stripHtmlTags(html) {
return html.replace(/<[^>]*>/g, '');
}
console.log(stripHtmlTags('<p>Hello <strong>World</strong></p>'));
// 'Hello World'
// 특정 태그만 추출
function extractTags(html, tagName) {
const regex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'gi');
const matches = [];
let match;
while ((match = regex.exec(html)) !== null) {
matches.push({
full: match[0],
content: match[1]
});
}
return matches;
}
const html = '<div>First</div><span>Skip</span><div>Second</div>';
console.log(extractTags(html, 'div'));
// [{ full: '<div>First</div>', content: 'First' }, ...]
// XSS 방지를 위한 스크립트 태그 제거
function sanitizeHtml(html) {
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+="[^"]*"/gi, '')
.replace(/on\w+='[^']*'/gi, '');
}
|
성능 고려사항
백트래킹 이해하기
정규표현식 엔진은 매칭 실패 시 백트래킹(backtracking)을 수행합니다.
1
2
3
4
5
6
7
8
9
10
| // 백트래킹 예시
const regex = /a+b/;
const str = 'aaaaaac';
// 엔진 동작:
// 1. a+ 가 'aaaaaa'를 매칭
// 2. b를 찾지만 'c'가 있음 -> 실패
// 3. 백트래킹: a+가 'aaaaa'만 매칭
// 4. 다음 문자 'a'에서 b 찾기 -> 실패
// 5. 반복... 결국 매칭 실패
|
치명적 백트래킹 방지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 나쁜 예: 치명적 백트래킹 발생 가능
const badRegex = /^(a+)+$/;
// 이 패턴은 'aaaaaaaaaaaaaaaaaaaaaaX' 같은 입력에서
// 기하급수적인 백트래킹 발생 (ReDoS 취약점)
// 좋은 예: 중첩 수량자 제거
const goodRegex = /^a+$/;
// 또는 소유 수량자 사용 (ES2024 제안 중, 현재 일부 환경만 지원)
// const atomicRegex = /^(?>a+)+$/; // 백트래킹 방지
// 실용적 조언
function safeRegexTest(regex, str, timeout = 100) {
const start = Date.now();
// 입력 길이 제한
if (str.length > 10000) {
throw new Error('Input too long');
}
return regex.test(str);
}
|
성능 최적화 팁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 1. 정규표현식 재사용 (컴파일 비용 절약)
// 나쁜 예
function checkEmailBad(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); // 매번 컴파일
}
// 좋은 예
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function checkEmailGood(email) {
return emailRegex.test(email); // 재사용
}
// 2. 구체적인 패턴 사용
// 나쁜 예
const slowRegex = /.*foo.*/;
// 좋은 예
const fastRegex = /foo/;
// 3. 앵커(^, $) 활용
// 앵커가 있으면 엔진이 불필요한 위치 검사 생략
// 4. 비캡처 그룹 사용
// 캡처가 필요 없으면 (?:...) 사용
// 5. 문자 클래스 최적화
// [0-9]보다 \d가 약간 빠를 수 있음 (엔진 의존)
|
정규표현식 디버깅 팁
단계적 테스트
1
2
3
4
5
6
7
8
9
10
11
12
| // 복잡한 패턴을 작은 단위로 분해하여 테스트
const fullPattern = /^(?<protocol>https?):\/\/(?<domain>[\w.-]+)(?<path>\/\S*)?$/;
// 단계별 테스트
const protocolTest = /^https?/;
const domainTest = /[\w.-]+/;
const pathTest = /\/\S*/;
const url = 'https://example.com/path';
console.log('Protocol:', protocolTest.test(url));
console.log('Domain:', domainTest.test('example.com'));
console.log('Path:', pathTest.test('/path'));
|
매칭 시각화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 매칭 위치 확인
function visualizeMatch(str, regex) {
const result = str.match(regex);
if (!result) {
console.log('No match found');
return;
}
const start = result.index;
const end = start + result[0].length;
console.log(str);
console.log(' '.repeat(start) + '^'.repeat(result[0].length));
console.log(`Matched: "${result[0]}" at index ${start}-${end - 1}`);
}
visualizeMatch('Hello World', /World/);
// Hello World
// ^^^^^
// Matched: "World" at index 6-10
|
온라인 도구 활용
정규표현식 테스트와 디버깅에 유용한 도구들:
- regex101.com: 상세한 설명과 디버거 제공
- regexr.com: 시각적 매칭 확인
- debuggex.com: 정규표현식 시각화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 개발 중 패턴 테스트 함수
function testPattern(pattern, testCases) {
console.log(`Testing: ${pattern}`);
console.log('-'.repeat(40));
testCases.forEach(({ input, expected }) => {
const result = pattern.test(input);
const status = result === expected ? 'PASS' : 'FAIL';
console.log(`${status}: "${input}" -> ${result} (expected: ${expected})`);
});
}
testPattern(/^[a-z]+@[a-z]+\.[a-z]+$/i, [
{ input: 'user@example.com', expected: true },
{ input: 'USER@EXAMPLE.COM', expected: true },
{ input: 'invalid', expected: false },
{ input: 'user@.com', expected: false }
]);
|
마치며
정규표현식은 처음에는 복잡해 보이지만, 기본 개념을 이해하고 나면 문자열 처리의 강력한 도구가 됩니다. 이 글에서 다룬 내용을 정리하면:
핵심 포인트
- 생성 방법: 리터럴(
/pattern/)은 고정 패턴에, 생성자(new RegExp())는 동적 패턴에 사용 - 메타문자:
., *, +, ?, ^, $, [], {}, (), | 등 특별한 의미 - 문자 클래스:
\d, \w, \s, \b 등 자주 쓰는 패턴의 단축형 - 수량자: 탐욕적(
*, +) vs 게으른(*?, +?) 동작 이해 - 그룹: 캡처 그룹, 명명된 그룹, 비캡처 그룹 활용
- 전방/후방탐색: 패턴 주변 컨텍스트 확인
- 유니코드:
u 플래그와 \p{} 프로퍼티로 다국어 지원 - 성능: 백트래킹 이해와 치명적 백트래킹 방지
추가 학습 자료
정규표현식은 연습이 중요합니다. 실제 프로젝트에서 유효성 검사, 텍스트 파싱, 검색/치환 등에 적극 활용해 보세요. 복잡한 패턴을 작성할 때는 반드시 다양한 테스트 케이스로 검증하고, 성능에 민감한 경우 입력 길이 제한과 타임아웃을 고려하세요.
참고 자료