정규식

JaeungE·2021년 7월 20일
0

JavaScript

목록 보기
12/16
post-thumbnail

프로그래밍을 하다 보면 문자열을 조작해야 되는 경우가 생각보다 자주 생긴다.

물론 indexOf()substring() 등의 메서드를 사용해도 문제를 해결하는 데 지장은 없다.

하지만, 정규식을 사용하면 문자열 조작을 해야 하는 상황에서 간결하고 효율적으로 문제를 해결할 수 있으므로, 알아두면 굉장히 유용하게 사용할 수 있다!😆

생각했던 것보다 굉장히 기능이 많기 때문에 나중에 기억이 잘 나지 않을 수도 있으니, 일단 이러한 기능이 있다는 것만 알아두도록 하자!





정규식 리터럴

JavaScript에서는 다음과 같이 / 기호를 이용하거나, new RegExp() 클래스(생성자 함수) 를 이용해 정규식의 생성이 가능하다.

정규식이 정적일 때는 / 기호를 사용하고, 정규식이 동적으로 바뀌어야 하는 경우에만 new RegExp() 클래스를 사용한다고 생각하면 된다.

그럼 한 번 문자열에서 ab의 개수를 세는 정규식을 만들어 보도록 하자!


const str = 'abba baab aa';
const reg1 = /a/;
const reg2 = new RegExp('b');

console.log(str.match(reg1)); // [ 'a', index: 0, input: 'abba baab aa', groups: undefined ]
console.log(str.match(reg2)); // [ 'b', index: 1, input: 'abba baab aa', groups: undefined ]

문자열에서 ab 라는 문자가 몇 개가 있는지 확인하려고 했는데, 원하는 결과가 나오지 않았다.

왜냐하면, 정규식은 기본적으로 좌측에서 우측으로 탐색을 진행하는데, 플래그를 따로 지정해주지 않았다면 제일 처음 발견한 결과 하나만 반환해주기 때문이다.

그렇다면 플래그에는 어떤 것이 있는지 확인해 보도록 하자!😉





플래그

플래그도 다양한 종류가 있지만, 여기서는 주로 사용하는 ig 플래그만 알아보도록 하겠다.


ig
대/소문자 무시(Ignore case)일치하는 패턴 모두 검색(Global)

위의 예제에 g 플래그를 추가한 것 부터 보도록 하자!😣


const str = 'abba baab aa';
const reg1 = /a/g;
const reg2 = new RegExp('b', 'g');

console.log(str.match(reg1)); // [ 'a', 'a', 'a', 'a', 'a', 'a' ]
console.log(str.match(reg2)); // [ 'b', 'b', 'b', 'b' ]

플래그는 정적인 정규식의 경우에는 닫는 / 기호의 뒤에, 동적인 정규식은 new RegExp()의 두 번째 인자로 플래그를 넘겨주면 된다.

g 플래그를 이용했더니 정규식과 일치하는 결과를 모두 보여주는 것을 알 수 있다.

이번엔 str에 있는 ab의 일부를 대문자로 변경해보자.


const str = 'aBbA baaB Aa';
const reg1 = /a/g;
const reg2 = new RegExp('b', 'g');

console.log(str.match(reg1)); // [ 'a', 'a', 'a', 'a' ]
console.log(str.match(reg2)); // [ 'b', 'b' ]

안타깝게도 소문자의 경우만 일치한다....😥

이럴 때 문자셋을 이용해서 대/소문자를 모두 일치하게 할 수도 있지만, 아래처럼 i 플래그를 이용하면 간단하게 해결이 가능하다!


const str = 'aBbA baaB Aa';
const reg1 = /a/ig;
const reg2 = new RegExp('b', 'ig');

console.log(str.match(reg1)); // [ 'a', 'A', 'a', 'a', 'A', 'a' ]
console.log(str.match(reg2)); // [ 'B', 'b', 'b', 'B' ]

대/소문자 모두 일치하는것을 볼 수 있다.

이 외에도 다양한 플래그가 존재한다. 더 자세히 알고싶다면 정규 표현식 | MDN을 참고하도록 하자!😄





문자셋

이번엔 a부터 z 까지 해당하는 모든 알파벳을 찾으려고 한다면, 정규식을 다음과 같이 작성할 수 있다.

const reg = /a|b|c|d|......|z/ig;

참고로 ... 부분은 생략된 내용이고, 일일이 모든 알파벳에 대응하도록 |과 알파벳을 넣어줘야 한다. 알파벳은 총 26개니까 |를 25번이나 사용해야 한다는 뜻이다.....😑

이런 어지러운 상황을 없애기 위해서 문자셋이 존재한다.

const reg = /[pattern]/ 처럼 패턴을 대괄호로 감싸주면 해당 패턴은 문자셋이 된다.

그럼 모든 알파벳을 찾아내는 정규식을 문자셋을 이용해 만들어보자!


const str = 'ab78=[[x3s';
const reg = /[a-z]/g; // === /[abcdefghijklmnopqrstuvwxyz]/g

console.log(str.match(reg)); // [ 'a', 'b', 'x', 's' ]

문자셋 안에서는 |가 생략된 효과를 얻을 수 있고, -로 범위를 정해줄 수도 있다.

결과를 보면 숫자와 특수기호가 섞여 있는 문자열에서 정확히 알파벳만 가져온 것을 볼 수 있다. 이 외에도 [0-9] 처럼 문자열에서 숫자만 가져오는 방법도 존재한다.

또한, 반대로 알파벳을 제외한 문자열을 가져오는 방법도 당연히 있다. 아래의 코드를 보도록 하자!


const str = 'ab78=[[x3s';
const reg = /[^a-z]/g;

console.log(str.match(reg)); // [ '7', '8', '=', '[', '[', '3' ]

/[^a-z]/ 처럼 문자셋의 앞에 ^ 기호를 붙여주면 NOT 처럼 동작한다.

^ 기호는 문자셋에 사용할 때와 패턴에 사용할 때의 역할이 다르니 꼭 기억해두자!

그리고 알파벳이나 숫자처럼 자주 사용하는 문자셋은 아래처럼 단축해놓았다.


단축 표기문자셋
\d[0-9]
\D[^0-9]
\s[ \t\n\r\v]
\S[^ \t\n\r\v]
\w[0-9a-zA-Z_]
\W[^0-9a-zA-Z_]





반복

이번엔 문자열에서 알파벳이 2번 연속으로 등장하는 문자열을 찾고싶다고 해보자.


const str = 'ai78[],.p5jk810cx';
const reg = /[a-z][a-z]/g;

console.log(str.match(reg)); // [ 'ai', 'jk', 'cx' ]

생각보다 간단하게 구할 수 있었다.

하지만 5번, 10번, 100번 연속된 알파벳을 구하려면 [a-z]를 해당 횟수만큼 써줘야 할 뿐만 아니라, 3번 이상 연속된 경우 혹은 2번 이상 5번 이하같이 조건이 있다면 거의 불가능에 가깝다.

이를 해결하기 위해 반복 메타 문자가 존재한다.

정규 표현식 | MDN 문서를 보면 반복 메타 문자가 아니라 수량자 라고 표현하기도 하는데, 같은 표현이니 헷갈리지 말자!

반복 메타 문자의 종류는 아래의 테이블을 보도록 하자.😊


메타 문자설명
{n}정확히 n개
{n,}n개 이상
{n,m}n개 이상, m개 이하
?0개 이상 1개 이하, {0, 1}과 같다.
*0개 이상, {0,}과 같다.
+1개 이상, {1,}과 같다.

그럼 이제 반복 메타 문자를 활용해서 알파벳이 3번 이상 연속되는 문자열을 찾아보자!


const str = '27][[vMsd38810aZi1C2fG328giYpq';
const reg = /[a-z]{3,}/ig;

console.log(str.match(reg)); // [ 'vMsd', 'aZi', 'giYpq' ]

CfG를 제외하고 알파벳이 3개 이상 연속되는 문자열을 아주 쉽게 구할 수 있다.

또한, 반복 메타 문자를 이용하면 아래와 같이 문자열에서 전화번호만 추출하는 것도 가능하다.


const str = 'Hi My name is Jaeung Lee and' +
            'my phone number is 010-1234-5678';
const reg = /\d{3}\W\d{4}\W\d{4}/ig;

console.log(str.match(reg)); // [ '010-1234-5678' ]

점점 정규식이 괴상해 보이기 시작하지만, 리터럴부터 반복까지 제대로 익혔다면 충분히 이해할 수 있다.



Lazy와 Greedy

반복 메타 문자는 일치 범위를 넓히는 데 있어서 Lazy 방식과 Greedy 방식이 있는데, 이것을 제대로 이해하지 못하면 원치 않은 결과를 가져올 수 있다.

기본적으로는 Greedy 방식으로 일치하는 패턴을 찾으며, 반복 메타 문자 뒤에 ? 기호를 붙이면 Lazy 방식이 된다. 일단 Greedy 방식부터 보도록 하자!😉


const str = 'xck[vy!12z]vha[z#^c][4as!@&16cq3]';
const reg = /\[[\w\W]+\]/g;

console.log(str.match(reg)); // [ '[vy!12z]vha[z#^c][4as!@&16cq3]' ]

의도했던 동작은 중괄호로 감싸진 문자열을 추출하려고 했는데, 중괄호가 처음 시작한 부분부터 마지막 중괄호가 닫히는 구간까지 모두 추출되었다.

이처럼 Greedy 방식은 일치하는 패턴을 이미 찾았어도, 이름 그대로 욕심이 많아서 탐색 범위를 최대한으로 넓히려고 하기 때문이다.

그래서 처음 중괄호가 닫히는 부분을 만나도 무시하고 계속해서 진행하고, 마지막 중괄호가 닫히는 부분에 도착해서야 만족하고 해당 결과를 반환한 것이다.

그렇다면 Lazy 방식은 어떻게 동작하는지 보도록 하자!


const str = 'xck[vy!12z]vha[z#^c][4as!@&16cq3]';
const reg = /\[[\w\W]+?\]/g;

console.log(str.match(reg)); // [ '[vy!12z]', '[z#^c]', '[4as!@&16cq3]' ]

반복 메타 문자 뒤에 ? 기호를 추가해서 Lazy 방식으로 바꾸었더니 원하던 결과를 얻어냈다.😨

바로 다음에 설명할 마침표반복 메타 문자를 같이 사용할 때, GreedyLazy 방식을 이해하지 못했다면, 결과를 이해할 수 없는 경우도 생길 수 있으므로 꼭 기억해두자!





마침표(.)

마침표는 개행 문자 \n을 제외한 나머지 모든 문자를 포함하는 특수한 문자다.

예시로 a 뒤에 아무 문자 4개가 연속된 문자열을 마침표 없이 찾는다고 해보자.


const str = 'a1[4 zxhv[y&$3adb@$%ujxd0 a].4!123';
const reg = /a[\S ]{4}/g;

console.log(str.match(reg)); // [ 'a1[4 ', 'adb@$', 'a].4!' ]

이렇게 문자셋에 '공백 문자를 제외한 나머지 문자들과 스페이스' 같이 지정해줘야 한다.

하지만 아래와 같이 마침표를 사용하면 훨씬 짧게 줄일 수 있다.


const str = 'a1[4 zxhv[y&$3adb@$%ujxd0 a].4!123';
const reg = /a.{4}/g;

console.log(str.match(reg)); // [ 'a1[4 ', 'adb@$', 'a].4!' ]

같은 결과를 보여주지만, 정규식이 훨씬 간결해졌다.

위에서 배운 반복 메타 문자마침표 그리고 아래에서 설명할 그룹을 사용하면 문자열에서 이메일을 추출하거나, 파일 확장자가 일치하는 문자열만 추출하는것도 가능해진다!😮





그룹

그룹은 정규식에서 하위 정규식을 소괄호로 묶는 것을 말한다.

그룹을 통해 생성된 결과를 저장하는 캡처링 그룹과 결과를 저장하지 않는 비 캡처링 그룹이 있는데, 그룹을 통한 결과가 필요하지 않다면, 비 캡처링 그룹이 성능이 더 좋기 때문에 잘 선택해서 사용하면 된다.

비 캡처링 그룹(?:regexp) 형태로 생성할 수 있다.

일단 비 캡처링 그룹으로 하위 디렉터리에 있는 파일명을 추출하는 정규식을 만들어보자!😀


const str = '/jaeung/dev/file/Frog1.jpg, ' + 
            '/img/dog/BbokBbok.png, ' + 
            '/un_known.jpeg, ' +
            '/wrongFile.fail';

const reg = /[\w]+\.(?:jpg|png|jpeg)/ig;

console.log(str.match(reg)); // [ 'Frog1.jpg', 'BbokBbok.png', 'un_known.jpeg' ]

파일명에 _를 제외한 특수 문자는 사용할 수 없다는 전제하에, 하위 디렉터리에 있는 파일명을 추출하는 정규식이다.

물론 /를 기준으로 split() 메서드를 이용해도 파일명을 추출할 수 있지만, 정규식을 사용하면 훨씬 간단하게 구할 수 있는 것을 볼 수 있다.

그렇다면 이번엔 캡처링 그룹을 이용한 예제를 보자!😆


const str1 = '<text1> [text2] <text3> [text4]';
const reg = /[\<\[](.*?)[\>\]]/ig;
const str2 = str1.replace(reg, '( $1 )');

console.log(str2); // ( text1 ) ( text2 ) ( text3 ) ( text4 )

이 코드를 실행시키면 <> 혹은 [] 괄호를 ()로 변경해준다. 이때, 괄호 안의 내용물은 변하지 않았는데, $1을 보면 알 수 있다.

캡처링 그룹은 보이는 것처럼 그룹 내의 정규식이 찾아낸 결과를 저장하고, $ 기호를 이용해 저장해둔 결과를 재사용 할 수 있다.

만약 그룹이 하나가 아니라 두 개, 세 개였다면 $2 혹은 $3을 통해 실행 결과를 재사용할 수 있다.

물론 $n 외에도 다양한 사용법이 존재한다. 아래 테이블을 참고하자!


기호설명
$n그룹 번호 n번의 결과
$&정규식 전체의 결과
$`일치한 결과의 앞에 있는 내용들
$'일치한 결과의 뒤에 있는 내용들





앵커

앵커는 ^$ 기호를 이용해 문자열의 처음과 끝을 알려준다.

일단 아래 예제를 보도록 하자.


const str = 'Hi Im test Strings';
const reg = /\w+/ig;

console.log(str.match(reg)); // [ 'Hi', 'Im', 'test', 'Strings' ]

이러면 공백을 제외한 문자열이 모두 추출된다. 하지만 앵커 기호 ^$를 사용하면 결과가 달라진다.


const str = 'Hi Im test Strings';
const reg1 = /^\w+/ig;
const reg2 = /\w+$/ig;

console.log(str.match(reg1)); // [ 'Hi' ]
console.log(str.match(reg2)); // [ 'Strings' ]

^를 사용하면 문자열의 맨 앞부분만 일치하는지 확인하고, $를 사용하면 문자열의 맨 뒷부분만 일치하는지 확인한다.

이렇게만 보면 별로 쓸모있는 기능은 아닌 것처럼 보이지만, 앵커는 문자열 사이에 개행 문자가 있다면, m 플래그를 이용해서 아래 예제처럼 라인마다 문자열의 처음과 끝을 알아낼 수 있다!😲


const str = 'Hi Im\ntest Strings';
const reg1 = /^\w+/mig;
const reg2 = /\w+$/mig;

console.log(str.match(reg1)); // [ 'Hi', 'test' ]
console.log(str.match(reg2)); // [ 'Im', 'Strings' ]

m 플래그는 여러 라인에 정규식을 적용해야 할 때 사용하는 플래그다.





단어 경계

단어의 경계를 나타내기 위한 메타 문자로 \b\B가 있다. \B\b와 정확히 반대로 동작한다고 이해하면 편하다.

예를 들어 'time timeout daytime teatime sentiment altimeter' 처럼 여러 단어가 있다고 생각해보자.

여기서 각 정규식에 매칭되는 단어는 아래 테이블을 참고하면 된다!


정규식매칭된 단어설명
/\btime/'time', 'timeout'time 으로 시작하는 단어
/time\b/'daytime', 'teatime'time 으로 끝나는 단어
/\Btime/'daytime', 'teatime', 'sentiment', 'altimeter'time으로 시작하지 않는 단어
/time\B/'timeout', 'sentiment', 'altimeter'time으로 끝나지 않는 단어
/\Btime\B/'sentiment', 'altimeter'time으로 시작하지도 끝나지도 않는 단어
/\btime\b/'time'time으로 시작하고 time으로 끝나는 단어

실제로 단어가 매칭되는 것은 아니고, 해당 조건을 만족하는 time을 찾을 뿐이니 헷갈리지 말자!😅



이렇게 정규식에는 굉장히 많은 기능이 있고, 문자열을 효율적으로 다룰 수 있도록 도와준다.

위에서 설명했던 기능들 외에도 Look ahead라는 고급 기법이 있으나, 아직은 완벽히 이해하지 못해서 좀 더 성장하고 나서 다루도록 하겠다...😥





참고 자료

[정규 표현식 - JavaScript | MDN]
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Regular_Expressions

0개의 댓글