[Javascript] RegExp.prototype.test()

hyewon ji·2023년 4월 26일
0

1.문제의 발견

<input>에 입력된 글자의 byte를 계산하기 위해 for loop 안에서 RegExp.test() 메서드를 호출했을때 예상과 다른 결과를 가져왔다!🧐

문제의 코드는 다음과 같다.

calculateBytes (value) {
  const string = value;
  let byte = 0;
  const REGEX_1BYTE = new RegExp(`[${this.1BYTE_REGEX_STR}]`, 'g'); // 영문, 숫자
  const REGEX_2BYTE = new RegExp(`[${this.2BYTE_REGEX_STR}]`, 'g'); // 한글

  for (let i = 0; i < value.length; i += 1) {
    byte += REGEX_1BYTE.test(value[i]) * 1 + REGEX_2BYTE.test(value[i]) * 2;
  }

  return byte;
},

calculateBytes('대기10'); // 3
calculateBytes('대1기0'); // 6

calculateBytes('대기10')calculateBytes('대1기0')의 결과값이 다르게 나온다고? 뭐가 문제지? RegExp.test()를 잘못 쓰고 있는건가?

2. 문제 파악 - RegExp.prototype.test()란 ?

Regex.prototype.test()는 특정 글자에 정규 표현식에 해당하는 글자가 있는지 여부를 Boolean 값으로 반환해준다.

예시

const regex = new RegExp('[0-9]'); // 0-9를 허용하는 정규 표현식

regex.test('1000'); // true
regex.test('한국'); // false

뭐가 문제인지 MDN에서 RegExp.prototype.test()를 다시 찾아봤고, 개념 설명 아래쪽에 예시가 있어 다시 자세히 보니 오잉? 이게 모지? 싶은게 있었다.

const str = 'table football';

const regex = new RegExp('foo*');
const globalRegex = new RegExp('foo*', 'g');

console.log(regex.test(str));       // 1
// Expected output: true

console.log(globalRegex.lastIndex); 
// Expected output: 0

console.log(globalRegex.test(str)); // 2
// Expected output: true

console.log(globalRegex.lastIndex);
// Expected output: 9

console.log(globalRegex.test(str)); // 3
// Expected output: false

여기서 보면, 첫번째 두번째 globalRegex.test()를 호출시 결과는 true다. 하지만 세번째 호출시 false가 나왔다.

regex 변수와 globalRegex 변수의 차이점은 Regex 인스턴스를 생성시 'g' flag 여부이다.
그럼 'g' flag가 있으면 globalRegex.lastIndex ( console.log(globalRegex.test(str)) 사이의 코드)가 달라져서 예상과 다른 결과가 나오는건가?

아래 조금 더 내려보니 이런 설명이 있었다.

exec()처럼, test()도 전역 탐색 플래그를 제공한 정규 표현식에서 여러 번 호출하면 이전 일치 이후부터 탐색합니다. exec()와 test()를 혼용해 사용하는 경우에도 해당됩니다.

'g' flag가 있는 정규 표현식에 test()를 호출할 때, 일치하는 글자를 찾으면 그 글자의 인덱스를 lastIndex에 저장해 다음 test() 호출시 해당 인덱스부터 탐색을 시작하는 것이다.

참고
exac()도 동일하게 'g'를 사용하면 lastIndex를 저장한다.

3. 문제가 된 코드 분석

문제가 된 코드를 다시 살펴보자

calculateBytes (value) {
  const string = value;
  let byte = 0;
  const REGEX_1BYTE = new RegExp(`[${this.1BYTE_REGEX_STR}]`, 'g'); // 영문, 숫자
  const REGEX_2BYTE = new RegExp(`[${this.2BYTE_REGEX_STR}]`, 'g'); // 한글

  for (let i = 0; i < value.length; i += 1) {
    byte += REGEX_1BYTE.test(value[i]) * 1 + REGEX_2BYTE.test(value[i]) * 2;
  }

  return byte;
},

calculateBytes('대기10'); // 3
calculateBytes('대1기0'); // 6

'대기10' 글자를 for loop 돌리면 발생하는 일 (숫자는 for loop 횟수를 의미한다.)

1. '대'
REGEX_1BYTE.lastIndxREGEX_2BYTE.lastIndx 모두 0이고, 글자를 0번째부터 탐색한다.
'대'가 REGEX_2BYTE에 해당되어 byte = 0 + 2 = 2가된다.
동시에, REGEX_2BYTE.lastIndex에 1이 저장된다. (REGEX_1BYTE.lastIndx는 여전히 0)

2. '기'
REGEX_1BYTE.lastIndx는 0이고, 글자의 0번째부터 탐색, REGEX_2BYTE.lastIndx는 1이고 글자의 첫번째 부터 탐색한다.
'기'는 글자수가 한개이므로 인덱스 1이 존재하지 않는다.
따라서REGEX_1BYTE는 해당하지 않아서 false, REGEX_2BYTE는 탐색할 글자가 없어서 false를 반환한다.
그리고 각 정규 표현식의 lastIndex는 0이 저장된다. (byte는 2)

3. '1'
REGEX_1BYTE.lastIndxREGEX_2BYTE.lastIndx 모두 0이고, 글자를 0번째부터 탐색한다.
'1'이 REGEX_1BYTE에 해당되어 byte = 2 + 1 = 3가된다.
마찬가지로, REGEX_1BYTE.lastIndex에 1이 저장된다. (REGEX_2BYTE.lastIndx는 0)

4. '0'
REGEX_1BYTE.lastIndx는 1이고 글자의 첫번째 부터탐색, REGEX_2BYTE.lastIndx는 0이고, 글자의 0번째부터 탐색한다.
'0'는 글자수가 한개이므로 인덱스 1이 존재하지 않는다.
따라서REGEX_1BYTE는 탐색할 글자가 없어서 false, REGEX_2BYTE는 해당하지 않아서 false를 반환한다.
그리고 각 정규 표현식의 lastIndex는 0이 저장된다. (byte는 3)

이런 이유로 calculateBytes('대기10')calculateBytes('대1기0')의 결과값이 다르게 나온것이다.

4. 문제 해결

즉, RegExp의 lastIndex를 초기화 시켜주는 방법을 찾으면 된다!

REGEX_1BYTE와, REGEX_2BYTEfor loop 안에서 선언해주면 각 for loop 마다 새로운 변수로 선언되기 때문에 lastIndex가 초기화 된다.

결과

calculateBytes (value) {
  const string = value;
  let byte = 0;

  for (let i = 0; i < value.length; i += 1) {
    /* 변경된 코드 시작 */
    const REGEX_1BYTE = new RegExp(`[${this.1BYTE_REGEX_STR}]`, 'g'); // 영문, 숫자
    const REGEX_2BYTE = new RegExp(`[${this.2BYTE_REGEX_STR}]`, 'g'); // 한글
    /* 변경된 코드 끝 */

    byte += REGEX_1BYTE.test(value[i]) * 1 + REGEX_2BYTE.test(value[i]) * 2;
  }

  return byte;
},

calculateBytes('대기10'); // 6
calculateBytes('대1기0'); // 6

calculateBytes('대기10')calculateBytes('대1기0')가 같은 결과를 얻는다!

😎 해결했다 😎

0개의 댓글