정규식 패턴을 표현한 이미지

문자열에서 "이런 모양"을 찾거나 걸러 내는 일은 InStr, Mid, Split을 조합해도 할 수 있습니다. 그런데 조건이 두세 개만 겹쳐도 코드가 금방 지저분해지죠. 전화번호인지 확인하고, 그중 국번만 뽑고, 하이픈 없는 입력도 받아 주는 식이면 더더욱 그렇습니다. 정규식은 이런 조건을 패턴 문자열 하나로 적어 두는 방법입니다. 문법이 처음엔 암호처럼 보이는 게 단점인데, 실제로 자주 쓰는 기호는 몇 개 안 됩니다.

광고

기호는 이 정도만 알면 읽힙니다

패턴은 기본적으로 글자 그대로 맞춰 봅니다. abc는 문자열 "abc"와 일치합니다. 여기에 특별한 의미를 가진 메타 문자가 섞이면서 패턴이 됩니다.

기호 의미
. 아무 문자 하나 (줄바꿈 제외)
^ 입력의 시작
$ 입력의 끝
* 앞 요소가 0번 이상
+ 앞 요소가 1번 이상 ({1,}과 같음)
? 앞 요소가 0번 또는 1번 ({0,1}과 같음)
\| 또는 (A\|B는 A 또는 B)
\ 다음 문자를 메타 문자가 아닌 글자로 취급
{n} 정확히 n번
{n,} n번 이상
{n,m} n번 이상 m번 이하

.*는 "아무 문자열"이라는 뜻이 되어 정말 자주 씁니다. a.b는 a와 b 사이에 반드시 한 글자가 있어야 하고, a..b는 두 글자가 있어야 합니다. 수량자에서 하나 조심할 점은 {2,5}처럼 쉼표 뒤에 공백을 넣지 않아야 한다는 것입니다. {2, 5}로 쓰면 대부분의 엔진이 수량자로 인식하지 않습니다.

대괄호 []는 "이 안의 문자 중 하나"입니다. 몇 글자를 넣든 결과는 한 글자예요. 나열하는 방법([aeho]는 a, e, h, o 중 하나)과 범위로 쓰는 방법([a-d]는 a~d)이 있고, [a-zA-Z]처럼 둘을 섞어도 됩니다. 대괄호 안 맨 앞에 ^를 쓰면 부정이 되어 [^a-zA-Z]는 영문자가 아닌 한 글자를 뜻합니다. 대괄호 밖의 ^(문자열 시작)와 뜻이 완전히 다르니 위치를 잘 봐야 합니다.

자주 쓰는 범위는 축약 기호가 있습니다.

기호 의미
\d / \D 숫자 / 숫자가 아닌 문자
\w / \W 영문자·숫자·밑줄(_) / 그 외
\s / \S 공백 문자(스페이스, 탭, 줄바꿈 등) / 그 외

자바에서는 \p{Alpha}([a-zA-Z]), \p{Digit}([0-9]), \p{Space}([ \t\n\x0B\f\r]) 같은 POSIX 이름 클래스도 쓸 수 있습니다.

점(.)과 [.]는 다릅니다

처음 배울 때 가장 헷갈리는 부분입니다. 그냥 .는 아무 문자 하나지만, 마침표 글자 자체를 찾으려면 \.로 써야 합니다. 그런데 대괄호 안에 넣은 [.]도 마침표 글자 자체를 뜻합니다. 대괄호 안에서는 대부분의 메타 문자가 특수한 의미를 잃기 때문입니다. 그래서 a\.b와 a[.]b는 같은 패턴이고, 둘 다 "a.b"만 찾습니다. 역슬래시를 겹겹이 이스케이프해야 하는 환경에서는 [.] 쪽이 읽기 편해서 일부러 이렇게 쓰기도 합니다.

소괄호는 묶음이자 번호표

()는 안의 내용을 한 덩어리로 묶습니다. (aa)와 a{2}는 같은 문자열 "aa"와 일치하지만, (ab){2}처럼 묶음 단위로 반복시키려면 괄호가 필요합니다. 조합하면 (kor[1-3]{2,}ea){2}는 "kor22eakor312ea" 같은 문자열과 맞습니다.

괄호는 동시에 그룹 번호도 매깁니다. 여는 괄호가 나오는 순서대로 번호가 붙고, 0번은 항상 패턴 전체입니다. (A)(B(C))라면 1번이 (A), 2번이 (B(C)), 3번이 (C)입니다. URL 전체를 찾으면서 프로토콜 부분만 따로 꺼내고 싶다면 그 부분을 괄호로 묶고 1번 그룹을 읽으면 됩니다. 자바에서는 java.util.regex.Matcher의 group(1), VB.NET에서는 Match.Groups(1).Value입니다.

바로 쓰는 패턴들

이메일 주소 검증입니다. 영문·숫자·일부 기호로 된 아이디, @, 도메인, 그리고 점 뒤에 두 글자 이상의 최상위 도메인이 오는지 봅니다.

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

URL은 간단히 확인할 때와 꼼꼼히 확인할 때 두 가지를 써 볼 수 있습니다. 두 번째는 도메인과 확장자, 선택적인 경로·쿼리스트링까지 따집니다.

https?://(?:www\.)?.+
https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?

전화번호도 엄격한 것과 느슨한 것 두 가지입니다. 첫 번째는 국내식 02-123-4567, 010-1234-5678 형태를, 두 번째는 국가 번호(+82), 괄호 친 지역 번호, 점이나 공백 구분까지 받아 줍니다.

\d{2,3}-\d{3,4}-\d{4}
\+?\d{1,3}?[-.\s]?\(?\d{1,3}\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}

비밀번호 복잡성 검사입니다. (?=...)는 전방탐색이라고 해서 "뒤 어딘가에 이게 있어야 한다"는 조건만 확인하고 위치는 움직이지 않습니다. 그래서 소문자, 대문자, 숫자, 특수문자 조건을 나란히 걸 수 있습니다. 여기에 전체 8자 이상 조건이 붙습니다.

^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$

허용 특수문자가 @$!%*?& 일곱 개로 정해져 있다는 점은 알고 쓰세요. 사용자가 #이나 ^를 넣으면 조건을 다 갖춰도 떨어집니다.

소스 코드에서 C 스타일 /* ... */ 주석을 찾는 패턴과, HTML에서 태그를 걷어 내는 패턴입니다.

/\*[^*]*\*+(?:[^/*][^*]*\*+)*/
<[^>]+>

주석 패턴은 .*?로 대충 쓰는 것보다 복잡해 보이지만, 여러 줄에 걸친 주석도 줄바꿈 옵션 없이 정확히 잡습니다. 다만 문자열 리터럴 안에 /*가 들어 있는 경우까지 구분하지는 못합니다. HTML 태그 패턴도 마찬가지로 간단한 텍스트 추출용입니다. 속성 값 안에 >가 들어 있거나 스크립트 블록이 있으면 결과가 어긋날 수 있어요.

VB.NET에서 쓰는 법

.NET은 System.Text.RegularExpressions.Regex 클래스를 씁니다. 검사, 추출, 치환이 각각 메서드 하나입니다.

Imports System.Text.RegularExpressions

' 형식 검사
Dim ok As Boolean = Regex.IsMatch(txtEmail.Text, "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")

' 그룹으로 일부만 꺼내기 : 프로토콜 부분
Dim m As Match = Regex.Match("주소는 https://program1472.com 입니다", "(https?)://\S+")
If m.Success Then
    Debug.Print(m.Groups(0).Value)  ' https://program1472.com (전체)
    Debug.Print(m.Groups(1).Value)  ' https
End If

' 치환 : HTML 태그 걷어 내기
Dim plain As String = Regex.Replace(html, "<[^>]+>", "")

VB.NET 문자열은 역슬래시를 이스케이프하지 않기 때문에 패턴을 그대로 붙여 넣으면 됩니다. 자바나 C#(일반 문자열)에서 넘어온 패턴은 \\d처럼 역슬래시가 두 개씩 들어 있으니, VB로 옮길 때 하나로 줄여야 합니다. 이걸 그대로 두면 "역슬래시 글자 + d"를 찾는 패턴이 되어 아무것도 안 걸립니다.

자바 split()이 이상하게 잘릴 때

자바는 JDK 1.4부터 java.util.regex 패키지로 정규식을 공식 지원합니다. 그래서 String.split()의 인자도 그냥 구분 문자가 아니라 정규식입니다. 이걸 모르면 "split이 가끔 제대로 안 잘린다"고 느끼게 되는데, 대부분 구분자로 메타 문자를 쓴 경우입니다. "ab|cd|ef".split("|")는 |를 "빈 문자열 또는 빈 문자열"로 해석해서 글자 하나하나로 쪼개 버립니다(| 자체도 한 조각이 됩니다). 원하는 결과(ab, cd, ef)를 얻으려면 split("\\|")로 써야 합니다. StringTokenizer는 구분자를 글자 그대로 받기 때문에 이런 문제가 없고, 그래서 둘의 동작이 달라 보이는 겁니다. 점(.)으로 나눌 때도 똑같이 split("\\.")로 써야 합니다.

String.trim()과 비슷한 동작을 정규식으로 흉내 내면 이런 모양이 됩니다.

public String trim() {
    return this.str.replaceAll("^\\p{Space}|\\p{Space}$", "");
}

다만 이 패턴은 앞뒤 공백을 한 글자씩만 지웁니다. 공백이 여러 개면 남아요. 앞뒤 공백을 모두 지우려면 +를 붙여야 합니다.

public String trim() {
    return this.str.replaceAll("^\\p{Space}+|\\p{Space}+$", "");
}

정규식은 한 번 짜 두면 편하지만, 길어질수록 나중에 본인도 못 읽게 됩니다. 패턴이 한 줄을 넘어가기 시작하면 검사를 두세 단계로 나누거나, 패턴 옆에 각 부분이 무엇을 뜻하는지 주석을 달아 두는 걸 권합니다. 그리고 실제 데이터 몇 줄을 정규식 테스트 사이트에 붙여 넣어 통과해야 할 것과 떨어져야 할 것을 함께 확인한 뒤 코드에 넣으면 시행착오가 훨씬 줄어듭니다.