(Regex) JS
JS Regex
regex에서 “또는 or”을 표현하기 위해 [(bar)(foo)]
처럼 사용하면 ()
로 묶어도 한 char 씩 잘라서 인식하기 때문에 ((bar)|(foo))
를 사용해야 한다.
()가 추가되기 때문에 캡쳐링 그룹 인덱스가 틀려질 수 있다는 점에 주의한다.
JS에서 정규 표현식은 보통 같은 문자열에 대해 반복해서 연산을 수행할 때 주목할 만한 성능상의 이점이 있다. 그래서 indexOf
를 쓰는 것 보다는 정규 표현식을 쓰는 것이 좋다.
정규식은 inline 보다는 변수에 저장해서(stored) 사용하는 것이 좋고, RegExp 객체를 사용하는 것 보다 literal을 사용하는 것이 좋다. 또한, 반복문 안에 넣으면 계속 컴파일 되는 경우가 있으므로 반복문 외부에 선언한다.
literal
1
const re = /ab+c/;
스크립트가 로드되었을 때 정규식의 컴파일을 제공한다. 정규식이 계속 지속 될것이라면 이렇게 사용하는 것이 좋다.
RegExp 객체의 생성자 함수
1
const re = new RegExp("ab+c");
정규식의 런타임 컴파일을 제공한다. 정규식 패턴이 바뀔 것이라고 알고 있거나, 패턴을 잘 모르거나, 사용자 입력과 같이 다른 소스로부터 패턴을 얻어 올 때에 생성자 함수를 쓰는 것이 좋은데, stored literal에 비해 많이 느리다. 역슬래시가 literal에서와는 조금 다른 의미로 보통 두 개의 역슬래시를 사용해야 하며 따옴표도 이스케이프시켜야 한다.
capturing group
= bar foo=
는 객체가 반환되기는 했으나 length == 0
[a-z]\*
가 0개이고 \s
가 붙는 것으로 잡혀서 객체가 반환되기는 된다.
=bar=
는 객체가 반환되지 않는다. ([a-z]\*)\s
가 아예 없는 것으로 잡힌다.
중요한 것은 둘 다 false로 취급된다는 것.
JS는 null, undefined, NaN, 빈 문자열 '', 0, false
를 false
로 본다. * 이 외의 값은 모두 참이다. 해서 test()
를 사용하지 않아도 if(matches[1])
로 검사할 수 있다.
RegExp.$_ / $1…9
RegExp.$_
: 최근에 탐색한 문자열 전체를 저장하고 있다. RegExp.$1...$9
: 가장 최근에 매치된 capturing group을 저장하고 있다.
test()
를 사용했더라도, group으로 묶었다면 RegExp.$1 ~ $9에 저장된다.
또한 exec()
의 반환값 [매칭된 부분 전체, captured group1, captured group2, ...]
은 [RegExp.$_, RegExp.$1, RegExp.$2, ...]
와 동일하다.
그래서 exec()
대신 text()
를 사용해도 구현상 문제는 없으며 text()
를 사용하는 것에 성능 이득을 보려면 캡쳐링 하지 않도록 (?:...)
을 지정해야 한다.
match()
capturing group을 사용하지 않는 경우 일반적으로 match()
를 사용하는게 훨씬 편하다. match()
를 사용할 때 g
flag가 없다면 js exec()
와 동일하게 동작하게 되므로 match()
를 사용하는 의미가 없어진다. 그래서 꼭 patten에 g
flag를 설정해야 한다. match()
메소드는 js exec()
와 달리 전체 문자열에서 매치되는 부분을 한 번에 Array로 반환해 줘서 반복 돌릴 필요가 없다는 장점이 있지만, captured groups을 반환하지 않는다. return type도 Object
로 이루어진게 아닌 String
들로 이루어진 Array
다. 즉 matched substrings를 반환한다. 실제로 match()
를 수행하고 RegExp
를 조사해 보면 RegExp.$_
는 존재하지만, RegExp.$1...9
는 비어있는 string으로 나온다. 그래서 matched substrings을 반환받는 동시에 captured groups도 반환받고 싶다면, exec()
를 반복 호출해야 한다.
exec()
g
flag를 설정하는 경우 몇 가지 주의할 점이 있다.
1
2
var re = /Hello/g;
var matches = re.exec("Hello world! Hello world2! Hello world3!);
직관적으로 생각해 보면 matches
에 [Hello, Hello, Hello]
가 반환될 것 같지만 아니다. 그건 match()
를 사용했을 경우다. exec()
가 반환하는 값은 항상 [매칭된 부분 전체, captured group1, captured group2, ...]
이다.
뒤에 있는 Hello를 잡고 싶다면, global flag를 그대로 두고 exec()
를 연속으로 호출해야 한다. g
flag는 다음 패턴 검색을 위해 어디서 부터 패턴 매칭을 수행해야 하는지를 저장(js RegExp.lastIndex
)해 두는 역할을 하기 때문에 exec()
를 연속으로 호출하면 뒤에 있는 Hello가 차례로 검출된다.
이상하게 동작하는 경우
이상하게 동작하는 경우는 보통 다음 두 가지 이유로 일어난다.
#1 각 exec()
를 호출하는 사이에 문자열이 변경되는 경우
#2 각 exec()
를 호출하는 사이에 정규표현식 객체를 재사용하는 경우.
정상적인 예제는 다음과 같다.
#1 각 exec()를 호출하는 사이에 문자열이 변경되는 경우
exec()
는 호출될 때 마다 매번 string에 접근하기 때문에 replace
등으로 string을 변경하는 경우 주의해야 한다. g
flag가 설정되어 있으면 어디까지 읽었는지 index를 저장해 놓았다가, 다음 js exec()
호출 시 거기서부터 읽어나가게 되는데, replace()
시 문자열의 각 문자의 index가 틀어질 수 있기 때문에 (문자열이 당겨지는 등) 조회가 안될 수 있다.
두 번째에 위치한 Hello world3
이 매치되지 않았다. 이 경우 g
flag가 설정되어 있지 않으면 제대로 매치 되기는 한다. 즉, g
flag가 없어야 제대로 global match 된다. g
flag가 없는 경우 매번 string의 처음부터 매치를 수행하게 되는데 string이 수정되면 수정된 string을 대상으로 매칭을 수행하기 때문에 이전에 매치된 substring이 변경되어 더 이상 매치되지 않기 때문에 그 다음 매치를 찾아주게 되어 제대로 동작하는 것 처럼 보인다. 그러나 이렇게 사용하는건 좋지 못하다.
그래서 문자열을 변경해야 하는 경우, 다음과 같이 원본 문자열을 buffer에 넣어놓고 이를 대상으로 match 검사를 수행하는 것이 좋다.
1
2
var match\_buf = line;
var matches = re.exec(match\_buf);
#2 각 exec()를 호출하는 사이에 정규표현식 객체를 재사용하는 경우.
g
flag가 있지만, 반복을 거듭해도 첫 번째 매치된 문자열만 계속 매치되며 무한 루프에 빠지게 된다. 이는 re
객체를 replace()
에서 재사용해서 어디까지 읽었는지 저장해놓은 index가 초기화되어, g
flag가 없을 때 처럼 매번 처음부터 다시 매치하기 때문이다.
그래서 re
객체를 재사용하는 것이 아니라, 반환된 matched substring과 captured group을 사용해야 한다.
1
line = line.replace(matches[0], "REP"+matches[1]);