입력 검증 정규식은 두 방향으로 틀려요. 잘못된 값을 통과시키거나, 멀쩡한 값을 막거나. 개발할 때는 앞쪽만 신경 쓰는데, 실제 서비스에서 문의가 들어오는 건 대부분 뒤쪽이에요. "가입이 안 돼요"라는 문의를 몇 번 받아 보면 검증 패턴을 보는 눈이 달라져요.
결론부터 말하면 정규식은 명백한 오타만 막을 만큼 느슨하게 쓰고, 그 앞에 정규화(trim, NFC, 숫자만 남기기)를, 그 뒤에 실제 확인(인증 메일·SMS)을 두는 게 정답이에요. 이번 글에서는 검색하면 흔히 나오는 이메일 패턴 다섯 개를 정상 주소 12개, 잘못된 주소 10개로 직접 시험해 보고, 전화번호와 한글 이름에서 실제로 사고가 나는 지점을 Node.js 22로 재현해 봤어요.
핵심 요약
엄격한 이메일 정규식은 플러스 주소, 대문자, 서브도메인, 긴 최상위 도메인을 막아요. 직접 시험한 엄격형 패턴은 정상 주소 12개 중 10개를 거부했어요.
전화번호는 숫자만 남기고 국가번호를 정리한 뒤 검사하고, 저장은 숫자만, 표시할 때 하이픈을 붙여요.
한글 이름은 macOS에서 붙여 넣은 NFD(자모 분리) 문자열 때문에 [가-힣]이 실패할 수 있어 NFC 정규화가 먼저예요.
2~4자 같은 이름 길이 제한은 복성·외국인 이름을 막으니 상한만 넉넉히 두세요.
검증 정규식이 틀리는 두 방향과 파이프라인
정규식 하나로 모든 걸 해결하려는 순간 패턴이 복잡해지고, 복잡한 패턴은 정상 사용자를 막아요. 그래서 저는 입력 검증을 네 단계로 나눠요.
- 정규화 — 앞뒤 공백, 보이지 않는 문자, 유니코드 정규화 형식, 하이픈·괄호 같은 장식을 정리해요.
- 느슨한 형식 검사 — 명백한 오타(@ 없음, 숫자 자릿수 틀림)만 막아요.
- 실제 확인 — 메일이 실제로 도착하는지, 번호로 SMS가 가는지 확인해요. 진짜 유효성은 여기서 판단돼요.
- 저장과 표시 분리 — 정규화된 형태로 저장하고, 화면에 보여줄 때 포맷을 입혀요.
이 구조를 머리에 두고 필드별로 볼게요.
이메일 정규식: 흔한 패턴 5개를 직접 시험해 봤다
이메일 주소 표준(RFC 5321·5322)을 정규식으로 온전히 옮기면 수천 자짜리 괴물이 나와요. 그걸 통과해도 실제로 메일을 받을 수 있는지는 알 수 없고요. 그래서 실무 패턴은 다 "근사치"인데, 근사의 방향에 따라 결과가 크게 달라져요. 검색하면 흔히 나오는 패턴 다섯 개를 골랐어요.
P1 엄격형 ^[a-z0-9]+@[a-z]+\.[a-z]{2,3}$
P2 치트시트형 ^[\w.+-]+@[\w-]+\.[a-z]{2,}$
P3 흔한 패턴 ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
P4 느슨형 ^[^\s@]+@[^\s@]+\.[^\s@]+$
P5 WHATWG 브라우저 <input type="email"> 이 쓰는 HTML 표준 패턴
테스트 세트는 실제로 존재할 수 있는 정상 주소 12개(name+shop@gmail.com, First.Last@Example.COM, user@mail.example.co.kr, hello@studio.photography, o'brien@example.ie 등)와 명백히 잘못된 값 10개(smith, smith@, a b@x.com, a@@x.com, smith@example..com 등)로 만들었어요.
결과를 보면 방향이 분명해요. P1은 오타를 하나도 통과시키지 않았지만 정상 주소 12개 중 10개를 막았어요. 플러스 주소, 대문자, 서브도메인, 하이픈, 4자 이상 최상위 도메인이 전부 걸렸죠. 반면 P4 느슨형은 정상 주소를 하나도 막지 않았고, 통과시킨 오류는 smith@example..com 하나였어요. 이런 오타는 어차피 인증 메일 단계에서 걸러져요.
P5(HTML 표준 패턴)가 a@x, smith@example 처럼 점 없는 도메인을 통과시키는 건 버그가 아니라 의도예요. 사내망의 admin@localhost 같은 주소도 유효하니까요. 공개 서비스라면 여기에 "점이 하나 이상" 조건만 더하면 충분해요.
엄격한 패턴이 자주 막는 정상 주소를 정리하면 이래요.
| 막히는 주소 | 원인 | 실제 사례 |
|---|---|---|
name+shop@gmail.com | 로컬 파트에 + 불허 | 지메일 플러스 주소로 가입처 추적 |
First.Last@Example.COM | 대문자 불허 | 도메인은 대소문자 구분 안 함 |
user@mail.example.co.kr | 서브도메인(점 여러 개) 불허 | 대학·기관·기업 메일 |
hello@studio.photography | 최상위 도메인 {2,3} 제한 | .company, .museum 등 |
o'brien@example.ie | 작은따옴표 불허 | 아일랜드계 성씨 |
이메일은 느슨하게 + 인증 메일로
그래서 실무 결론은 이래요.
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function checkEmail(raw) {
const v = raw.normalize('NFC').trim();
if (v.length > 254) return { ok: false, reason: '너무 깁니다' };
if (!EMAIL.test(v)) return { ok: false, reason: '이메일 형식을 확인해 주세요' };
return { ok: true, value: v }; // 이후 인증 메일 발송
}
254자는 이메일 주소 전체 길이의 실질적 상한(RFC 5321 경로 길이 제한에서 나온 값)이에요. 중복 가입 체크용으로는 도메인 부분만 소문자로 바꿔 저장하는 게 좋아요. 로컬 파트(앞부분)는 표준상 대소문자를 구분할 수 있지만, 실제 대부분의 메일 서버는 구분하지 않아서 서비스 정책으로 정하면 돼요.
전화번호 정규식: 형식보다 정규화가 먼저
휴대폰 번호 패턴을 ^010-\d{4}-\d{4}$ 로 짜면 하이픈 없이 입력한 사람, 공백으로 띄운 사람, +82 를 붙인 사람, 괄호를 쓴 사람이 전부 막혀요. 순서를 바꾸는 게 나아요.
function normalizePhone(input) {
let d = input.replace(/\D/g, ''); // 숫자만
if (d.startsWith('82')) d = '0' + d.slice(2); // +82 10... → 010...
if (d.startsWith('00')) d = d.slice(1); // +82 010... 처럼 0이 겹친 경우
return /^(010\d{8}|01[16789]\d{7,8})$/.test(d) ? d : null;
}
직접 넣어 본 결과예요.
| 입력 | 결과 |
|---|---|
010-1234-5678 | 01012345678 |
010 1234 5678 | 01012345678 |
+82 10-1234-5678 | 01012345678 |
+82-010-1234-5678 | 01012345678 |
(010)1234-5678 | 01012345678 |
011-123-4567 | 0111234567 (옛 식별번호, 정책에 따라) |
010-1234-567 | null (010은 항상 11자리) |
02-123-4567 | null (유선 번호) |
흔히 보는 ^01[016789]\d{7,8}$ 은 010-1234-567 같은 10자리 010 번호를 통과시켜요. 010 번호는 가운데가 항상 4자리라 위처럼 010과 옛 번호를 나눠 쓰는 게 정확해요. 011, 016 같은 옛 식별번호를 받을지는 서비스 정책이에요.
저장은 숫자만 남긴 형태로 하고, 표시할 때 하이픈을 붙여요. 그래야 검색이나 중복 체크가 쉬워요.
'01012345678'.replace(/^(\d{3})(\d{3,4})(\d{4})$/, '$1-$2-$3'); // '010-1234-5678'
한글 이름 정규식: [가-힣]이 실패하는 의외의 이유
한글 이름 검증에 흔히 쓰는 ^[가-힣]{2,4}$ 에는 함정이 세 개 있어요.
첫째, 유니코드 정규화(NFD). macOS의 파일명이나 일부 앱에서 복사한 한글은 "김"이 한 글자(U+AE40)가 아니라 초성·중성·종성 세 글자(U+1100, U+1175, U+11B7)로 분리된 NFD 형태일 수 있어요. 화면에선 똑같이 보이지만 정규식은 실패해요.
const nfd = '김철수'.normalize('NFD');
nfd.length; // 8 (눈에는 3글자)
/^[가-힣]{2,4}$/.test(nfd); // false
/^[가-힣]{2,4}$/.test(nfd.normalize('NFC')); // true
둘째, 보이지 않는 문자. 앞뒤 공백은 trim() 으로 지워지지만, 제로폭 공백(U+200B)은 trim() 으로 안 지워져요. 메신저나 웹 문서에서 복사한 이름에 자주 섞여요.
function cleanText(s) {
return s.normalize('NFC').replace(/[-]/g, '').trim();
}
셋째, 길이 제한. 직접 테스트한 결과 ^[가-힣]{2,4}$ 은 이런 이름을 막았어요.
| 이름 예시 | 결과 | 이유 |
|---|---|---|
| 김철수, 이산, 남궁민수 | 통과 | 2~4자 |
| 황보하늘별 | 거부 | 복성 + 세 글자 이름 = 5자 |
| 제갈 공명 | 거부 | 띄어쓰기 |
| 마이클 잭슨 | 거부 | 외국인 이름, 공백 |
남궁, 황보, 제갈, 선우 같은 복성에 이름 세 글자면 5자예요. 귀화인이나 외국인의 한글 표기는 훨씬 길 수 있고, 영문 이름이 필요한 사용자도 있어요. 이름은 검증을 최소화해야 하는 대표 필드예요. 저는 길이 상한(예: 50자)과 제어 문자·이모지 정도만 막고 나머지는 받아요.
입력 도중의 미완성 글자(ㄱ, 가ㄴ)를 실시간으로 검사하면 사용자가 타이핑하는 중간에 에러가 깜빡거려요. 한글 IME 조합 중에는 compositionend 이후나 blur 시점에 검증하세요.
공통 체크리스트
- 앵커
^$를 붙였는지. 빠지면 부분 일치로 통과해요. - 파이썬이면
re.match대신re.fullmatch.re.match는 앞부분만 맞아도 통과하고,$는 끝의 줄바꿈 하나를 허용해요. - 정규화 먼저: trim, NFC, 보이지 않는 문자 제거, 숫자만 남기기.
- 클라이언트 검증은 편의 기능일 뿐, 서버에서도 같은 검증을 해요.
- 검증 정규식에 g 플래그 금지.
lastIndex때문에 같은 값이 번갈아 통과·실패해요. - 사용자 입력으로 정규식을 만들지 말고, 만들어야 한다면 이스케이프해요. 복잡한 패턴은 백트래킹 폭주(ReDoS)를 일으킬 수 있어요.
- 테스트 케이스를 "통과해야 하는 값"과 "막아야 하는 값" 두 묶음으로 준비해요.
마지막 항목을 할 때 정규식 테스터가 편해요. 여러 줄에 테스트 값을 쭉 적어 두고 패턴을 고치면, 어느 줄이 매치되고 어느 줄이 빠지는지 한 번에 보여요. 멀티라인 플래그(m)를 켜는 것만 잊지 마세요.
자주 묻는 질문
완벽한 이메일 정규식이 있나요?
실무에서 쓸 만한 완벽한 정규식은 없어요. 표준을 그대로 옮기면 너무 복잡하고, 형식이 맞아도 실제로 메일을 받을 수 있는지는 알 수 없어요. ^[^\s@]+@[^\s@]+\.[^\s@]+$ 정도로 느슨하게 검사하고 인증 메일로 확인하는 게 표준적인 방법이에요.
휴대폰 번호 정규식은 하이픈 포함으로 검사해야 하나요?
하이픈 유무로 검사하지 말고, 먼저 숫자만 남긴 뒤 검사하세요. +82, 공백, 괄호까지 정리하면 사용자가 어떤 형식으로 입력해도 받을 수 있어요. 저장은 숫자만, 표시할 때 하이픈을 붙이는 게 검색과 중복 체크에 유리해요.
한글 이름이 맞는데 [가-힣] 정규식에서 실패해요.
입력이 NFD(자모 분리) 형태이거나 제로폭 공백 같은 보이지 않는 문자가 섞였을 가능성이 커요. normalize('NFC') 로 정규화하고 U+200B 같은 문자를 제거한 뒤 검사해 보세요. 띄어쓰기가 포함된 이름이라면 패턴이 공백을 허용하는지도 확인하세요.
이름 글자 수 제한은 몇 자가 적당한가요?
하한은 1~2자, 상한은 넉넉하게 두는 게 좋아요. 복성에 세 글자 이름이면 5자이고, 외국인 이름의 한글 표기는 10자를 넘기도 해요. DB 컬럼 길이에 맞춘 상한(예: 50자)과 제어 문자 차단 정도면 충분해요.
프런트에서 검증했는데 서버에서도 해야 하나요?
네, 반드시 해야 해요. 프런트 검증은 개발자 도구나 직접 API 호출로 쉽게 우회돼요. 같은 규칙을 서버에서 한 번 더 적용하고, 사용자 경험을 위한 즉각적인 피드백은 프런트에서 하는 구조가 좋아요.
정리
검증 정규식의 목표는 "완벽한 판별"이 아니라 "명백한 오타 거르기"예요. 직접 시험해 보니 엄격한 이메일 패턴은 정상 주소 대부분을 막았고, 느슨한 패턴은 오타 하나만 통과시켰어요. 정규화 → 느슨한 검사 → 실제 확인 → 정규화된 저장, 이 파이프라인만 지키면 "가입이 안 돼요" 문의가 확 줄어요.
정규식 문법 자체가 낯설다면 정규식 기초 문법 치트시트부터 보시고, 복잡한 패턴이 서버를 멈추게 하는 문제는 탐욕적·게으른 수량자와 ReDoS에서 이어서 볼 수 있어요. NFD·NFC 같은 한글 인코딩 이야기는 UTF-8과 EUC-KR 한글 바이트에 더 있어요.