웹 개발을 하다 보면 사용자 입력값을 검증해야 할 순간이 수도 없이 많다. 이메일 형식이 맞는지, 전화번호가 올바른지, 비밀번호가 정책을 만족하는지 확인해야 하는데, 대부분의 개발자들은 이런 작업을 할 때 라이브러리에 의존하거나 부정확한 if 문으로 때우곤 한다. 다만 정규표현식(regex)의 핵심인 preg_match 함수를 정확히 이해하고 있는 개발자는 생각보다 적다. 이번에는 preg_match의 정확한 동작 원리, 실무에서 자주 쓰이는 패턴들, 그리고 실제 코드 예제를 통해 완벽하게 정리해서 소개하겠다.
1단계: preg_match의 기초 이해하기
preg_match는 PHP의 정규표현식 함수 중 가장 기본이면서 강력한 녀석이다. 주어진 문자열이 특정 패턴과 일치하는지를 판단하고, 일치하면 1을 반환하고 일치하지 않으면 0을 반환한다. 함수의 기본 형태는 다음과 같다.
preg_match($pattern, $subject, $matches);
여기서 $pattern은 정규표현식 패턴, $subject는 검사할 문자열, $matches는 매칭된 결과를 담는 배열(선택사항)이다. preg_match는 첫 번째 매칭 지점을 찾으면 즉시 반환하므로, 여러 번 매칭을 원하면 preg_match_all을 써야 한다.
패턴의 기본 구조
정규표현식 패턴은 슬래시 기호로 감싸인다. 예를 들어 /^[a-zA-Z0-9]+$/ 처럼 작성한다. 패턴 끝의 슬래시 뒤에는 플래그가 올 수 있다.
| 기호 | 의미 | 예제 |
|---|---|---|
| ^ | 문자열의 시작 | /^hello/ (hello로 시작) |
| $ | 문자열의 끝 | /world$/ (world로 끝남) |
| + | 1개 이상 반복 | /a+/ (a가 1개 이상) |
| * | 0개 이상 반복 | /a*/ (a가 0개 이상) |
| ? | 0개 또는 1개 | /colou?r/ (color 또는 colour) |
| [abc] | a, b, c 중 하나 | /[aeiou]/ (모음 하나) |
| [^abc] | a, b, c가 아닌 것 | /[^0-9]/ (숫자가 아닌 것) |
| d | 숫자 (0-9) | /d{3}/ (3자리 숫자) |
| w | 단어 문자 (a-z, A-Z, 0-9, _) | /w+/ (단어) |
| s | 공백 (스페이스, 탭, 줄바꿈) | /s+/ (공백 1개 이상) |
2단계: 실무에서 자주 쓰이는 패턴들
정규표현식은 무한하지만, 웹 개발에서 정말 자주 쓰이는 패턴들은 정해져 있다. 다음 예제들은 실제 프로젝트에서 즉시 복사해서 쓸 수 있다.
이메일 검증
$email = "user@example.com";
if (preg_match("/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/", $email)) {
echo "유효한 이메일";
} else {
echo "잘못된 이메일";
}
// 결과: 유효한 이메일
전화번호 검증 (010-XXXX-XXXX 형식)
$phone = "010-1234-5678";
if (preg_match("/^01[0-9]-d{3,4}-d{4}$/", $phone)) {
echo "유효한 전화번호";
} else {
echo "잘못된 전화번호";
}
// 결과: 유효한 전화번호
비밀번호 강도 검증 (8자 이상, 숫자+대문자+소문자 필수)
$password = "MyPass123";
if (preg_match("/^(?=.*[a-z])(?=.*[A-Z])(?=.*d).{8,}$/", $password)) {
echo "강력한 비밀번호";
} else {
echo "약한 비밀번호";
}
// 결과: 강력한 비밀번호
여기서 (?=...)는 긍정 선행 단언(positive lookahead)으로, 실제로 문자를 소비하지 않고 뒤에 특정 패턴이 있는지만 확인한다.
URL 검증
$url = "https://www.example.com/page";
if (preg_match("/^https?://(www.)?[a-zA-Z0-9.-]+.[a-zA-Z]{2,}(:/.*)?$/", $url)) {
echo "유효한 URL";
} else {
echo "잘못된 URL";
}
// 결과: 유효한 URL
3단계: $matches 배열로 매칭된 데이터 추출하기
preg_match의 세 번째 매개변수 $matches를 사용하면 패턴과 일치한 부분을 추출할 수 있다.
단순 추출
// ✗ 잘못된 방법: 매칭 결과를 추출하지 않음
$text = "제 이름은 Kim입니다";
if (preg_match("/이름은 (w+)이", $text)) {
echo "패턴 매칭됨"; // 매칭 됐는지만 알 뿐, 추출된 값을 모름
}
// ✓ 올바른 방법: 괄호로 그룹화하고 $matches에 저장
if (preg_match("/이름은 (w+)이", $text, $matches)) {
echo "이름: " . $matches[1]; // 결과: 이름: Kim
// $matches[0]은 전체 매칭 문자열 "이름은 Kim이"
// $matches[1]은 첫 번째 그룹 "Kim"
}
복수 그룹 추출
$date = "2024-12-25";
if (preg_match("/^(d{4})-(d{2})-(d{2})$/", $date, $matches)) {
echo "연도: " . $matches[1] . "
";
echo "월: " . $matches[2] . "
";
echo "일: " . $matches[3];
}
// 결과:
// 연도: 2024
// 월: 12
// 일: 25
4단계: 실전 예제 - 사용자 입력 검증 함수 만들기
실무에서는 검증 로직을 반복해서 쓰게 되므로, 함수로 만들어두는 게 좋다.
/**
* 다목적 입력값 검증 함수
*/
function validateInput($type, $value) {
$patterns = [
'email' => "/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/",
'phone' => "/^01[0-9]-d{3,4}-d{4}$/",
'username' => "/^[a-zA-Z0-9_]{4,16}$/", // 4~16자, 영문/숫자/_만 허용
'password' => "/^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[!@#$%^&*]).{8,}$/", // 특수문자 추가
'url' => "/^https?://(www.)?[a-zA-Z0-9.-]+.[a-zA-Z]{2,}(:/.*)?$/",
'number' => "/^d+$/", // 양의 정수만
];
if (!isset($patterns[$type])) {
return false;
}
return preg_match($patterns[$type], $value) === 1;
}
// 사용 예제
echo validateInput('email', 'test@example.com') ? "유효" : "무효"; // 유효
echo validateInput('phone', '010-1234-5678') ? "유효" : "무효"; // 유효
echo validateInput('username', 'user_123') ? "유효" : "무효"; // 유효
echo validateInput('password', 'MyPass@123') ? "유효" : "무효"; // 유효
5단계: 흔한 실수와 주의사항
✗ 실수 1: 반환값을 if 조건으로 쓸 때 === 1과 == true 혼동
// ✗ 잘못된 코드
if (preg_match("/test/", "this is a test")) { // 0, 1, false 반환 가능
echo "매칭됨";
}
// 이 경우 false는 if 조건에서 false로 취급되지만, 명확하지 않음
// ✓ 올바른 코드
if (preg_match("/test/", "this is a test") === 1) {
echo "매칭됨";
}
// 또는
if (preg_match("/test/", "this is a test")) {
echo "매칭됨";
} // 0은 false, 1과 false는 true 취급이므로 실제로는 작동
다만 preg_match는 0(일치 없음), 1(일치), false(에러)를 반환할 수 있으므로, 에러를 구분하고 싶다면 === 1을 명시적으로 사용해야 한다.
✗ 실수 2: 특수 문자 이스케이프 깜빡하기
// ✗ 잘못된 코드: 점(.)을 그대로 사용하면 모든 문자를 의미함
if (preg_match("/test.com/", "test@com")) {
echo "매칭됨"; // 원하지 않게 매칭됨!
}
// ✓ 올바른 코드: 역슬래시로 이스케이프
if (preg_match("/test.com/", "test@com")) {
echo "매칭됨"; // 매칭 안 됨 (올바름)
}
if (preg_match("/test.com/", "test.com")) {
echo "매칭됨"; // 매칭됨
}
✗ 실수 3: 대소문자 구분 무시하기
// ✗ 잘못된 코드: 대소문자를 구분함
if (preg_match("/hello/", "HELLO")) {
echo "매칭됨"; // 매칭 안 됨
}
// ✓ 올바른 코드: i 플래그로 대소문자 무시
if (preg_match("/hello/i", "HELLO")) {
echo "매칭됨"; // 매칭됨
}
✗ 실수 4: 멀티바이트 문자 처리
// ✗ 잘못된 코드: 한글 같은 멀티바이트 문자에서 예상치 못한 동작
$text = "안녕하세요";
if (preg_match("/^.{5}$/", $text)) { // .은 1바이트를 의미
echo "5글자"; // 예상과 다름
}
// ✓ 올바른 코드: u 플래그로 UTF-8 모드 활성화
if (preg_match("/^.{5}$/u", $text)) {
echo "5글자"; // 올바르게 작동
}
마무리: preg_match는 입력 검증의 기초
preg_match는 PHP에서 정규표현식을 다루는 가장 기본이면서 가장 강력한 도구다. 패턴이 복잡해 보일 수 있지만, 한 번 패턴을 만들어두면 프로젝트 전체에서 재사용할 수 있다. 이 글의 검증 함수와 패턴들을 참고해서 자신의 프로젝트에 맞는 함수를 만들어두면, 나중에 사용자 입력값을 검증할 때 일관성 있게 처리할 수 있을 것이다. 무엇보다 정규표현식 패턴 작성 실수를 줄이는 것만으로도 버그를 크게 줄일 수 있다는 점을 잊지 말자.