11장(1). 자바스크립트 표준 라이브러리 (Set, Map, 정규 표현식)
11.1 세트와 맵
자바스크립트의 Object 타입은 프로퍼티 이름인 문자열과 임의의 값을 연결하는 다재다능한 데이터 구조다.
그리고 연결된 값이 true처럼 고정된 값이면 그 객체는 문자열 세트나 마찬가지다.
자바스크립트 프로그래밍에서는 객체를 맵이나 세트처럼 사용하는 일이 흔하지만 객체에는 키가 문자열이어야 한다는 제약이 있고,
객체에서 일반적으로 상속하는 toString
같은 프로퍼티를 실제 맵과 세트에서 사용하는 경우는 드물기 때문에 쓸 데 없이 복잡해지기도 한다.
ES6에서 이 문제를 해결하고자 진정한 Set와 Map 클래스를 도입했다.
11.1.1 Set 클래스
세트는 배열과 마찬가지로 값의 집합이다. 하지만 배열과 달리 세트는 순서가 없고 인덱스도 없으며, 중복을 허용하지 않는다. 값은 세트의 요소이거나 요소가 아닐 뿐, 그 값이 세트에 몇 개 있는지 알 수는 없다. Set 객체는 Set() 생성자로 만든다.
let s = new Set(); // 새로운 빈 세트
let t = new Set([1, 2]); // 요소가 둘 있는 세트
Set() 생성자의 인자로는 배열과 Set 객체를 포함한 이터러블 객체 모두 허용된다.
let t = new Set(s); // s의 요소를 복사한 새 세트
let unique = new Set("Mississippi"); // "M", "i", "s", "p"
세트는 size 프로퍼티는 배열의 length 프로퍼티와 마찬가지로 세트에 포함된 값의 개수를 반환한다.
unuque.size // => 4
세트를 생성하면서 동시에 초기화할 필요는 없다. 언제든 add(), delete(), clear()
로 요소를 추가하거나 제거할 수 있다.
세트는 중복을 허용하지 않으므로 이미 세트에 존재하는 값을 추가해도 아무 효과가 없다.
let s = new Set(); // 빈 세트로 시작한다.
s.size // => 0
s.add(1); // 숫자를 추가한다.
s.size // => 1; 이제 세트에는 요소가 하나 있다.
s.add(1); // 같은 숫자를 다시 추가한다.
s.size // => 1; 크기는 그대로다.
s.add(true); // => 다른 값을 추가한다. 타입이 달라도 상관없다.
s.size // => 2
s.add([1, 2, 3]); // 배열을 추가한다.
s.size // => 3; 배열이 추가되었다. 배열의 개별 요소가 추가된 것은 아니다.
s.delete(1); // => true: 요소 1을 성공적으로 삭제했다.
s.size // => 2;
s.delete("test"); // => false; "test"라는 요소는 없으므로 삭제에 실패했다.
s.delete(true); // => true: 요소 true를 성공적으로 삭제했다.
s.delete([1, 2, 3]) // => false: [1, 2, 3]은 세트에 포함된 배열과 다른 존재다. (주소가 다름)
s.size // => 1: 세트에는 여전히 배열이 존대한다.
s.clear(); // => 세트의 요소를 모두 제거한다.
s.size // => 0
- add()
메서드는 인자를 하나 받는다. 배열을 전달하면 배열 자체를 추가한다. add()
는 항상 자신을 호출한 세트를 반환하므로 세트에 여러가지 값을 추가할 때는 s.add('a').add('b').add('c');
처럼 메서드를 체인으로 연결할 수 있다.
- delete()
메서드는 세트 요소를 한 번에 하나씩 삭제한다. delete
는 add()
와 달리 불 값을 반환한다. 지정한 값이 실제로 세트의 요소라면 그 요소를 제거하고 true를 반환한다. 그렇지 않다면 아무 일도 하지 않고 false를 반환한다.
- 세트는 일치 여부를 판단할 때, ===
연산자처럼 엄격하게 체크한다. 세트는 숫자 1과 문자열 "1"을 별개의 값으로 간주하므로 세트 요소로 이 둘을 모두 포함할 수 있다. 값이 객체, 배열, 함수일 경우에도 ===
로 비교한다. 위 예제에서 세트의 배열을 삭제할 수 없었던 이유가 이 때문이다. 삭제에 성공하려면 정확히 같은 배열을 가리키는 참조를 전달해야 한다.
세트로 할 수 있는 가장 중요한 일은 요소를 추가하거나 제거하는 일이 아니라 지정된 값이 세트의 요소인지 아닌지 체크하는 일이다.
has()
메서드가 그 일을 담당한다.
let oneDigitPrimes = new Set([2, 3, 5, 7]);
oneDigitPrimes.has(2) // => true: 2는 한자리 소수다.
oneDigitPrimes.has(4) // => false: 4는 소수가 아니다.
oneDigitPrimes.has("5") // => false: "5"는 문자열이지 숫자가 아니다.
세트는 요소의 존재 여부를 확인하는 데 최적화되어 있으며, 세트에 요소가 얼마나 많든 has()
메서드는 아주 빠르게 원하는 요소의 존재 여부를 파악할 수 있다. 배열도 includes()
메서드를 통해 요소의 존재 여부를 확인할 수 있지만, 배열의 크기에 따라 속도가 달라지며, 배열을 세트처럼 사용하면 실제 Set 객체에 비해 훨씬 느릴 수 있다.
책에서는 이렇게 말했지만 옮긴이가 테스트해본 바로는
배열의 길이가 길어져도 배열 검색의 시간은 비슷한 반면 오히려 세트 검색의 시간이 세트 크기에 비례해 증가했다.
항상 배열의 검색 시간이 느린 것은 아닐 수도 있다는 것을 알아두자.
Set 클래스는 이터러블이므로 for/of 루프로 세트의 요소를 열거할 수 있다.
let sum = 0;
for(let p of oneDigitPrimes) {
sum += p;
}
sum // => 17: 2 + 3 + 5 + 7
Set 객체는 이터러블이므로 분해 연산자 ...
를 써서 배열이나 인자 리스트로 변환할 수 있다.
[...oneDigitPrimes] // => [2,3,5,7]: 세트를 배열로 변환한다.
Math.max(...oneDigitPrimes) // => 7: 세트 요소를 함수 인자로 전달한다.
세트는 종종 '순서 없는 집합'이라고 표현되지만 자바스크립트의 Set 클래스는 그렇지 않다. 자바스크립트 세트는 인덱스가 없으므로 배열처럼 첫 번째 요소가 뭔지, 세 번째 요소가 뭔지 알 수는 없다. 하지만 자바스크립트 Set 클래스는 요소가 삽입된 순서를 기억하고 있으며, 세트를 순회할 때는 항상 이 순서를 지킨다. 세트에 첫 번째로 삽입한 요소는 순회할 때도 첫 번째로 반환되며, 마지막에 삽입한 요소는 마지막으로 반환된다.
Set 클래스는 이터러블이기도 하지만 forEach()
메서드 또한 지원한다.
let product = 1;
oneDigitPrimes.forEach(n => { product *= n });
product // => 210: 2 * 3 * 5 * 7
배열의 forEach()
메서드는 배열 인덱스를 두 번째 인자로 전달하지만 세트에는 인덱스가 없으므로 Set 클래스의 forEach()
는 첫 번째와 두 번째 인자 모두에 요소 값을 전달한다.
11.1.2 Map 클래스
Map 객체는 키로 구성된 값 집합이며 각 키는 다시 다른 값과 연결된다. 어떤 면에서는 맵도 배열과 비슷하지만, 연속된 정수를 키로 사용하는 대신 임의의 값을 '인덱스'로 사용할 수 있다. 배열과 마찬가지로 맵도 아주 빠른 데이터 구조를 갖는다. 맵의 크기와 상관없이 키와 연결된 값을 빨리(배열만큼 빠르지는 않지만) 찾을 수 있다.
새 맵을 생성할 때는 Map()
생성자를 사용한다.
let m = new Map(); // 빈 맵을 생성한다.
let n = new Map([ // 문자열 키를 숫자와 연결하는 새 맵을 만든다.
["one", 1],
["two", 2]
]);
Map()
생성자의 선택 사항인 인자는 [key, value]
배열을 전달하는 이터러블 객체여야 한다. 따라서 맵을 생성하는 동시에 초기화하고 싶다면 원하는 키와 값을 배열의 배열 형태로 준비해야 한다. 하지만 다음과 같이 Map()
생성자로 다른 맵을 복사하거나 기존 객체의 프로퍼티 이름과 값을 복사할 수도 있다.
let copy = new Map(n); // 맵 n과 같은 키와 값을 가진 새 맵
let o = { x: 1, y: 2 } // 프로퍼티가 두 개 있는 객체
let p = new Map(Object.entries(o)); // new Map([["x",1],["y",2]])과 같다.
맵 객체를 만들면 get()
으로 주어진 키와 연결된 값을 검색할 수 있고, set()
으로 키-값 쌍을 추가할 수 있다. 하지만 맵은 키의 집합이며 각 키가 값과 연결될 뿐, 키-값 쌍 집합은 아니다. 맵에 이미 존재하는 키로 set()를 호출하면 해당 키에 연결된 값을 수정할 뿐, 새 키-값 쌍을 맵에 추가하는 것은 아니다. 맵 클래스는 get()
과 set()
외에도 Set와 비슷한 메서드를 가진다.
지정된 키가 맵에 존재하는지 확인할 때는 has()
를, 키와 해당 키에 연결된 값을 제거할 때는 delete()
, 맵에서 키-값 쌍을 모두 제거할 때는 clear()
를, 맵에 포함된 키의 개수를 확인할 때는 size
프로퍼티를 사용한다.
let m = new Map();
m.size // => 0
m.set("one", 1);
m.set("two", 2);
m.get("two") // => 2
m.get("three") // => undefined
m.set("one", true); // 기존 키의 값 변경
m.size // => 2: 크기는 바뀌지 않는다.
m.has("one") // => true
m.has(true) // => false: 맵에는 true라는 키가 존재하지 않는다.
m.delete("one") // => true: 성공적으로 삭제했다.
m.size // => 1
m.delete("three") // => false: 존재하는 키는 삭제할 수 없다.
m.clear(); // => 맵에서 키와 값을 모두 제거한다.
세트의 add()
메서드와 마찬가지로 맵의 set()
메서드 역시 체인으로 연결할 수 있으므로, 맵을 초기화할 때 배열의 배열을 사용하지 않아도 된다.
let m = new Map().set("one", 1).set("two", 2).set("three", 3);
m.size // => 3
m.get("two") // => 2
세트와 마찬가지로 자바스크립트 값이라면 어떤 것이든 맵의 키나 값으로 사용할 수 있다.
null, undefined, NaN, 객체, 배열 다 가능하다. 또한 Set 클래스와 마찬가지로 맵도 키를 비교할 때 동등성이 아닌 일치성으로 비교하므로 객체나 배열을 키로 사용한다면 그 프로퍼티와 요소가 정확히 일치하더라도 항상 다른 것으로 판단한다.
let m = new Map();
m.set({}, 1);
m.set({}, 2);
m.size // => 2: 키가 둘 다 빈 객체지만 주소가 다르므로 다른 키로 인식된다.
m.get({}) // => undefined
m.set(m, undefined) // 맵 자신을 키로 하고 undefined 값을 연결한다.
m.has(m) // => true
m.get(m) // => undefined
Map 객체는 이터러블이며 순회할 때 반환되는 값은, 첫 번째 요소가 키이고 두 번째 요소가 값인 배열이다. Map 객체에 분해 연산자를 사용하면 Map()
생성자에 전달했을 배열의 배열을 반환한다. for/of 루프로 맵을 순회할 때는 다음과 같이 분해 할당을 이용해 키와 값을 별도의 변수에 할당하는 것이 일반적이다.
let m = new Map([["x", 1], ["y", 2]]);
[...m] // => [["x", 1], ["y", 2]]
for(let [key, value] of m) {
// 첫 번째 반복에서 키는 "x", 값은 1입니다. 두 번째 반복에서 키는 "y", 값은 2입니다.
console.log([key, value]);
// ["x", 1]
// ["y", 2]
}
맵의 키나 값 중 하나만 순회하고 싶을 때는 keys()
와 values()
메서드를 사용하면 된다.
이들은 키나 값을 삽입 순서대로 순회하는 이터러블 객체를 반환한다. entries()
메서드가 키-값 쌍으로 이루어진 이터러블 객체를 반환하긴 하지만 이는 맵을 직접 순회하는 것과 차이가 없다.
[...m.keys()] // => ["x", "y"]: 키만
[...m.values()] // => [1, 2]: 값만
[...m.entries()] // => [["x",1],["y",2]]: [...m]과 같다.
Map 객체는 Array 클래스의 forEach()
메서드도 지원한다.
let m = new Map([["x", 1], ["y", 2]]);
m.forEach((value, key) => { // key, value가 아니라 value, key
console.log(key, value);
// x 1
// y 2
})
맵은 정수인 배열 인덱스를 임의의 키 값으로 대체한 일반화된 배열이다. 배열의 forEach()
메서드는 배열 요소 다음에 배열 인덱스를 전달하므로, 맵의 forEach()
메서드도 이에 맞게 맵의 값을 먼저 전달하고 그다음에 맵의 키를 전달한다.
11.3 정규 표현식과 패턴 매칭
11.3.1 정규 표현식 정의
자바스크립트에서는 RegExp 객체로 정규 표현식을 표현한다.
물론 RegExp()
생성자로도 RegExp 객체를 생성할 수는 있지만, 보통은 특별한 리터럴 문법을 더 자주 사용한다.
문자열 리터럴이 따옴표 안에 문자를 쓰는 것과 마찬가지로, 정규 표현식 리터럴은 슬래시(/) 한 쌍 안에 문자를 쓴다.
let pattern = /s$/;
위 RegExp 객체는 s로 끝나는 문자열 전체에 일치한다. 이 정규 표현식은 다음과 같이 RegExp()
생성자로도 정의할 수 있다.
let pattern = new RegExp("s$");
영문자와 숫자를 포함해 대부분의 문자는 문자 그대로 해석한다. 따라서 정규 표현식 /java/
는 'java'가 포함된 모든 문자열에 일치한다. 정규 표현식에는 해당 문자에 그대로 일치하지 않고 특별한 의미를 갖는 문자도 있다.
예를 들어 정규 표현식 /s$/
에는 문자가 두 개 있는데, 첫 번째인 s는 문자 그대로 일치한다.
두 번째인 $
는 문자열 끝에 일치하는 메타 문자다. 따라서 이 정규 표현식은 마지막 글자가 s인 모든 문자열에 일치한다. 따라서 이 정규 표현식은 마지막 글자가 s인 모든 문자열에 일치한다.
정규 표현식은 그 동작 방식을 지정하는 하나 이상의 플래그 문자를 가질 수 있다.
플래그는 RegExp 리터럴의 두 번째 슬래시 다음에 쓰거나 RegExp()
생성자의 두 번째 인자로 쓴다. 예를 들어 s나 s로 끝나는 문자열을 찾고 싶다면 다음과 같이 i 플래그를 써서 대소문자를 가리지 않는다고 지정한다.
let pattern = /$s/i;
리터럴 문자
정규 표현식 알파벳 문자와 숫자는 모두 문자 그대로 해석한다. 자바스크립트 정규 표현식 문법은 역슬래시로 시작하는 이스케이프 시퀀스를 통해 알파벳이 아닌 문자도 지원한다. 예를 들어 \n
은 문자열에 포함된 뉴라인 문자와 일치한다.
문자 | 일치하는 문자 |
영문자와 숫자 | 자기 자신 |
\0 |
NUL 문자 (\u0000 ) |
\t |
탭 (\u0009 ) |
\n |
뉴라인 (\u000A ) |
\v |
세로 탭 (\u000B ) |
\f |
폼 피드 (\u000C ) |
\r |
캐리지 리턴 (\u000D ) |
\xnn |
16진수 숫자 nn으로 나타낸 라틴 문자. 예를 들어 \x0A 는 \n 과 같다. |
\uxxxx |
16진수 숫자 xxxx로 나타낸 유니코드 문자. 예를 들어 \u0009 는 \t 와 같다. |
\u{n} |
코드 포인트 n으로 나타낸 유니코드 문자. n은 0과 10FFFF 사이에 있는 여섯 개 이하의 16진수 숫자다. 이 문법은 u 플래그를 사용하는 정규 표현식에만 지원된다. |
\cX |
제어 문자 ^X. 예를 들어 \cJ 는 뉴라인 문자 \n 과 동등하다. |
문자 클래스
/[abc]/
는 a, b, c 모두와 일치한다.
/[^abc]/
는 a, b, c를 제외한 모든 문자와 일치한다.
문자 클래스 안에 하이픈(-
)을 써서 문자 범위를 표현할 수 있다.
알파벳 소문자 전체를 찾으려면 /[a-z]/
, 알파벳 대소문자와 숫자 전체를 찾으려면 /[a-zA-Z0-9]/
를 사용하면 된다.
실제로 하이픈도 찾고 싶다면 오른쪽 대괄호 바로 앞에 하이픈을 쓰면 된다.
문자 | 일치하는 문자 |
[...] |
대괄호 안에 있는 어떤 문자에든 일치 |
[^...] |
대괄호 안에 포함되지 않은 어떤 문자에든 일치 |
. |
뉴라인을 비롯한 유니코드 줄 끝 문자(line terminator)를 제외한 모든 문자에 일치. 정규 표현식에 s 플래그가 있다면 마침표는 줄 끝 문자를 포함해 모든 문자에 일치한다. |
\w |
ASCII 단어 문자 전체에 일치. [a-zA-z0-9_] |
\W |
ASCII 단어가 아닌 문자 전체에 일치. [^a-zA-z0-9_] |
\s |
유니코드 공백 문자 전체에 일치. 스페이스, 탭 등 |
\S |
유니코드 공백 문자를 제외한 문자 전체에 일치 |
\d |
ASCII 숫자 전체에 일치. [0-9] |
\D |
ASCII 숫자를 문자 전체에 일치. [0-9] |
[\b] |
리터럴 백스페이스(특별한 경우) |
특수한 문자 클래스 이스케이프는 대괄호 안에 쓸 수 있다.
** ES2018에서는 u
플래그를 사용한 정규 표현식에서 문자 클래스 \p{...}
와 그 부정 \P{...}
를 지원한다.
이들 문자 클래스는 유니코드 표준에서 정의하는 프로퍼티를 기반으로 만들어졌으며 유니코드가 발전함에 따라 클래스가 늘어날 수 있다.
\d
문자 클래스는 ASCII 숫자에만 일치한다.
\p{Decimal_Number}
로 각국의 언어에서 사용하는 숫자를 찾을 수 있다. 반대로 숫자가 아닌 문자를 찾으려면 p를 대문자로 써서 \P{Decimal_Number}
를 사용한다. 분수와 로마 숫자를 포함해 숫자 비슷한 문자를 찾을 때는 \p{Number}
를 사용한다.
여기서 Decimal_Number
와 Number
는 자바스크립트 또는 정규 표현식 문법에서만 사용하는 것이 아니라 유니코드 표준에서 정의한 문자 카테고리 이름이다.
\w
문자 클래스는 ASCII 텍스트에만 일치하지만, \p
를 써서 다음과 같이 대략적인 국제화 버전을 만들 수 있다.
\p
문법을 써서 특정 알파벳이나 언어 체계에 일치하는 정규 표현식을 만들 수 있다.
let greekLetter = /\p{Script=Greek}/u;
let cyrillicLetter = /\p{Script=Cyrillic}/u;
반복
/\d\d/
: 숫자 두 개
/\d\d\d\d/
: 숫자 네 개
이런 식으로 찾을 수도 있지만
숫자가 몇 개든 관계 없이 찾는다거나, 글자 세 개 뒤에 숫자가 있어도 되고 없어도 된다는 식으로 찾는 것은 불가능하다.
정규 표현식에서는 특정 요소가 몇 번 반복될지 정의할 수 있다.
반복 횟수를 지정하는 문자는 항상 반복할 패턴 뒤에 붙여 쓴다.
예를 들어 +
는 앞의 패턴이 한 번 이상 나타난다는 뜻이다.
문자 | 의미 |
{n,m} |
n번 이상, m번 이하 |
{n,} |
n번 이상 |
{n} |
정확히 n번 |
? |
0번 또는 1번. 즉 앞의 패턴이 없어도 된다. {0,1} 와 동등하다. |
+ |
한 번 이상. {1,} 와 동등하다. |
* |
0번 이상. {0,} 와 동등하다. |
let r = /\d{2,4}/; // 2~4개의 숫자
r = /\w{3}\d?/; // 정확히 세 글자, 그 뒤에 숫자가 있어도 되고 없어도 된다.
r = /\s+java\s+/; // "java", 앞뒤에 스페이스 하나 이상
r = /[^(]*/; // (를 제외한 문자 0개 이상
소극적 반복
위 표의 반복 문자는 일치하는 것을 최대한 많이 찾으려고 한다. 이런 반복 방식을 '적극적(greedy)' 반복이라고 부른다.
이와 달리 소극적으로 반복하도록 지정할 수도 있다. 반복 문자 뒤에 물음표를 붙여서 +?
, *?
, {1,5}?
같은 형태로 만들면 된다.
![]() |
![]() |
/a+/
는 글자 a가 하나 이상 있으면 일치하므로 문자열 "aaa"에 적용했을 때 세 글자 전체에 일치한다.
반면 /a+?/
는 하나 이상의 a에 일치하는 것은 그대로지만, 일치하는 것을 최소한으로만 찾으므로 "aaa"에 적용하면 첫 번째 a에만 일치한다.
주의!
a를 하나 하나 짚어가며 'ab' 만 찾아주면 좋겠지만, b 앞에 a가 하나 이상 있는 것을 찾아버리면 탐색이 중단되어 한 번에 일치시킨다.
대체, 그룹, 참조
[대체 옵션]
일치하는 것을 찾을 때까지 왼쪽에서 오른쪽으로 진행한다.
왼쪽에서 옵션을 찾으면, 오른쪽에 있는 옵션이 '더 잘' 일치하더라도 무시된다.
정규 표현식은 괄호를 여러 가지 용도로 사용한다.
[그룹 옵션]
아이템을 그룹으로 묶어서 |, *, +, ?
등이 이를 하나의 단위로 취급하도록 한다.
ex1)
/java(script)?/
는 "java" 뒤에 "script"가 있거나 없을 때 일치한다.
ex2)
[그룹 옵션]
하위 패턴을 정의한다.
괄호를 사용한 하위 표현식을 쓰면 같은 정규 표현식에서 해당 하위 표현식을 참조할 수 있다.
\
다음에 숫자를 쓰면 된다. 여기서 숫자는 정규 표현식 안에서 하위 표현식의 위치다. 예를 들어 \1
은 첫 번째 하위 표현식을, \3
은 세 번째 하위 표현식을 참조한다. 하위 표현식은 중첩할 수 있으므로 숫자는 왼쪽 괄호의 위치다.
예를 들어 다음 정규 표현식에서 하위 표현식 ([Ss]cript)는 \2
로 참조한다.
/([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/
하위 표현식에 대한 참조는 해당 하위 표현식이 아니라 그 패턴에 일치하는 텍스트를 참조한다. 따라서 참조를 사용해 문자열의 서로 다른 부분이 정확히 같은 문자를 포함하도록 제한을 추가할 수 있다.
예를 들어 다음 정규 표현식은 큰따옴표 또는 작은 따옴표 사이에 있는 0개 이상의 문자에 일치한다. 하지만 열고 닫는 따옴표가 일치해야 한다는 제한은 없다.
/['"][^'"]*['"]/
다음과 같이 참조를 사용해 따옴표 종류도 일치해야 한다는 제한을 추가할 수 있다.
/(['"])[^'"]*\1/
![]() |
![]() |
\1
는 '첫 번째 하위 표현식에 일치한 텍스트'에 일치한다. 앞에서 '
가 일치했다면 맨 뒤에도 '
가 일치해야 하고,
앞에서 "
가 일치했다면 맨 뒤에도 "
가 일치해야 한다.
일치 위치 지정
정규 표현식 요소 중 상당수는 문자열에서 문자 하나에 일치한다. 예를 들어 \s
는 공백 문자 하나에 일치한다.
정규 표현식 요소 중에는 실제 문자가 아니라 문자 사이의 위치에 일치하는 것도 있다. 예를 들어 \b
는 ASCII 단어 경계다.
따라서 ASCII 단어 문자인 \w
와 단어가 아님을 뜻하는 \W
문자 사이의 경계에 일치하고, ASCII 단어 문자와 문자열의 시작 또는 끝 사이의 경계에도 일치한다. \b
같은 요소는 문자를 지정하지 않고 일치가 있을 만한 위치를 지정한다. 위치를 지정한다는 의미에서 '정규 표현식 앵커'라고 부르기도 한다. 가장 널리 쓰이는 앵커는 문자열의 시작을 나타내는 ^
, 문자열의 끝을 나타내는 $
이다.
예를 들어 JavaScript라는 단어와 있는 그대로 일치하는 문자열을 찾을 때는 정규 표현식 /^JavaScript$/
를 사용한다.
Java를 JavaScript의 접두사가 아니라 그 자체로 하나의 단어인 것만 찾고 싶을 때, 단어 앞뒤에 공백 문자를 지정하는 /\sJava\s/
를 쓸 수 있다. 하지만 이 방법에는 두 가지 문제가 있다. 첫 번째는 Java가 문자열 맨 앞이나 맨 뒤에 있는 경우, 양옆에 모두 공백 문자가 있지 않으므로 찾을 수 없다는 것이다. 두 번째는 이 패턴으로 일치하는 것을 찾으면 좌우에 반드시 스페이스가 있을 텐데 반드시 그런 경우여야만 하는것은 아닐 수도 있다는 것이다. 따라서 \s
로 공백 문자를 찾는 게 아니라 \b
로 단어 경계를 찾아야 한다.
![]() |
![]() |
반대로 \B
는 단어 경계에 해당하지 않는 위치에 일치한다.
따라서 패턴 /\B[Ss]cript/
는 JavaScript와 postscript에는 일치하지만 script나 Scripting에는 일치하지 않는다.
임의의 정규 표현식을 앵커 조건으로 쓸 수도 있다. (?=표현식)
이런 식으로 쓰면 lookahead assertion이 되며, 이 표현식은 그런 부분이 반드시 있어야 한다고 지정하지만 실제로 일치시키지는 않는다. 예를 들어, 'Java'나 'JavaScript'뒤에 콜론이 붙은 형태만 찾고 싶다면 /[Jj]ava([Ss]cript)?(?=\:)/
같은 표현식을 쓸 수 있다. 이 패턴은 JavaScript: The Definitive Guide에 포함된 JavaScript는 찾지만, Java in a Nutshell에 포함된 Java는 뒤에 콜론이 없으므로 찾지 않는다.
![]() |
![]() |
문자 | 의미 |
^ |
문자열의 처음, m 플래그를 사용했다면 행의 처음에 일치한다. |
$ |
문자열의 끝, m 플래그를 사용했다면 행의 끝에 일치한다. |
\b |
단어 경계에 일치한다. 즉 \w 문자와 \W 문자 사이, \w 문자와 문자열의 시작 또는 끝 사이에 일치한다. |
\B |
단어 경계에 해당하지 않는 위치에 일치한다. |
(?=p) |
긍정 lookahead assertion. 이어지는 문자가 패턴 p에 일치하길 요구하지만 그 문자를 실제로 사용하지는 않는다. |
(?!p) |
부정 lookahead assertion. 이어지는 문자가 패턴 p에 일치하지 않기를 요구한다. |
ES2018은 자바스크립트 정규 표현식을 확장해 'lookbehind assertion'을 도입했다.
'lookbehind'는 'lookahead'와 비슷하지만 현재 일치하는 위치보다 앞에 있는 텍스트를 참조한다.
긍정 lookbehind assertion은 (?<=표현식)
, 부정 lookbehind assertion은 (?<!표현식)
을 사용한다.
예를 들어 미국 우편 주소는 다섯 자리 숫자로 이루어진 우편 번호 앞에 두 글자로 이루어진 주 이름 약어가 들어가 있는 형태다.
/(?<=[A-Z]{2})\d{5}/
플래그
플래그는 정규 표현식 리터럴 마지막의 /
뒤에 쓰거나 RegExp()
생성자의 두 번째 인자로 전달한다.
g
정규 표현식이 '전역'이라는 의미. 즉, 이 정규 표현식에 첫 번째로 일치하는 것을 찾는 즉시 끝내지 않고 일치하는 것을 모두 찾는다는 의미다. 이 플래그는 패턴 매칭 방법 자체를 바꾸지는 않지만 문자열의 match()
메서드와 정규 표현식의 exec()
메서드가 동작하는 방법을 바꾼다.
i
대소문자를 가리지 않고 일치하는 것을 찾으라는 뜻.
m
'여러 행' 모드에서 일치하는 것을 찾으라는 뜻.
이 플래그를 사용하면 여러 행으로 이루어진 문자열을 대상으로 정규 표현식을 사용한다는 뜻.
^
과 $
를 문자열 전체의 시작과 끝 뿐 아니라 각 행의 시작과 끝에서도 찾는다는 뜻이다.
s
정규 표현식에서 .
문자는 일반적으로 줄 끝 문자를 제외한 문자에 일치하지만,
s
플래그를 사용하면 줄 끝 문자를 포함한 모든 문자에 일치한다.
u
유니코드를 뜻하며 정규 표현식이 16비트 값이 아닌 유니코드 코드 포인트를 기준으로 동작하게 만든다.
y
정규 표현식이 문자열의 처음, 또는 이전에 일치한 것의 첫 번째 문자에 일치해야 한다는 뜻이다.
y
플래그는 문자열 안에서 일치하는 것을 반복적으로 찾는 정규 표현식에서 더 유용하다. 이런 경우 문자열의 match()
메서드와 정규 표현식의 exec()
메서드에서 일치하는 것을 찾을 때 이전에 찾은 텍스트가 끝나는 위치 바로 뒤에서 찾도록 한다.
플래그는 여러 개를 순서 상관 없이 조합해 쓸 수 있다.
ex) uig
, gui
11.3.2 패턴 매칭 문자열 메서드
RegExp 객체 API
search()
이 메서드는 정규 표현식 인자를 받고 패턴에 첫 번째로 일치하는 부분 문자열의 위치를 반환하며, 일치하는 것이 없으면 -1을 반환한다.
"JavaScript".search(/script/ui) // => 4
"Python".search(/script/ui) // => -1
search()
의 인자가 정규 표현식이 아니면 먼저 RegExp 생성자를 사용해 정규 표현식으로 변환한다. search()
는 전역 검색을 지원하지 않으며 정규 표현식 인자의 g
플래그를 무시한다.
replace()
찾아 바꾸기 동작을 수행한다. 이 메서드는 정규 표현식을 첫 번째 인자로 받고 대체할 문자열을 두 번째 인자로 받는다. 메서드를 호출하면 문자열에서 지정된 패턴에 일치하는 것을 찾는다. 정규 표현식에 g
플래그가 있으면 replace()
메서드는 문자열에서 일치하는 것을 전부 대체할 문자열로 교체하며, g
플래그가 없으면 첫 번째로 일치하는 것만 교체한다. replace()
는 첫 번째 인자가 문자열일 경우 문자열을 문자 그대로 검색한다. search()
가 RegExp()
생성자를 호출해 인자를 정규 표현식으로 변환하는 것과는 다르다. 다음 예제는 문자열에서 JavaScript를 대소문자 가리지 않고 찾아 전부 JavaScript로 교체한다.
text.replace(/javascript/gi, "JavaScript");
replace()
에는 이보다 강력한 사용 방법이 많다. 정규 표현식에서 괄호로 감싼 하위 표현식은 왼쪽에서 오른쪽으로 숫자를 부여받으며 정규 표현식은 각 하위 표현식에 일치하는 텍스트를 기억한다. 대체할 문자열에 $ 뒤에 숫자 하나가 연달아 포함되어 있다면, replace()
는 두 문자를 지정된 하위 표현식에 일치하는 텍스트로 대체한다. 예를 들어 이 기능을 사용해 문자열에 포함된 따옴표를 다른 문자로 바꿀 수 있다.
// quote는 따옴표 뒤에 따옴표 아닌 문자가 임의의 숫자만큼 이어지며
// 그 뒤에 따옴표가 오는 형태다. (따옴표 안의 문자는 캡처한다.)
let quote = /"([^"]*)"/g;
// 직선인 따옴표를 이중 꺾쇠(≪)로 교체하고 $1에 저장된 텍스트는 바꾸지 않는다.
'He said "stop"'.replace(quote, '≪$1≫') // => 'He said ≪stop≫'
정규 표현식에서 이름 붙은 캡처 그룹을 사용한다면 숫자 대신 이름으로 텍스트를 참조할 수 있다.
let quote = /"(?<quotedText>[^"]*)"/g;
'He said "stop"'.replace(quote, '≪$<quotedText>≫'); // => 'He said ≪stop≫'
두 번째 인자로 바꿀 문자열 대신 함수를 전달해 값을 반환하도록 해도 된다.
이 함수는 여러 가지 인자를 받으며 호출된다.
let string = 'hajongon is 33'
let regexp = /(?<age>[0-9]{1,})/g
1. 정규 표현식에서 일치한 부분.
2. 정규 표현식에서 일치한 부분. (왜 같은 게 있죠)
=> 같은 게 있는 게 아니라 그룹 1번을 출력한 것이다. 그룹 1번 매치된 것이 전체 매치된 것과 같기 때문에...
그룹이 더 많으면 그룹 처음부터 끝까지 (따로 따로) 인자로 넣을 수 있다.
3. 문자열에서 일치하는 것을 찾은 위치.
4. replace()
를 호출한 문자열 전체.
5. (정규 표현식에 이름 붙은 캡처 그룹이 있을 때) 객체 { 캡처 그룹 이름: 일치하는 텍스트 }
match()
문자열 정규 표현식 메서드 중에서 가장 많이 쓰인다.
정규 표현식 하나만 인자로 받으며, 일치하는 것을 배열에 담아 반환하고 일치하는 것이 없다면 null
을 반환한다.
인자가 정규 표현식이 아니면 RegExp()
생성자를 호출한다. 정규 표현식에 g
플래그가 있으면 문자열에서 일치하는 것을 모두 찾아 배열로 반환한다.
const matched = "7 plus 8 equals 15".match(/\d+/g);
console.log(matched); // ['7', '8', '15']
정규 표현식에 g
플래그가 없으면 첫 번째로 일치하는 것만 반환한다.
이 경우에도 배열을 반환하는 것은 마찬가지지만 배열 요소는 완전히 다르다. g
플래그가 없으면 반환된 배열의 첫 번째 요소는 match()
를 호출한 문자열 전체이며 나머지 요소는 정규 표현식에서 괄호로 감싼 캡처 그룹에 일치하는 부분 문자열이다.
따라서 match()
가 a
배열을 반환했다면 a[0]
은 문자열 전체이고, a[1]
은 괄호로 감싼 첫 번째 표현식에 일치하는 부분이며, a[2]
는 두 번째 식이다. a[1]
은 $1, a[2]
는 $2 식이라는 측면에서 replace()
메서드와 비슷하다고 할 수 있다.
예를 들어 다음 코드로 URL을 분석해보자.
let url = /(\w+):\/\/([\w.]+)\/(\S*)/;
let text = "Visit my blog at http://www.hajongon.com/~home";
let match = text.match(url);
let fullurl, protocol, host, path;
if (match !== null) {
fullurl = match[0];
protocol = match[1];
host = match[2];
path = match[3];
}
'이름 붙은 캡처 그룹' 사용
문자열의 match()
메서드에 문자열의 g
플래그가 없는 정규 표현식을 전달하면 정규 표현식의 문자열의 exec()
메서드에 같은 문자열을 전달한 것과 마찬가지라는 점도 알아두자. 반환되는 배열과 프로퍼티가 모두 일치한다.
matchAll()
g
플래그가 있는 정규 표현식을 받는다. 이 메서드는 match()
처럼 일치하는 부분 문자열을 담은 배열을 반환하는 것이 아니라, match()
에 g
플래그가 없는 정규 표현식을 전달해 호출할 때 반환하는 객체와 비슷한 이터레이터를 반환한다. 따라서 문자열에서 일치하는 것을 순회할 때 가장 쉽고 범용적으로 사용할 수 있다.
다음과 같이 matchAll()
을 써서 문자열에서 찾은 단어를 순회할 수 있다.
RegExp 객체의 lastIndex
프로퍼티를 바꿔서 matchAll()
메서드가 검색을 시작할 인덱스를 지정할 수도 있다.
다른 패턴 매칭 메서드와는 달리 matchAll()
은 절대 lastIndex
프로퍼티를 수정하지 않으므로 코드에서 버그가 생길 가능성이 적다.
split()
이 메서드는 인자를 구분자로 사용해 나눈 부분 문자열을 배열에 담아 반환한다.
"123,456,789".split(",") // => ["123","456","789"]
split()
메서드는 인자로 정규표현식도 받을 수 있다. 다음 예제는 구분자 옆에 공백이 몇 칸 있든 따지지 않는다.
"1, 2, 3, \n4, 5".split(/\s*,\s*/) // => ["1", "2", "3", "4", "5"]
split()
에 전달한 정규 표현식에 캡처 그룹이 있으면 반환된 배열에는 캡처 그룹에 일치하는 텍스트 역시 포함된다.
const htmlTag = /<([^>]+)>/ // < (> 아닌 문자 하나 이상) >
"Testing<br/>1,2,3".split(htmlTag) // => ["Testing", "br/", "1,2,3"]
11.3.3 RegExp 클래스
RegExp()
생성자는 문자열 한 개 또는 두 개를 받아 새 RegExp 객체를 생성한다.
생성자의 첫 번째 인자는 정규 표현식의 바디가 될 문자열, 즉 정규 표현식 리터럴에서 슬래시 사이에 있는 텍스트다. 문자열 리터럴과 정규 표현식 모두 이스케이프 시퀀스에 \
문자를 사용하므로 RegExp()
에 정규 표현식을 문자열 리터럴로 전달할 때는 반드시 \
문자를 \\
로 바꿔야 한다. 두 번째 인자는 선택 사항이며 전달할 경우 정규 표현식 플래그로 사용된다. (g, i, m, s, u, y
)
// 문자열에서 다섯 자리 숫자를 모두 찾는다. 역슬래시가 두 번인 것에 주의
let zipcode = new RegExp("\\d{5}", "g");
정규 표현식을 동적으로 생성해야 하고 정규 표현식 리터럴 문법은 사용할 수 없을 때 RegExp()
생성자를 사용한다.
예를 들어 사용자가 입력하고 있는 문자열을 즉각적으로 검색해야 할 때는 반드시 런타임에 RegExp()
를 써서 정규 표현식을 생성해야 한다.
RegExp()
의 첫 번째 인자에는 문자열 뿐만 아니라 RegExp 객체 역시 전달할 수 있다. 이를 이용해 정규 표현식을 복사하면서 플래그를 바꿀 수도 있다.
let exactMatch = /JavaScript/;
let caseInsensitive = new RegExp(exactMatch, "i");
[정규 표현식 프로퍼티]
RegExp 객체에는 다음과 같은 프로퍼티가 있다.
source
읽기 전용 프로퍼티이며 정규 표현식의 소스 텍스트, 즉 정규 표현식 리터럴에서 슬래시 사이에 있는 문자들.
flags
정규 표현식의 플래그인 읽기 전용 프로퍼티.
global
g
플래그가 있는지 나타내는 읽기 전용 불 프로퍼티.
ignoreCase
i
플래그가 있는지 나타내는 읽기 전용 불 프로퍼티.
multiline
m
플래그가 있는지 나타내는 읽기 전용 불 프로퍼티.
dotAll
s
플래그가 있는지 나타내는 읽기 전용 불 프로퍼티.
unicode
u
플래그가 있는지 나타내는 읽기 전용 불 프로퍼티.
sticky
y
플래그가 있는지 나타내는 읽기 전용 불 프로퍼티.
lastIndex
읽고 쓸 수 있는 정수 프로퍼티. g
나 y
플래그를 사용한 패턴에서 이 프로퍼티는 다음 검색을 시작할 위치를 지정한다.
test()
와 exec()
메서드가 이 프로퍼티를 사용한다.
test()
이 메서드는 문자열을 인자로 받고 문자열이 패턴에 일치하면 true
, 일치하지 않으면 false
를 반환한다.
test()
는 exec()
메서드를 호출하고 exec()
가 null
이 아닌 값을 반환하면 true
를 반환한다.
따라서 g
나 y
플래그를 사용하는 정규 표현식에서 test()
를 호출하면 RegExp 객체의 lastIndex
프로퍼티 값에 따라 동작하며 예상치 못한 결과가 발생할 수 있다.
exec()
이 메서드는 문자열 하나를 인자로 받아 일치하는 것이 없으면 null
, 있으면 배열을 반환하며 이 배열은 g
플래그가 없는 match()
메서드가 반환하는 배열과 같다. 이 배열의 인덱스 0은 문자열 전체이며 이어지는 배열 요소는 캡처 그룹에 일치한 부분 문자열이다. 반환된 배열에는 이름 붙은 프로퍼티도 있다.
index
프로퍼티는 일치하는 것을 찾은 문자 위치, input
프로퍼티는 검색한 문자열, groups
프로퍼티는 이름 붙은 캡처 그룹에 일치하는 부분 문자열을 담은 객체다.
문자열의 match()
메서드와 달리 exec()
는 정규 표현식에 g
플래그가 있든 없든 같은 배열을 반환한다. match()
는 g
플래그가 있는 정규 표현식을 받았을 때 일치하는 것들의 배열을 반환하지만, exec()
는 일치하는 것을 항상 하나 반환하며 그에 관한 정보를 모두 제공한다. g
플래그나 y
플래그가 있는 정규 표현식에서 exec()
를 호출하면 RegExp 객체의 lastIndex
프로퍼티를 참조해 검색을 시작할 위치를 판단한다.
새로 생성된 RegExp 객체의 lastIndex
프로퍼티는 0이므로 문자열 처음부터 검색을 시작한다.
exec()
는 일치하는 것을 찾을 때마다 lastIndex
를 바로 다음 인덱스로 업데이트 한다. 일치하는 것을 찾지 못하면 lastIndex
를 0으로 리셋한다. 이런 특별한 동작 방식 덕에 문자열에서 일치하는 것을 모두 찾을 때까지 루프 안에서 exec()
를 반복적으로 호출할 수 있다.
예를 들어 다음 코드의 루프는 두 번 실행된다.
let pattern = /Java/g;
let text = "JavaScript > Java";
let match;
while((match = pattern.exec(text)) !== null) {
console.log(`Matched ${match[0]} at ${match.index}`);
console.log(`Next search begins at ${pattern.lastIndex}`);
}