본문 바로가기
개발 팁

정규식 실전 치트시트 — 개발자가 자주 쓰는 패턴 모음

by 루까(Luka) 2026. 3. 18.
반응형

왜 이 글을 쓰나

정규식은 쓸 때마다 구글링한다. 이메일 검증 패턴, URL 추출, 전화번호 포맷 — 머릿속에 완전히 외워지지 않는다. 매번 Stack Overflow나 Regex101에서 찾는데, 검색 결과가 너무 다양해서 어떤 게 맞는 건지 확인하는 데 시간이 또 걸린다.

자주 쓰는 것만 한 곳에 정리했다. JavaScript 기준이지만, 패턴 자체는 Python, PHP, Java 등 대부분의 언어에서 그대로 쓰거나 조금만 고치면 된다.


정규식 기본 문법 요약

코드를 읽기 전에, 자주 등장하는 메타문자를 빠르게 훑고 간다.

문자 의미 예시
. 임의의 한 문자 (줄바꿈 제외) a.c → abc, aXc
* 0회 이상 반복 ab* → a, ab, abb
+ 1회 이상 반복 ab+ → ab, abb (a 단독 불가)
? 0회 또는 1회 colou?r → color, colour
{n,m} n~m회 반복 \d{2,4} → 2~4자리 숫자
^ 문자열 시작 ^Hello
$ 문자열 끝 world$
[] 문자 클래스 [aeiou], [0-9]
[^] 부정 문자 클래스 [^0-9] → 숫자가 아닌 것
\d 숫자 ([0-9])  
\w 단어 문자 ([a-zA-Z0-9_])  
\s 공백 문자 (스페이스, 탭, 줄바꿈)  
\b 단어 경계 \bcat\b
(...) 그룹 (ab)+
(?:...) 캡처 없는 그룹 (?:ab)+
a|b a 또는 b cat|dog

JavaScript에서 정규식은 /패턴/플래그 리터럴이나 new RegExp('패턴', '플래그')로 쓴다. 주요 플래그:

플래그 의미
g 전역 검색 (모든 매치)
i 대소문자 무시
m 멀티라인 (^, $가 각 줄에 적용)
s .이 줄바꿈도 포함

자주 쓰는 패턴 카테고리별 정리

이메일

가장 자주 쓰는 것 중 하나다. RFC 5322 완전 준수 패턴은 너무 복잡해서 실용적이지 않다. 실무에서 쓰기에 충분한 수준으로 정리했다.

// 기본 이메일 검증
const emailRegex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;

emailRegex.test('user@example.com');       // true
emailRegex.test('user.name+tag@sub.co.kr'); // true
emailRegex.test('invalid@');               // false
emailRegex.test('@nodomain.com');          // false

{2,}로 TLD를 최소 2글자로 제한했다. .a같은 건 막힌다. .museum, .photography 같은 긴 TLD도 통과한다.


URL

// HTTP/HTTPS URL
const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&\/=]*)$/;

urlRegex.test('https://example.com');                    // true
urlRegex.test('http://sub.domain.co.kr/path?q=1&p=2'); // true
urlRegex.test('ftp://not-http.com');                    // false

// URL에서 도메인만 추출
const extractDomain = (url) => {
  const match = url.match(/^https?:\/\/(?:www\.)?([^\/\?#]+)/i);
  return match ? match[1] : null;
};

extractDomain('https://www.example.com/path?q=1'); // 'example.com'

전화번호

한국 번호 형식이 다양해서 여러 케이스를 다 커버해야 한다.

// 한국 전화번호 (010, 011, 016, 017, 018, 019)
const mobileRegex = /^01[016789]-?\d{3,4}-?\d{4}$/;

mobileRegex.test('010-1234-5678'); // true
mobileRegex.test('01012345678');   // true
mobileRegex.test('0101234567');    // false (9자리)

// 유선 전화 포함
const phoneRegex = /^(0[2-9]\d?)-?\d{3,4}-?\d{4}$/;

phoneRegex.test('02-123-4567');   // true
phoneRegex.test('031-1234-5678'); // true
phoneRegex.test('010-1234-5678'); // false (유선 전용)

// 전화번호 포맷 통일 (하이픈 추가)
const formatPhone = (num) => {
  const digits = num.replace(/\D/g, '');
  if (digits.startsWith('02')) {
    return digits.replace(/^(02)(\d{3,4})(\d{4})$/, '$1-$2-$3');
  }
  return digits.replace(/^(\d{3})(\d{3,4})(\d{4})$/, '$1-$2-$3');
};

formatPhone('01012345678');  // '010-1234-5678'
formatPhone('0212345678');   // '02-1234-5678'

날짜

날짜 포맷은 서비스마다 달라서 여러 형식을 다뤄야 할 때가 많다.

// YYYY-MM-DD
const dateISO = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;

dateISO.test('2026-03-17'); // true
dateISO.test('2026-13-01'); // false (월 범위 초과)
dateISO.test('2026-03-32'); // false (일 범위 초과)

// YYYY.MM.DD 또는 YYYY/MM/DD
const dateFlexible = /^\d{4}[.\/-](0[1-9]|1[0-2])[.\/-](0[1-9]|[12]\d|3[01])$/;

dateFlexible.test('2026.03.17'); // true
dateFlexible.test('2026/03/17'); // true

// 날짜에서 연/월/일 추출
const parseDate = (str) => {
  const match = str.match(/^(\d{4})[.\/-](0[1-9]|1[0-2])[.\/-](0[1-9]|[12]\d|3[01])$/);
  if (!match) return null;
  return { year: match[1], month: match[2], day: match[3] };
};

주의: 정규식으로 날짜 포맷은 검증할 수 있지만, 2026-02-30 같은 논리적으로 존재하지 않는 날짜는 통과한다. 최종 검증은 new Date()로 한 번 더 확인하는 게 안전하다.


비밀번호

보안 정책마다 요건이 다르다. 자주 쓰는 조건들을 레벨별로 정리했다.

// 최소 8자, 영문 + 숫자 조합
const pwBasic = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/;

// 최소 8자, 영문 대소문자 + 숫자 + 특수문자
const pwStrong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,}$/;

// 최소 10자, 3가지 이상 조합 (lookahead로 각 조건 체크)
const pwCustom = /^(?=(.*[a-z]){1,})(?=(.*[A-Z]){1,})(?=(.*\d){1,})(?=(.*[!@#$%]){1,}).{10,}$/;

pwBasic.test('mypassword1');    // true
pwBasic.test('password');       // false (숫자 없음)
pwStrong.test('MyPass1!');      // true
pwStrong.test('mypass1!');      // false (대문자 없음)

// 비밀번호 강도 체크 함수
const checkPasswordStrength = (pw) => {
  const checks = {
    length: pw.length >= 8,
    lowercase: /[a-z]/.test(pw),
    uppercase: /[A-Z]/.test(pw),
    number: /\d/.test(pw),
    special: /[!@#$%^&*]/.test(pw),
  };
  const score = Object.values(checks).filter(Boolean).length;
  return { checks, score, level: score <= 2 ? 'weak' : score === 3 ? 'medium' : 'strong' };
};

(?=...) 는 긍정 전방 탐색(positive lookahead)이다. 실제로 소비하지 않고 해당 패턴이 존재하는지만 확인한다. 여러 조건을 AND로 묶을 때 이 방식을 쓴다.


카드번호 / 주민번호

// 신용카드 번호 (Visa, MasterCard, Amex 등 공통)
const cardRegex = /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12})$/;

// 카드번호 포맷 (4자리씩 하이픈)
const formatCard = (num) => num.replace(/\D/g, '').replace(/(.{4})/g, '$1-').replace(/-$/, '');

formatCard('4111111111111111'); // '4111-1111-1111-1111'

// 주민등록번호 형식 검증 (포맷만, 실제 유효성은 별도 로직 필요)
const ssn = /^\d{6}-[1-4]\d{6}$/;

ssn.test('900101-1234567'); // true
ssn.test('900101-5234567'); // false (두 번째 부분 첫 자리는 1~4)

주민번호는 포맷 검증만으로는 부족하다. 실제 유효성(체크섬 계산)은 별도 로직이 필요하다.


IP 주소

// IPv4
const ipv4 = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/;

ipv4.test('192.168.1.1');   // true
ipv4.test('255.255.255.0'); // true
ipv4.test('256.0.0.1');     // false
ipv4.test('192.168.1');     // false

// IPv6 (간략 버전)
const ipv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;

25[0-5]는 250

255, 2[0-4]\d는 200

249, [01]?\d\d?는 0

199를 커버한다. 이 세 그룹으로 0

255를 완전히 커버한다.


한글 / 영문 / 숫자 체크

// 한글만
const korean = /^[가-힣]+$/;
korean.test('안녕하세요'); // true
korean.test('hello');     // false

// 영문만
const english = /^[a-zA-Z]+$/;

// 한글 + 공백 (이름 등)
const koreanName = /^[가-힣\s]{2,10}$/;

// 영문 + 숫자 (아이디)
const userId = /^[a-zA-Z0-9]{4,20}$/;

// 특수문자 포함 여부 확인
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{}|;':",.<>?\/\\`~]/;
hasSpecial.test('hello!'); // true
hasSpecial.test('hello');  // false

// 공백 제거
const trimAll = (str) => str.replace(/\s+/g, '');
const trimEdges = (str) => str.replace(/^\s+|\s+$/g, ''); // trim()과 동일

숫자 포맷

// 정수만 (부호 포함)
const intRegex = /^[+-]?\d+$/;

// 소수점 포함
const floatRegex = /^[+-]?\d+(\.\d+)?$/;

// 천 단위 콤마 추가
const addComma = (num) => String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ',');

addComma(1234567); // '1,234,567'
addComma(1234.56); // '1,234.56'

// 콤마 제거 후 숫자 변환
const removeComma = (str) => Number(str.replace(/,/g, ''));

\B(?=(\d{3})+(?!\d)) 이 패턴이 핵심이다. \B는 단어 경계가 아닌 위치, (?=(\d{3})+(?!\d))는 뒤에 3의 배수 개 숫자가 있는 위치를 탐색한다. 소수점 뒤에는 적용되지 않는다.


HTML 태그 / 코드 처리

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

stripTags('<p>Hello <strong>World</strong></p>'); // 'Hello World'

// 특정 태그만 추출 (예: src 속성)
const extractSrc = (html) => {
  const matches = html.matchAll(/src="([^"]+)"/g);
  return [...matches].map(m => m[1]);
};

// 스크립트 태그 제거 (XSS 방어 1차)
const removeScript = (html) => html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');

// HTML 엔티티 이스케이프
const escapeHtml = (str) => str.replace(/[&<>"']/g, (m) => ({
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[m]));

HTML을 정규식으로 파싱하는 건 일반적으로 권장하지 않는다. 완전한 파싱이 필요하면 DOMParser나 전용 라이브러리를 써야 한다. 위 코드는 간단한 처리 용도다.


파일 경로 / 확장자

// 파일 확장자 추출
const getExt = (filename) => {
  const match = filename.match(/\.([^.]+)$/);
  return match ? match[1].toLowerCase() : '';
};

getExt('photo.JPG');      // 'jpg'
getExt('archive.tar.gz'); // 'gz'
getExt('noextension');    // ''

// 특정 확장자만 허용 (이미지)
const isImage = /\.(jpg|jpeg|png|gif|webp|svg)$/i;
isImage.test('photo.jpg');  // true
isImage.test('file.pdf');   // false

// Windows/Unix 경로에서 파일명만 추출
const getFilename = (path) => path.match(/[^/\\]+$/)[0];

getFilename('/usr/local/bin/node');  // 'node'
getFilename('C:\\Users\\Luka\\file.txt'); // 'file.txt'

패턴 요약 테이블

카테고리 패턴 플래그 주요 특징
이메일 /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/   TLD 2자 이상
URL /^https?:\/\/(www\.)?...$/   http/https만
전화번호(모바일) /^01[016789]-?\d{3,4}-?\d{4}$/   하이픈 선택적
날짜(ISO) /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/   월/일 범위 체크
비밀번호(강) /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%]).{8,}$/   lookahead 조합
IPv4 /^(?:25[0-5]|2[0-4]\d|[01]?\d\d?\.){3}...$/   0~255 정확히
한글 /^[가-힣]+$/   자모 단독 불가
천 단위 콤마 /\B(?=(\d{3})+(?!\d))/g g replace에 사용
HTML 태그 제거 /<[^>]*>/g g 완전 파싱 불가
파일 확장자 /\.([^.]+)$/ i 마지막 . 이후

삽질 기록

1. test()g 플래그 쓰면 결과가 달라진다

이게 꽤 고약한 버그다.

const re = /\d+/g;

re.test('abc123'); // true
re.test('abc123'); // false  ← ???
re.test('abc123'); // true

g 플래그가 붙은 정규식 객체는 lastIndex를 내부적으로 기억한다. 두 번째 test() 호출 시 이전 매치가 끝난 위치부터 탐색을 시작하기 때문에 이런 일이 생긴다.

해결 방법은 두 가지다:

// 방법 1: 매번 새 정규식 리터럴 사용
/\d+/.test('abc123'); // 매번 새로 생성

// 방법 2: lastIndex 리셋
const re = /\d+/g;
re.test('abc123');
re.lastIndex = 0;
re.test('abc123'); // 이제 true

test()에는 g 플래그가 필요 없는 경우가 대부분이다. 있으면 빼는 게 낫다.


2. match()matchAll()의 차이

const str = 'cat bat sat';

// g 플래그 없으면 첫 번째 매치만, 캡처 그룹 포함
str.match(/[a-z]at/); // ['cat', index: 0, ...]

// g 플래그 있으면 모든 매치, 캡처 그룹 없음
str.match(/[a-z]at/g); // ['cat', 'bat', 'sat']

// matchAll: 모든 매치 + 캡처 그룹도 포함 (g 필수)
[...str.matchAll(/([a-z])(at)/g)];
// [['cat', 'c', 'at', ...], ['bat', 'b', 'at', ...], ['sat', 's', 'at', ...]]

캡처 그룹과 전체 매치를 동시에 뽑아야 할 때는 matchAll()을 써야 한다.


3. 이메일 정규식의 함정

const emailRegex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;

// 이건 실제로 유효한 이메일인데 통과 못 함
emailRegex.test('user@xn--p1ai'); // 퓨니코드 도메인 - 통과는 함
emailRegex.test('"user name"@example.com'); // RFC 허용, 하지만 우리 패턴에서 false

// 이건 통과되면 안 되는데 통과됨
emailRegex.test('user@example.c'); // true (TLD 1자짜리라 안 되는데...)
// 위 패턴에서 {2,}이니까 실제론 false. 확인 필요

// 이건 실제로 통과되면 안 되는 케이스
emailRegex.test('user@@example.com'); // false (정상 동작)
emailRegex.test('.user@example.com'); // true ← 실제로 유효하지 않은 경우

완벽한 RFC 준수 이메일 정규식은 수백 글자가 넘는다. 실무에서는 "서버 전송 후 인증 메일로 최종 확인"이 진짜 검증이고, 정규식은 명백한 오타만 걸러내는 1차 필터로 쓰는 게 맞다.


4. .을 이스케이프 안 하는 실수

// 잘못된 패턴: .이 임의 문자로 해석됨
const wrong = /192.168.0.1/;
wrong.test('192X168Y0Z1'); // true (의도치 않게)

// 올바른 패턴
const correct = /192\.168\.0\.1/;
correct.test('192X168Y0Z1'); // false

도메인이나 IP를 패턴으로 쓸 때 .\.으로 이스케이프하지 않으면 엉뚱한 문자열도 매칭된다. 이건 진짜 자주 하는 실수다.


마무리

정규식은 완벽하게 외울 필요가 없다. 동작 방식을 이해하고, 자주 쓰는 패턴을 어디서 찾는지 알면 된다.

이 글의 패턴들은 실무에서 충분히 쓸 수 있는 수준이다. 더 복잡한 케이스가 생기면 Regex101에서 실시간으로 테스트하고 디버깅하는 게 가장 빠르다. 패턴 설명까지 자동으로 나온다.

한 가지 원칙만 기억한다. 정규식은 포맷 검증에 쓰고, 로직 검증은 코드로 한다. 날짜 유효성, 주민번호 체크섬, 카드번호 유효성 같은 건 정규식만으로 해결하려 하면 패턴이 지나치게 복잡해지거나 버그가 생긴다.


언어: JavaScript (패턴은 Python, PHP, Java 등에서도 대부분 호환)
테스트 도구: Regex101, RegExr

반응형