오늘은 문자열을 찾기 위해 사용되는 정규표현식에 대해서 다뤄보려고 합니다.
정규표현식(= Regular Expression)은 문자열에서 일부 문자열을 검색하거나 추출할 때 사용하며 RegExp, Regex라고 불리기도 합니다.
정규표현식은 JS뿐만 아니라 다양한 프로그래밍 언어에서 사용되기 때문에 한번 익혀두면 다양한 환경에서 유용하게 사용할 수 있습니다.
리터럴 방식의 경우 슬래시 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' ]
정규표현식에는 다양한 문자열을 찾기 위해 여러가지의 패턴과 플래그가 존재합니다.
그래서 패턴과 플래그는 어떤 종류들이 있는지 알아보겠습니다.
캐릭터 클래스는 특정 종류(숫자, 문자 등)의 문자를 찾기 위한 패턴들입니다.
패턴 | 설명 |
---|---|
. | 개행 문자(\n)와 캐리지 리턴(\r)을 제외한 모든 문자를 찾습니다. |
\d (digit) | 모든 숫자를 찾으며 아래 Groups에서 다룰 /[0-9]/와 같은 결과값을 반환합니다. |
\D | 숫자를 제외한 문자들을 찾습니다. |
\w (word) | 알파벳 소문자와 대문자, 숫자, _(언더바)기호를 찾습니다. |
\W | \w에 속하지 않는 문자들을 찾습니다. |
\s (space) | 스페이스 공백, 탭 공백(\t), 개행 문자(\n) 등 다양한 공백을 찾습니다. |
\S | \s를 제외한 문자들을 찾습니다. |
\t | 탭 공백을 찾습니다. |
\n | 줄 바꿈시 생기는 공백을 말하는 것으로 JS에서는 주로 백틱을 이용해 줄넘김 했을 때 찾을 수 있습니다. |
\ | escape 문자로 특수문자를 찾고자 할 때 사용하며 특수문자를 찾고 싶을때는 앞에 \를 붙여주어야 합니다. Ex. /\=/, /\\/, /\./ 등 |
설명을 보시면 유추할 수 있듯이 대문자를 사용하는 캐릭터 클래스들은 모두 소문자의 반대되는 문자를 찾는 공통점을 가지고 있습니다.
이 부분은 예시가 필요할 것 같아 "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)x | yx의 문자열을 찾으며 x만 결과값으로 반환합니다. Ex. /(?<=H)e/ => "e" |
(?<!y)x | y가 이어지지 않는 x만 찾습니다. Ex. /(?<!H)e/ => null |
패턴 | 설명 |
---|---|
[abc] = [a-c] | 대괄호안에 포함된 문자들 중 하나라도 포함되면 해당됩니다. 또한 [a-c]와 같은 형식으로도 사용할 수 있으며 소문자는 [a-z], 대문자는 [A-Z], 숫자는 [0-9]와 같이 사용할 수 있습니다. |
[^abc] = [^a-c] | 위와 반대로 대괄호안에 있는 글자들을 제외하고 찾습니다. 위의 assertion의 ^기호와 헷갈릴 수 있는데 대괄호 내부인지 외부인지에 따라서 쓰임새가 달라집니다. |
x|y | x 또는 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
// ]
// ]
이 부분은 문자의 수량을 표현하는 기호들입니다.
패턴 | 설명 |
---|---|
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과 같은 정규표현식을 연습할 수 있는 사이트를 통해 자주 접해보면 금방 숙달할 수 있습니다.