회원가입 폼, 주문서, 엑셀에서 넘어온 데이터 정리처럼 "이 값이 형식에 맞나?"를 따져야 하는 곳에는 거의 항상 정규표현식이 들어갑니다. 매번 검색해서 복사해 쓰다 보면 어디서 가져온 패턴인지, 어떤 경우에 틀리는지 모르고 쓰게 되는데, 그게 나중에 버그가 됩니다. 그래서 자주 쓰는 패턴을 모아 두면서 각 패턴이 실제로 무엇을 허용하는지도 같이 적어 둡니다.
메타문자부터 짚고 가기
패턴을 읽을 줄 알아야 고칠 수도 있습니다. 아래 기호만 알면 이 글에 나오는 패턴은 전부 해석할 수 있습니다.
| 기호 | 의미 |
|---|---|
^ |
문자열의 시작 |
$ |
문자열의 끝 |
. |
줄바꿈을 제외한 임의의 문자 하나 |
* |
앞 문자가 0번 이상 반복 |
+ |
앞 문자가 1번 이상 반복 |
? |
앞 문자가 없거나 1번 |
[] |
문자 집합. a-z처럼 -로 범위 지정, 맨 앞에 ^가 오면 "이 문자들 제외" |
{} |
반복 횟수나 범위. {4}, {2,3} |
() |
묶어서 하나의 단위로 취급(그룹) |
\| |
또는(OR) |
\s / \S |
공백 문자 / 공백이 아닌 문자 |
\w / \W |
영문자·숫자·밑줄 / 그 외 문자 |
\d / \D |
숫자([0-9]) / 숫자가 아닌 문자 |
\ |
이스케이프. 일반 문자 앞에 붙으면 특수 의미(\d), 특수 문자 앞에 붙으면 그 문자 자체(\.) |
(?i) |
패턴 앞에 두면 대소문자 구분 안 함(Java 등) |
자바스크립트에서 바로 쓰는 검증 패턴
폼 검증용으로 많이 쓰는 것들입니다. test()가 true면 형식에 맞는 값입니다.
// 모든 공백 체크
var regExp = /\s/g;
// 숫자만 체크
var regExp = /^[0-9]+$/;
// 이메일 체크
var regExp = /^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i;
// 휴대폰 번호
var regExp = /^\d{3}-\d{3,4}-\d{4}$/;
var regExp = /^01([0|1|6|7|8|9]?)-?([0-9]{3,4})-?([0-9]{4})$/;
// 일반 전화번호
var regExp = /^\d{2,3}-\d{3,4}-\d{4}$/;
// 모든 전화번호 검증
var regExp = /^(01[016789]{1}|02|0[3-9]{1}[0-9]{1})-?[0-9]{3,4}-?[0-9]{4}$/;
// 아이디나 비밀번호
var regExp = /^[a-z0-9_]{4,20}$/;
휴대폰 번호를 실제로 검사하는 형태는 이렇습니다.
var regPhone = /^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/;
if (regPhone.test(text) === true) {
alert('입력된 값은 휴대전화번호입니다.');
}
이메일도 같은 방식입니다.
var regEmail = /^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/;
if (regEmail.test(text) === true) {
alert('입력된 값은 이메일입니다.');
}
휴대폰(01x), 지역번호(02, 031 등), 050·070 같은 번호를 한 번에 받고 싶다면 아래 패턴을 씁니다. 하이픈은 넣어도 되고 빼도 되지만, 넣는다면 정해진 자리에 있어야 통과합니다.
var regExp = /^(01[016789]{1}|02|0[3-9]{1}[0-9]{1})-?[0-9]{3,4}-?[0-9]{4}$/;
var tel = '010-1234-1234';
if( !regExp.test(tel) ) {
alert('메시지');
}
주의할 점이 두 가지 있습니다. 첫째, [0|1|6|7|8|9]는 흔히 보이는 표기지만 문자 클래스 안에서 |는 OR가 아니라 그냥 문자입니다. 그래서 01| 같은 값도 통과할 여지가 생깁니다. 의도대로라면 [016789]로 쓰는 게 맞습니다. 둘째, 이메일 패턴 끝의 [a-zA-Z]{2,3}은 최상위 도메인을 2~3글자로 제한합니다. .info, .store처럼 네 글자 이상인 도메인은 걸러져 버리니, 실제 서비스에서는 {2,} 정도로 풀어 두는 편이 안전합니다.
jQuery로 입력 중에 숫자만 남기기
검증을 제출 시점에만 하면 사용자가 다 입력하고 나서야 틀렸다는 걸 압니다. 입력하는 순간 걸러 주려면 키 이벤트에 붙입니다. 키를 뗀 뒤에야 입력란 값이 바뀌어 있기 때문에 keydown이나 keypress가 아니라 keyup을 씁니다(keydown은 누르는 동안, keypress는 눌리는 순간 발생).
$("#" + id).bind("keyup", function(event) {
var regNumber = /^[0-9]*$/;
var temp = $("#" + id).val();
if(!regNumber.test(temp))
{
console.log('숫자만 입력하세요');
$("#"+id).val(temp.replace(/[^0-9]/g,""));
}
});
/^[0-9]*$/로 숫자만 있는지 보고, 아니면 replace(/[^0-9]/g, "")로 숫자 아닌 문자를 전부 지워서 다시 넣습니다. 한글이나 영문, 기호를 쳐도 바로 사라지니 결과적으로 숫자만 입력됩니다.
금액처럼 세 자리마다 쉼표를 찍어 보여주고 싶다면 위 코드에서 val()에 넣는 줄만 아래로 바꾸면 됩니다.
$("#"+id).val(temp.replace(/\B(?=(\d{3})+(?!\d))/g, ","));
전화번호 입력에 하이픈을 자동으로 넣는 것도 그룹 치환으로 됩니다. 02 지역번호, 01x 휴대폰, 그 외 세 자리 국번을 앞 그룹으로 잡고, 마지막 네 자리를 따로 떼어 $1-$2-$3으로 이어 붙이는 방식입니다.
/(^02.{0}|^01.{1}|[0-9]{3})([0-9]+)([0-9]{4})/g
// 입력값에 하이픈 자동 삽입
( /(^02.{0}|^01.{1}|[0-9]{3})([0-9]+)([0-9]{4})/, "$1-$2-$3" );
주민등록번호와 다른 형태의 이메일 패턴도 같이 적어 둡니다.
// 주민등록번호
^(?:[0-9]{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[1,2][0-9]|3[0,1]))-[1-4][0-9]{6}$/g
// 이메일
/^[-A-Za-z0-9_]+[-A-Za-z0-9_.]*[@]{1}[-A-Za-z0-9_]+[-A-Za-z0-9_.]*[.]{1}[A-Za-z]{1,5}$/g
주민번호 패턴은 생년월일 자리(월 01~12, 일 01~31)와 뒷자리 첫 숫자(1~4)까지만 봅니다. 실제 존재하는 날짜인지(2월 30일 등)나 체크섬은 확인하지 않으니, 정말 유효한 번호인지는 별도 로직이 필요합니다.
Java의 Pattern과 Matcher
서버 쪽에서 다시 한 번 검증해야 할 때는 java.util.regex를 씁니다. 클라이언트 검증은 우회가 가능하니 서버 검증은 생략하면 안 됩니다. 먼저 콘솔에서 입력받은 값이 숫자일 때만 정수로 바꾸는 예제입니다.
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class OnlyInteger {
public static void main(String[] args) {
Pattern p = Pattern.compile("(^[0-9]*$)");
int onlyNum;
String inputVal;
Scanner iStream = new Scanner(System.in);
inputVal = iStream.nextLine();
Matcher m = p.matcher(inputVal);
if(m.find())
{
onlyNum = Integer.parseInt(inputVal);
System.out.println(onlyNum);
}
else
{
System.out.println("숫자가 아닌데..?");
}
}
}
Pattern.compile()로 패턴 객체를 만들고, matcher()에 검사할 문자열을 넘겨 Matcher를 얻습니다. find()는 패턴과 맞는 부분이 있으면 true를 돌려줍니다. 패턴 ^[0-9]*$는 시작(^)부터 끝($)까지 0~9 숫자만 개수 제한 없이(*) 있어야 한다는 뜻입니다.
여기서 하나 걸리는 게 있습니다. *는 0번 반복도 허용하니 빈 문자열도 통과합니다. 엔터만 치면 Integer.parseInt("")에서 NumberFormatException이 납니다. 빈 값을 막으려면 * 대신 +를 쓰세요. 숫자가 너무 길어 int 범위를 넘는 경우도 마찬가지로 예외가 나니 자릿수 제한({1,9} 등)을 같이 거는 게 좋습니다.
자주 쓰는 패턴을 Java 문자열에 넣을 때 참고할 목록입니다.
1) 숫자만 : ^[0-9]*$
2) 영문자만 : ^[a-zA-Z]*$
3) 한글만 : ^[가-힣]*$
4) 영어 & 숫자만 : ^[a-zA-Z0-9]*$
5) E-Mail : ^[a-zA-Z0-9]+@[a-zA-Z0-9]+$
6) 휴대폰 : ^01(?:0|1|[6-9]) - (?:\d{3}|\d{4}) - \d{4}$
7) 일반전화 : ^\d{2,3} - \d{3,4} - \d{4}$
8) 주민등록번호 : \d{6} \- [1-4]\d{6}
9) IP 주소 : ([0-9]{1,3}) \. ([0-9]{1,3}) \. ([0-9]{1,3}) \. ([0-9]{1,3})
6~9번은 보기 좋게 하려고 기호 양옆에 공백을 넣은 형태입니다. 정규식에서 공백은 실제 공백 문자로 해석되니 코드에 넣을 때는 공백을 빼야 합니다. 5번 이메일은 점(.)이 들어간 도메인을 못 받으니 간단한 테스트용으로만 쓰세요.
Matcher를 따로 만들 필요 없이 한 번만 검사하면 되는 경우에는 Pattern.matches()가 간단합니다. 이 메서드는 문자열 전체가 패턴과 맞아야 true입니다.
import java.util.regex.Pattern;
public class EngPattern {
public static void main(String[] args) {
String pattern = "^[a-zA-Z]*$";
String input = "ABzzzDAWRAWR";
boolean i = Pattern.matches(pattern, input);
if(i==true)
{
System.out.println(input+"는 패턴에 일치함.");
}
else
{
System.out.println("패턴 일치하지 않음.");
}
}
}
[a-zA-Z]로 대소문자 알파벳을 모두 허용하니, 알파벳으로만 이루어져 있으면 길이와 상관없이 통과합니다.
파일 확장자 검사
업로드 파일 확장자를 제한할 때 쓰는 패턴입니다. (?i)를 붙여서 ABC.TXT처럼 대문자로 들어와도 통과하게 했습니다.
package Pattern;
import java.util.regex.Pattern;
public class ExtendtionPattern {
public static void main(String[] args) {
String pattern = "^\\S+.(?i)(txt|pdf|hwp|xls)$";
String input = "abc.txt";
boolean i = Pattern.matches(pattern, input);
if(i==true)
{
System.out.println(input+"는 패턴에 일치함.");
}
else
{
System.out.println("패턴 일치하지 않음.");
}
}
}
Java 문자열 안에서는 \ 자체를 표현하려고 \\로 두 번 써야 해서 \\S가 정규식의 \S(공백 아닌 문자)가 됩니다. 뒤의 (txt|pdf|hwp|xls)는 네 확장자 중 하나만 허용합니다.
다만 이 패턴에는 함정이 있습니다. 가운데 .이 이스케이프되지 않아서 "점"이 아니라 "아무 문자 하나"로 해석됩니다. 그래서 점이 없는 abctxt 같은 이름도 통과합니다. 확장자 앞의 점을 정확히 요구하려면 아래처럼 \\.로 바꿔야 합니다(수정한 버전입니다).
String pattern = "^\\S+\\.(?i)(txt|pdf|hwp|xls)$";
tar.gz처럼 확장자가 두 단계인 파일은 그룹을 하나 더 둡니다. 아래 예제는 앞의 패턴과 이중 확장자 패턴을 공통 메서드로 함께 검사합니다.
import java.util.regex.Pattern;
public class extensionPattern {
public static void main(String[] args) {
String pattern = "^\\S+.(?i)(txt|pdf|hwp|xls)$";
String input = "Java.pdf";
String pattern2 = "(.+?)((\\.tar)?\\.gz)$";
String input2 = "library.tar.gz";
vaildPattern(pattern, input);
vaildPattern(pattern2, input2);
}
public static void vaildPattern(String pattern, String input)
{
boolean i = Pattern.matches(pattern, input);
if(i==true)
{
System.out.println(input+"는 패턴에 일치함.");
}
else
{
System.out.println("패턴 일치하지 않음.");
}
}
}
(.+?)((\\.tar)?\\.gz)$에서 (\\.tar)?는 .tar가 있어도 되고 없어도 된다는 뜻이라 library.tar.gz와 library.gz를 모두 받습니다. 앞쪽 (.+?)는 게으른 수량자라서 파일명 부분을 최소한으로 잡고 확장자 그룹을 최대한 뒤에서 떼어냅니다. 그룹으로 나눠 두었으니 Matcher의 group(1), group(2)로 파일명과 확장자를 따로 꺼내 쓰는 것도 가능합니다.
확장자 검사는 어디까지나 1차 필터입니다. 파일 이름은 사용자가 얼마든지 바꿀 수 있으니, 업로드를 받는 서버라면 실제 파일 내용(MIME 타입이나 파일 시그니처)까지 확인하는 단계를 하나 더 두는 게 좋습니다.