개발하다 보면 사용자가 입력한 데이터를 검증해야 할 순간이 자주 온다. 이메일 형식이 맞는지, 전화번호가 올바른지, 특정 문자열 패턴을 찾아야 하는데 문제는 대부분의 개발자들이 정규표현식(Regex)을 복사해 붙이기만 할 뿐, 정확히 어떤 원리로 동작하는지 이해하지 못한 채 사용한다는 것이다. 결국 약간 다른 형식의 데이터가 들어오면 예상 밖의 결과가 나오거나 유지보수가 불가능해진다. 이번에는 정규표현식이 정확히 뭔지, 왜 필요한지, 어떻게 작성하고 실무에 어떻게 적용하는지 완벽하게 정리해서 소개하겠다.

 

1단계: 정규표현식 기초 이해하기

정규표현식은 특정 문자열 패턴을 찾거나 매칭시키기 위한 표기법이다. 텍스트 검증, 문자열 치환, 데이터 추출 등 여러 목적으로 사용된다. JavaScript에서는 /패턴/플래그 형태로 작성하거나 new RegExp('패턴', '플래그') 생성자로 만든다.

먼저 정규표현식의 기본 구성 요소를 표로 정리해보자.

요소 설명 예시
메타문자 특수한 의미를 가진 문자 (. * + ? ^ $ | ( ) [ ] { }) . = 모든 문자 1개
문자 클래스 대괄호 안의 문자 중 하나 ([abc], [a-z], [0-9]) [a-z] = 소문자 한 글자
수량자 앞 패턴의 반복 횟수 (*, +, ?, {n,m}) a+ = a가 1번 이상
앵커 문자열의 시작과 끝 (^, $) ^abc = abc로 시작
플래그 정규표현식 동작 방식 (g, i, m) i = 대소문자 구분 안함

 

자주 쓰는 메타문자 설명

. (점) = 줄바꿈을 제외한 모든 문자 1개를 의미한다. 예를 들어 a.c는 "abc", "adc", "a1c" 모두 매칭된다.

* (별) = 앞 패턴이 0번 이상 반복된다는 뜻이다. 예를 들어 ab*c는 "ac", "abc", "abbc", "abbbc" 모두 매칭된다.

+ (더하기) = 앞 패턴이 1번 이상 반복된다는 뜻이다. 예를 들어 ab+c는 "abc", "abbc"는 매칭되지만 "ac"는 매칭되지 않는다.

? (물음표) = 앞 패턴이 0번 또는 1번 나타난다는 뜻이다. 예를 들어 colou?r는 "color"와 "colour" 모두 매칭된다.

^ (캐럿) = 문자열의 시작을 의미한다. 예를 들어 ^hello는 "hello world"는 매칭되지만 "say hello"는 매칭되지 않는다.

$ (달러) = 문자열의 끝을 의미한다. 예를 들어 world$는 "hello world"는 매칭되지만 "world peace"는 매칭되지 않는다.

 

2단계: JavaScript 정규표현식 메서드 이해하기

JavaScript에서 정규표현식을 실제로 사용하려면 문자열 메서드와 RegExp 메서드를 알아야 한다.

메서드 반환값 설명
test() boolean 패턴이 문자열에 있으면 true, 없으면 false
exec() 배열 또는 null 첫 번째 매칭된 부분과 캡처 그룹 반환
match() 배열 또는 null 모든 매칭된 부분 배열로 반환 (g 플래그 필요)
search() 숫자 매칭된 부분의 인덱스 반환 (없으면 -1)
replace() 문자열 매칭된 부분을 다른 문자열로 치환
split() 배열 정규표현식 패턴으로 문자열 분할

 

3단계: 실전 예제로 배우기

 

예제 1: 이메일 형식 검증

✓ 올바른 방법

const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;

const testEmails = [
  'user@example.com',      // true
  'invalid.email@',         // false
  'user@domain',            // false
  'user name@example.com'   // false
];

testEmails.forEach(email => {
  console.log(email + ': ' + emailRegex.test(email));
});

패턴 설명: ^[^s@]+@[^s@]+.[^s@]+$

  • ^ = 문자열 시작
  • [^s@]+ = 공백과 @를 제외한 문자가 1개 이상
  • @ = @ 기호
  • [^s@]+ = 도메인명 (공백과 @ 제외)
  • . = 점 기호 (이스케이프 필요)
  • [^s@]+ = 최상위 도메인 (공백과 @ 제외)
  • $ = 문자열 끝

 

예제 2: 전화번호 형식 검증 및 자동 하이픈 삽입

✗ 잘못된 방법 (하드코딩으로 불편함)

// 각 경우마다 별도로 처리해야 함 → 유지보수 어려움
function formatPhoneWrong(phone) {
  if (phone.length === 10) {
    return phone.substring(0, 3) + '-' + phone.substring(3, 6) + '-' + phone.substring(6);
  } else if (phone.length === 11) {
    return phone.substring(0, 3) + '-' + phone.substring(3, 7) + '-' + phone.substring(7);
  }
  return phone;
}

console.log(formatPhoneWrong('01012345678')); // 010-1234-5678

✓ 올바른 방법 (정규표현식으로 간단하고 확장 가능)

function formatPhone(phone) {
  // 숫자만 추출
  const cleaned = phone.replace(/D/g, '');
  
  // 국내 휴대폰 형식: 010-1234-5678
  const regex = /^(d{3})(d{4})(d{4})$/;
  
  return cleaned.replace(regex, '$1-$2-$3');
}

console.log(formatPhone('01012345678'));     // 010-1234-5678
console.log(formatPhone('0101234 5678'));    // 010-1234-5678
console.log(formatPhone('010-1234-5678'));   // 010-1234-5678

패턴 설명: ^(d{3})(d{4})(d{4})$

  • d = 숫자 한 글자
  • {3} = 정확히 3개
  • () = 캡처 그룹 (나중에 $1, $2, $3로 참조 가능)
  • $1-$2-$3 = 첫 번째 그룹 - 두 번째 그룹 - 세 번째 그룹

 

예제 3: HTML 태그 제거

✗ 잘못된 방법 (모든 < > 기호를 제거하면 부등호 연산자까지 없어짐)

const htmlText = '

가격: 10,000 < 15,000

'; const wrongResult = htmlText.replace(//g, ''); console.log(wrongResult); // p>가격: 10,000 15,000/p> 이상함

✓ 올바른 방법 (HTML 태그 패턴만 정확히 제거)

const htmlText = '

가격: 10,000 < 15,000

할인'; const regex = /<[^>]*>/g; // < 와 > 사이의 모든 문자 제거 const result = htmlText.replace(regex, ''); console.log(result); // 가격: 10,000 < 15,000할인

패턴 설명: <[^>]*>

  • < = < 기호 (이스케이프)
  • [^>] = > 기호를 제외한 모든 문자
  • * = 0번 이상 반복
  • > = > 기호 (이스케이프)
  • g 플래그 = 모든 태그 제거

 

예제 4: 단어 경계를 이용한 특정 단어만 치환

✗ 잘못된 방법 (부분 매칭으로 의도하지 않은 부분까지 바뀜)

const text = 'The cat is catching a cat in the catalog.';
const wrong = text.replace(/cat/g, 'dog');
console.log(wrong); 
// The dog is dogging a dog in the doglog. ← "catalog" 와 "catching"도 바뀜

✓ 올바른 방법 (단어 경계 b로 정확한 단어만 치환)

const text = 'The cat is catching a cat in the catalog.';
const correct = text.replace(/bcatb/g, 'dog');
console.log(correct); 
// The dog is catching a dog in the catalog. ← "cat"이라는 단어만 바뀜

패턴 설명: b = 단어 경계 (영숫자와 비영숫자 사이의 경계)

 

예제 5: URL에서 쿼리 파라미터 추출

✓ 최적화된 방법 (정규표현식과 객체 활용)

function getUrlParams(url) {
  const params = {};
  const regex = /([^?=&]+)=([^&]*)/g;
  let match;
  
  while ((match = regex.exec(url)) !== null) {
    params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
  }
  
  return params;
}

const url = 'https://example.com?name=John&age=30&city=Seoul';
const result = getUrlParams(url);
console.log(result);
// { name: 'John', age: '30', city: 'Seoul' }

패턴 설명: ([^?=&]+)=([^&]*)

  • ([^?=&]+) = 캡처 그룹 1: ? = & 를 제외한 문자 (파라미터 이름)
  • = = 등호 기호
  • ([^&]*) = 캡처 그룹 2: & 를 제외한 문자 (파라미터 값)
  • g 플래그 = 모든 파라미터 추출
  • exec() = 루프로 반복 실행하여 모든 매칭 처리

 

4단계: 주의사항과 흔한 실수

 

✗ 실수 1: 백슬래시 이스케이프 잊기
// 잘못된 코드 → SyntaxError
const wrong = /d+/;  // ← 리터럴 문법에서도 백슬래시가 필요

// 올바른 코드
const correct1 = /d+/;                    // 리터럴
const correct2 = new RegExp('\\d+');     // 생성자 (백슬래시 4개 필요!)

// 생성자 사용 시 백슬래시를 두 배로 이스케이프해야 함
console.log(correct1.test('123'));  // true
console.log(correct2.test('123'));  // true

 

✗ 실수 2: g 플래그 없이 replace 사용
const text = 'apple apple apple';

// 잘못된 코드 → 첫 번째 매칭만 치환됨
const wrong = text.replace(/apple/, 'orange');
console.log(wrong);  // orange apple apple

// 올바른 코드 → 모든 매칭 치환
const correct = text.replace(/apple/g, 'orange');
console.log(correct);  // orange orange orange

 

✗ 실수 3: 특수문자 이스케이프 누락
// 잘못된 코드 → . 이 모든 문자를 의미해서 예상 밖 결과
const wrong = /3.14/;  // "314", "3a14" 모두 매칭됨

// 올바른 코드 → 점을 이스케이프
const correct = /3.14/;  // "3.14"만 매칭됨

console.log(wrong.test('314'));    // true (예상 밖!)
console.log(correct.test('314'));  // false (원래 의도)
console.log(correct.test('3.14')); // true

 

✗ 실수 4: test() 메서드의 lastIndex 버그
// g 플래그가 있는 정규표현식은 내부 lastIndex 상태를 유지함
const regex = /d/g;

console.log(regex.test('abc'));  // false
console.log(regex.test('123'));  // true (첫 번째 호출)
console.log(regex.test('123'));  // true (두 번째 호출, lastIndex = 3)
console.log(regex.test('123'));  // false (lastIndex 초기화되며 결과 불안정!)

// 해결책: 리터럴 정규표현식 사용하거나 exec() 사용
const regex2 = /d/g;
regex2.lastIndex = 0;  // 수동으로 초기화
console.log(regex2.test('123'));  // true

 

✗ 실수 5: 복잡한 정규표현식을 직관적으로 사용
// 이런 패턴은 읽기 어려움
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;

// 해결책: 주석을 달거나 변수명으로 의도를 명확히
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/; // 이메일

// 또는 더 나은 방법: 함수로 감싸기
function isValidEmail(email) {
  const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
  return emailRegex.test(email);
}

console.log(isValidEmail('user@example.com'));  // true

 

5단계: 실무에서 바로 쓸 수 있는 유용한 패턴 모음

 

공백 제거 (앞뒤 및 중간)
// 앞뒤 공백만 제거
const trimBoth = '  hello world  '.replace(/^s+|s+$/g, '');
console.log(trimBoth);  // "hello world"

// 모든 공백 제거
const removeAll = 'h e l l o'.replace(/s/g, '');
console.log(removeAll);  // "hello"

// 중간 연속 공백을 하나로
const normalize = 'hello    world'.replace(/s+/g, ' ');
console.log(normalize);  // "hello world"

 

숫자 천 단위 구분
function formatNumber(num) {
  return num.toString().replace(/B(?=(d{3})+(?!d))/g, ',');
}

console.log(formatNumber(1000));      // "1,000"
console.log(formatNumber(1000000));   // "1,000,000"
console.log(formatNumber(123));       // "123"

 

강력한 비밀번호 검증 (최소 8자, 대문자, 소문자, 숫자, 특수문자)
function isStrongPassword(password) {
  const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])[A-Za-zd@$!%*?&]{8,}$/;
  return regex.test(password);
}

console.log(isStrongPassword('weak'));           // false
console.log(isStrongPassword('Weak1234'));       // false (특수문자 없음)
console.log(isStrongPassword('Strong@1234'));    // true

 

결론: 정규표현식 마스터하기

정규표현식은 초반에 배우기 어렵지만, 한 번 이해하면 문자열 처리에서 강력한 무기가 된다. 이메일 검증, 데이터 추출, 문자열 치환 같은 작은 최적화들이 모여서 코드의 가독성과 유지보수성을 크게 향상시킨다는 점을 잊지 말자. 이 글의 기본 패턴과 메타문자 설명을 참고해 간단한 검증부터 시작하고, 점차 복잡한 패턴으로 확장해나가면, 정규표현식을 진정으로 마스터할 수 있을 것이다. 정규표현식 학습 사이트(regexr.com, regex101.com)를 활용하면 패턴을 실시간으로 테스트하며 학습할 수 있으니 적극 추천한다.