🔌 정규 표현식 ( RexExp )
정규 표현식은 텍스트의 패턴을 정의하는 객체입니다.
즉, 복잡한 텍스트에서 특수한 패턴을 가진 텍스트만 골라내는데 유용하게 사용할 수 있습니다.
( ex) 해시태그, 전화번호, 비밀번호 등등 )
0️⃣ 정규 표현식 정의 방법
RegExp
생성자 함수 사용 ((1)
)- 정규 표현식 리터럴 사용 (
(2)
)
일반적으로 정규 표현식 리터럴을 사용합니다. ( 더 간단함 )
1
2
3
4
5
// (1)
let pattern1 = new RegExp("^a", "giu");
// (2)
let pattern2 = /^a"/giu;
1️⃣ 플래그 ( flag )
문자 | 의미 |
---|---|
g | 일치하는 모든 문자열을 찾음 |
i | 대소문자 구문하지 않음 |
m | 여러 행에서 일치 여부를 찾음 ( ^ , $ 가 각 라인의 시작과 끝도 확인함 ) |
s | TODO: |
u | 16비트를 초과하는 문자 탐색 ( 한자, 이모지 등 ) |
y | TODO: |
정규 표현식의 매칭 방법을 결정하는 방법입니다.
같은 정규 표현식이라도 플래그에 따라서 선택될 수 있고 안될 수 있습니다.
플래그는 생성자 함수의 두 번째 인자나, 정규 표현식 리터럴 바로 뒤에 사용할 수 있습니다.
2️⃣ 문자 클래스
문자 | 일치하는 문자 |
---|---|
[] | 대괄호 안에 있는 어떤 문자든 일치 |
[^] | 대괄호 안에 없는 어떤 문자든 일치 |
\w | [a-zA-Z0-9_] 와 동등 |
\W | [^a-zA-Z0-9_] 와 동등 |
\s | 공백 문자 전체 일치 |
\S | 공백 문자 제외한 전체 일치 |
\d | [0-9] 와 동등 |
\D | [^0-9] 와 동등 |
[]
로 감싸서 만들며 대괄호 안에 포함된 어떤 것과도 일치합니다.
( 아래 코드를 정상적으로 테스트하려면 반드시 g
플래그가 있어야 합니다. )
1
2
3
4
5
6
7
8
const regexp1 = /[\w]/gim;
console.log("a1 b2 c3".match(regexp1)); // ["a", "1", "b", "2", "c", "3"]
const regexp2 = /[\d]/gim;
console.log("a1 b2 c3".match(regexp2)); // ["1", "2", "3"]
const regexp3 = /[^\d]/gim;
console.log("a1 b2 c3".match(regexp3)); // ["a", " ", "b", " ", "c"]
3️⃣ 반복 문법
문자 | 의미 |
---|---|
{n,m} | n 번 이상, m 번 이하 |
{n,} | n 번 이상 |
{n} | n 번 |
? | 0 or 1 번 |
+ | 1 번 이상 |
* | 0 번 이상 |
0번을 사용할 때는 주의해야 합니다.
?
, *
는 찾는 것이 아예 없는 경우도 포함하기 때문에 원치 않는 결과를 얻을 수 있습니다.
1
2
3
4
5
6
7
8
9
10
const string = "aa11 4455";
const regexp1 = /\d?/gim;
console.log(string.match(regexp1)); // ["", "", "1", "1", "", "4", "4", "5", "5", ""]
const regexp2 = /\d+/gim;
console.log(string.match(regexp2)); // ["11", "4455"]
const regexp3 = /\d*/gim;
console.log(string.match(regexp3)); // ["", "", "11", "", "4455", ""]
4️⃣ 대체, 그룹, 참조
문자 | 의미 |
---|---|
| | 대체: 좌/우측중 하나의 표현식과 일치 |
() | 그룹: 하나의 표현식으로 묶어서 사용 및 참조로 등록 (좌측부터 등록) |
(?:) | 그룹은 하지만 참조로 등록하지 않음 |
\n | 참조로 등록된 n 번째 그룹 (1부터 시작) |
1. 대체
대체(|
)를 사용해서 조건중에 하나를 찾는 방식입니다.
좌측에서 우측으로 탐색하고 가장 먼저 탐색된 g
플래그가 없다면 가장 먼저 탐색된 대상만 찾습니다.
( 더 정확하게 일치하는 것에 대해서는 신경쓰지 않습니다. )
1
2
3
4
// "a"거나 "b"가 1개 이상
const regexp = /a|b+/gim;
console.log("aa11 bbcc4455 abab".match(regexp)); // ["aa", "bb", "a", "b", "a", "b"]
2. 그룹
그룹(()
)은 여러 가지 용도로 사용할 수 있습니다.
탐색을 할 때 그룹으로 묶어서 탐색할 수 있습니다.
( *
, |
, +
등의 반복 문법을 하나의 그룹에 사용 )
아래에서 그룹()
이 없으면 a
또는 b
를 탐색하지만 그룹으로 묶었기 때문에 ab
를 탐색하게 됩니다.
1
2
3
4
// "a"또는 "b"가 1개 이상
const regexp = /(a|b)+/gim;
console.log("aa11 bbcc4455 abab".match(regexp)); // ["aa", "bb", "abab"]
3. 참조 그룹
정규 표현식에서 일반적인 그룹(()
)을 사용하면 하위 패턴을 참조할 수 있습니다.
즉, 그룹화한 대상은 이후에 참조를 통해서 다시 조건을 사용하거나 JavaScript
의 특정 메서드를 이용해서 그 대상을 선택할 수 있습니다.
\1
, \2
처럼 사용합니다.
아래처럼 사용하면 첫 번째 하위 패턴인 (['"])
를 \1
에서 참조하게 됩니다.
즉, /(['"])[^'"]+(['"])/gim
와 같게 동작합니다.
1
2
3
const regexp = /(['"])[^'"]+\1/gim; // /(['"])[^'"]+(['"])/gim
console.log(`a"Hello", b'RegExp'`.match(regexp)); // ['"Hello"', "'RegExp'"]
그룹으로만 묶는 용도로 ()
를 사용하고 싶을 수 있습니다.
즉, 참조를 사용하고 싶지 않다면 (?:)
형태로 사용하면 됩니다.
아래 예시에는 앞에 그룹((?:a|b)
)이 있어도 \1
은 (['"])
를 참조하게 됩니다.
1
2
3
const regexp = /(?:a|b){1}(['"])[^'"]+\1/gim; // /(a|b){1}(['"])[^'"]+(['"])/gim
console.log(`a"Hello", b'RegExp'`.match(regexp)); // ['a"Hello"', "b'RegExp'"]
4. 캡쳐 그룹
캡쳐된 그룹에 이름을 붙이는 방법입니다.
(?<이름>)
형태로 사용하면 이름을 등록한 캡쳐가 되고, \k<이름>
처럼 사용해서 참조할 수 있습니다.
여기서는 유용하지 않고 더 복잡해 보이지만, String.prototype.match()
에서는 더 유용하게 사용할 수 있습니다.
1
2
3
const regexp = /(a|b){1}(?<quotation>['"])[^'"]+\k<quotation>/gim;
console.log(`a"Hello", b'RegExp'`.match(regexp)); // ['a"Hello"', "b'RegExp'"]
5️⃣ 정규 표현식 앵커
문자 | 의미 |
---|---|
^ | 문자열의 처음 |
$ | 문자열의 끝 |
\b | 단어의 경계 |
\B | TODO: |
(?=) | 패턴이 반드시 일치하는지 확인하지만, 그 문자는 포함하지 않음 |
(?!) | 패턴이 반드시 불일치해야하고, 그 문자는 포함하지 않음 |
1. 룩어헤드 어서션 ( lookabead assertion )
(?=
와 )
사이에 사용하고 사이에 적힌 내용이 반드시 있어야 하지만 실제로 검색에는 포함하지 않습니다.
1
2
3
4
5
6
7
8
const regexp = /\d(?=r?em)/gim;
const string = `
font-size: 2rem;
border-radius: 4px;
margin: 0 1em;
`;
console.log(string.match(regexp)); // ["2", "1"]
2. 부정 룩어헤드 어서션
(?!
와 )
사이에 사용하고 사이에 적힌 내용이 없어야 하고 포함하지도 않습니다.
1
2
3
4
5
6
7
8
const regexp = /\d(?=r?em)/gim;
const string = `
font-size: 2rem;
border-radius: 4px;
margin: 0 1em;
`;
console.log(string.match(regexp)); // ["2", "1"]
3. 룩비하인드 어서션
TODO:
6️⃣ 구두점 문자
구두점 문자를 문자 그대로 사용하려면 \
를 붙여줘야 합니다.
구두점 문자가 아니더라도 \
를 붙여줘도 되지만, 글자와 숫자에는 \
와 조합된 특수한 의미를 갖는 경우가 있기 때문에 애매하더라도 특수 문자에만 붙이는 것이 좋습니다.
\k
, \1
, \2
같이 특수한 의미를 갖는 경우가 있기 때문에, \\
, \"
처럼 특수 문자를 그대로 표현할 때만 사용하는 것이 좋습니다.
🔋 JavaScript와 RegExp
JavaScript
에서 정규 표현식을 어떻게 사용하는지 알아보겠습니다.
0️⃣ String.prototype.search()
정규 표현식을 인자로 받고 패턴에 첫 번째로 일치하는 인덱스를 반환합니다.
만약 일치하는 값이 없다면 -1
을 반환합니다.
전역 검색을 지원하지 않습니다. ( g
플래그 무시 )
1
console.log("abcdef".search(/cd/gm)); // 2
1️⃣ String.prototype.replace()
정규 표현식을 입력할 경우 정규 표현식에 일치하는 값을 교체합니다.
1. g 플래그가 있는 경우
정규 표현식에 일치하는 모든 값을 교체합니다.
1
2
3
4
const regexp = /ab/gim;
const string = "a1 b1 ab aabb abab";
console.log(string.replace(regexp, "**")); // "a1 b1 ** a**b ****"
2. g 플래그가 없는 경우
정규 표현식과 일치하는 첫 번째 값만 교체합니다.
1
2
3
4
const regexp = /ab/im;
const string = "a1 b1 ab aabb abab";
console.log(string.replace(regexp, "**")); // "a1 b1 ** aabb abab"
3. 그룹과 참조 활용
정규 표현식의 그룹과 참조를 활용하면 많은 것들을 할 수 있습니다.
이전에 그룹과 참조에서 사용했던 것처럼 참조를 활용할 수 있습니다.
정규 표현식에서는 \n
을 사용했지만, JavaScript
에서는 $n
을 사용해서 참조를 사용할 수 있습니다.
1
2
3
4
5
6
7
const regexp = /(He)/gim;
const string = "He said stop";
console.log(string.replace(regexp, `"$1"`)); // "He" said stop
// "(?:)"를 사용하면 그룹화만 하고 캡쳐되지 않습니다.
console.log(string.replace(regexp, `"$1"`)); // "$1" said stop
4. 캡쳐 그룹 활용
캡쳐 그룹을 사용할 수도 있습니다.
1
2
3
4
const regexp = /(?<subject>He)/gim;
const string = "He said stop";
console.log(string.replace(regexp, `"$<subject>"`)); // "He" said stop
5. 함수 사용하기
정규 표현식과 함수를 사용하면 복잡한 변형을 쉽게 처리할 수 있습니다.
1
2
3
4
const regexp = /\d+/gim;
const string = "5 + 7 = 57";
console.log(string.replace(regexp, (v) => `"${v}"`)); // "5" + "7" = "57"
2️⃣ String.prototype.match()
주어진 정규 표현식과 일치하는 지를 확인합니다.
단, 정규 표현식의 g
플래그 유무에 따라서 다르게 동작합니다.
( 인자가 정규 표현식이 아니라면 RegExp
생성자를 호출합니다. )
1. g 플래그가 있는 경우
주어진 일치하는 것을 모두 찾아 배열로 반환합니다.
만약 일치하는 것이 없다면 null
을 반환합니다.
1
2
3
4
const regexp = /\d+/gim;
const string = "5 + 7 = 57";
console.log(string.match(regexp)); // ["5", "7", "57"]
2. g 플래그가 없는 경우
배열을 반환하고 배열의 인자들은 캡쳐된 문자열들이 순서대로 들어갑니다.
단, 첫 번째 인자는 정규 표현식을 적용한 전체 문자열입니다.
아래에서는 (?:https?)
를 사용했기 때문에 protocol
은 캡쳐되지 않아서 match()
로도 추출하지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const regexp = /(?:https?):\/\/([\w.]+)(?:\:)?(\d+)?/im;
const string = "http://localhost:3000";
console.log(string.match(regexp));
/**
* [
* 'http://localhost:3000', // 전체 문자열
* 'localhost', // 첫 번째 추출된 값
* '3000', // 두 번째 추출된 값
*
* // 위에 부분이 배열에 들어간 데이터
* // 아래 부분이 배열의 프로퍼티로 들어간 데이터
*
* index: 0, // 첫 번째 찾은 값의 인덱스 ( 캡쳐링은 안되는데 인덱스로는 잡힘 )
* input: 'http://localhost:3000', // 입력값
* groups: undefined // 그룹 데이터들 ( 캡쳐 그룹 사용 시 이용 )
* ]
*/
캡쳐 그룹을 사용하면 간단하게 원하는 문자열을 추출할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const regexp = /(?<protocol>https?):\/\/(?<host>[\w.]+)(?:\:)?(?<port>\d+)?/im;
const string = "http://localhost:3000";
console.log(string.match(regexp));
/**
* [
* 'http://localhost:3000',
* 'http',
* 'localhost',
* '3000',
*
* // =======
*
* index: 0,
* input: 'http://localhost:3000',
*
* // 캡쳐 그룹
* groups: [Object: null prototype] {
* protocol: 'http',
* host: 'localhost',
* port: '3000'
* }
* ]
*/
const { protocol, host, port } = string.match(regexp).groups;
console.log("protocol >> ", protocol); // "protocol >> http"
console.log("host >> ", host); // "host >> localhost"
console.log("port >> ", port); // "port >> 3000"
3️⃣ String.prototype.matchAll()
String.prototype.match()
에서 g
플래그 없이 사용할 때 반환하는 배열을 이터러블하게 갖는 객체를 반환합니다.
단, g
플래그가 없다면 에러가 발생합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
const regexp = /(\d)/gim;
const string = "a1 b2 cd 34";
for (const digit of string.matchAll(regexp)) {
console.log(digit);
}
/**
* [ '1', '1', index: 1, input: 'a1 b2 cd 34', groups: undefined ]
* [ '2', '2', index: 4, input: 'a1 b2 cd 34', groups: undefined ]
* [ '3', '3', index: 9, input: 'a1 b2 cd 34', groups: undefined ]
* [ '4', '4', index: 10, input: 'a1 b2 cd 34', groups: undefined ]
*/
4️⃣ String.prototype.split()
주어진 정규 표현식을 기준으로 문자열을 나눠서 넣은 배열을 반환합니다.
1
2
3
4
5
6
7
8
9
10
11
12
const regexp = /\s*\b\s*/gim;
const string = `
a1
b2 c3
d4
z5
`;
console.log(string.trim().split(regexp)); // [ 'a1', 'b2', 'c3', 'd4', 'z5' ]
5️⃣ RegExp 클래스
1. RegExp 생성자와 리터럴의 차이
생성자 함수를 사용하는 경우에는 \\
를 두 번 적어야 하는 번거로움이 있습니다.
그래서 대부분의 경우에는 정규 표현식 리터럴을 사용합니다.
하지만 런타임에 정규 표현식을 생성하고 적용하고 싶다면 정규 표현식 생성자 함수를 사용((1)
)해야합니다.
문자열을 이용해서 정규 표현식을 동적으로 생성할 수 있기 때문에 런타임에 정규 표현식을 생성할 수 있습니다.
1
2
3
4
5
6
7
8
const regexp1 = new RegExp("\\d+", "gim"); // (1)
const regexp2 = /\d+/gim;
const regexp3 = new RegExp(regexp2);
const string = "12a34b";
console.log(string.match(regexp1)); // ["12", "34"]
console.log(string.match(regexp2)); // ["12", "34"]
console.log(string.match(regexp3)); // ["12", "34"]
2. RegExp 객체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const regexp1 = new RegExp("\\d+", "gim");
const regexp2 = /\d+/gim;
const regexp3 = new RegExp(regexp2);
const string = "12a34b";
console.dir(regexp1); // 모두 같은 결과 출력
console.dir(regexp2); // 모두 같은 결과 출력
console.dir(regexp3); // 모두 같은 결과 출력
/**
* lastIndex: 0
* dotAll: false
* flags: "gim"
* global: true
* hasIndices: false
* ignoreCase: true
* multiline: true
* source: "\\d+"
* sticky: false
* unicode: false
*/
- 속성
source
:/
와/
사이에 있는 문자열 ( 읽기 전용 )flags
: 적용된 모든 플래그들 ( 읽기 전용 )global
:g
플래그 여부 ( 읽기 전용 )ignoreCase
:i
플래그 여부 ( 읽기 전용 )multiline
:m
플래그 여부 ( 읽기 전용 )dotAll
:s
플래그 여부 ( 읽기 전용 )unicode
:u
플래그 여부 ( 읽기 전용 )sticky
:y
플래그 여부 ( 읽기 전용 )lastIndex
:g
,y
를 이용한 검색에서 다음 검색 시작 위치를 가리키는 프로퍼티
3. RegExp.prototype.test()
정규 표현식에 문자열이 일치하면 true
아니면 false
를 반환하는 메서드입니다.
1
2
3
4
5
6
7
8
9
10
const regexpString = /\w+/gim;
const regexpNumber = /\d+/gim;
const string = "string";
const number = "1234";
console.log(regexpString.test(string)); // true
console.log(regexpString.test(number)); // false
console.log(regexpNumber.test(string)); // false
console.log(regexpNumber.test(number)); // true
4. RegExp.prototype.exec()
일치하지 않으면 null
, 일치하면 배열을 반환합니다.
배열은 일치한 문자열이 들어있고, index
에는 일치한 인덱스, input
에는 실행한 문자열, groups
에는 그룹 데이터들이 들어갑니다.
g
, y
플래그의 유무에 따라서 lastIndex
의 변경이 달라집니다.
1
2
3
4
5
6
7
8
9
10
const regexpString1 = /\w/gim;
const regexpString2 = /\w/im;
const string = "string";
console.log(regexpString1.exec(string)); // [ 's', index: 0, input: 'string', groups: undefined ]
console.log(regexpString2.exec(string)); // [ 's', index: 0, input: 'string', groups: undefined ]
console.log(regexpString1.lastIndex); // 1
console.log(regexpString2.lastIndex); // 0
만약 아래 정규 표현식 객체에 g
플래그가 없다면 lastIndex
가 변하지 않아서 일치하는 문자열이 있다면 무한 루프에 빠지게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const regexpString = /\w/gim;
const string = "string";
let match = null;
while ((match = regexpString.exec(string)) !== null) {
console.group();
console.log(`매치된 문자: "${match[0]}", 매치된 인덱스: "${match.index}"`);
console.log(`다음 탐색 인덱스: "${regexpString.lastIndex}"`);
console.groupEnd();
}
/**
* 매치된 문자: "s", 매치된 인덱스: "0"
* 다음 탐색 인덱스: "1"
* 매치된 문자: "t", 매치된 인덱스: "1"
* 다음 탐색 인덱스: "2"
* 매치된 문자: "r", 매치된 인덱스: "2"
* 다음 탐색 인덱스: "3"
* 매치된 문자: "i", 매치된 인덱스: "3"
* 다음 탐색 인덱스: "4"
* 매치된 문자: "n", 매치된 인덱스: "4"
* 다음 탐색 인덱스: "5"
* 매치된 문자: "g", 매치된 인덱스: "5"
* 다음 탐색 인덱스: "6"
*/
5. lastIndex 사용 시 주의 사항
exec()
같은 경우는 간접적으로 lastIndex
를 통해서 일치하는 모든 문자열을 찾습니다.
하지만 이것이 직접 수정도 가능하고, 잘못 사용될 가능성도 있습니다.
<p>
가 몇 개인지 찾아보는 코드를 아래처럼 작성했다고 가정하겠습니다.
아래 코드는 무한 루프에 빠지게 됩니다.
/<p>/g
라는 리터럴 정규 표현식을 반복마다 새로 생성하기 때문에 lastIndex
가 계속 0
으로 초기화되기 때문이죠.
이런 구분하기 힘든 문제가 발생할 수 있기 때문에 교재에서는 정규 표현식으로 모든 데이터를 상세하게 추출할 때는 String.prototype.matchAll()
을 사용하는 것을 권장합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
const html = `
<div>
<p>lorem - a</p>
<p>lorem - b</p>
<p>lorem - c</p>
</div>
`
let match = null;
let count = 0;
while ((match = /<p>/g.exec(html)) !== null) {
count++;
}
📮 레퍼런스
- « 자바스크립트 완벽 가이드 11장 3 » ( 데이비드 플래너건 지음, 한성용 옮김, 인사이트, 2022 )