[Regular Expression] 정규표현식 수량자와 문자 매칭 개수 선택

수량자(Quantifier)는 정규 표현식에서 문자의 반복되는 횟수를 표시하는데 사용하는 메타 문자입니다.

수량자 바로 앞의 선행 문자가 얼마나 반복되는지를 표시하며 “?”, “*”, “+” 3개가 사용됩니다.

문자가 0번, 또는 1번만 나오면 “?”, 0번이상 나오면 “*”, 1번 이상 나오면 “+”문자를 뒤에 붙여서 연속 반복 횟수를 표시합니다.

정규 표현식 /abc+/는 “ab”가 나오고 연달아서 “c”가 1번 이상 무조건 나오는 것을 말합니다. 이 정규 표현식은 문자열 “abccccab”에서 “abcccc”가 일치합니다.

선행 문자는 문자 1개, 또는 단어, 정규 표현식일 수 있습니다.

단어, 또는 정규 표현식일 때는 반복할 내용을 괄호로 감싸서 하나의 그룹을 생성하고 이 그룹을 반복하게 됩니다. 괄호로 감싼 그룹을 캡처 그룹(Capture Group)이라고 합니다.

정규 표현식 /(abc)+/는 “abc”가 1번이상 반복되는 것입니다. 문자열 “abcab cab”에서는 “abcabc”가 일치합니다. 앞의 문자열 “abccccab”에서는 “abc” 한 개만 매치됩니다.

선택적 문자 표시(?)

수량자 “?”는 문자가 없거나, 또는 한 개를 일치하는 선택적 표시에 사용합니다. 온/오프 스위치와 같은 기능을 하는 메타 문자입니다.

메타문자 “?”는 수량자 외에도 미리보기 등으로 사용하기 때문에 선행 문자 뒤에 붙었는지 문자 클래스의 여는 대괄호([)뒤에 붙었는지에 따라 다른 의미로 사용됩니다.

선택적인 문자 한 개를 표시하는 것은 선행 문자가 리터럴 문자일 때입니다. 정규 표현식 /ab?c/는 “abc”, 또는 “ac”를 모두 일치합니다.

선택적 문자는 문자 한 개일 수도 있지만 단어, 정규 표현식 일수도 있습니다.

영문 월 표시를 할 때 /Sep(tember)?/로 일치를 하면 9월의 짧은 표현인 “Sep”와 긴 표현인 “September”를 모두 일치합니다.

 

“2023년/4월/19일”과 “2023/04/19”를 모두 일치하려면 /\d{4}년?\/\d{1,2} 월?\/\d{1,2} 일?/처럼 “년”, “월”, “일”을 선택 문자로 일치할 수 있습니다.

빈 문자열 일치 문제(*)

0개 이상 일치 수량자(*)를 사용하면 해당 문자가 없는 경우에도 일치를 하게 됩니다.

하위 문자열을 일치하는 대부분의 경우 문제가 없지만, 특정 조건에서 “*” 수량자는 의도하지 않은 결과를 반환합니다.

/.*/g 정규 표현식은 “test” 문자열에 매치를 하면 몇 개의 결과를 반환할까요?

[“test”, “”] 배열을 반환합니다. * 수량자는 문자가 0개인 경우도 일치를 하므로 일치가 없는 문자열 끝에서 길이가 0인 빈 문자열 1개의 일치가 발생합니다.

이런 극단적인 경우가 아니어도 전체 문자열에 대해서 단일 조건을 일치하는 경우에 “*” 수량자는 유사한 문제를 발생시킵니다.

“010-1234-5678”문자열에서 식별번호, 국번, 번호 3개의 숫자 부분을 정규 표현식으로 일치해서 배열로 반환 받으려고 할 때, 정규 표현식을 /\d*/g처럼 작성할 수 있습니다.

우리의 예상은 [“010”, “1234”, “5678”] 이지만 실제로는 [“010”, “”, “1234”, “”, “5678”, “”] 이 반환됩니다.

 

앞서의 빈 문자열 일치 문제가 여기서도 동일하게 발생합니다.

이 정규 표현식으로 전화번호가 3영역으로 분리되는지를 확인(반환 배열의 길이가 3인지 확인)해 정상 전화번호를 판단하는 조건체크 코드를 작성했다면 이 코드는 항상 false가 됩니다.

이 문제를 피하려면 수량자를 “*”가 아니라 “+”로 사용해야 합니다. 수량자 “*”는 전체 문자열을 단일 조건으로 일치할 때는 특별히 주의를 해야 합니다.

HTML 태그 일치

자바스크립트로 정규 표현식 일치를 할 때 가장 빈번한 매칭이 HTML의 태그 일치입니다.

기본적인 태그 매칭은 /<[a-zA-Z][a-zA-Z0-9]*>/ 입니다. HTML 태그의 첫 번째 문자는 무조건 알파벳이어야 하기 때문에 별도로 문자 한 개를 우선 일치시켜야 합니다.

이 태그 정규 표현식은 <div class=“”> 처럼 속성이 있는 태그는 일치를 할 수 없습니다.

앞의 정규 표현식을 약간 개선해서 /<[a-zA-Z][^>]*>/ 로 수정하면 속성이 있는 태그도 일치합니다. 태그 이름 뒤에 오는 닫는 화살표(>)가 아닌 모든 문자를 선택([^>]*)하는 방식으로 처리할 수 있습니다.

태그 일치가 잘 되지만, 이 정규 표현식은 닫는 태그는 일치하지 않습니다. 정규 표현식을 조금 더 개선해서 여는 화살표(<) 뒤에 슬래시(/)가 선택 문자로 오도록 메타문자를 추가합니다.

완성된 정규 표현식은 /<\/?[a-zA-Z][^>]*>/ 이 됩니다.

역추적(Backtracking)

역추적(Backtracking)과 역참조(Backreference)는 정규 표현식에서 전혀 다른 의미로 사용합니다. 혼동하지 않도록 주의해야 합니다.

역추적은 정규 표현식의 최대 매칭 알고리즘에 대한 것이고, 역참조는 매칭 값의 재사용에 대한 것입니다.

역참조는 뒤에서 자세히 배우게 됩니다.

역추적은 정규 표현식의 문자를 입력 문자열의 왼쪽에서 오른쪽으로 일치해 나가다 더 이상 일치가 안되면 한 글자씩 반대(왼쪽) 방향으로 이동하면서 정규 표현식의 다음 문자가 일치하는지를 확인하는 역방향 탐색 과정을 말합니다.

정규 표현식의 문자는 일치가 되는 최대한의 위치까지 입력 문자열과 일치를 해 나갑니다. 일치가 되지 않는 위치가 되면 비로소 직전에 일치했던 위치로(왼쪽으로 한 문자) 이동한 후, 일치하지 않았던 직전의 문자와 다음 정규 표현식 문자가 일치하는지 확인합니다.

쉽게 말하면 갈 수 있는 만큼 최대한 일치시키면서 앞으로 갔다가, 뒤로 한 칸씩 물러나면서 현재 매칭하는 문자 다음의 정규 표현식 문자가 일치하는지 비교하는 과정을 역추적이라고 합니다.

정규 표현식의 비교 문자가 입력 문자열에서 일치하는 최대한만큼 입력 문자열의 오른쪽으로 일치를 해 나갑니다. 이것을 최대일치(Greedy) 특징을 가지고 있다고 합니다. 영어를 그대로 써서 “그리디하다”, 또는 “탐욕적이다”라고도 합니다. 최대일치 특징은 정규 표현식의 기본 설정 값입니다.

정규 표현식에서만 쓰는 낯선 단어지만 아주 중요한 개념입니다.

예를 들어보겠습니다. 정규 표현식 /ab+c/를 “aabbbcc” 문자열에서 일치를 하면 다음 순서로 탐색이 진행됩니다.

 

먼저 정규 표현식의 처음 문자인 “a”를 입력 문자열의 시작 위치에서부터 비교합니다.

일치하는 결과를 얻었고 “a”는 하나만 일치하면 되므로 정규 표현식의 “b” 문자를 일치시킵니다.

이때 입력 문자열의 일치 위치는 두 번째 문자가 됩니다. “b” != “a”이므로  역추적을 하고 싶지만, 시작 위치이므로 역추적 없이 종료합니다.

이제 입력 문자열의 두번째 문자부터 정규 표현식을 다시 일치시키는 재탐색을 합니다.

정규 표현식의 첫 문자 “a”와 입력 문자열의 두 번째 문자 “a”를 비교합니다. 일치합니다.

다음으로 정규 표현식의 두 번째 문자 “b”와 입력 문자열의 세 번째 문자 “b”를 비교합니다. 일치합니다.

정규 표현식의 문자 “b”는 수량자 “+”로 1회 이상 최대한 연속으로 일치하는 만큼 일치를 합니다. 정규 표현식의 문자 “b”와 입력 문자열의 4번째, 5번째 “b”가 일치합니다.

정규 표현식의 문자 “b”와 입력 문자열의 6번째 문자 “c”를 비교합니다. 일치하지 않습니다.

비교 위치를 역추적해서 마지막 일치했던 위치인 입력 문자열의 한 칸 왼쪽으로 이동한 후 입력 문자열의 “c”를 정규 표현식 문자인 “c”와 비교합니다. 일치합니다.

더 이상 일치할 정규 표현식 문자가 없으므로 탐색을 종료합니다.

최종적으로 탐색 과정이 종료되고 마지막 일치한 입력 문자열은 “abbbc”가 됩니다.

 

정규 표현식과 입력 문자열이 길어지면 이 과정을 계속 반복하면서 최대한 일치하는 문자열 결과를 얻게 됩니다.

최대일치(Greedy), 최소일치(Lazy), 그리고 단방향일치(Po ssessive)

한글로 정확하게 의미가 전달되도록 표현하기 어려운 정규 표현식의 표준 용어입니다.

3가지 방식이 있고, 최초 정규 표현식이 만들어지던 시기부터 사용하던 용어이기 때문에 용어의 의미가 쉽게 전달이 되지 않아도 의미를 이해를 하고 사용해야 합니다.

영어 단어 그대로 “그리디”, “레이지”, “포제시브”로 사용하기도 하고 최대일치 (Greedy), 최소일치(Lazy), 단방향일치(Possessive)로 사용할 수도 있습니다.

간단하게 요약하면 수량자로 반복 일치를 할 때 역추적을 하는 방식의 차이입니다. 최대일치와 최소일치는 역추적을 하지만 얼마나 적극적으로 일치를 하느냐 차이가 있고, 단방향 일치는 역추적을 하지 않습니다.

정규 표현식의 역추적 기본 설정(default)은 최대일치(Greedy)입니다.

최대일치(Greedy)

기본 탐색 설정. 입력 문자열에서 최대한 일치하는 위치까지 많은 일치를 한 후, 더 이상 일치하지 않으면 역추적 과정을 통해 일치했던 문자를 하나씩 버리면서 정규 표현식의 다음 문자 일치를 확인합니다.

앞서의 문자열 매칭 과정을 다이어그램으로 표현하면 다음과 같습니다.

 

최소일치(Lazy)

최소일치는 수량자가 최소한의 일치가 성공하면 수량자 바로 다음에 오는 정규 표현식 비교 문자가 일치할 때까지 “역추적 + 비교”를 하면서 계속 전진합니다.

HTML 태그를 일치하는 최소 정규 표현식인 /<.+>/gi을 문자열 “<div>test</div>”에 적용하면 문자열 전체가 매칭됩니다.

여는 화살표 괄호(<) 뒤에 모든 문자를 선택하는 마침표(.) 메타문자로 1개 이상 일치 (+)를 하면 문자열의 맨 마지막 닫는 화살표 괄호(>) 앞까지 매칭을 합니다. 기본 설정이 최대일치이기 때문에 문자열 중간의 화살표 괄호들은 모두 마침표 메타문자로 매칭됩니다.

수량자 뒤에 “?”를 붙여서 수량자가 최소일치로 동작하도록 /<.+?>/gi로 정규 표현식을 바꿔보겠습니다. 수량자가 최소일치가 되도록 정규 표현식을 /<.+?>/gi 로 변경하면 [“<div>”, “</div>”] 2개의 일치가 반환됩니다.

이유를 요약해서 설명하겠습니다.

첫번째 문자인 “<” 꺽쇠 일치 후 마침표로 태그 이름의 첫 문자인 “d”를 일치합니다. 레이지 모드는 수량자를 최소한의 일치만 하기 때문에 “.+”일치는 여기서 완료되고 다음 문자를 정방향으로 역추적합니다.

닫는 화살표 괄호(>)가 일치할 때까지 비교를 하면서 전진하면 “<div>”까지가 일치하게 됩니다.

“g” 플래그로 전역 일치를 했으므로 끝나는 태그인 “</div>”도 일치가 되면서 2개의 결과를 반환하게 됩니다.

최소일치를 정규 표현식 전체의 수량자에 적용하는 정규 표현식 플래그(Flag)인 대문자 “U”는 자바스크립트에서는 지원되지 않으며 펄 호환 정규 표현식을 지원하는 개발 언어(예: PHP)에서만 지원됩니다.

자바스크립트에서는 수량자 마다 개별적으로 역추적 설정을 해야 합니다.

 

단방향일치(Possessive)

자바스크립트에서는 지원되지 않으며 PHP 등 일부 개발 언어에서만 지원됩니다. 연속 일치를 확인하는 반복 수량자에 대해 역추적을 하지 않도록 합니다. 수량자에만 제한적으로 적용할 수 있습니다.

단방향일치를 사용하면 정규 표현식의 매칭 속도가 훨씬 빨라집니다. 대신 아주 제한적인 조건의 비교에만 적용할 수 있습니다.

수량자 뒤에 “+” 기호를 붙여 “*+”, “++”로 표기해서 단방향 일치임을 표시합니다. 수량자로 최대한 일치시킨 문자를 포기하지 않도록(역추적 안함) 합니다. “aabbbcc” 문자열을 /ab+d/ 정규 표현식으로 일치하면 일치하는 문자열이 없습니다.

다만, “abbb”까지 일치를 찾은 후 “d”를 일치하는 과정에서 역추적을 통해 “ab”까지 되돌아가는 과정이 엔진 내부에서 발생합니다. 최종적으로 역추적이 끝난 시점에 매칭이 종료됩니다.

정규 표현식을 /ab++d/로 변경해서 수량자가 단방향일치가 되도록 하면 “abbb” 일치 후 “d”가 일치하지 않으면 즉시 매칭 과정을 종료합니다. 

문자의 개수를 한정하는 수량자 사용 핵심 요약

 기호

의미 

.

임의의 문자 한 개를 표현합니다. 임의의 문자 1개 뒤에 x 문자가 오는 것을 말합니다. 줄 바꿈(\n, \r) 문자는 제외됩니다.

x+

x 문자가 한번 이상 반복됨을 말합니다.

정규 표현식 /yaho+/은 “yahooooo”는 매치되지만 “yah”는 매치되지 않습니다.

x*

x 문자가 0번 또는 그 이상 반복됨을 말합니다.

정규 표현식 /yaho*/은 “yahooooo”가 매치되고, “yah”또한 매치됩니다.

x?

x 문자가 안나오거나 1번 나옵니다. or 조건입니다.

x{n}

x 문자가 n번 반복됩니다. 정확한 개수(n)만큼 일치해야 합니다.

정규 표현식 /u{2}/은 “seoul”은 매치되지 않으며 “seouuul”은 처음 나오는 “uu”가 매치됩니다.

n은 양수입니다.

x{n,}

x 문자가 n번 이상 반복됨을 말합니다. x 문자가 n번 이상 나오면 모두 매칭됩니다.

정규 표현식 /u{2,}/은 “seouuul”의 “uuu”가 매치됩니다.

n은 양수입니다.

x{n,m}

x 문자가 최소 n번 이상 최대 m 번 이하로 반복됩니다. 0 <= n < m 이어야 합니다.

정규 표현식 /u{2,4}/은 “seouuuuul”의 처음 “uuuu”가 매치됩니다.

x*?
x+?
x??
x{n}?
x{n,}?
x{n,m}?

문자 개수로 매칭을 하는 정규 표현식은 매칭이 가능한 최대의 문자열을 찾지만, 수량자(*, +) 뒤에 물음표를 붙인 정규 표현식은 조건을 만족하는 최소 조건이 되면 매칭을 완료합니다.

정규 표현식 /u+/은 “seouuuuul” 문자열에서 “uuuuu”를 매칭하지만, /u+?/는 최소 조건인 “u”를 매칭하고 종료합니다.

  • x,y는 문자이며, n, m은 0보다 크거나 같은 정수입니다. n <= m입니다.

일치 개수를 한정하는 수량 연산자 {}

개수 제한이 없는 수량자 “*”, “+”와 달리 반복하는 개수를 명확하게 한정할 수 있는 메타 문자입니다.

수량 연산자는 중괄호({}) 안에 반복할 개수 범위를 정해서 표현하며, 다음 3가지 방법으로 사용할 수 있습니다.

수량 연산자

기능

{n}

n >= 0. 선행 문자를 n회 반복합니다. a{3}은 “aaa”와 일치합니다.

{n,}

n >= 0. 선행 문자를 n회 이상 반복. 최소 반복 개수만 한정합니다. n은 0 이상의 정수입니다. {0,}은 메타문자 “*”와 같습니다. {1,}은 메타문자 “+” 와 같습니다.

{n,m}

n >= 0, m >= 0, n <= m. 선행 문자를 n회 이상 m회 이하로 반복. n <= 반복횟수 <= m입니다. n이 0, m이 1이면({0,1}) 수량자 “?”와 같습니다.

 

선행 문자는 문자 한 개일수도 있고, 단어, 또는 정규 표현식일 수도 있습니다. 단어, 정규 표현식일 때는 괄호로 감싸서 캡처 그룹을 만든 후 수량 연산자를 적용해야 합니다.

하나의 문자열로 생성한 주문 상품 정보를 담은 문자열 “상품명1[옵션1][옵션2][옵션 3]”을 정규 표현식으로 매칭할 때 옵션이 0~3개가 가변으로 온다면 /[\w가-힣]+ \s*(\[[\w가-힣]+ \s*\]){0,3}/ 로 매치를 할 수 있습니다.

수량 연산자 {0,3}는 앞의 괄호로 감싼 옵션 문자열 일치 정규 표현식 그룹(“(\[[\w가-힣]+\s*\])”)을 0~3개 사이에서 반복 일치합니다.

숫자 범위를 일치

수량 연산자를 사용하면 숫자 표현의 자릿수를 제한해서 숫자 범위를 일치할 수 있습니다. 10000 ~ 99999 까지의 만 단위 숫자만 일치하려면 /[1-9][0-9]{4}/로 일치할 수 있습니다.

조금 더 명확하게 숫자를 일치하려면 단어 경계를 추가해서 /\b[1-9][0-9]{4}\b/로 일치할 수 있습니다.