본문으로 건너뛰기
개발 머꼬
개발 노트JavaScript
hohyeon.dev30

같은 정규식으로 test()를 두 번 불렀는데 true와 false가 번갈아 나온 이유

  • #Debugging
  • #Engineering Note
  • #JavaScript

문제 발생

이메일 형식을 검사하는 정규식을 모듈 상단 상수로 뒀습니다.

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가 초기화된다고 경고합니다.

해결 방안

  1. 검증용 정규식에는 g를 붙이지 않습니다. 존재 여부만 볼 때 g는 아무 이득이 없고 상태만 만듭니다. 앞뒤 고정이 필요하면 ^...$를 씁니다.
const EMAIL = /^\w+@\w+\.\w+$/;
  1. 재사용해야 한다면 호출 전에 위치를 되돌립니다. 상태가 있다는 사실이 코드에 드러납니다.
EMAIL.lastIndex = 0;
  1. 모든 매치가 필요하면 matchAll을 씁니다. 문서 설명대로 matchAll은 내부적으로 정규식의 복제본을 만들기 때문에 exec()와 달리 문자열을 훑는 동안 원본의 lastIndex가 바뀌지 않습니다. 대신 g 플래그가 없으면 TypeError를 던집니다.
for (const match of text.matchAll(/\w+@\w+\.\w+/g)) {
  console.log(match[0], match.index);
}
  1. exec() 루프를 직접 돌 때는 정규식을 루프 밖에 둡니다. 그리고 길이 0 매치가 가능한 패턴(/^/gm 등)에서는 문서 권고대로 lastIndex를 직접 1 늘려 같은 자리에 갇히지 않게 합니다.

  2. 모듈 최상단의 정규식 상수를 의심합니다. 이런 버그는 "가끔 통과하고 가끔 실패하는 검증"으로 나타나 재현이 어렵습니다. gy가 붙은 공유 정규식이 있으면 그것부터 봅니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.