[JavaScript] 정규표현식(RegExp)이란?

coderH·2022년 7월 31일
0

JavaScript 연대기

목록 보기
11/11
post-thumbnail

오늘은 문자열을 찾기 위해 사용되는 정규표현식에 대해서 다뤄보려고 합니다.

정규표현식(= Regular Expression)은 문자열에서 일부 문자열을 검색하거나 추출할 때 사용하며 RegExp, Regex라고 불리기도 합니다.

정규표현식은 JS뿐만 아니라 다양한 프로그래밍 언어에서 사용되기 때문에 한번 익혀두면 다양한 환경에서 유용하게 사용할 수 있습니다.

JS에서 정규표현식을 만드는 방법 2가지

정규표현식 리터럴 방식

리터럴 방식의 경우 슬래시 2개를 감싸 사용하며 변수 선언 없이 바로 사용할 수 있습니다.

const str = "abcdefg";

console.log(str.match(/bcd/)); // [ 'bcd', index: 1, input: 'abcdefg', groups: undefined ]

이런식으로 정규표현식을 사용하는 메소드의 인자로 넣어서도 사용이 가능하며

const str = "abcdefg";
const regex = /bcd/;

console.log(str.match(regex)); // [ 'bcd', index: 1, input: 'abcdefg', groups: undefined ]

변수에 넣어서도 사용이 가능합니다.

정규표현식 객체 이용

JavaScript에는 정규표현식을 위한 객체인 RegExp 라는 객체가 있습니다.
이 객체를 이용해서 정규표현식을 사용할 수 있습니다

const str = "abcdefg";
const regex = new RegExp('bcd');

console.log(str.match(regex)); // [ 'bcd', index: 1, input: 'abcdefg', groups: undefined ]

이 때는 슬래시 없이 일반 문자열을 작성하듯이 따옴표와 함께 찾고자 하는 문자열을 인자로 넣어주면 됩니다.

이 두가지 방법에는 각각 장단점이 있는데
리터럴 방식의 경우 사용하기 간편하다는 장점이 있지만 동적으로 패턴이 변경되는 경우를 대응할 수 없습니다. 그래서 패턴이 변하지 않을 때 사용하기 유용합니다.

반면, 객체 방식의 경우 패턴을 동적으로 변경할 수 있기 때문에 사용자의 입력이나 공통함수로 사용하면서 상황에 따라 문자열을 유동적으로 변경할 수 있습니다.

const str = "abcdefgh"

function findStr(query) {
    const regex = new RegExp(query);

    return str.match(regex);
}

console.log(findStr("bcd")); // [ 'bcd', index: 1, input: 'abcdefgh', groups: undefined ]

console.log(findStr("abc")); // [ 'abc', index: 0, input: 'abcdefgh', groups: undefined ]

정규표현식 구성요소

정규표현식은 패턴과 플래그로 이루어져 있습니다.
패턴은 찾고자 하는 문자열을 말하고 플래그는 정규표현식을 사용할 때 옵션과 같은 역할을 합니다.

대표적인 플래그는 g (=global)가 있는데 정규표현식은 기본적으로 지정한 패턴과 일치하는 문자열을 찾게되면 바로 종료됩니다.
따라서 뒤쪽에 일치하는 문자열이 더 있더라도 진행하지 않고 한개의 결과값만 반환합니다.

하지만, g 플래그를 사용 할 경우 문자열 전체에서 일치하는 모든 문자열을 찾아서 반환합니다.
플래그는 리터럴 방식의 경우 가장 뒤쪽에 사용하고 객체 방식의 경우 패턴과 플래그를 콤마를 기준으로 구분하며 문자열 형태로 사용합니다.

const str = "abcdefa";

// 1. 리터럴
console.log(str.match(/a/g)); // [ 'a', 'a' ]

// 2. 객체
const regex = new RegExp("a", "g");

console.log(str.match(regex)); // [ 'a', 'a' ]

정규표현식에는 다양한 문자열을 찾기 위해 여러가지의 패턴과 플래그가 존재합니다.
그래서 패턴과 플래그는 어떤 종류들이 있는지 알아보겠습니다.

패턴의 종류

Character classes

캐릭터 클래스는 특정 종류(숫자, 문자 등)의 문자를 찾기 위한 패턴들입니다.

패턴설명
.개행 문자(\n)와 캐리지 리턴(\r)을 제외한 모든 문자를 찾습니다.
\d (digit)모든 숫자를 찾으며 아래 Groups에서 다룰 /[0-9]/와 같은 결과값을 반환합니다.
\D숫자를 제외한 문자들을 찾습니다.
\w (word)알파벳 소문자와 대문자, 숫자, _(언더바)기호를 찾습니다.
\W\w에 속하지 않는 문자들을 찾습니다.
\s (space)스페이스 공백, 탭 공백(\t), 개행 문자(\n) 등 다양한 공백을 찾습니다.
\S\s를 제외한 문자들을 찾습니다.
\t탭 공백을 찾습니다.
\n줄 바꿈시 생기는 공백을 말하는 것으로 JS에서는 주로 백틱을 이용해 줄넘김 했을 때 찾을 수 있습니다.
\escape 문자로 특수문자를 찾고자 할 때 사용하며 특수문자를 찾고 싶을때는 앞에 \를 붙여주어야 합니다.
Ex. /\=/, /\\/, /\./ 등

설명을 보시면 유추할 수 있듯이 대문자를 사용하는 캐릭터 클래스들은 모두 소문자의 반대되는 문자를 찾는 공통점을 가지고 있습니다.

Assertions

이 부분은 예시가 필요할 것 같아 "Hello world!"문자열을 기준으로 설명하겠습니다.

패턴설명
^찾고자 하는 문자열이 가장 앞에 위치해야 할 때 사용하며 대상 문자열의 앞쪽에 사용해주어야 합니다.
Ex. /^Hello/
$위와 반대로 찾고자 하는 문자열이 가장 뒤에 위치할 때 사용하며 이 기호는 문자열 뒤쪽에 사용해주어야 합니다.
Ex. /world!$/
\b (word boundary)공백을 말하며 보통 단어와 단어사이의 공백을 찾고자 할 때 사용합니다.
\s와의 차이점은 결과값에 포함시키는지 여부이며 \s는 포함되고 \b는 포함되지 않습니다.
Ex. /\bw/ => "w"
\B해당 위치에 공백이 아닌 문자가 있어야 합니다.
Ex. /\Bw/ => null
x(?=y)xy의 연속된 문자열을 찾으며 반환값으로는 x만 반환합니다.
Ex. /H(?=e)/ => "H"
x(?!y)위와 반대로 x뒤에 y가 이어지지 않는 x일 경우에만 반환합니다.
Ex. /H(?!e)/ => null
(?<=y)xyx의 문자열을 찾으며 x만 결과값으로 반환합니다.
Ex. /(?<=H)e/ => "e"
(?<!y)xy가 이어지지 않는 x만 찾습니다.
Ex. /(?<!H)e/ => null

Groups and Ranges

패턴설명
[abc] = [a-c]대괄호안에 포함된 문자들 중 하나라도 포함되면 해당됩니다.
또한 [a-c]와 같은 형식으로도 사용할 수 있으며 소문자는 [a-z], 대문자는 [A-Z], 숫자는 [0-9]와 같이 사용할 수 있습니다.
[^abc] = [^a-c]위와 반대로 대괄호안에 있는 글자들을 제외하고 찾습니다.
위의 assertion의 ^기호와 헷갈릴 수 있는데 대괄호 내부인지 외부인지에 따라서 쓰임새가 달라집니다.
x|yx 또는 y를 찾는것으로 [xy]와의 차이점은 대괄호 방식의 경우 한 단어씩만 사용할 수 있지만 이 기호의 경우 단어 단위 /we|our/ 형태로도 사용할 수 있습니다.
(x)x를 하나의 그룹으로 묶습니다.
(?<Name>x)x에 해당하는 문자열들을 결과값에 Name이라는 이름을 가진 속성에 넣을 수 있습니다.
(?:x)결과값에는 반영되지만 그룹으로는 저장하지 않습니다.
보통 여러개의 그룹으로 문자를 묶어야할 때 특정 부분만 그룹에 포함되지 않도록 할 때 사용됩니다.

개인적으로 그룹 부분에서 헷갈리는게 많아서 추가적인 예시를 들어보면 아래와 같습니다.

const a = "abcdef123";

// (x), 그룹화
console.log([...a.matchAll(/\D+(\d+)/g)]);

// [
//     [
//       'abcdef123',  // 일치한 문자열
//       '123',       // 그룹화 된 문자열
//       index: 0,
//       input: 'abcdef123',
//       groups: undefined
//     ]
// ]

// (?<Name>x)
console.log([...a.matchAll(/\D+(?<Nums>\d+)/g)]);

// [
//     [
//       'abcdef123',
//       '123',
//       index: 0,
//       input: 'abcdef123',
//       groups: [Object: null prototype] { Nums: '123' }
         // Nums라는 이름으로 따로 그룹화
//     ]
// ]

특히 (?:x) 이 기호에 대한 정보가 많이 없어 찾아보았는데 주로 여러개의 그룹화를 진행할 때 많이 사용한다고 합니다.

여러개의 그룹 중 이 기호를 쓴 그룹만 그룹화를 하지 않습니다. 물론 문자열을 찾을때는 함께 찾습니다.

// const str = "Hello world!";

const str = "Hello world!";

console.log([...str.matchAll(/(Hello) (world)/g)]);

// [
//     [
//         'Hello world',
//         'Hello',            // 문자열이 그룹으로 나누어져 있음.
//         'world',            // 문자열이 그룹으로 나누어져 있음.
//         index: 0,
//         input: 'Hello world!',
//         groups: undefined
//     ]
// ]

// (?:)를 사용했을 때
console.log([...str.matchAll(/(?:Hello) (world)/g)]);

// [
//     [
//         'Hello world',
//         'world',           // world문자열만 그룹화
//         index: 0,
//         input: 'Hello world!',
//         groups: undefined
//     ]
// ]

Quantifiers

이 부분은 문자의 수량을 표현하는 기호들입니다.

패턴설명
x*x가 0개 혹은 1개 이상이여야 합니다.
x+x가 1개 이상일 때만 일치하며 아래에서 설명할 x{1,}과 같은 결과값을 가집니다.
x?x가 0개 혹은 1개여야만 합니다.
x{n}x가 정확히 n만큼 반복하는 문자열을 찾습니다.
Ex. a{2} = aa
x{n,}x가 2개 이상 연속되는 문자를 찾습니다.
Ex. ab{2,} = abab~
x{n,m}x가 n이상 m이하만큼 반복되는 문자열을 찾습니다.
Ex. c{2, 4} = cc ~ cccc

플래그의 종류

플래그설명
g (global)패턴과 일치하는 모든 문자를 반환합니다.
i (ignore case)대소문자를 구분하지 않고 탐색합니다.
이 기호를 사용 할 경우 모든 알파벳을 찾고 싶을 때 /[a-z][A-Z]/ 로 사용하지 않고 /[a-z]/i 로 사용해도 똑같은 결과값을 반환합니다.
m (multi line)문자열이 여러줄일 때 각 줄을 기준으로 탐색합니다.
특히 ^, $기호를 사용할때 m 플래그가 없으면 오직 가장 앞에 있는 문자만 해당되지만 m 플래그를 사용하면 각 줄을 기준으로 모두 탐색합니다.
s (dotAll)이 플래그를 설정하면 .을 사용할 때 개행 문자인 \n도 포함하여 검색하도록 합니다.
u (unicode)이모지, 한글 등 유니코드를 사용해야하는 문자열들을 찾을 수 있습니다.
유니코드 기준으로 사용해야하며 charCodeAt등의 메소드를 통해 반환되는 코드는 아스키코드이므로 헷갈리지 않도록 주의해야 합니다.
y (sticky)정규표현식에는 탐색을 시작할 인덱스를 지정하는 lastIndex라는 속성이 있습니다.
일반적으로 정규표현식은 lastIndex부터 문자열의 끝까지 패턴을 찾지만 y플래그를 선택하고 lastIndex를 지정하면 찾고자하는 패턴이 정확하게 해당 인덱스부터 시작해야만 일치하다고 판단합니다.

정규표현식을 사용하는 메소드의 종류

패턴설명
String.match(regexp)일치하는 문자열을 찾아서 배열 형태로 반환하며 찾지 못 할 경우 null을 반환합니다.
String.matchAll(regexp)match함수와 같지만 "g" flag를 꼭 사용해주어야 합니다.
String.replace(regexp, newSubstr | replacerFunction)특정 문자열을 새로운 문자열로 교체할 때 사용하는 메소드입니다.
첫번째 인자는 정규 표현식, 두번째 인자는 교체할 문자열이나 콜백함수가 위치하며 교체된 문자열을 반환하며 기존 문자열은 변경하지 않습니다.
String.replaceAll(regexp, newSubstr | replacerFunction)replace함수와 같지만 "g" flag를 꼭 사용해주어야 합니다.
String.search(regexp)일치하는 해당 문자열의 첫 글자의 인덱스를 반환하며 찾지 못 할 경우 -1을 반환합니다.
String.split(separator, limit)separator인자에 문자열 또는 정규표현식을 사용할 수 있습니다.
RegExp.test(str)인자로 탐색이 필요한 문자열을 받으며 정규표현식의 일치 여부에 따라 boolean값을 반환합니다.
RegExp.exec(str)위와 마찬가지로 탐색할 문자열을 인자로 받으며 결과를 배열 또는 null로 반환합니다.

위 메소드들을 보시면 match와 replace함수에는 g 플래그를 사용하면 되는거 아닌가?
굳이 matchAll과 replaceAll이 왜 필요하지? 라는 생각이 들수도 있을텐데

먼저 이들은 정규표현식 뿐만 아니라 일반 문자열도 인자로 받을 수 있는 메소드들입니다.

문자열로만 해당 메소드를 호출 할 때는 g 플래그를 사용할 수 없기 때문에 위 두개의 메소드를 사용하면 g 플래그를 사용한 것과 같은 효과를 볼 수 있습니다.

특히, match와 matchAll의 경우 두 메소드의 결과값이 다르게 나오는데

match메소드는 일치하는 문자열을 배열형태로 결과값으로 반환합니다.

반면, matchAll의 경우 일치하는 문자열뿐만 아니라 index, 전체문자열, groups등의 정보가 담긴 2차원 배열 형태로 반환합니다.

const str = "abcded";

// 1. match에 g플래그만 사용했을 경우
console.log(str.match(/d/g)); 
// [ 'd', 'd' ]

// 2. matchAll 메소드를 사용했을 경우
console.log([...str.matchAll(/d/g)]);
// [
//   [ 'd', index: 3, input: 'abcded', groups: undefined ],
//   [ 'd', index: 5, input: 'abcded', groups: undefined ]
// ]

정규표현식 예시

// URL
/https?\:\/\/([a-z0-9]+\.)?[a-z0-9]+\.[a-z]{2,3}(\.[a-z]{2})*/

// 전화번호, 02|031 - 123|1234 - 5678
/0\d{1,2}-\d{3,4}-\d{4}/

// 휴대폰번호, 010-1234-5678
/010-\d{4}-\d{4}/

// MAC주소, "00-11-22-33-AA-BB"
// 맥주소는 16진수로 2자리씩 끊어 표현하며 사이는 - 기호로 구분합니다.
/([A-Z0-9]){2}(\-([A-Z0-9]){2}){4,}/

// IP주소 v4, 192.168.0.1
/\d{1,3}(\.\d{1,3}){3}/

정규표현식은 한번에 외우기에는 분량이 많기 때문에 regexr과 같은 정규표현식을 연습할 수 있는 사이트를 통해 자주 접해보면 금방 숙달할 수 있습니다.

0개의 댓글