[JavaScript] 정규 표현식, Regular Expressions(regexp) in JavaScript

참고 문서

개요

정규 표현식이란 문자열에서 특정한 캐릭터 조합을 찾아내기 위한 패턴이다. 줄여서 정규식이라고도 한다.

초기화

리터럴 표기법 Literal Notation

/pattern/
/pattern/flags
/MSIE/.test(window.navigator.userAgent);

Constructor

new RegExp(pattern)
new RegExp(pattern, flags)
RegExp(pattern)
RegExp(pattern, flags)
var re = RegExp('ab+c', 'i');

생성자 방식은 기본적으로 리터럴 표기법과 결과는 같다. 이 방법은 정규식 패턴을 변수로 다뤄야할 때 유용하다:

let value = 'abc';
let from = 'C';
let to = '';
value = value.replace(RegExp(from, 'gi'), to); // 'ab씨'

flag

  • g: 전역 검색 플래그. 대상 문자열에서 패턴과 일치하는 모든 부분을 찾는다. 이 플래그가 없으면 정규식 패턴과 일치하는 첫 번째 문자만 찾고 검색을 중지한다.
  • i: 대/소문자 무시.
  • gi: 대/소문자 무시하고 전역 검색.
  • m: 멀티 라인 검색. g 플래그가 문자열 전체에서 검색이라 이 플래그와 기능이 겹치는 것 같지만 다르다. 설명에 따르면 시작 위치와 관련된 검색일 때 m 플래그가 있으면 각 줄의 시작 위치에서 새로 검색을 시작한다.
  • y: Sticky 모드 활성화 플래그. RegExp 인스턴스를 사용할 때만 유효하다. g 플래그와 비슷하지만 약간 다르다. 저 아래에서 추가 설명함.

정규 표현식과 함께 사용하는 함수

String.prototype.search()

string.search(pattern)

정규식 패턴에 첫 번째로 일치하는 문자열의 인덱스를 반환하며, 패턴과 일치하는 문자열이 하나도 없으면 -1을 반환한다.

검색한 문자의 인덱스 반환. 글로벌g 플래그는 무시된다(하나 찾으면 반환하며 종료).

var str = 'javascript';
str.search(/vas/); // 2
str.search(/qwe/); // -1

String.prototype.match()

string.match(pattern)

패턴과 일치하는 문자들을 배열로 반환한다. 일치하는 문자가 없으면 null을 반환한다. g 플래그가 없으면 맨 처음 찾은 하나만 반환하고 종료한다.

var str = 'ecmascript javascript';
str.match(/script/); // [ "script" ]
str.match(/script/g); // [ "script", "script" ]
str.match(/foobar/g); // null

String.prototype.replace()

string.replace(pattern, replacement)

패턴과 일치하는 문자를 replacement로 대체한다.

'123'.replace('2', 'two'); // "1two3" 
'a|b|c'.replace(/\|/g, ''); // "abc"
'abcde'.replace(/\w/g, 'f'); // "fffff"

String.prototype.replaceAll()

string.replaceAll(pattern, replacement)

패턴과 일치하는 모든 문자를 replacement로 대체한다. pattern이 정규식일 때 g 플래그가 없으면 TypeError가 발생한다.

'aaabbaaa'.replaceAll(/a/g, ''); // "bb"
'a'.replaceAll(/a/, ''); // Uncaught TypeError: replaceAll must be called with a global RegExp

RegExp.prototype.test()

regexp.test(testString)

일치하는 패턴이 있으면 true를, 그렇지 않으면 false 반환한다.

/[-|-|-]/.test('한글'); // true

g 플래그가 주어진 Regex 인스턴스는 test()를 호출할 때 내부에서 lastIndex*를 마지막 검색 위치의 바로 다음 인덱스로 업데이트한다:

* RegExp.prototype.lastIndex: 다음 검색 시작 위치를 저장하는 RegExp 인스턴스 프로퍼티

var regex = /a/g;

console.log(regex.lastIndex); // 0 (초기값)

console.log(regex.test('a'));  // true (검색 성공)
console.log(regex.lastIndex);  // 1 (검색 위치의 다음 인덱스로 변경됨)

console.log(regex.test('a'));  // false (이전 검색이 끝난 위치에서 다시 검색해서 false)
console.log(regex.lastIndex);  // 0 (일치하는 게 없으면 0으로 재설정됨)

console.log(regex.test('a'));  // true (다시 처음부터 검색해서 true)
console.log(regex.lastIndex);  // 1

RegExp.prototype.exec()

regexp.exec(testString)

지정된 패턴과 일치하는 패턴을 검색하여 배열을 반환하는데, 일치하는 패턴이 없으면 null을 반환한다.

var rxp = RegExp(/\d+/);
console.log(rxp.exec('1234')); // Array [ "1234" ]
console.log(rxp.exec('abc')); // null

var rxp2 = RegExp(/\d*/);
console.log(rxp2.exec('1234')); // Array [ "1234" ]
console.log(rxp2.exec('abc')); // Array [ "" ] // 빈 문자열 배열이 반환되는 이유는 0개 이상의 숫자를 의미하는 정규식 때문

g 플래그가 있으면 단순 패턴 일치 검색이 아니게 된다.

var regx = RegExp(/\w/, 'g');
var str = 'ab';
console.log(regx.exec(str)); // Array [ "a" ]
console.log(regx.lastIndex); // 1

console.log(regx.exec(str)); // Array [ "b" ]
console.log(regx.lastIndex); // 2

console.log(regx.exec(str)); // null
console.log(regx.lastIndex); // 0

글로벌 모드일 때 패턴과 일치하는 문자를 찾으면 exec()는 (RegExp.prototype.test()와 마찬가지로) 찾은 문자열 배열을 반환하고 regexp.lastIndex를 다음 검색을 시작할 위치(인덱스)로 변경한다. 문자열의 끝에 도달하면 null을 반환하고 lastIndex 프로퍼티는 0으로 변경된다.

위 코드를 예시로 설명하면, 첫 번째 exec() 호출 후 lastIndex는 'a'의 다음 인덱스인 1이다. 두 번째 호출 후에 lastIndex는 'b'의 다음 인덱스인 2가 되고, 세 번째 호출에선 더 찾을 문자가 없으로 lastIndex0이 되는 것이다.

lastIndex는 강제로 변경할 수 있다. 다음 예시를 보자:

var regx = RegExp(/\w/, 'g');
var str = 'ab';

regx.lastIndex = 1
console.log(regx.exec(str)); // Array [ "b" ]

regx.lastIndex = 1
console.log(regx.exec(str)); // Array [ "b" ]

⚠️ 특정 브라우저에서는 콘솔창에 붙여넣은 표현식을 미리 실행한다. 그러니까 작성한 메서드가 실제로는 두 번 이상 호출된다는 말이다(특히 파이어폭스가 그렇다). 따라서 regexp.test() 혹은 regexp.exec()는 콘솔창에서 테스트 하지 말자.

y 스티키(Sticky) 플래그와 g 글로벌 플래그의 차이

y 플래그는 스티키 모드를 활성화하는데, g 플래그와 비슷하게 작동하지만, 패턴 일치가 정확히 lastIndex 부터 발생하지 않으면 검색 실패로 간주한다.

var str = 'abcde';

var regx = /[bd]/g;
regx.lastIndex = 2;
console.log(regx.exec(str)); // Array [ "d" ]
console.log(regx.lastIndex); // 4

var regx2 = /[bd]/y;
regx2.lastIndex = 2;
console.log(regx2.exec(str)); // null
console.log(regx2.lastIndex); // 0

문자열 'abcde'에서 'b' 혹은 'd'를 찾는 패턴일 때 gy의 차이를 보여주는 코드다. g 플래그는 'c'부터 찾기 시작해서 'd'를 반환한다. 이에 비해 y 플래그는 'c'가 'b' 혹은 'd'가 아니므로 바로 null을 반환한다.

🚧 RegExp.compile()

regexp.compile(pattern, [flags])

과거에 정규식을 컴파일할 때 사용하던 메서드. 현재는 인스턴스 생성 시 자동으로 컴파일한다. 따라서 없는 메서드로 취급하면 된다.

정규 표현식에서 사용하는 특수문자

MDN에서는 Assertions, Character classes, Groups and backreferences, Quantifiers로 나누어 설명한다.

  • Assertions: ^, $, \b, \B, x(?=y), x(?!y), (?<=y)x, (?<!y)x
  • Character classes: [xyz], [^xyz], ., \d, \D, \w, \W, \s, \S, \t, \r, \n, \v, \f, [\b], \0, \cX, \xhh, \uhhhh, \u{hhhh}, x|y
  • Groups and backreferences: (x), (?<Name>x), (?:x), \n, \k<Name>
  • Quantifiers: x*, x+, x?, x{n}, x{n,}, x{n,m}

Assertions

^

입력 문자열의 시작 위치를 검색. ^A 는 검색하고자 하는 문장의 시작문자가 A인지를 검사. ^qwe는 정확히 'qwe'로 시작하는지를 검사한다. 여기서 문장은 문자열 전체를 의미할 수도 있고, 줄 바꿈 문자를 기준으로 한 각 라인일 수도 있다. 이것은 정규식 플래그에 따라 달라지는데, 멀티 라인(m) 플래그가 있으면 각 라인마다 지정한 문자로 시작하는지를 검사한다.

검색할 때 자주 쓰인다. 예를 들어 (멀티 라인 플래그가 있다고 가정하고) \n은 모든 줄 바꿈 문자인데 ^\n은 라인의 시작이 줄 바꿈인 것, 그러니까 줄 바꿈 문자만 있는 줄을 의미한다. ### 두 개가 포함된 모든 문자를 찾지만, ^## 라고 작성하면 # 두 개로 시작하고 공백 하나가 바로 이어지는 문자를 찾는다. (문서 내 모든 헤더2를 찾을 때 쓴다)

// 라인이 "x"로 시작하는지 검사
/^x/

$

입력 문자열의 끝 위치를 검색. A$ 는 검색하고자 하는 문장의 마지막문자가 A인지를 검사한다.

qwe$는 문자열이나 라인이 정확히 'qwe'로 끝나는지를 검사한다. 위의 ^와 조합하면 ^qwe$인데 문자열이나 라인에 'qwe'만 존재하는지를 검사한다.

// 라인이 "x"로 끝나는지 검사
/x$/

\b

단어 경계(word boundary) 위치가 일치하는지 검색한다. 단어 경계란 단어 구성 문자(word character)* 뒤에 공백이나 쉼표 같은 비단어 구성 문자(non-word character, 비문자라고 하기도 함)가 이어지는 것을 말한다.

  • 정규식 \babc\b는 '(공백)abc,'과 일치하지만 'qabce,'는 일치하지 않는다.
  • 정규식 er\b는 'never'에서 'er'은 찾지만 'verb'의 'er'은 일치하지 않는다.

* 정규식에서 한글 같은 유니코드는 '단어 구성 문자'로 취급되지 않으니 주의할 것.

\B

\b의 반대격. 단어와 비경계를 찾는다.

정규식이 er\B일 때 'verb'의 'er'과 일치하고, 'never'에서의 'er'은 일치하지 않는다.

x(?=y)

이름은 긍정형 전방 탐색(Positive lookahead)이다.

TODO

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Assertions#other_assertions

x(?!y)

이름은 부정형 전방 탐색(Negative lookahead).

TODO

(?<=y)x

이름은 긍정형 후방 탐색(Positive lookbehind). yx 앞에 있는 경우만 일치하다고 판단한다.

(?<=foo)bar는 정확히 'foo'가 앞에 있는 'bar'와 일치하는 패턴이다. 'foobar'만 일치하고, 'bar', 'fobar', 'fooobar', 'abar'는 일치하지 않는다.

(?<!y)x

이름은 부정형 후방 탐색(Negative lookbehind). yx 앞에 오지 않는 경우만 일치하다고 판단한다.

(?<!foo)bar는 'foo'로 시작하지 않는 'bar'와 일치하는 패턴이다.

Character classes

[xyz]

괄호 안의 문자 중 하나를 검색. 예를 들어 정규식 [a-z]는 'a'부터 'z'까지를 의미한다. abc는 정확히 'abc'를 찾지만 [abc]는 'a', 'b', 'c'를 각각 찾는다. [lmn]은 'cnj'에서 'n'을 찾는 패턴이다.

[^xyz]

제외 문자 집합. [^abc]는 'acn'의 'n'과 일치함

[a-z]

문자 범위. 지정한 범위 안의 문자 검색. [a-z]는 a부터 z사이의 모든 문자(여기선 소문자) 검색

[^a-z]

제외문자 범위 검색. [^a-z]는 'a'부터 'z' 사이에 없는 모든 문자를 검색

.

줄 바꿈\n을 제외한 모든 단일 문자를 검색. 줄 바꿈 문자를 포함한 모든 문자를 찾는 패턴은 [.\n]. *과 조합한 .*로 길이 제한이 없는 모든 문자 일치 패턴으로 자주 사용된다. .*foo.*는 'foo'를 포함하는 라인 한 줄 전부를 찾는다.

\d

0부터 9까지의 아라비아 숫자와 찾는다. [0-9]와 같음

\D

비 숫자 문자를 찾는다. [^0-9] 혹은 [^\d]와 같음

\w

단어 구성 문자(word character). 밑줄을 포함한 기본 라틴 알파벳의 모든 영숫자 문자(alphanumeric character)를 찾는다. [A-Za-z0-9_]와 같음

\W

\w의 반대. 기본 라틴 알파벳의 단어 구성 문자가 아닌 모든 문자. [^A-Za-z0-9_]와 같음

\s

공백, 탭, 폼피드 등의 공백을 검색([ \t\n\r\f\v]와 같음)

\S

\s가 아닌 문자(공백이 아닌 문자)를 검색. [^ \t\n\r\f\v]와 같음

\t

탭 문자를 검색(\x09\cI와 같음)

\r

캐리지 리턴 문자를 검색(\x0d\cM과 같음)

\n

줄 바꿈 문자(linefeed)를 검색(\x0a\cJ와 같음)

\nm

8진수 이스케이프 값이나 역 참조를 나타낸다.

\nm 앞에 최소한 nm개의 캡처된 부분식이 나왔다면 nm은 역참조이며 \nm 앞에 최소한 n개의 캡처가 나왔다면 n은 역참조이고 뒤에는 리터럴 m이 온다. 이 두 경우가 아닐 때 nm0-7 사이의 8진수면 \nm은 8진수 이스케이프 값 nm을 찾는다.

\nml

n0-3 사이의 8진수이고 ml0-7 사이의 8진수면 8진수 이스케이프 값 nml을 찾는다.

\v

수직 탭 문자를 검색(\x0b\cK와 같음)

\f

폼피드 문자(form-feed)를 검색(\x0c\cL과 같음)

[\b]

백스페이스 검색

\cX

X가 나타내는 제어 문자를 찾는다. \cMControl-M, 즉 캐리지 리턴 문자를 찾는다.

\xhh

hh을 검색 여기서 hh은 정확히 두 자리의 16 진수 이스케이프 값이다. \x41은 'A'를 찾고 \x041\x04와 '1'과 같다.

\uhhhh, \u{hhhh}

hhhh는 4 자리의 16진수로 표현된 유니코드 문자. \u00A9는 저작권 기호(ⓒ)를 찾는다.

x|y

x 또는 y를 검색한다는 뜻이다. 파이프|Disjunction 혹은 Alternation이라 부르며 부분합연산(OR)을 수행한다. 예를 들어 c|cginjs는 'c' 또는 'cginjs'와 일치한다.

부분적인 조건을 판단하려면 소괄호와 같이 쓴다. fla(?:vor|me)flavorflame을 찾는다. (여기서 ?:는 소괄호()로 그룹을 만들고 싶지만 캡처는 필요 없을 때 사용함)

Groups and backreferences

()

소괄호()는 Grouping(그룹화)와 Capturing(캡처화)에 사용된다. 그룹화는 어떤 패턴을 하나의 그룹으로 묶는 것, 캡처화는 패턴과 일치하는 문자를 재사용하기 위해 추출 혹은 별도로 기억하는 것을 말한다. ()로 그룹을 만들면 이 그룹은 자동으로 캡처 된다.

그룹화는 세 가지 패턴이 있다:

  • (x): Capturing group
  • (?<Name>x): Named capturing group
  • (?:x): Non-capturing group. 캡처가 필요 없는 그룹을 만들 때 쓴다.

재사용 방법은 여러가지인데, 예를 들어 RegExp.exec()()로 묶인 그룹을 배열로 반환한다:

/(\d{3})-(\d{4})-(\d{4})/.exec('010-1234-5678'); // Array(4) [ "010-1234-5678", "010", "1234", "5678" ]

다른 방법으로 정규식 내부 혹은 String.prototype.replace() 메서드에서 capturing된 그룹을 참조하는 방식이 있다. 이것은 그룹 참조(group reference)라고 한다. TODO 맞는지 확인

정규식 내에서의 그룹 참조는 \숫자 형태로 작성한다:

'aabbcc'.replace(/(.+)\1/, 'a'); // abbcc

String.replace(regexp, replaceText)replaceText에선 그룹 참조를 $숫자 형태로 작성할 수 있다:

// A 혹은 B 혹은 C로 시작하고(이 부분을 그룹1로 묶음) 숫자로 이어지는 패턴을 검색, 그룹1은 그대로 두고 숫자부분은 'R'로 치환
'A1 B2 C3'.replace(/(A|B|C)\d/gm, '$1R'); // "AR BR CR"

// 알파벳으로 시작하고 숫자(이 부분을 그룹1로 묶음)로 이어지는 패턴을 검색, 알파벳은 'R'로 치환하고 숫자는 그대로 출력
'A1 B2 C3'.replace(/[A-Z](\d)/gm, 'R$1'); // "R1 R2 R3" 

// 소괄호 세 개를 '$숫자'로 기억해서 숫자넷-숫자둘-숫자둘 형식으로 replace
'20220301'.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');

// 천 단위마다 쉼표 추가
'1000000'.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');

Quantifiers

Quantifiers는 찾으려는 문자나 문자열 패턴이 몇 번 반복될 것인지를 지정한다. 항상 Quantifiers가 아닌 패턴과 조합해 사용한다.

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

x*

0개 이상의 문자, 즉 길이 제한 없이 모든 문자를 검색한다. {0,}라고 작성한 것과 같다. 항상 어떤 문자 다음에 위치해야 한다.

por*는 'po' 다음에 'r'이 0번 이상 존재하는 문자열을 찾는다. 0번 이므로 'r'이 없는 경우도 포함해 'po', 'por', 'porr' 등을 찾는다.

줄 바꿈을 제외한 모든 문자를 의미하는 .과 조합하여 모든 문자열 검색에 활용할 수 있다. 'qwer'을 포함하는 라인 전체를 선택하려면 .*qwer.*라고 작성한다.

x+

x가 1개 이상인 패턴을 의미한다({1,} 같음). 'x'는 반드시 하나 이상 있어야 하는 필수적인 요소다. w+h는 'wh', 'wwh', 'wwwh' 같은 'h'앞에 'w'가 1개 이상이 있는 패턴과 일치한다.

x?

x가 0개 또는 1개인 패턴을 의미한다({0, 1}과 같음). 'x'는 있을 수도 있고 없을 수도 있는 선택적인 요소다. w?h는 'wh', 'h'만 일치하는 패턴이다. 1개만 있는지 판단하기 때문에 'wwwh'처럼 'w'가 여러 개 있어도 'wh'만 선택한다.

.+와 조합한 .+? 패턴이 있다. .+?는 lazy quantifier 혹은 non-greedy, reluctant, minimal, ungreedy quantifier 등으로 알려져 있다. 간단히 요약하면 가능한 모든 것을 찾는 것과, 한 개 찾으면 관두는 것 정도의 차이다. 예를 들어 'waaagh'에서 wa.+는 'waaagh' 전체를 선택하지만 wa.+?는 'waa'까지만 선택한다. (모든 문자.를 만족하는 'a' 하나만 더 있으면 일치하는 패턴이기 때문)

관련 글: https://stackoverflow.com/questions/14213848/difference-between-and

어쨋든 .+?는 최소 한 개 이상의 문자 중 가장 짧은 것을 의미한다. 이 패턴은 얼핏 보면 .{1}와 같은 것처럼 보이는데, 뒤에 정규식이 조금 더 붙으면 얘기가 달라진다. 가령 'waaagh'에서 wa.{1}h 패턴과 일치하지 않지만 wa.+?h 패턴과는 일치한다. (무조건 하나를 의미하는 게 아니라 '한 개 이상인 것 중 가장 짧은 것'이기 때문이다)

x{n}

정확히 n개의 문자(n은 음수가 아닌 정수)

x{n,}

n개 이상의 문자 검색(n은 음수가 아닌 정수).

c{2,}는 2개 이상인 것을 찾기 때문에 'cnj'에선 아무것도 일치하지 않지만 'bcccccccccf'에선 'c' 9개와 일치한다.

x{n,m}

최소 n개에서 최대 m개 검색. b{1,4}은 'bcccccccccf'의 처음 네 개의 c와 일치한다. 쉼표와 숫자 사이에는 공백을 넣을 수 없음.