~ / posts / developer
smith@lab:~/posts$ cat developer/regex-validation-pitfalls.md

이메일·전화번호·한글 이름 정규식 검증, 정상 사용자를 막는 함정

입력 검증 정규식은 두 방향으로 틀려요. 잘못된 값을 통과시키거나, 멀쩡한 값을 막거나. 개발할 때는 앞쪽만 신경 쓰는데, 실제 서비스에서 문의가 들어오는 건 대부분 뒤쪽이에요. "가입이 안 돼요"라는 문의를 몇 번 받아 보면 검증 패턴을 보는 눈이 달라져요.

결론부터 말하면 정규식은 명백한 오타만 막을 만큼 느슨하게 쓰고, 그 앞에 정규화(trim, NFC, 숫자만 남기기)를, 그 뒤에 실제 확인(인증 메일·SMS)을 두는 게 정답이에요. 이번 글에서는 검색하면 흔히 나오는 이메일 패턴 다섯 개를 정상 주소 12개, 잘못된 주소 10개로 직접 시험해 보고, 전화번호와 한글 이름에서 실제로 사고가 나는 지점을 Node.js 22로 재현해 봤어요.

핵심 요약

엄격한 이메일 정규식은 플러스 주소, 대문자, 서브도메인, 긴 최상위 도메인을 막아요. 직접 시험한 엄격형 패턴은 정상 주소 12개 중 10개를 거부했어요.

전화번호는 숫자만 남기고 국가번호를 정리한 뒤 검사하고, 저장은 숫자만, 표시할 때 하이픈을 붙여요.

한글 이름은 macOS에서 붙여 넣은 NFD(자모 분리) 문자열 때문에 [가-힣]이 실패할 수 있어 NFC 정규화가 먼저예요.

2~4자 같은 이름 길이 제한은 복성·외국인 이름을 막으니 상한만 넉넉히 두세요.

검증 정규식이 틀리는 두 방향과 파이프라인

정규식 하나로 모든 걸 해결하려는 순간 패턴이 복잡해지고, 복잡한 패턴은 정상 사용자를 막아요. 그래서 저는 입력 검증을 네 단계로 나눠요.

입력 검증 4단계: 정규화, 느슨한 형식 검사, 인증 메일이나 SMS 같은 실제 확인, 정규화된 값 저장과 포맷된 표시
정규식은 파이프라인의 한 단계일 뿐
  1. 정규화 — 앞뒤 공백, 보이지 않는 문자, 유니코드 정규화 형식, 하이픈·괄호 같은 장식을 정리해요.
  2. 느슨한 형식 검사 — 명백한 오타(@ 없음, 숫자 자릿수 틀림)만 막아요.
  3. 실제 확인 — 메일이 실제로 도착하는지, 번호로 SMS가 가는지 확인해요. 진짜 유효성은 여기서 판단돼요.
  4. 저장과 표시 분리 — 정규화된 형태로 저장하고, 화면에 보여줄 때 포맷을 입혀요.

이 구조를 머리에 두고 필드별로 볼게요.

이메일 정규식: 흔한 패턴 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 등)로 만들었어요.

이메일 정규식 5종 시험 결과 (정상 12개 · 오류 10개)
정상 주소를 거부잘못된 값을 통과
02.557.510P1 엄격형 · 정상 주소를 거부 10개P1 엄격형 · 잘못된 값을 통과 0개P1 엄격형P2 치트시트형 · 정상 주소를 거부 6개P2 치트시트형 · 잘못된 값을 통과 0개P2 치트시트형P3 흔한 패턴 · 정상 주소를 거부 1개P3 흔한 패턴 · 잘못된 값을 통과 1개P3 흔한 패턴P4 느슨형 · 정상 주소를 거부 0개P4 느슨형 · 잘못된 값을 통과 1개P4 느슨형P5 WHATWG · 정상 주소를 거부 0개P5 WHATWG · 잘못된 값을 통과 2개P5 WHATWG

엄격할수록 사용자를 막고, 느슨할수록 오타가 조금 새어 들어온다

단위: 개 · 자료: Node.js 22.18 에서 직접 만든 테스트 세트로 RegExp.test 실행 (테스트 세트는 본문 참고)
표로 보기
구분정상 주소를 거부잘못된 값을 통과
P1 엄격형100
P2 치트시트형60
P3 흔한 패턴11
P4 느슨형01
P5 WHATWG02

결과를 보면 방향이 분명해요. 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-567801012345678
010 1234 567801012345678
+82 10-1234-567801012345678
+82-010-1234-567801012345678
(010)1234-567801012345678
011-123-45670111234567 (옛 식별번호, 정책에 따라)
010-1234-567null (010은 항상 11자리)
02-123-4567null (유선 번호)

흔히 보는 ^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 시점에 검증하세요.

공통 체크리스트

마지막 항목을 할 때 정규식 테스터가 편해요. 여러 줄에 테스트 값을 쭉 적어 두고 패턴을 고치면, 어느 줄이 매치되고 어느 줄이 빠지는지 한 번에 보여요. 멀티라인 플래그(m)를 켜는 것만 잊지 마세요.

자주 묻는 질문

완벽한 이메일 정규식이 있나요?

실무에서 쓸 만한 완벽한 정규식은 없어요. 표준을 그대로 옮기면 너무 복잡하고, 형식이 맞아도 실제로 메일을 받을 수 있는지는 알 수 없어요. ^[^\s@]+@[^\s@]+\.[^\s@]+$ 정도로 느슨하게 검사하고 인증 메일로 확인하는 게 표준적인 방법이에요.

휴대폰 번호 정규식은 하이픈 포함으로 검사해야 하나요?

하이픈 유무로 검사하지 말고, 먼저 숫자만 남긴 뒤 검사하세요. +82, 공백, 괄호까지 정리하면 사용자가 어떤 형식으로 입력해도 받을 수 있어요. 저장은 숫자만, 표시할 때 하이픈을 붙이는 게 검색과 중복 체크에 유리해요.

한글 이름이 맞는데 [가-힣] 정규식에서 실패해요.

입력이 NFD(자모 분리) 형태이거나 제로폭 공백 같은 보이지 않는 문자가 섞였을 가능성이 커요. normalize('NFC') 로 정규화하고 U+200B 같은 문자를 제거한 뒤 검사해 보세요. 띄어쓰기가 포함된 이름이라면 패턴이 공백을 허용하는지도 확인하세요.

이름 글자 수 제한은 몇 자가 적당한가요?

하한은 1~2자, 상한은 넉넉하게 두는 게 좋아요. 복성에 세 글자 이름이면 5자이고, 외국인 이름의 한글 표기는 10자를 넘기도 해요. DB 컬럼 길이에 맞춘 상한(예: 50자)과 제어 문자 차단 정도면 충분해요.

프런트에서 검증했는데 서버에서도 해야 하나요?

네, 반드시 해야 해요. 프런트 검증은 개발자 도구나 직접 API 호출로 쉽게 우회돼요. 같은 규칙을 서버에서 한 번 더 적용하고, 사용자 경험을 위한 즉각적인 피드백은 프런트에서 하는 구조가 좋아요.

정리

검증 정규식의 목표는 "완벽한 판별"이 아니라 "명백한 오타 거르기"예요. 직접 시험해 보니 엄격한 이메일 패턴은 정상 주소 대부분을 막았고, 느슨한 패턴은 오타 하나만 통과시켰어요. 정규화 → 느슨한 검사 → 실제 확인 → 정규화된 저장, 이 파이프라인만 지키면 "가입이 안 돼요" 문의가 확 줄어요.

정규식 문법 자체가 낯설다면 정규식 기초 문법 치트시트부터 보시고, 복잡한 패턴이 서버를 멈추게 하는 문제는 탐욕적·게으른 수량자와 ReDoS에서 이어서 볼 수 있어요. NFD·NFC 같은 한글 인코딩 이야기는 UTF-8과 EUC-KR 한글 바이트에 더 있어요.

#이메일 정규식#전화번호 정규식#한글 이름 정규식#입력 검증#정규식 검증 실수#NFD 한글#휴대폰 번호 검증
← 이전 글정규식 탐욕적·게으른 매칭 차이와 ReDoS, 서버를 멈추는 패턴다음 글 →액세스 토큰·리프레시 토큰 만료 시간 정하는 법과 갱신 흐름 설계