프로그램 등을 작성하기 위해 텍스트 기반 에디터를 한번쯤이라도 사용해 봤다면, 이 '정규식' 이라는 용어를 어디선가 본 기억이 있을 것입니다. 본인은 전혀 본 기억이 없다구요..? 그렇다면 '정규식에 의한 치환'은 어떠한가요? 어디선가 본 것 같지 않나요?^^
뭐 어디서 본적도 없다거나, 어디서 본 것 같긴 한데 그게 어디였는지 잘 모르겠다면, 지금 즐겨 사용하는 텍스트 에디터를 실행하고, [편집(Edit)] - [찾아 바꾸기(Find/Replace)] 메뉴로 들어가 보세요. 팝업창의 세부 옵션에 정규식에 의한 치환(Use Regular Expression) 혹은 이와 비슷한 항목이 있을 것입니다.
정규식은 기본적으로 문자열을 나타내고 다루기 위한 도구입니다. 정규식을 활용하면 사람이 문단 속에서 특정 단어나 문맥을 찾아내는 것처럼 다소 복잡한 문자열 연산을 수행할 수 있습니다. 특히, 사람이 직접 입력한 문자열이나 문서 등을 분석하고 처리해서 의미있는 정보를 추출하는 크롤러를 만들기 위해서는 이 정규식에 대한 빠삭한 이해가 필수적이라고 할 수 있습니다.
C언어 등을 활용해서 문자열을 다뤄봤다면, 기본적으로 다음과 같은 문자열 처리 연산들을 다뤄봤을 것입니다.
- 길이(Length) 계산
- 비교(Compare)
- 추출(Extract)
- 접합(Concatenation)
- 자르기(Tokenization)
이들 기초 문자열 처리 연산들은 문자열의 처음부터 끝까지 한 번 훑으면서 처리할 수 있을 정도로 간단합니다. 따라서 C언어 입문 서적에 이들 기능을 직접 구현해 보는 예제들이 심심치 않게 등장합니다.
이들 연산들은 꼭 정규식을 사용하지 않더라도 수행할 수 있는 것들입니다. (오히려 정규식을 사용하지 않는 편이 더 빠르고, 최적화에 도움이 됩니다.)
그렇다면, 다음과 같은 문자열 처리 연산들은 어떤가요?
- 사용자 입력 유효성 검증: 이메일 주소, URL, 전화번호, 주민등록번호
- 패턴에 기반한 감지 및 치환: SQL Injection 탐지, HTML 태그 및 (악성)스크립트 제거
이들은 실제로 정규식이 가장 많이 활용되는 대표적인 사례입니다. 위에 나열된 예시들 중 하나라도 (정규식을 사용하지 않고) 직접 구현해야 한다고 하면, 아마 머리에 쥐가 나기 시작할 것입니다.^^;;
예를 들어, 온라인 회원가입 폼의 클라이언트 사이드 프로그램을 작성하는데 필요한 이메일 주소 검증 루틴을 작성하는 방법에 대해 살펴보도록 하겠습니다.
다음은 이메일 주소의 유효성을 검증해서 True/False를 반환하는 자바스크립트 함수의 예시입니다.
function IsValidEmailAddress(inputStr) { return ((inputStr.indexOf("@") !== -1) && (inputStr.indexOf(".") !== -1)); }
이메일 주소 검사라고 하면, 가장 쉽게 접근할 수 있는 방법은 바로 이메일 주소에 반드시 들어가는 문자인 '@'과 '.'의 유무를 조사하는 것입니다. 위의 예시에서 [String].indexOf(...) 메소드는 [String] 내에 특정 문자열이 존재하는 위치를 반환하며, 문자열이 존재하지 않을 경우 -1을 반환합니다.
겉보기에는 문제가 없어 보입니다. 하지만, 만약 '@foo.com'이나, 극단적으로 '@.'와 같은 입력이 전달된다면 어떨까요?
이들은 분명 잘못된 이메일 주소 형식이지만, 위의 검증 루틴으로는 이들을 감지해낼 수 없습니다. 보다 세분화되고 정밀한 검증 루틴이 필요합니다. '@'과 '.'의 유무를 조사함과 더불어, 이들 문자 사이사이에 한 글자 이상의 문자열이 존재하는지 여부도 함께 검사해야 합니다.
... 그러면 끝일까요?
더불어, 통상적으로 이메일 주소에 사용할 수 없는 특수문자들이 입력되었는지 여부도 검사해야 합니다. 이쯤 되면 본격적으로 머리에 쥐가 나기 시작할 것입니다.
반면, 정규식을 사용한다면, 이들 모든 과정을 다음과 같이 한큐에 해결할 수 있습니다.
(중간에 보이는 외계어(?)에서는 우선 눈을 떼 주세요..^^ 뒤에서 천천히 설명해 드리도록 하겠습니다.)
function IsValidEmailAddress(inputStr) { return new RegExp(/^[a-z0-9_+.-]+@([a-z0-9-]+\.)+[a-z0-9]{2,4}$/i).test(inputStr); }
여기에는 위에서 언급한 검증 과정을 모두 포함하고 있습니다. 세부적으로 나열해 보면, 한 줄로 표현된 정규식 패턴검사 루틴에서는 다음과 같은 검증을 수행합니다.
- '~~~@~~~.~~~' 형식이다.
- '@' 앞에 오는 이메일 계정 이름에는 알파벳과 숫자, '_', '-', '+', '.'의 조합만 허용한다.
- '@'과 '.' 사이의 도메인명은 알파벳과 숫자, '-'의 조합만 허용한다.
- '.' 뒤의 최상위 도메인은 알파벳과 숫자만 사용할 수 있으며, 그 길이는 2~4글자이다.
- 대소문자는 무시한다.
이 정도 검사를 수행한다면, 아무리 귀찮아도 정상적인 형태의 이메일 주소를 입력해야만 검증을 통과할 수 있을 것입니다.
간편하면서도 강력하다는 점이 바로 이 정규식을 활용한 검증의 장점입니다. 좀 더 세분화 해 보면, 정규식을 활용하여 다음과 같은 문자열 연산을 할 수 있습니다.
- 패턴 검사: 패턴과 부합하는지 여부 검증
- 패턴 추출: 패턴과 부합하는 문자열 토큰 추출
- 패턴 치환: 패턴과 부합하는 문자열 토큰 치환
이 글을 보고 있다면, 아마 평소 정규식에 대해 들어보긴 했고 대략 무엇을 하는데 쓰는지는 알지만 뭔가 복잡하고 오묘한(?)것을 또 공부해야 한다는 귀차니즘에 다음번에 공부해야지 하고 미뤄왔을 가능성이 높을 것입니다. (제가 처음에 그랬[...])
그런 분들을 위해 예전에 작성해서 고이 간직해 왔던 정규식에 대한 정리 자료를 한 편의 글로 정리해 보았습니다. 정규식에 대해 전혀 모르더라도 이해할 수 있도록 기초부터 설명한 뒤, 자바스크립트의 정규식 객체를 활용한 예제를 설명하는 순서로 진행하도록 하겠습니다.
정규식의 기본 형태
정규식의 패턴은 두 개의 슬래시(/) 사이에 입력하고, 뒤에는 수식자(Modifier)를 기술합니다. 경우에 따라서 수식자를 생략할 수도 있습니다.
위 예시에서 두 개의 빨간색 슬래시 사이에 있는 외계어가 정규식 패턴이고, 뒤따라 나오는 gi는 수식자입니다.
참고로, 이 패턴은 '-23.534E5'와 같은 공학적 숫자 표현(Engineering Number Format)을 의미합니다.
수식자 (Modifiers)
수식자는 패턴 검색 과정에서 전체적으로 적용할 규칙을 기술합니다. 기본적으로 다음과 같이 세 개의 수식자를 지원하며, 사용하는 정규식 엔진에 따라서 추가적인 옵션을 제공할 수도 있습니다.
- i : 대소문자 무시 (Ignore case)
- g : 모든 패턴 검색 (Global match)
- m : 여러 줄에 걸친 패턴 허용 (Multiline match)
'모든 패턴 검색' 옵션은 패턴 연산에서 일치하는 첫 번째 패턴을 찾았을 경우 검색을 중단할 지 여부를 지정합니다. 이 옵션을 지정하지 않으면 치환 연산에서 처음 발견한 패턴만 치환하고 연산을 종료합니다.
한정자 (Quantifiers)
이제 본격적으로 위에서 언급한 외계어가 구체적으로 무엇을 의미하는지 하나씩 뜯어보도록 하겠습니다.
정규식은 특정 문자열이 아닌, 패턴(Pattern)을 다루는 연산식이기 때문에 이들 패턴을 나타내는 다양한 한정자들을 지원합니다. 한정자들을 조합해서 원하는 패턴을 나타내는 정규식을 작성할 수 있습니다.
.
하나의 문자.
단, 수식자 m이 지정되지 않은 경우 New line 문자(\n)는 무시됩니다.
[]
문자 클래스.
여러 개의 문자를 묶어서 '이들 중 하나', 혹은 '이들 제외'를 의미합니다.
()
문자 그룹.
기본적으로 패턴 추출 연산에서 추출할 패턴을 지정할 때 사용하며, 연산의 우선순위를 지정할 때도 사용할 수 있습니다.
\
Escape.
\t(Tab 문자), \s(White space), \d(숫자)와 같은 특수문자를 나타내거나, \.(Period '.'), \^(^)와 같이 한정자를 일반 문자로 인식시켜야 하는 경우 사용합니다.
|
OR.
예) (http|ftp) : 'http' 또는 'ftp'
^
NOT. / 줄의 시작.
문자 클래스의 전단에 지정해서 '나열된 문자를 제외한 임의의 문자'를 의미합니다.
정규식의 Leading Slash 직후에 사용된 경우, 줄의 시작을 의미합니다.
예) [^!&*;:'"\\/] : !, &, *, ;, :, ', ", \, /를 제외한 임의의 문자
$
줄의 끝.
정규식의 Trailing Slash 직전에 사용되어 줄의 끝을 의미합니다.
예) /^.*$/ : 빈 줄을 포함한 한 줄
*
0회 이상 반복.
직전에 나타낸 문자/문자 클래스/문자 그룹이 0회 이상 반복됨을 의미합니다.
e.g.) .* : 빈 문자열("")을 포함한 아무 문자열
+
1회 이상 반복.
직전에 나타난 문자/문자 클래스/문자 그룹이 1회 이상 반복됨을 의미합니다.
예) .+ : 빈 문자열("")을 제외한 아무 문자열
?
0회 또는 1회.
직전에 나타난 문자/문자 클래스/문자 그룹이 1회 나타나거나 아예 나타나지 않음을 의미합니다.
예) https? : 'http' 또는 'htttps'
{m}
m회 반복.
직전에 나타난 문자/문자 클래스/문자 그룹이 m회 반복됨을 의미합니다.
예) [\d]{3} : '123', '032', '564', ...
{m,}
m회 이상 반복.
직전에 나타난 문자/문자 클래스/문자 그룹이 m회 이상 반복됨을 의미합니다.
예) a{2,}b : 'aab', 'aaab', 'aaaab', ...
{,n}
n회 이하 반복.
직전에 나타난 문자/문자 클래스/문자 그룹이 n회 이하 반복됨을 의미합니다.
예) a{,2}b : 'b', 'ab' 또는 'aab'
{m,n}
m~n회반복.
직전에 나타난 문자/문자 클래스/문자 그룹이 m~n회 반복됨을 의미합니다.
예) a{2,3}b : 'aab' 또는 'aaab'
정규식 예제
자주 사용되는 정규식의 예제를 몇 개 나열해 보았습니다. 위에서 언급한 다양한 한정자들이 어떻게 조합되어 패턴을 나타내는지 주의 깊게 살펴보세요.
Hex color code
예) '#00AABC', '#EFEFEF'
전화번호
예) 02-123-4567
이메일 주소
예) admin-08@gmail.com
웹 페이지 URL
예) http://google.com:80
그림 파일명
예) favicon.png
자바스크립트에서 정규식 사용하기
자바스크립트에서는 정규식 연산을 위한 정규식 객체(Regular Expression Object)를 제공합니다.
정규식 객체(Regular Expression Object) 정의
다음과 같이 new RegExp(...)를 사용하거나
var myRegExp = new RegExp("pattern", "modifier");
혹은 다음과 같이 더 간단한 방법으로 정규식 객체를 정의할 수 있습니다.
var myRegExp = /pattern/modifier
미리 정의되어 고정된 정규식을 사용할 경우 두 번째 방법을 사용하는 편이 훨씬 간편하지만, 사용자 입력 등으로부터 전달된 문자열을 기반으로 정규식을 동적으로 생성해야 하는 경우 반드시 첫 번째 방법을 사용해야 합니다.
다음은 위 두 방법을 사용하여 정의한 정규식 객체입니다.
var imageRegExp = new RegExp("^.+\\.(jpg|png|gif|bmp)$", "i"); var phoneRegExp = /^\d{2,3}-\d{3,4}-\d{4}$/g;
첫 번째 방법을 사용하여 정규식을 정의하는 경우, Escape 문자(\)의 사용에 유의해야 합니다. 위 예시에서 imageRegExp 를 정의할 때 Period(.)문자를 Escape하기 위해 두 개의 \를 사용(\\.)하였습니다.
패턴 검사
test(...) 메소드는 문자열이 정규식 패턴에 부합하는지 여부를 검사합니다.
var matchTest = [RegExpObject].test("String");
다음은 전화번호를 나타내는 정규식을 정의하고 test(...) 메소드를 활용하여 '010-1234-5678' 문자열이 정규식 패턴에 부합하는지 여부를 검사합니다.
var phoneRegExp = /^\d{2,3}-\d{3,4}-\d{4}$/; var isValidPhoneNumber = phoneRegExp.test("010-1234-5678"); alert(isValidPhoneNumber); // true
패턴 추출
exec(...) 메소드는 문자열에서 정규식 패턴에 일치하는 서브문자열(Substring)을 추출합니다.
var matchFind = [RegExpObject].exec("String");
모든 서브문자열를 추출하기 위해서는 exec(...) 메소드가 NULL을 반환할 때까지 반복하여 호출하면 됩니다. 다음 예제는 문자열에서 알파벳이 세 번 반복되는 패턴을 모두 찾아서 출력합니다.
var threeAlphaRegExp = /[a-z]{3}/gi; var testString = "abc-deFgh/!34ij4klm"; while ( matchFind = threeAlphaRegExp.exec(testString) ) { document.write(matchFind + "<br />"); }
* 실행 결과:
abc deF klm
패턴 치환
replace(...) 메소드는 문자열에서 정규식 패턴에 일치하는 서브문자열을 찾아서 지정한 문자열로 치환합니다.
var output = [String].replace([RegExpObject], "replace");
exec(...) 메소드와 달리 replace(...) 메소드는 전역 치환을 하기 위해 반복 호출할 필요는 없으며, 전역 치환을 수행할 지 여부는 전적으로 수식자 g의 유무에 따릅니다.
다음 예제는 문자열에서 알파벳이 세 번 반복되는 패턴을 모두 찾아서 "###"으로 치환합니다.
var findRegExp = /[a-z]{3}/gi; var inputString = "abc-deFgh/!34ij4klm"; document.write(inputString.replace(findRegExp, "###"));
* 실행 결과:
###-###gh/!34ij4###
정규식을 시각적으로 도식화해서 보기
끝으로, 정규식을 제대로 작성했는지 검증하는 과정에서 유용하게 사용할 수 있는 툴을 제공하는 REGEXPER 사이트를 소개하며 글을 마치도록 하겠습니다.
http://regexper.com
정규식이 제대로 동작하는지 검증하려면 다양한 테스트케이스들을 입력해 보아야 하는데, 이 사이트를 활용하면 그런 노다가 없이 직관적인 도식을 보면서 검증하고 디버깅할 수 있습니다.
수학에서 사용하는 수식도 의미는 같지만 다양한 형태로 표현 가능한 수식이 존재하는 것처럼, 정규식도 하나의 패턴을 나타내는데 있어 서로 다른 하나 이상의 정규식이 존재합니다.
무엇을 하던 마찬가지겠지만, 정규식에 익숙해 지는 방법은 직접 작성해 보는 것입니다. (구글로 검색해서 Ctrl+C, Ctrl+V만 하지 말구요!)
혹은, 위 사이트에 Ctrl+V하고 도식화해서 분석하거나 수정해 보는 것도 좋은 공부 방법입니다.