자바스크립트 정규표현식 코딩팁

의외로 잘 모르는 정규표현식 사용법 4가지

자바스크립트에서는 RegExp라는 내장 객체를 통해 정규표현식을 다룬다. 꽤 오래 전부터 존재했고 ES6, 7에 이르기까지 거의 변화가 없으며 관련 메소드도 몇 개 없어서 상대적으로 배우기 쉬운 편이다. 물론, 이 객체를 사용하기 위해 필수적으로 익혀야 하는 정규표현식 자체는 그에 관한 서적이 별도로 존재할 정도로 굉장히 장벽이 높지만.

이 글에서는 정규표현식 객체를 사용함에 있어 잘 안 알려지거나 흔하지 않은 사용 방법 몇 가지를 소개하고자 한다.

일치하는 부분 문자열 추출

URL이 https://taegon.kim/aboutme와 같을 때 프로토콜인 https를 정규표현식으로 추출하고 싶다고 가정해보자. 방법은 여러 가지가 있겠지만 여기서는 간단하게 다음과 같이 작성했다.

/^https?(?=:\/\/)/

이 글에서는 패턴에 관해 설명하지는 않으므로 혹시 위 패턴이 잘 이해되지 않는다면 위에 링크한 RegExp 관련 문서를 읽어보도록 하자. 여하튼, 이 패턴이 일치한다면 프로토콜은 다음과 같이 구할 수 있다.

var url = 'https://taegon.kim/aboutme';
var protocol = /^https?(?=:\/\/)/.exec( url );
console.log( protocol ); // [ 'https' ]

exec 메소드에서 반환하는 값은 일치하는 문자열의 배열이고 이 배열의 첫 번째 원소는 패턴에 일치하는 문자열, 즉 프로토콜이 된다. 따라서 배열의 첫 번째 원소에 바로 접근하면 프로토콜 문자열을 바로 변수에 저장할 수 있다.

var url = 'https://taegon.kim/aboutme';
var protocol = /^https?(?=:\/\/)/.exec( url )[0];
console.log( protocol ); // 'https'

그런데 이 패턴을 사용하다 보면 종종 문제가 발생한다. 위 코드에서는 관계없이지만 만약 url 변수의 값이 변할 수 있고 그 값이 우리가 원하는 형태가 아니라면 어떻게 될까? 예를 들어 빈 문자열이거나 프로토콜이 생략된 형태라서 exec에서 일치하는 결과를 반환하지 않으면? 그럴 때 이 코드는 ‘null에서 0이라는 프로퍼티를 읽을 수 없다’는 오류를 반환하게 된다. 정규식 패턴이 일치하지 않을 때 execnull을 반환하기 때문이다. 그래서 위 코드를 조금 더 안전하게 만드는 동시에 일치하는 값이 없을 때는 빈 문자열을 protocol에 저장하게 하려면 다음과 같이 수정하면 된다.

var url = 'https://taegon.kim/aboutme';
var protocol = /^https?(?=:\/\/)/.exec( url );
if ( protocol ) {
  protocol = protocol[0];
} else {
  protocol = '';
}
console.log( protocol ); // 'https'

하지만 코드가 더 지저분해진 느낌이 든다. 이 때 다음과 같이 OR 연산자(||)를 사용하면 에러도 방지하는 깔끔한 코드를 얻을 수 있다.

var url = 'https://taegon.kim/aboutme';
var protocol = ( /^https?(?=:\/\/)/.exec( url ) || [''] )[0];
console.log( protocol ); // 'https'

일치하는 부분 위치 찾기

이번에는 다음과 같이 단어를 찾는 패턴을 사용해자. 다만, global 플래그를 붙여서 대상 문자열에서 일치하는 부분을 모두 추출해내려고 한다.

/\w+/g

대상 문자열은 “The quick brown fox jumps over the lazy dog”로 정했는데, 여기서 일치하는 부분은 다음과 같이 가져올 수 있다.

var str = 'The quick brown fox jumps over the lazy dog';
var regex = /\w+/g;

regex.exec( str )[0]; // 'The'
regex.exec( str )[0]; // 'quick'
regex.exec( str )[0]; // 'brown'
...
regex.exec( str )[0]; // 'dog'
regex.exec( str )[0]; // Error: Cannot read property '0' of null

하나의 RegExp 인스턴스를 같은 문자열에 계속 실행하면 위와 같이 자동으로 다음 위치로 이동하며 일치하는 패턴을 찾는다. 문자열 끝까지 찾고나면 더 이상 일치하는 부분이 존재하지 않으므로 exec 메소드는 null을 반환한다. 위 코드는 다음과 같이 수정하면 에러가 발생하지 않고 조금 더 효율적으로 동작한다.

var str = 'The quick brown fox jumps over the lazy dog';
var regex = /\w+/g;
var match;
while ( match = regex.exec( str ) ) {
  console.log( match[0] );
}

그런데 일치하는 단어가 전체 대상 문자열에서 어디쯤에 있는지 궁금할 때가 있다. 이럴 때는 RegExp 객체의 인스턴스(위 코드에선 regex)의 lastIndex 속성을 참조하면 알 수 있다. lastIndex는 일치한 문자열이 끝나는 위치를 알려주므로 시작 위치를 알고 싶다면 일치한 문자열의 길이만큼을 lastIndex에서 빼면 된다.

var str = 'The quick brown fox jumps over the lazy dog';
var regex = /\w+/g;
var match;
while ( match = regex.exec( str ) ) {
  console.log( 'Match: ' + match[0], ', Position: ' + ( regex.lastIndex - match[0].length ) );
}

예를 들어 위 코드를 실행하면 콘솔에 출력되는 첫 번째 결과는 다음과 같습니다.

Match: The , Position: 0

참고로 일치하는 모든 부분을 찾는 것만이 목적이라면 사실 String 객체에 있는 match 메소드를 사용하는 쪽이 더 편리하다. 예를 들어 다음은 match를 사용해 같은 기능을 하는 코드를 작성한 것이다.

var str = 'The quick brown fox jumps over the lazy dog';
var regex = /\w+/g;
var matches = str.match( regex );
console.log( matches ); // [ 'The', 'quick', 'brown', ... , 'dog' ]

검색 시작 위치 정하기

어느 언어를 막론하고 정규표현식을 지원하는 함수나 메소드를 사용할 때는 보통 검색 시작 위치도 정할 수 있다. 예를 들어 PHP의 preg_match 함수에는 $offset이라는 인수가 있고, Python 정규식 객체의 searchmatch 메소드에도 시작 위치를 정하는 인수가 있으며, Kotlin Regex 객체의 find 메소드에도 시작 인덱스를 정할 수 있다. 기능 자체에 대해 잘 이해되지 않는다면 다음의 간단한 Python 예제를 하나 살펴보자.

pattern = re.compile("d")
pattern.search("dog")     # 0번째 위치에서 일치
pattern.search("dog", 1)  # 일치하는 문자열 없음. 설정한 시작 위치 이후에는 "d" 문자를 찾을 수 없음

그런데 자바스크립트에는 이런 기능이 없다. 자바스크립트의 정규표현식 객체에서 문자열 검색을 위해 사용하는 메소드는 exec() 하나 밖에 없는데 인수로는 검색 대상 문자열 단 하나만을 허용한다. 따라서 정상적인 방법으로는 시작 위치를 정할 수 없다. 물론 방법은 있다. 위에서 다루었던 lastIndex를 사용하면 된다. 사실 lastIndex는 쓰기도 가능한 속성이며, execlastIndex부터 검색을 시작한다. 따라서 처음 정규표현식 객체를 만든 후 lastIndex 속성의 값을 살펴보면 0이 되고, 이 값을 수정하면 검색 시작 위치를 임의로 설정할 수 있다.

var str = 'The quick brown fox jumps over the lazy dog';
var regex = /\w+/g;
console.log(regex.lastIndex) // 0
regex.lastIndex = 10; // 검색 시작 위치를 10으로 설정
console.log( regex.exec(str)[0] ); // 'brown'

동적인 정규식 생성

워낙 많은 책과 자료에서 정규표현식 리터럴만 주로 다루다보니 간혹 JS에 익숙하지 않은 분들 중에는 변수값을 집어넣어서 정규표현식 만드는 법을 모르는 경우가 있다. 물론 아주 간단하게 해결할 수 있다. RegExp 생성자를 사용하면 된다. 예를 들어 문자열에 특정 단어가 포함됐는지 구하는 함수를 만든다고 생각해보자. 단, 다른 단어에 포함된 단어는 구하지 않는다. 다시 말해 ‘Good enough”라는 문자열이 있을 때 “Good”과 “enough”는 단어로 검색되지만 “Go”는 검색되지 않는다.

정규표현식 패턴을 작성할 때는 문자 경계(\b)를 의미하는 특수 문자를 사용하면 간단하게 단어만 구할 수 있다. 만약 “Good”이라는 단어를 검색한다면 /\bGood\b/ 패턴을 검색하면 되는 것이다. RegExp 생성자를 사용할 때는 리터럴을 의미하는 양쪽 슬래시(/)를 제외한 나머지 패턴을 첫 번째 인수로 입력하고 두 번째 인수로는 플래그(flag)를 입력하면 된다. 따라서 우리가 만드는 함수는 다음과 같이 작성할 수 있다.

function searchWord( word, input ) {
  var regex = new RegExp( '\b' + word + '\b' );
  return (regex.exec( input ) || [''])[0];
}
console.log( searchWord( 'Good', 'Good enough' ) ); // ''

사실 위 함수에는 의도적으로 실수한 부분이 있어서 원하는 대로 동작하지 않고, 항상 빈 문자열만 반환할 것이다. 어느 부분이 실수였을까? 스크롤하기 전에 잠깐 생각해보자. 답은 광고 이후에 있다.

.

.

.

.

정답은 바로 '\b' 부분이다. RegExp 생성자에 전달하는 첫 번째 인수는 문자열이므로 리터럴의 /\b/를 표현하려면 '\b'가 아니라 '\\b'처럼 이스케이프한 문자열을 전달해야 한다. 그렇지 않으면 이 함수는 백스페이스 문자(아스키 코드 8번)가 양쪽에 있는 단어를 찾을 것이다. 실수를 수정한 완성된 함수는 다음과 같다.

function searchWord( word, input ) {
  var regex = new RegExp( '\\b' + word + '\\b' );
  return (regex.exec( input ) || [''])[0];
}
console.log( searchWord( 'Good', 'Good enough' ) ); // 'Good'

Leave a Reply