정규 표현식 ( RegExp )
포스트
취소

정규 표현식 ( RegExp )

🔌 정규 표현식 ( RexExp )

정규 표현식은 텍스트의 패턴을 정의하는 객체입니다.
즉, 복잡한 텍스트에서 특수한 패턴을 가진 텍스트만 골라내는데 유용하게 사용할 수 있습니다.
( ex) 해시태그, 전화번호, 비밀번호 등등 )

0️⃣ 정규 표현식 정의 방법

  1. RegExp 생성자 함수 사용 ((1))
  2. 정규 표현식 리터럴 사용 ((2))

일반적으로 정규 표현식 리터럴을 사용합니다. ( 더 간단함 )

1
2
3
4
5
// (1)
let pattern1 = new RegExp("^a", "giu");

// (2)
let pattern2 = /^a"/giu;

1️⃣ 플래그 ( flag )

문자의미
g일치하는 모든 문자열을 찾음
i대소문자 구문하지 않음
m여러 행에서 일치 여부를 찾음 ( ^, $가 각 라인의 시작과 끝도 확인함 )
sTODO:
u16비트를 초과하는 문자 탐색 ( 한자, 이모지 등 )
yTODO:

정규 표현식의 매칭 방법을 결정하는 방법입니다.
같은 정규 표현식이라도 플래그에 따라서 선택될 수 있고 안될 수 있습니다.
플래그는 생성자 함수의 두 번째 인자나, 정규 표현식 리터럴 바로 뒤에 사용할 수 있습니다.

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단어의 경계
\BTODO:
(?=)패턴이 반드시 일치하는지 확인하지만, 그 문자는 포함하지 않음
(?!)패턴이 반드시 불일치해야하고, 그 문자는 포함하지 않음

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
 */
  • 속성
    1. source: //사이에 있는 문자열 ( 읽기 전용 )
    2. flags: 적용된 모든 플래그들 ( 읽기 전용 )
    3. global: g 플래그 여부 ( 읽기 전용 )
    4. ignoreCase: i 플래그 여부 ( 읽기 전용 )
    5. multiline: m 플래그 여부 ( 읽기 전용 )
    6. dotAll: s 플래그 여부 ( 읽기 전용 )
    7. unicode: u 플래그 여부 ( 읽기 전용 )
    8. sticky: y 플래그 여부 ( 읽기 전용 )
    9. 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++;
}

📮 레퍼런스

  1. « 자바스크립트 완벽 가이드 11장 3 » ( 데이비드 플래너건 지음, 한성용 옮김, 인사이트, 2022 )
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.