정규표현식, 왜 이제야 제대로 배웠나 싶었다
코드에서 /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/ 같은 걸 보면 눈이 풀렸다. "이게 뭔 외계어야?" 싶어서 스택오버플로우에서 복붙하고 "일단 돌아가네" 하고 넘어갔다.
그런데 프로젝트에서 유저 입력 검증, CSV 파싱, 로그 필터링을 하다 보니 매번 복붙만 할 수는 없었다. 정규표현식 없이 텍스트 처리하려니까 for 문 중첩에 if 문 도배가 되더라. 50줄짜리 코드가 정규표현식 한 줄로 끝나는 걸 보고 "아, 이거 제대로 배워야겠다" 싶었다.
결론부터 말하면, 정규표현식 전체를 마스터할 필요는 없었다. 실제로 자주 쓰는 패턴 20개만 익히니까 웬만한 텍스트 처리는 다 해결됐다. 마치 요리할 때 칼질 20가지 기술 다 배우는 게 아니라, 채썰기·다지기·저미기만 익히면 되는 것처럼.
정규표현식의 핵심: 패턴 매칭이라는 마법
정규표현식(Regular Expression, RegExp)은 결국 "텍스트에서 패턴을 찾는 도구"다. "이메일 형식인지 확인", "전화번호 추출", "비밀번호 규칙 검사" 같은 게 모두 패턴 매칭이다.
처음엔 /^[a-zA-Z]/ 같은 게 암호문 같았는데, 하나씩 뜯어보니 그냥 "알파벳으로 시작하는 문자열"이라는 뜻이었다. 각 기호가 의미하는 바만 알면 레고 블록처럼 조합할 수 있더라.
기본 문법: 레고 블록 조립법
정규표현식의 핵심 기호들은 이렇다.
1. 와일드카드와 앵커
.아무 문자나 하나 (공백 포함)*0번 이상 반복+1번 이상 반복?0번 또는 1번^문자열 시작$문자열 끝
2. 문자 클래스
[abc]a, b, c 중 하나[^abc]a, b, c가 아닌 것[a-z]소문자 알파벳\d숫자 (0-9)\w단어 문자 (알파벳, 숫자, 언더스코어)\s공백 문자\D숫자가 아닌 것 (대문자는 부정)
3. 그룹과 양화사
()그룹화 및 캡처(?<name>)이름 있는 그룹{3}정확히 3번{3,}3번 이상{3,5}3~5번
4. 탐욕적 vs 게으른 매칭
.*탐욕적 (최대한 많이).*?게으른 (최소한만)
이걸 조합하면 복잡한 패턴도 만들 수 있다.
// 이메일 검증 (간단 버전)
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
console.log(emailRegex.test('user@example.com')); // true
console.log(emailRegex.test('invalid-email')); // false
// 전화번호 추출 (한국 형식)
const phoneRegex = /010-\d{4}-\d{4}/g;
const text = '연락처: 010-1234-5678, 사무실: 010-9876-5432';
console.log(text.match(phoneRegex)); // ['010-1234-5678', '010-9876-5432']
// HTML 태그 제거
const htmlRegex = /<[^>]+>/g;
const html = '<p>Hello <strong>World</strong></p>';
console.log(html.replace(htmlRegex, '')); // "Hello World"
처음엔 "이걸 어떻게 외워?" 싶었는데, 자주 쓰다 보니 패턴이 보이더라. \d+는 "숫자 여러 개", \w+는 "단어", \s*는 "공백 있을 수도 없을 수도" 이런 식으로.
실제 패턴 20개: 복붙용 컬렉션
실제로 자주 쓰는 패턴들을 정리했다. 외울 필요 없고, 필요할 때 복붙하면 된다.
1. 이메일
/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/
2. URL
/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b/
3. 한국 전화번호 (하이픈 있음)
/^01[016789]-\d{3,4}-\d{4}$/
4. 한국 전화번호 (하이픈 없음)
/^01[016789]\d{7,8}$/
5. 한글만
/^[가-힣]+$/
6. 영문+숫자만
/^[a-zA-Z0-9]+$/
7. 비밀번호 (8자 이상, 영문+숫자+특수문자)
/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/
8. 날짜 (YYYY-MM-DD)
/^\d{4}-\d{2}-\d{2}$/
9. 시간 (HH:MM)
/^([01]\d|2[0-3]):([0-5]\d)$/
10. 신용카드 (4자리씩 4그룹)
/^\d{4}-\d{4}-\d{4}-\d{4}$/
11. IPv4
/^(\d{1,3}\.){3}\d{1,3}$/
12. 16진수 컬러코드
/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
13. 주민등록번호 (앞자리만)
/^\d{6}$/
14. 양의 정수
/^\d+$/
15. 소수점 포함 숫자
/^\d+(\.\d+)?$/
16. 파일 확장자 추출
/\.([a-zA-Z0-9]+)$/
17. 공백 제거
/\s+/g
18. 연속 공백을 하나로
/ +/g
19. URL에서 도메인 추출
/^https?:\/\/([^\/]+)/
20. 마크다운 이미지 문법
/!\[([^\]]*)\]\(([^)]+)\)/g
JavaScript에서 정규표현식 활용법
RegExp 메서드 4종
JavaScript에서 정규표현식은 두 가지 방식으로 쓴다.
1. test() - 매칭 여부만 확인
const regex = /\d{3}-\d{4}/;
console.log(regex.test('010-1234')); // true
console.log(regex.test('abc-defg')); // false
2. exec() - 첫 번째 매칭 정보
const regex = /(\d{3})-(\d{4})/;
const result = regex.exec('전화: 010-1234');
console.log(result[0]); // "010-1234" (전체)
console.log(result[1]); // "010" (첫 번째 그룹)
console.log(result[2]); // "1234" (두 번째 그룹)
3. String.match() - 모든 매칭 결과
const text = 'CSS: #FF5733, #C70039, #900C3F';
const colors = text.match(/#[A-Fa-f0-9]{6}/g);
console.log(colors); // ["#FF5733", "#C70039", "#900C3F"]
4. String.matchAll() - 그룹 포함 모든 매칭
const text = '가격: $100, $200, $300';
const regex = /\$(\d+)/g;
for (const match of text.matchAll(regex)) {
console.log(`전체: ${match[0]}, 숫자: ${match[1]}`);
}
// 전체: $100, 숫자: 100
// 전체: $200, 숫자: 200
// 전체: $300, 숫자: 300
String 메서드와 정규표현식
1. replace() / replaceAll()
const text = 'Hello World, Hello Universe';
console.log(text.replace(/Hello/g, 'Hi')); // "Hi World, Hi Universe"
// 그룹 활용
const html = '<p>Text</p>';
console.log(html.replace(/<([^>]+)>/g, '[$1]')); // "[p]Text[/p]"
// 콜백 함수
const caps = 'hello world'.replace(/\b\w/g, char => char.toUpperCase());
console.log(caps); // "Hello World"
2. split()
const csv = 'apple,banana, cherry, orange';
console.log(csv.split(/\s*,\s*/)); // ["apple", "banana", "cherry", "orange"]
3. search()
const text = 'The price is $99.99';
console.log(text.search(/\$\d+/)); // 13 (위치)
이름 있는 그룹: 가독성 개선
ES2018부터 (?<name>) 문법으로 그룹에 이름을 붙일 수 있다.
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2026-02-21'.match(dateRegex);
console.log(match.groups.year); // "2026"
console.log(match.groups.month); // "02"
console.log(match.groups.day); // "21"
// replace에서도 사용 가능
const formatted = '2026-02-21'.replace(
dateRegex,
'$<day>/$<month>/$<year>'
);
console.log(formatted); // "21/02/2026"
숫자 인덱스(match[1], match[2])보다 의미가 명확해서 코드 가독성이 훨씬 좋아진다.
폼 검증: Zod와 정규표현식
실제로 정규표현식을 가장 많이 쓰는 곳이 폼 검증이다. Zod 같은 스키마 검증 라이브러리와 조합하면 깔끔하다.
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
phone: z.string().regex(/^01[016789]-\d{3,4}-\d{4}$/, '전화번호 형식이 올바르지 않습니다'),
password: z.string().regex(
/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
'비밀번호는 8자 이상, 영문+숫자+특수문자 포함'
),
username: z.string().regex(/^[a-zA-Z0-9_]{3,20}$/, '3~20자 영문, 숫자, 언더스코어만 가능'),
});
// 사용
const result = userSchema.safeParse({
email: 'user@example.com',
phone: '010-1234-5678',
password: 'Pass123!',
username: 'my_username'
});
if (!result.success) {
console.log(result.error.issues);
}
React Hook Form과 조합하면 프론트엔드 검증이 한 번에 끝난다.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(userSchema)
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('phone')} />
{errors.phone && <span>{errors.phone.message}</span>}
<button type="submit">가입</button>
</form>
);
}
정규표현식 디버깅과 최적화
온라인 도구: regex101.com과 regexr.com
정규표현식 작성할 때 가장 큰 문제는 "이게 제대로 작동하는지" 확인하기 어렵다는 것이다. 코드에 넣어보기 전에 온라인 도구로 테스트하면 시간이 엄청 절약된다.
regex101.com
- 패턴 입력하면 실시간으로 매칭 결과 보여줌
- 각 구문 설명 제공
- 테스트 문자열 여러 개 입력 가능
- JavaScript, Python, PHP 등 언어별 차이 확인
regexr.com
- 시각적으로 더 깔끔한 UI
- 패턴 라이브러리 제공 (자주 쓰는 패턴 모음)
- 실시간 매칭 하이라이트
복잡한 패턴 만들 때는 이 도구들 없이는 못 하겠더라. 특히 그룹 캡처 확인할 때 필수다.
성능 주의: Catastrophic Backtracking
정규표현식은 잘못 쓰면 성능이 끔찍하게 나빠진다. 특히 "Catastrophic Backtracking" 문제는 CPU를 100% 먹는 괴물이 될 수 있다.
// 위험한 패턴 (절대 쓰지 말 것)
const badRegex = /(a+)+b/;
console.time('bad');
badRegex.test('aaaaaaaaaaaaaaaaaaaaaaaac'); // 엄청 느림
console.timeEnd('bad');
// 개선된 패턴
const goodRegex = /a+b/;
console.time('good');
goodRegex.test('aaaaaaaaaaaaaaaaaaaaaaaac'); // 빠름
console.timeEnd('good');
문제가 되는 패턴:
(a+)+중첩된 양화사(a*)*반복된 반복(a|a)*겹치는 선택지
해결법:
- 중첩 피하기
- Atomic Group 사용 (일부 언어에서만 지원)
- 타임아웃 설정
실제로는 대부분 간단한 패턴만 써서 문제가 잘 안 생기지만, 복잡한 파싱 할 때는 주의해야 한다.
VS Code에서 정규표현식 활용
코드 편집기에서 정규표현식을 쓰면 리팩토링이 엄청 빨라진다.
1. Find & Replace (Cmd/Ctrl + H)
- Regex 모드 켜기 (
.*아이콘) - 그룹 캡처 후 치환
예: 모든 console.log를 주석 처리
찾기: (console\.log\(.+\))
바꾸기: // $1
예: 함수 이름 변경 (camelCase → snake_case)
찾기: function ([a-z]+)([A-Z][a-z]+)
바꾸기: function $1_$2
2. 여러 파일 검색 (Cmd/Ctrl + Shift + F)
- Regex 모드로 패턴 검색
- 예: 사용하지 않는 import 찾기
^import .+ from ['"].+['"];?\s*$
3. Multi-cursor로 일괄 수정
- 정규표현식 검색 → 모든 매칭에 커서 생성
- Alt + Enter (VS Code)
이걸 알고 나서 "100개 파일에서 특정 패턴 바꾸기" 같은 게 10초 만에 끝난다.
Lookahead와 Lookbehind: 고급 패턴
정규표현식에서 "조건은 확인하지만 결과에는 포함 안 함" 같은 게 필요할 때가 있다. 이때 Lookahead/Lookbehind를 쓴다.
Lookahead (앞쪽 확인)
(?=...)긍정 전방탐색 (뒤에 ... 가 있어야 함)(?!...)부정 전방탐색 (뒤에 ... 가 없어야 함)
// 비밀번호: 최소 하나씩 영문, 숫자, 특수문자 포함
const password = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&]).{8,}$/;
console.log(password.test('Password123!')); // true
console.log(password.test('password')); // false (숫자, 특수문자 없음)
// 가격 추출 ($99.99에서 99.99만)
const price = /\$(?=\d+\.\d{2})/g;
console.log('$99.99 and $49.50'.replace(price, '')); // "99.99 and 49.50"
Lookbehind (뒤쪽 확인) - ES2018+
(?<=...)긍정 후방탐색 (앞에 ... 가 있어야 함)(?<!...)부정 후방탐색 (앞에 ... 가 없어야 함)
// $99에서 99만 추출
const amount = /(?<=\$)\d+/g;
console.log('Price: $99'.match(amount)); // ["99"]
// "not a winner"가 아닌 "winner"만 찾기
const winner = /(?<!not a )winner/;
console.log(winner.test('winner')); // true
console.log(winner.test('not a winner')); // false
처음엔 "이게 언제 필요해?" 싶었는데, 비밀번호 검증이나 복잡한 파싱에서 엄청 유용하더라.
정규표현식, 결국 도구일 뿐
정규표현식을 완벽하게 마스터할 필요는 없다. 필요할 때 패턴 하나 찾아서 쓰고, 조금씩 익숙해지면 된다.
마치 칼질처럼, 처음엔 서툴러도 자주 쓰다 보면 손에 익는다. "이메일 검증", "전화번호 추출", "HTML 태그 제거" 같은 패턴 몇 개만 알아도 코드가 훨씬 간결해진다.
정규표현식이 외계어처럼 느껴졌던 때가 있었다. 하지만 이제는 "텍스트 처리할 일 있으면 일단 정규표현식"이라는 생각이 먼저 든다. 복붙용 패턴 20개 저장해두고, regex101.com 북마크 해두고, 필요할 때마다 조금씩 배워가면 된다.
결국 정규표현식은 도구다. 만능은 아니지만, 텍스트 처리만큼은 이것만 한 게 없다.