같은 정규식으로 test()를 두 번 불렀는데 true와 false가 번갈아 나온 이유
문제 발생
이메일 형식을 검사하는 정규식을 모듈 상단 상수로 뒀습니다.
const EMAIL = /\w+@\w+\.\w+/g;
export function isEmail(value) {
return EMAIL.test(value);
}같은 값을 두 번 넣었는데 결과가 달랐습니다.
isEmail("[email protected]"); // true
isEmail("[email protected]"); // false
isEmail("[email protected]"); // true원인 분석
g 플래그 때문입니다. MDN은 이렇게 설명합니다 — JavaScript RegExp 객체는 global 또는 sticky 플래그가 설정되어 있을 때(예: /foo/g, /foo/y) 상태를 가지며, 이전 매치의 lastIndex를 저장합니다.
test()도 이 값을 읽고 갱신합니다. 첫 호출에서 매치한 뒤 lastIndex가 문자열 끝으로 이동하고, 두 번째 호출은 그 위치부터 찾기 시작해 실패합니다. 실패하면 lastIndex가 0으로 돌아가므로 세 번째는 다시 성공합니다 — true/false가 번갈아 나오는 정확한 이유입니다.
문자열이 바뀌어도 초기화되지 않습니다. 문서가 명시합니다 — lastIndex는 다른 문자열을 검색할 때도 초기화되지 않으며, 기존 lastIndex에서 검색을 시작합니다. 그래서 목록을 순회하며 검증하면 일부 항목만 조용히 실패합니다.
반대 방향의 함정도 있습니다. exec()를 while로 도는 코드에서는 g 플래그가 없으면 lastIndex가 전혀 전진하지 않아 무한 루프가 되고, 문서는 또 while 조건 안에 정규식 리터럴을 두지 말라 — 매 반복마다 정규식이 새로 만들어져 lastIndex가 초기화된다고 경고합니다.
해결 방안
- 검증용 정규식에는
g를 붙이지 않습니다. 존재 여부만 볼 때g는 아무 이득이 없고 상태만 만듭니다. 앞뒤 고정이 필요하면^...$를 씁니다.
const EMAIL = /^\w+@\w+\.\w+$/;- 재사용해야 한다면 호출 전에 위치를 되돌립니다. 상태가 있다는 사실이 코드에 드러납니다.
EMAIL.lastIndex = 0;- 모든 매치가 필요하면
matchAll을 씁니다. 문서 설명대로matchAll은 내부적으로 정규식의 복제본을 만들기 때문에exec()와 달리 문자열을 훑는 동안 원본의lastIndex가 바뀌지 않습니다. 대신g플래그가 없으면TypeError를 던집니다.
for (const match of text.matchAll(/\w+@\w+\.\w+/g)) {
console.log(match[0], match.index);
}-
exec()루프를 직접 돌 때는 정규식을 루프 밖에 둡니다. 그리고 길이 0 매치가 가능한 패턴(/^/gm등)에서는 문서 권고대로lastIndex를 직접 1 늘려 같은 자리에 갇히지 않게 합니다. -
모듈 최상단의 정규식 상수를 의심합니다. 이런 버그는 "가끔 통과하고 가끔 실패하는 검증"으로 나타나 재현이 어렵습니다.
g나y가 붙은 공유 정규식이 있으면 그것부터 봅니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.