이메일 유효성 검사가 생각보다 어려운 이유

이메일 주소 형식은 RFC 5322와 그 이전 규격에 의해 정의됩니다. 전체 명세는 기술적으로 유효하지만 놀라울 정도로 특이하게 보이는 주소를 허용합니다:

  • "spaces in quotes"@example.com — 로컬 부분의 인용 문자열
  • user+tag@example.com — 플러스 주소 지정(Gmail 필터링에 널리 사용)
  • user@subdomain.example.co.uk — 여러 하위 도메인과 국가 코드 TLD
  • user@[192.168.1.1] — 도메인으로서의 IP 주소 리터럴
  • very.unusual."@".unusual.com@example.com — 기술적으로 유효하지만 거의 사용되지 않음
  • user@xn--nxasmq6b.com — 국제화된 도메인 이름(Punycode)

완전히 RFC 5322를 준수하는 정규식은 이 모든 것을 처리하면서 기본 형식 규칙에 실패하는 주소는 거부해야 합니다. 결과는 본질적으로 유지보수가 불가능한 1000자짜리 패턴입니다. 실제로 목표는 완전한 RFC 준수가 아니라 — 실제 주소를 차단하지 않으면서 오타를 잡는 것입니다.

간단한 정규식 (99%의 경우에 충분)

대부분의 애플리케이션에서 이 최소한의 패턴이 올바른 선택입니다. @ 앞에 무언가가 있고, 도메인이 있고, TLD가 있는 필수 구조를 검증합니다 — 합법적인 주소에 대한 거짓 음성 없이:

/^[^\s@]+@[^\s@]+\.[^\s@]+$/

분석:

  • ^ — 문자열 시작
  • [^\s@]+ — 공백이나 @가 아닌 하나 이상의 문자 (로컬 부분)
  • @ — 리터럴 @
  • [^\s@]+ — 공백이나 @가 아닌 하나 이상의 문자 (도메인)
  • \. — 리터럴 점
  • [^\s@]+ — 하나 이상의 문자 (TLD)
  • $ — 문자열 끝

올바르게 수용/거부하는 항목:

유효 (수용):

✓ user@example.com
✓ first.last@company.co.uk
✓ user+tag@gmail.com
✓ 123@numbers.org

무효 (거부):

✗ plainstring
✗ @nodomain.com
✗ user @example.com (공백)
✗ user@

주요 약점은 user@@example.coma@b.c(기술적으로 유효하지만 의심스러운)를 수용한다는 것입니다. 대부분의 가입 양식과 API 입력에서 이는 수용 가능한 트레이드오프입니다.

포괄적인 정규식 (RFC 5322 준수)

더 엄격한 유효성 검사가 필요한 경우 — 예를 들어, 무효한 주소가 반송을 유발하고 발신자 평판을 손상시키는 이메일 발송 시스템에서 — 더 철저한 패턴이 필요합니다. 이것은 널리 인용되는 RFC 5322 파생 패턴입니다:

/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

이 패턴은 더 길지만 훨씬 더 많은 케이스를 처리합니다. 주요 부분 분석:

  • 로컬 부분 (@ 앞): 선택적 점으로 구분된 세그먼트가 있는 허용 문자 시퀀스 [^<>()...]+(\.[^<>()...]+)*, 또는 "john doe"@example.com 같은 주소를 위한 인용 문자열 ".+".
  • 도메인 (@ 뒤): 대괄호 안의 IP 주소 리터럴 \[[0-9]{1,3}...\], 또는 최소 2자 TLD를 가진 표준 호스트 이름 ([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}.
  • TLD 최소 2자: 1자 TLD를 거부하면서 .museum이나 .technology 같은 긴 TLD는 수용합니다.

이 패턴이 올바르게 처리하는 추가 케이스:

✓ "quoted string"@example.com
✓ user@[192.168.1.1]
✓ user@subdomain.long-domain.co.uk

여전히 올바르게 거부됨:

✗ user@example (TLD 없음)
✗ user @example.com (@ 앞 공백)
✗ user@@example.com (이중 @)

JavaScript에서의 이메일 유효성 검사

두 패턴을 사용한 실용적인, 복사 가능한 JavaScript 구현입니다:

간단한 유효성 검사 함수

/**
 * 대부분의 사용 사례를 위한 이메일 형식 검증.
 * 빠르고, 가독성 좋으며, 거짓 음성이 매우 적습니다.
 */
function isValidEmail(email) {
  const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return pattern.test(email.trim());
}

// 사용법
isValidEmail("user@example.com");      // true
isValidEmail("not-an-email");          // false
isValidEmail("user+tag@gmail.com");    // true

상세 피드백을 제공하는 엄격한 유효성 검사

function validateEmail(email) {
  const trimmed = email.trim();

  if (!trimmed) {
    return { valid: false, error: "Email is required" };
  }

  if (trimmed.length > 254) {
    return { valid: false, error: "Email address too long (max 254 characters)" };
  }

  if (!trimmed.includes("@")) {
    return { valid: false, error: "Missing @ symbol" };
  }

  const [local, ...domainParts] = trimmed.split("@");
  const domain = domainParts.join("@");

  if (!local || local.length > 64) {
    return { valid: false, error: "Invalid local part (before @)" };
  }

  if (!domain || !domain.includes(".")) {
    return { valid: false, error: "Invalid domain (missing TLD)" };
  }

  const rfcPattern = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

  if (!rfcPattern.test(trimmed)) {
    return { valid: false, error: "Invalid email format" };
  }

  return { valid: true, email: trimmed.toLowerCase() };
}

// 사용법
const result = validateEmail("  User@Example.COM  ");
// { valid: true, email: "user@example.com" }

HTML5 이메일 유효성 검사

JavaScript에 의존하기 전에 브라우저가 무료로 제공하는 것을 고려하십시오. input type="email" 요소에는 양식 제출 시 실행되는 내장 유효성 검사가 있습니다:

<!-- 기본 내장 유효성 검사 -->
<input type="email" name="email" required>

<!-- 더 엄격한 유효성 검사를 위한 사용자 정의 패턴 -->
<input
  type="email"
  name="email"
  required
  pattern="[^\s@]+@[^\s@]+\.[^\s@]+"
  title="유효한 이메일 주소를 입력해 주십시오"
>

브라우저의 type="email" 내장 알고리즘은 HTML5 명세(RFC 5322의 단순화된 하위 집합)를 따릅니다. 명백한 비이메일을 거부하고, 네이티브 유효성 검사 툴팁을 표시하며, JavaScript 없이 작동합니다. pattern 속성을 사용하면 추가 제약 조건을 위에 계층화할 수 있습니다.

알아야 할 제한 사항: 내장 유효성 검사는 양식 제출 시에만 작동하며 사용자 입력 시 실시간으로는 작동하지 않습니다. 실시간 피드백을 위해서는 JavaScript가 필요합니다. 또한 :invalid CSS 의사 클래스를 사용하면 무효한 입력에 스타일을 적용할 수 있지만, :not(:placeholder-shown)도 함께 사용하지 않으면 손대지 않은 빈 필드에서도 작동합니다:

/* 사용자 상호작용 후에만 빨간 테두리 표시 */
input[type="email"]:not(:placeholder-shown):invalid {
  border-color: #ef4444;
  outline-color: #ef4444;
}

input[type="email"]:not(:placeholder-shown):valid {
  border-color: #34d399;
}

일반적인 예외 케이스

대부분의 이메일 유효성 검사 구현에서 문제가 되는 케이스들입니다:

플러스 주소 지정(하위 주소 지정)

user+tag@gmail.com은 유효하며 널리 사용됩니다. Gmail, Outlook 및 대부분의 최신 이메일 제공업체가 필터링을 위해 지원합니다. 정규식은 로컬 부분에서 + 문자를 거부해서는 안 됩니다. 과도하게 제한적인 많은 패턴이 이 부분에서 실패합니다.

하위 도메인

first.last@mail.company.co.uk는 로컬 부분과 도메인 모두에 점이 있는 유효한 이메일입니다. 도메인에 세 개의 수준이 있습니다. 좋은 패턴은 도메인에서 여러 점으로 구분된 레이블을 처리할 수 있어야 합니다.

긴 TLD

TLD는 더 이상 2자 또는 3자로 제한되지 않습니다. .museum, .photography, .technology 및 수백 개의 다른 gTLD가 유효합니다. TLD 최대 길이를 4자 또는 6자로 제한하는 정규식은 거짓 음성을 생성합니다. 최소값(2자)만 적용하고 최대값은 적용하지 마십시오.

국제화된 도메인 (IDN)

도메인 이름에는 Punycode로 인코딩된 비ASCII 문자가 포함될 수 있습니다. user@munchen.de는 유효하며 — 실제 도메인은 ASCII 형식으로 xn--mnchen-3ya.de입니다. 대부분의 정규식 패턴은 Punycode를 직접 유효성 검사할 수 없습니다. IDN 지원이 필요한 경우 전용 이메일 유효성 검사 라이브러리를 사용하십시오.

IP 주소 리터럴

user@[192.168.1.1]은 RFC 5321에 따라 기술적으로 유효하지만 실제로는 거의 사용되지 않습니다. SMTP 구현을 구축하는 것이 아니라면 이 케이스를 안전하게 무시할 수 있습니다.

과도한 유효성 검사를 피하십시오

이 글에서 가장 중요한 실용적 조언입니다: 클라이언트 측 이메일 유효성 검사의 목적은 오타를 잡는 것이지 배달 가능성을 확인하는 것이 아닙니다.

이메일 주소가 실제로 존재하는지 또는 그 받은 편지함이 메일을 수신하는지는 어떤 정규식으로도 알 수 없습니다. 받은 편지함에 실제로 배달해 보아야만 확인할 수 있습니다. 과도한 유효성 검사(정규식이 잘못 무효로 판단하는 주소를 거부)는 실제 사용자를 잃게 합니다. 부족한 유효성 검사(잘못된 형식의 주소를 수용)는 반송 비용이 발생합니다.

올바른 접근 방식은 이중 계층 전략입니다:

  1. 간단한 정규식으로 명백한 형식 오류를 잡습니다(@ 누락, TLD 누락, 공백).
  2. 주소가 실제이고 사용자가 그것을 제어하는지 확인하기 위해 확인 또는 검증 이메일을 보냅니다.

확인 이메일이 유일하게 신뢰할 수 있는 관문입니다. 그 외의 모든 것은 휴리스틱입니다. user@localhosta@b.io를 거부할지 논의하고 있다면 한 발 물러나십시오 — 이메일을 보내고 SMTP 계층에서 처리하도록 하십시오.

인기 있는 이메일 정규식 패턴 비교

가장 흔히 사용되는 패턴의 실용적 비교로, 트레이드오프를 포함합니다:

패턴 유형 장점 단점 커버리지
/^[^\s@]+@[^\s@]+\.[^\s@]+$/ 간단 가독성 좋음, 거짓 음성 최소, 빠름 a@@b.c 수용, 매우 관대함 ~95%
/^[\w.+-]+@[\w-]+\.[\w.]{2,}$/ 일반 짧음, 대부분의 실제 이메일 처리 로컬 부분의 유효한 특수 문자 거부 ~92%
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ 균형 널리 사용됨, 엄격도와 커버리지의 좋은 균형 일부 유효한 인용 문자열 로컬 거부 ~97%
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}...)/ RFC 5322 인용 문자열, IP 리터럴, 긴 TLD 처리 길고, 읽기 어렵고, 여전히 100% RFC 준수는 아님 ~99%

"균형" 패턴 상세

위 표의 세 번째 패턴은 간단한 패턴보다 더 많은 것을 원하지만 전체 RFC 5322 복잡성은 필요하지 않을 때 프로덕션 사용에 가장 실용적인 선택입니다:

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

분석:

  • [a-zA-Z0-9._%+-]+ — 로컬 부분: 영숫자와 ., _, %, +, -
  • @ — 필수 @
  • [a-zA-Z0-9.-]+ — 도메인: 영숫자, 하이픈, 점(하위 도메인 처리)
  • \. — TLD 앞 점 구분자
  • [a-zA-Z]{2,} — TLD: 최소 2글자, 최대 없음(긴 TLD 처리)

직접 만들지 않고 라이브러리 사용

이메일 유효성이 중요한 애플리케이션(트랜잭션 이메일 시스템, B2B SaaS 가입 흐름)의 경우 직접 정규식을 작성하기보다 전용 유효성 검사 라이브러리를 고려하십시오:

// Node.js — validator.js (가장 인기 있음)
import validator from 'validator';
validator.isEmail('user@example.com'); // true

// Node.js — email-validator (경량)
import { validate } from 'email-validator';
validate('user@example.com'); // true

// Python — email-validator (RFC 준수)
# pip install email-validator
from email_validator import validate_email, EmailNotValidError
try:
    info = validate_email("user@example.com")
except EmailNotValidError as e:
    print(str(e))

이메일 정규식 테스트하기

모든 정규식을 이해하는 가장 좋은 방법은 포괄적인 테스트 입력 세트에 대해 실행하는 것입니다 — 수용할 것으로 예상되는 유효한 주소와 거부해야 할 무효한 주소 모두. Regex 테스터를 사용하면 위의 패턴을 붙여넣고, 테스트 세트를 구축하고, 실시간으로 강조 표시된 일치 결과를 확인할 수 있습니다.

이메일 정규식을 실시간으로 테스트하기

이메일 정규식 패턴을 붙여넣고 직접 입력한 데이터로 테스트하십시오. 플래그, 캡처 그룹, 여러 줄 매칭을 지원합니다. 브라우저에서 완전히 실행됩니다.

Regex 테스터 열기 →

관련 개발자 도구