[완벽 가이드] 11장. 자바스크립트 표준 라이브러리

Ninto·2023년 2월 20일
3

학습 기록

목록 보기
4/17

📌 목차

  • 🌱 API와 라이브러리, 프레임워크
    • API
    • Library
    • Framework

  • 🌱 Set과 Map
    • Set
    • Map
    • WeakSet과 WeakMap

  • 🌱 정규 표현식 (RegExp)
    • 정규 표현식이란?
    • 정규 표현식의 정의와 표현
    • 자바스크립트 정규표현식 활용


📌 API와 라이브러리, 프레임워크

프로그래밍 언어를 배우는 것은 단순히 문법을 마스터 한다고 끝나는 일이 아닙니다.

언어에 포함된 도구 전체에 익숙해질 수 있도록 표준 라이브러리를 공부하는 것도 마찬가지로 중요합니다.

자바스크립트를 사용하다보면 굉장히 많은 API와 라이브러리, 프레임워크를 제공받고 사용할 수 있다는 것을 확인할 수 있습니다.

그렇다면, 간단하게 API와 라이브러리, 프레임워크의 특징과 차이점에 대해 살펴봅시다.



🔗 API

API란 무엇일까요? 🤔

API (Application Programming Interface)란, 응용 프로그램에서 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스입니다.

조금 더 쉽게 말하자면, 프로그램 사이를 연결을 시켜주는 다리라고 생각할 수 있습니다.

API의 특징

  • 구현과 독립적으로 사양만 정의되어 있습니다.
  • API에 따라서 접근 권한이 필요할 수 있습니다.
  • 예시로 Java API, 여러 기업들의 오픈 API가 있습니다.

🔗 Library

단어적으로만 보았을 때, Library는 도서관을 떠올리기 쉽습니다.
책들을 카테고리별로 모아놓은 것이 책장이라면, 도서관은 그런 책장들이 굉장히 많이 있는 것을 생각할 수 있습니다.

라이브러리란 무엇일까요? 🤔

Library란, 응용 프로그램 개발을 위해 필요한 기능(함수)을 모아 놓은 소프트웨어입니다.

Library의 특징

  • 독립성을 가집니다. (해당 라이브러리는 다른 라이브러리를 의존하지 않는걸 의미합니다.)
  • 응용 프로그램이 능동적으로 라이브러리를 사용합니다.
  • Apache Commons, Guava, Lombok, jQuery 등이 있습니다.

앞서 살펴본 API와 라이브러리는 구현 로직의 유무에서 차이점을 가집니다.


🔗 Framework

단어적으로만 보았을 때, Frame(틀) + work(일하다)로 어떠한 틀 안에서 일을 한다고 볼 수 있습니다.

프레임워크란 무엇일까요? 🤔

Framework란, 응용 프로그램이나 소프트웨어의 솔루션 개발을 수월하게 하기 위해 제공된 소프트웨어 환경입니다.

Framework의 특징

  • 상호협력하는 클래스와 인터페이스의 집합입니다.
  • 응용 프로그램이 수동적으로 프레임워크에 의해 사용됩니다.
  • Spring Framework, Junit, Ruby on Rails 등이 있습니다.

라이브러리와 프레임워크의 차이점은 응용 프로그램의 흐름 주도권이 다르다는 점입니다.
이 둘의 차이점을 간단하게 표로 정리해보면 아래와 같습니다.

라이브러리프레임워크
공통점재사용과 관련이 있다.
목적설계 재사용(일부 코드 재사용)코드 재사용
제어흐름프레임워크 -> 코드코드 -> 라이브러리
쓰임새범용적특정 영역에서 강력


📌 Set과 Map

Set과 Map의 도입 배경

const obj = { key: value };

자바스크립트의 Object 타입은 프로퍼티 이름인 문자열(key)과 임의의 값을 연결하는 다재다능한 데이터 구조입니다.

자료구조인 Map과 Set을 도입하기 전에는 자유도가 높은 자바스크립트 객체를 실제로 Map과 Set처럼 사용하는 일이 흔했습니다.

하지만 key가 문자열(혹은 Symbol)이어야 한다는 제약이 있고 객체에서 일반적으로 상속하는 toString 같은 프로퍼티를 실제 Map과 Set에서 사용하는 경우는 드물기 때문에 과도하게 복잡해지는 문제가 있었습니다.

따라서 ES6에서 이 문제를 해결하고자 진정한 Set와 Map 클래스를 도입했습니다.

쉽게 생각해서 Set은 배열과 유사하고, Map은 객체와 유사하다고 볼 수 있습니다.

그렇다면, Set과 Map이 가지고 있는 각각의 특징과 함께 주로 어디에서 사용하면 좋을지에 대해 알아봅시다.



🔗 Set

Set의 가장 큰 특징은 중복을 허용하지 않는다는 점입니다.

// 생성자로 Set 만들기
const s = new Set();
const t = new Set([1, s]);

let t = new Set(s);
const unique = new Set("Mississippi"); // {'M', 'i', 's', 'p'}
unique.size; // 4

기본적으로 Set 객체는 Set() 생성자로 만듭니다.

Set은 배열과 마찬가지로 값의 집합이지만, 배열과 달리 Set은 순서가 없고 인덱스도 없으며, 중복을 허용하지 않습니다. (Unique한 배열)

값은 Set의 요소이거나 요소가 아닐 뿐, 그 값이 Set에 몇 개 있는지 알 수 없습니다.

const s = new Set([1,2,3]);
s; // {1, 2, 3}

const s2 = new Set(123); // TypeError: number 123 is not iterable

const s3 = new Set('123');
s3; // {'1', '2', '3'}

Set()생성자의 인자로는 배열과 Set 객체를 포함한 이터러블 객체 모두 허용됩니다.

Set을 생성하면서 동시에 초기화할 필요는 없습니다.
언제든 add(), delete(), clear()로 요소를 추가하거나 제거할 수 있습니다.


Set 프로퍼티

Set 프로퍼티기능
size()Set에 포함된 값의 개수를 반환합니다.
add()인자를 하나 받으며, Set에 값을 추가합니다.
delete()Set에 존재하는 요소라면 제거하고 true를 반환합니다. (그렇지 않다면 아무일도 일어나지 않고 false를 반환)
has()Set 객체에 주어진 요소가 존재하는지 여부를 판별해 반환합니다.
clear()Set 객체를 비웁니다.
const s = new Set([1,2,3,2,3]);
s.size; // 3

Set의 size 프로퍼티는 배열의 length 프로퍼티와 마찬가지로 Set에 포함된 값의 개수를 반환합니다.

const s = new Set();

s.add(1); 
console.log(s); // {1}

s.add(1); // 기존에 있는 요소와 같은 값을 추가
console.log(s); // {1} 추가되지 않음

s.add([1]); // 1을 배열로 추가
console.log(s); // {1, Array(1)} 배열에 담겨서 추가됨

s.add('1');
console.log(s); // {1, Array(1), '1'} 문자1이 추가됨

s.add(11); // 숫자 11로 추가됨
console.log(s); // {1, Array(1), '1', 11}

s.add('11'); // 문자 11로 추가됨
console.log(s); // {1, Array(1), '1', 11, '11'}

add()메서드는 인자를 하나 받습니다.
배열을 전달하면 개별 배열 요소가 아니라 배열 자체를 추가합니다.

Set은 중복을 허용하지 않으므로 이미 Set에 존재하는 값을 추가해도 아무 효과가 없습니다.

const s = new Set();

s.add('a').add('b').add('c');

console.log(s); // {'a', 'b', 'c'}

add()는 항상 자신을 호출한 Set를 반환하므로 Set에 여러 가지 값을 추가할 때는 위 코드 처럼 add 메서드를 체인으로 연결할 수 있습니다.

let s = new Set("abc");
s.delete(1); // false
s.delete("a"); // true
console.log(s); // { 'b', 'c' }

delete()메서드는 세트 요소를 한 번에 하나씩 삭제합니다. 불 값을 반환하며, 지정한 값이 실제로 세트의 요소라면 그 요소를 제거하고 true를 반환합니다.
그렇지 않다면 아무 일도 하지 않고 false를 반환합니다.

let s = new Set();
s.add([1, 2, 3]).add('4');
s.delete(4); // false
s.delete('4'); // true
s.delete([1, 2, 3]); // false

Set은 일치 여부를 판단할 때 ===연산자처럼 엄격하게 체크합니다.

Set는 숫자 1과 문자열 '1'을 별개의 값으로 간주하므로 세트 요소로 이 둘을 모두 포함할 수 있습니다.
값이 객체, 배열, 함수일 경우에도 ===로 비교합니다.

Set의 배열을 삭제할 수 없었던 이유가 이 때문입니다.
배열을 세트에 추가한 다음 delete()메서드에 요소는 같지만 다른 배열을 전달해 제거하려고 시도해서 실패했습니다.

const arr = [1, 2, 3];
const s = new Set();
s.add(arr);
s.delete(arr); // true

삭제에 성공하려면 정확히 같은 배열을 가리키는 참조를 전달해야 합니다.

현실에서 Set로 할 수 있는 가장 중요한 일은 요소를 추가하거나 제거하는 일이 아니라 지정된 값이 Set의 요소인지 체크하는 일입니다.

has()메서드가 바로 그 일을 담당합니다.

let s = new Set([2, 3, 5, 7]);
s.has(2); // true
s.has(4); // false
s.has("3"); // false

Set에 관해 이해해야 할 가장 중요한 점은 Set가 요소의 존재 여부를 확인하는데 최적화 되어 있으며, Set에 요소가 얼마나 많든 has()메서드는 아주 빠르다는 점입니다.

let n = 20000;

// includes Test: 0.01708984375 ms
let arr = Array.from({ length: n }, (v, i) => i + 1); 
console.time( 'includes Test' );
arr.includes(10000)
console.timeEnd( 'includes Test' );

// has Test: 0.00390625 ms
let set = new Set( arr );
console.time( 'has Test' );
set.has(10000)
console.timeEnd( 'has Test' );

배열의 includes()메서드도 요소의 존재 여부를 확인하지만 배열 크기에 따라 속도가 달리지며, 배열을 Set처럼 사용하면 실제 Set 객체에 비해 훨씬 느릴 수 있습니다.

Set 클래스는 이터러블이므로 for/of 루프로 Set의 요소를 열거하거나 분해 연산자 ...를 써서 배열이나 인자 리스트로 변환할 수 있습니다.

let s = new Set([2, 3, 5, 7]);

let sum = 0;
// for of문 사용가능
for (let p of s) {
  sum += p;
}
sum; // 17

// spread 사용가능
const max = Math.max(...s); // 7
const arr = [...s]; // [2, 3, 5, 7]

let product = 1;

// forEach 사용가능
s.forEach((n) => (product *= n));
product; // 210

Set는 종종 순서 없는 집합이라고 표현되지만 자바스크립트의 Set 클래스는 그렇지 않습니다.

자바스크립트 Set는 인덱스가 없으므로 배열처럼 첫 번째 요소가 무엇인지, 혹은 마지막 요소가 무엇인지 알 수 없습니다.

하지만 자바스크립트 Set 클래스는 항상 요소가 삽입된 순서를 기억하고 있으며, Set를 순회할 때 항상 이 순서대로 순회합니다.
Set에 첫 번째로 삽입한 요소는 순회할 때도 첫 번째로 반환되며, 마지막에 삽입한 요소는 마지막으로 반환됩니다.

const arr = [1, 2, 3];
const s = new Set([1, 2, 3]);

arr.forEach((value, index) => console.log(value, index));
/*
1 0
2 1
3 2
*/

s.forEach((value, sameValue) => console.log(value, sameValue));
/*
1 1
2 2
3 3
*/

Set 클래스는 이터러블이기도 하지만 forEach()메서드 또한 지원합니다.
배열의 forEach()메서드는 배열 인덱스를 두 번째 인자로 전달하지만 세트에는 인덱스가 없으므로 Set클래스의 forEach()는 첫 번째와 두번째 인자 모두에 요소 값을 전달합니다.



🔗 Map

Map의 가장 큰 특징은 key와 value를 자유롭게 설정할 수 있다는 점입니다.

일반 객체는 key가 Symbol 아니면 String 둘 중 하나만 되는 반면, Map은 key와 value를 자유롭게 설정할 수 있습니다.

Set와 마찬지로 자바스크립트 값이라면 어떤 것이든 Map의 키나 값으로 사용할 수 있습니다.
null, undefined, NaN, 객체와 배열같은 참조 타입도 가능합니다.

const m = new Map();
const obj = {};

m.set(obj, 'e'); // 객체도 key로 사용 가능
console.log(m); // {Object => "e"}

다만, 서로 다른 객체는 참조값이 다르기 때문에 객체를 map의 key로 설정했을 때 가져오는 경우를 주의해야 합니다. (변수에 저장해서 사용)

Map 객체는 키로 구성된 값 집합이며 각 키는 다시 다른 값과 연결됩니다.

어떤 면에서는 Map도 배열과 비슷하지만, 연속된 정수를 키로 사용하는 대신 임의의 값을 인덱스로 사용할 수 있습니다.

배열과 마찬가지로 Map도 아주 빠른 데이터 구조를 갖습니다.
Map의 크기와 상관없이 키와 연결된 값을 빨리 찾을 수 있습니다. (배열만큼 빠르지는 않습니다.)

// Map 생성자
const m = new Map();

// Map 생성과 동시에 초기화
const n = new Map([
  ["one", 1],
  ["two", 2],
]);

// 다른 Map을 복사
const copy = new Map(n); // [['one', 1], ['two', 2]]

const obj = { x: 1, y: 2 };
const p = new Map(Object.entries(obj)); // [['x', 1], ['y', 2]];

새 Map을 생성할 때는 Map()생성자를 사용합니다.

Map() 생성자의 선택 사항인 인자[key, value]배열을 전달하는 이터러블 객체여야 합니다.

따라서 Map을 생성하는 동시에 초기화하고 싶다면 원하는 키와 값을 배열의 배열 형태로 준비해야 합니다.

하지만 다음과 같이 Map() 생성자로 다른 Map을 복사하거나 기존 객체의 프로퍼티 이름과 값을 복사할 수 있습니다.


Map 프로퍼티

Map 프로퍼티기능
new Map()새로운 Map을 만듭니다.
set(key, value)key를 이용해 value를 저장합니다.
get(key)key에 해당하는 값을 반환합니다.(key가 존재하지 않으면 undefined를 반환합니다.)
had(key)key가 존재하면 true, 존재하지 않으면 false를 반환합니다.
delete(key)key에 해당하는 값을 삭제합니다.
clear()맵 안의 모든 요소를 제거합니다.
size()요소의 개수를 반환합니다.

Map 클래스는 get()set() 외에도 Set과 비슷한 메서드를 가집니다.
지정된 키가 맵에 존재하는지 확인할 때는 has()를,
키를 제거하고 연결된 값도 제거할 때는 delete()를,
맵에서 키-값 쌍을 모두 제거할 때는 clear()를,
맵에 포함된 키의 개수를 확인할 때는 size 프로퍼티를 사용합니다.

// Map 생성자
const m = new Map();

m.set('a', 'b'); // set(키, 값)으로 Map에 속성 추가
m.set(3, 'c'); // 문자열이 아닌 값을 키로 사용가능

const obj = {};
m.set(obj, 'e'); // 객체도 가능
console.log(m); // {'a' => 'b', 3 => 'c', {…} => 'e'}

m.get(obj); // get(키)로 속성값 조회
console.log(m.get(obj)); // e

m.size; // size로 속성 개수 조회
console.log(m.size); // 3

// Map 객체는 for of문, 반복문에 바로 넣어 사용가능합니다.
for (const [k, v] of m) {
	console.log(k, v); 
}

/*
속성 간의 순서도 보장됩니다.
a b
3 'c'
{} 'e'
*/

// forEach도 사용가능합니다.
m.forEach((v, k) => {
 console.log(k, v); // 결과는 위와 동일
})

// 키와 값을 모두 객체로 줄 수 있습니다.
m.set({ a: 'b' }, { c: 'd' });
m.get({a : 'b'}) // 참조값이 다르면 아무것도 출력 안됨

// 객체로 키를 설정할때는 변수를 사용
const obj = { key: 'key' };
m.set(obj, 123); // Map(4) {'a' => 'b', 3 => 'c', {…} => 'e', {…} => 123}
m.get(obj); // 123

m.has(d); // has(키)로 속성 존재 여부를 확인합니다.
console.log(m.has(d)); // true

m.delete(d); // delete(키)로 속성을 삭제합니다
m.clear(); // clear()로 전부 제거합니다.

console.log(m.size); // 0
let m = new Map();
let n = new Map([
  ["one", 1],
  ["two", 2],
]);

let copy = new Map(n); // [['one', 1], ['two', 2]]
let obj = { x: 1, y: 2 };
let p = new Map(Object.entries(obj)); // [['x', 1], ['y', 2]];

Map 객체를 만들면 get()으로 주어진 키와 연결된 값을 검색할 수 있고, set()으로 키-값 쌍을 추가할 수 있습니다.

하지만 Map은 키의 집합이며 각 키가 값과 연결 될 뿐, 키-값 쌍 집합이 아닙니다.

Map에 이미 존재하는 키로 set()을 호출하면 해당 키에 연결된 값을 수정할 뿐, 새 키-값 쌍을 맵에 추가하는 것은 아닙니다.

또한 Map도 key를 비교할 때 동등성(==)이 아니라 일치성(===)으로 비교하므로 객체나 배열을 키로 사용한다면 그 프로퍼티와 요소가 정확히 일치하더라도 항상 다른 것으로 판단합니다.

let m = new Map().set("one", 1).set("two", 2).set("three", 3);
m.size; // 3
m.get("two"); // 2

Map의 set()메서드 역시 체인으로 연결할 수 있으므로 Map을 초기화할 때 배열의 배열([[key, value]])을 사용하지 않아도 됩니다.

let m = new Map([
  ["x", 1],
  ["y", 2],
]);
[...m]; // [['x', 1], ['y', 2]]

// key와 value를 분해할당 후 반복에 사용
for (let [key, value] of m) {
  console.log(key, value);
}

/*
x 1
y 2
*/

[...m.keys()]; // ['x', 'y']
[...m.values()]; // [1, 2]
[...m.entries()]; // [['x', 1], ['y', 2]]

// Map 객체에 forEach 사용가능
m.forEach((value, key) => {
  console.log(value, key);
});

/*
1 'x'
2 'y'
*/

Map 객체는 이터러블이며 순회할 때 반환되는 값은, 첫 번째 요소는 key이고 두번째 요소는 값인 배열입니다.

Map 객체에 분해 연산자를 사용하면 Map()생성자에 전달했을 배열의 배열을 반환합니다.

for/of 루프로 Map을 순회할 때는 다음과 같이 분해 할당을 써서 키와 값을 별도의 변수에 할당하는 것이 일반적입니다.

Set 클래스와 마찬가지로 Map 클래스 역시 삽입된 순서대로 순회합니다.

Map에 처음으로 추가한 키-값 쌍이 순회할 때도 첫 번째로 반환되며, 마지막으로 추가한 키-값 쌍이 순회할 때도 마지막으로 반환됩니다.

Map의 키나 값 중 하나만 순회하고 싶을 때는 keys()values()메서드를 사용하면 됩니다.

이들은 키나 값을 삽입 순서대로 순회하는 이터러블 객체를 반환합니다.

entries()메서드가 키-값 쌍으로 이루어진 이터러블 객체를 반환하긴 하지만 Map을 직접 순회하는 것과 차이가 없습니다.

Map객체는 Array 클래스의 forEach() 메서드도 지원합니다.

Map은 정수인 배열 인덱스를 임의의 키 값으로 대체한 일반화된 배열이라고 볼 수 있습니다.

배열의 forEach()메서드는 배열 요소 다음에 배열 인덱스를 전달하므로, 맵의 forEach()메서드는 이에 맞게 맵의 값을 먼저 전달하고, 그 다음에 맵의 키를 전달합니다.



🔗 WeakMap과 WeakSet

JavaScript 엔진은 도달 가능한 (그리고 추후 사용될 가능성이 있는)값을 메모리에 유지합니다.

// 아래 객체는 obj라는 참조를 통해 접근할 수 있습니다.
let obj = { name: 'foo' };

// 참조를 null로 덮어쓰면 위 객체에 더이상 도달 불가능하므로 메모리에서 삭제됩니다.
obj = null;

let obj2 = { name: 'foo' };
let arr = [ obj2 ];

obj2 = null;

// obj2를 나타내는 객체는 배열의 요소이기 때문에 가비지 컬렉션의 대상이 되지 않는다
console.log(arr[0]); // {name: 'foo'}

자료구조를 구성하는 요소도 자신이 속한 자료구조가 메모리에 남아있는 동안 대개 도달 가능한 값으로 취급되어 메모리에서 삭제되지 않습니다.

객체의 프로퍼티나 배열의 요소, Map이나 Set을 구성하는 요소들이 이에 해당합니다.

WeakMap과 WeakSet을 사용하는 이유는 무엇일까요? 🤔

이 둘의 주 용도는 객체와 함께 '추가'데이터를 저장하는데 쓰인다는 점입니다.


🔗 WeakMap

WeakMap은 Map의 변형이지만 서브클래스는 아닙니다.

이 WeakMap 클래스는 키 값이 가비지 컬렉션에 포함되지 않게 막아 주지 않습니다.

일반적인 Map은 키 값을 강하게 참조하며, 키 값에 대한 다른 참조가 더 이상 남아 있지 않더라도 참조 관계를 유지합니다.

반면 WeakMap은 키 값을 약하게 참조하므로 WeakMap을 통해 해당 키 값을 참조할 수 없고, 키 값이 WeakMap에 존재하더라도 메모리를 회수할 수 있습니다.

WeakMap()생성자는 Map() 생성자와 비슷하지만 WeakMap과 Map 사이에는 중요한 차이가 있습니다.

  • WeakMap의 키는 반드시 객체 또는 배열이어야 합니다.
  • 기본 값은 가비지 컬렉션 대상이 되지 않으며 키로 사용할 수 없습니다.
  • WeakMap에는 get(), set(), has(), delete()메서드만 있습니다.
  • 특히 WeakMap은 이터러블이 아니며 keys(), values(), forEach()메서드가 없습니다.
    (WeakMap이 이터러블이었다면 키에 접근할 수 있게 되므로 약한 참조가 성립할 수 없습니다.)
  • WeakMap에는 size 프로퍼티가 없습니다.
    (WeakMap의 크기는 가비지 컬렉션이 일어날 때마다 언제든 바뀔 수 있기 때문입니다.)

WeakMap의 설계 의도는 메모리 누수를 방지하면서 객체와 값을 연결할 수 있게 하는 겁니다.

Map 객체를 통해 캐시를 구현한다면 객체에서 사용하는 메모리를 회수할 가능성이 전혀 없지만, WeakMap을 사용하면 그런 문제가 생기지 않습니다.


🔗 WeakSet

WeakSet는 객체가 가비지 컬렉션에 포함되도록 허용하는 객체 세트입니다.

  • WeakSet는 기본 값을 요소로 허용하지 않습니다.
  • WeakSet는 오직 add(), has(), delete()메서드만 가지며 이터러블이 아닙니다.
  • WeakSet에는 size 프로퍼티가 없습니다.

WeakSet는 자주 사용되지는 않으며 용도는 WeakMap과 비슷합니다.

예를 들어 객체에 특별한 프로퍼티나 타입이 있다고 분류하고 싶다면 WeakSet에 추가하는 방법이 있습니다.

그리고 다른 코드에서 그 프로퍼티나 타입을 체크할 때 WeakSet의 요소인지 확인하면 됩니다.

일반적인 Set로 이런 일을 하면 마크된 객체는 모두 가비지 컬렉션 대상이 되지 않지만, WeakSet를 사용하면 그런 걱정이 필요 없습니다.



📌 정규 표현식

정규표현식(Regular Expression)은 줄여서 RegExp라고 합니다.

정규표현식 또는 정규식은 문자열에서 특정 문자 조합을 찾기 위한 패턴입니다. -MDN

const regExp = /abc/g;
typeof regExp; // 'object'

쉽게 말하자면, 정규표현식은 텍스트 패턴을 정의하는 객체입니다.

JavaScript의 RegExp 클래스는 정규표현식이며,
문자열과 RegExp 모두 정규표현식을 사용해 텍스트에서 패턴을 찾고 대체하는 메서드를 정의합니다.

  • RegExp의 exec()test()메서드
  • String의 match(), matchAll(), replace(), replaceAll(), search(), split()등의 메서드

정규표현식의 가장 큰 기능은 특정 패턴을 가지고, 문자열을 찾을 수 있다는 점 입니다.

대체적으로 문자열에서 특정 내용을 찾거나 대체 또는 발췌 하는데에 주로 사용합니다.

정규 표현식은 그 자체로 하나의 작은 프로그래밍 언어입니다.

따라서 RegExp API를 효율적으로 사용하려면 정규 표현식 문법을 사용해 텍스트 패턴을 정의하고 만드는 방법을 알아야 합니다.

정규표현식을 굳이 왜 사용해야 하나요? 🤔

만약, 비밀번호의 조건을 설정해야 하는 로직을 구현해야 한다고 했을 때
비밀번호의 조건으로 아래의 2가지가 주어졌다고 가정해봅시다.

  • 비밀번호는 8글자 이상, 16글자 이하여야 합니다.
  • 숫자, 문자, 특수문자가 모두 포함되어 있어야 합니다.
const isValidLength = (str) => {
  str.length >= 8 && str.length <= 16
}

const isIncludeNum = (str) => {
  str.some((character) => character >= '0' && character <= '9';)
}

const ...

간단한 조건이라면 이런식으로 함수 또는 if문을 사용해서 구현할 수 있습니다.
하지만, 조건이 점점 늘어날수록 코드는 길어지고 복잡해지는 문제가 발생하게 됩니다.

바로 이런 상황에서 정규표현식을 사용할 수 있습니다.

정규표현식의 장점은 패턴으로 검증이 가능하여 if문을 많이 쓰지 않아도 된다는 점입니다.

간단한 검증일때는 if문으로 해결하여 메서드 명을 통해 가독성을 높이고,
복잡한 검증이 있을 때만 정규표현식을 사용하는 것을 권장합니다.

정규표현식이 구체적으로 자주 사용되는 예시

  • 컴파일러의 파서
  • CLI 환경을 주로 사용하는 경우, grep, sed, awk를 통해 쓰임
  • 이메일, 주소, 전화번호 규칙 검증
  • 입력에서 불필요한 입력 검증
  • 개발도구에서 문자열 치환
  • 로깅에서 찾아볼 때
  • 코딩 테스트 등...


📌 정규표현식 정의와 표현

자바스크립트에서는 RegExp 객체로 정규표현식을 표현합니다.

/패턴/플래그;

기본적으로 정규표현식은 패턴구분자 시작 + 작성할 패턴 + 패턴구분자 끝 + 패턴 변경자로 구성됩니다

정규표현식은 크게 메타문자와 수량자, 그룹화 등의 여러가지 기능을 가지고 있고, 옵션으로 뒤에 flag를 붙일 수 있습니다.

🔗 정규표현식 리터럴

// 문자열 리터럴
const str = "abc";

// 생성자로 정규표현식 생성
const regExp1 = new RegExp("abc");

// 자주 사용하는 문법
const regExp2 = /abc/;

문자열 리터럴이 따옴표 안에 문자를 쓰는 것과 마찬가지로,
정규 표현식 리터럴은 슬래시 (/) 한 쌍 안에 문자를 씁니다.

위 코드처럼 생성자를 통해 RegExp 객체를 생성할 수 있지만, 보통은 슬래쉬(/)로 감싸는 특별한 리터럴 문법을 더 자주 사용합니다.


🔗 정규표현식 메타문자

메타문자는 문자를 나타내는 문자를 의미합니다.

자주 사용하는 메타문자의미
.모든 문자 (숫자,문자,공백,특수문자 등...)
[]대괄호 안에 들어가 있는 문자를 찾습니다.
[^]대괄호 안에서 ^은 not을 의미합니다.
|or
\s공백 (tab과 space 등)
\d소문자 d는 숫자를 의미합니다. [0-9]
\D대문자 D는 숫자를 제외한 문자를 의미합니다.
\w소문자 w는 word의 약어로, 문자를 나타낼 때 사용합니다.(대문자, 소문자, 언더스코어까지 포함 [A-Za-z_])
\W대문자 W는 문자가 아닌 것 들을 의미합니다. (\w와 반대)
\bboundary의 약자로 위치(경계)를 나타냅니다.
^찾으려는 문자열의 시작 위치를 나타냅니다.
$찾으려는 문자열의 끝 위치를 나타냅니다.

이 중에서 \s\b를 살펴보자면,

둘 다 공백을 의미하지만
\s는 Selecting 되는 반면, \b는 Selecting에 포함되지 않습니다.


🔗 정규표현식 수량자

수량자는 앞 문자의 수를 제한할 수 있습니다.

/str{N, M}/ //str이 N번이상 M번 이하 반복됨을 뜻합니다.

기본적으로 문자 뒤에 중괄호{}를 사용하게 되면 문자의 수를 표현할 수 있습니다.

중괄호 앞의 문자열이 첫번째 숫자 이상, 두번째 숫자 이하 반복됨을 의미합니다.

수량자의미
a{n,m}a가 n번이상, m번 이하 반복됨을 뜻합니다.
{n,}앞 문자가 n개 이상임을 뜻합니다. (m이 생략된 형태)
{n}앞 문자가 n개
?앞에 있는 문자열이 없거나, 1개 임을 뜻합니다. {0,1}
+해당 문자열이 1번 이상 반복됨을 뜻합니다. {1,}
*앞에 문자가 0번 이상 반복됨을 뜻합니다. {0,}

🔗 정규표현식 Group

정규표현식에서는 Group이라는 기능을 소괄호()를 통해서 사용할 수 있습니다.

const regExr = /(\d{2})/g;
그룹의미
()소괄호로 감싼 문자열을 그룹핑하여 캡처링해줍니다.
(?:)캡처링을 제외하고 그룹핑만을 해줍니다.
(?<groupName>)<>안에 그룹의 이름을 지정할 수 있습니다. (match나 exec의 groups 프로퍼티에서 해당 name으로 접근이 가능)

기본적으로 소괄호로 문자열을 감싸면 해당 그룹을 캡처링 할 수 있습니다.
캡처링 된 그룹들은 matchexec메서드를 통해 확인할 수 있습니다.

그룹핑과 많이 혼동하는게 (?=)키워드 입니다.
해당 키워드는 전방 탐색 키워드로 일치영역을 발견해도 그 값을 반환하지 않습니다.


🔗 정규표현식 flags

자주 사용하는 플래그의미
iignore의 약자, 대소문자를 구분하지 않습니다.
gglobal의 약자, 전 구역을 복수로 검사합니다.
mmultiline의 약자, 다중행 검사를 합니다.

정규표현식은 기본적으로 슬래쉬 안에 패턴을 삽입하고,
flags를 옵션으로 삽입할 수 있습니다.


🔗 정규표현식 활용 메서드

  • String.prototype.match() : 정규식과 겹치는 전체 문자열을 첫번째 요소로 포함하는 Array를 반환한 다음, 괄호 안에 캡처된 결과를 리턴합니다.
"I am Super Man 12345".match(/\d/); // ['1', index: 15, input: 'I am Super Man 12345', groups: undefined]

"I am Super Man 12345".match(/\d/g); // ['1', '2', '3', '4', '5']

match와 group 네이밍을 활용해서 다음과 같이 주민번호에서 생년월일을 손 쉽게 추출해 낼 수 있습니다.

const { year, month, date } = "970201-1111111".match(
  /^(?<year>\d{2})(?<month>\d{2})(?<date>\d{2})/
).groups;
console.log(year, "년", month, "월", date, "일생"); // 97 년 02 월 01 일생
  • RegExp.prototype.test() : 주어진 문자열이 정규표현식을 만족하는지를 Boolean으로 반환해줍니다.
/^01[\d][-\s/]\d{3,4}[-\s/]\d{4}$/.test("010-1111-2222"); // true
/^01[\d][-\s/]\d{3,4}[-\s/]\d{4}$/.test("010-1111-222"); // false

const tel1 = "010-1234-567팔";
const tel2 = "010-1234-5678";

// 정규 표현식 리터럴로 휴대폰 전화번호 패턴을 정의한다.
const regExp = /^\d{3}-\d{4}-\d{4}$/;

// tel이 휴대폰 전화번호 패턴에 매칭하는지 테스트(확인)한다.
regExp.test(tel1); // false
regExp.test(tel2); // true

휴대폰 번호의 조건을 활용하여 해당 문자열이 조건을 만족하는지 확인할 수 있습니다.

const str = "ABC";
const regExp = /abc/i;
const regExp2 = /abc/;

regExp.test(str); // true;
regExp2.test(str); // false;
  • RegExp.prototype.exec() : 주어진 문자열에서 일치 탐색을 수행한 결과를 배열 혹은 null로 반환합니다.
const str = "IS this all there is?";
const regexp = /is/gi;
const regexp2 = /is/i;
const regexp3 = /is/;

str.match(regexp); //  ['IS', 'is', 'is']
str.match(regexp2); // ['IS', index: 0, input: 'IS this all there is?', groups: undefined]

regexp3.exec(str); // ['is', index: 5, input: 'IS this all there is?', groups: undefined]
regexp2.exec(str); // ['IS', index: 0, input: 'IS this all there is?', groups: undefined]
regexp.exec(str);
  • String.prototype.replace() : 패턴에 일치하는 문자열의 일부 또는 모든 부분을 교체한 문자열을 반환합니다.
const reg = /(-\d)(\d*)/
'980828-2345'.replace(reg, '*'); // '980828*' 뒷 번호를 *으로 변환

String.replace(reg, function(match, p1, p2, ..., offset, string) {
  return ...;
})

'980828-2345'.replace(reg, (match, p1, p2,) => p1 + ('*'.repeat(p2.length))); // '980828-2***'

replace의 두 번째 인자로 함수를 넣을 수 있습니다.

replace의 첫번째 인자로는 정규식을 담은 후 두번째 인자로 함수를 넣으면, 함수의 파라미터로 4가지 종류의 인자를 받을 수 있습니다.

  • match : 매치된 문자열
  • p1, p2, ... : 그룹화된 문자열 순서대로 인자로 받을 수 있습니다.
  • offset: 매치된 문자열의 index
  • string: 조사된 전체 문자열

위의 예시와 같이 찾은 문자열에 따라 동적으로 변환을 원할때 유용하게 사용할 수 있습니다.



📌 자바스크립트 RegExp 활용

정규표현식으로 작성을 완료한 테스트가 항상 옳을 수는 없습니다.
그렇기때문에, 테스트는 필수적이며 https://regexr.com 사이트를 이용하면 여러가지 테스트 케이스를 작성하면서 오류가 있는지 확인할 수 있습니다.

🔗 간단한 문자 조건설정

  • 영어 대문자, 소문자, 한글, 숫자가 한번 이상 반복되는 문자열을 전역 검색
const regExp = /[A-Za-z가-힣ㄱ-ㅎ0-9]+/g;
  • 문자열에서 알파벳 대문자, 숫자, 더하기(+), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거하시오.
function removeChar(input) {
  return input.replace(/[^A-Z0-9+_.]/g, "");
}

const input = "aAb0c+d_e.f";
console.log(removeChar(input)); // A0+_.
  • 문자열에서 마침표(.)가 3번 이상 연속된 부분을 하나의 마침표(.)로 치환하시오.
function changeDot(input2) {
  return input2.replace(/[.]{3,}/g, ".");
}

const input2 = "a...b...c..d.e";
console.log(changeDot(input2)); // a.b.c..d.e
  • 반복되는 문자열 검사
const target = "A AA B BB Aa Bb AAA";

// 'A'가 최소 1번, 최대 2번 반복되는 문자열을 전역 검색
const regExp = /A{1,2}/g;

target.match(regExp); //  ['A', 'AA', 'A', 'AA', 'A']

// 공백이 들어가있는 단어에 대해서만 찾음
const regExp2 = /A{1,2}\s/g;
target.match(regExp2); // ['A ', 'AA ']

// 'A'가 2개 이상 들어가 있는 문자열 검색
const regExp3 = /A{2,}/g;
target.match(regExp3); // ['AA', 'AAA']

🔗 휴대폰 번호 조건설정

  • 첫 세글자는 01x(x는 숫자)의 형태입니다.
  • 구분자는 -, 공백, /를 허용합니다.
  • 두번째 구역은 3글자 이상 4글자 이하 숫자입니다.
  • 마지막 구역은 4글자의 숫자로 이루어집니다.
const regExp = /^01\d[-\s/]\d{3,4}[-\s/]\d{4}$/g;

🔗 URL 조건설정

// https로 시작하는 문자열인지 테스트
const target = "https://www.naver.com";

const regExp = /^https/;
regExp.test(target); // true

// com으로 끝나는지 테스트
const regExp2 = /com$/;
regExp2.test(target); // true

// https:// 또는 http:// 로 시작하는지 테스트
// 안쪽에서 슬래쉬를 사용하고 싶을땐 이스케이프 사용
const regExp3 = /^https?:\/\//.test(target); // true
regExp3.test("httppps://"); // false

🔗 이메일 조건설정

const regExp = /^(?=[^_])+[₩w-.]+@[a-zA-Z0-9]+₩.[a-zA-Z0-9]+$/;

🔗 주민등록번호 조건설정

const regExp = /^[\d]{2}(?:(?:0[13578]|1[02])(?:[0-2]\d|3[01])|(?:02(?:[01]\d|2[0-9]))|(?:0[469]|11)(?:[0-2]\d|30))[-/.][1-4]\d{6}$/;

정규표현식은 복잡한 조건에서 사용할 수 있지만, 그렇다고 항상 좋은 점만 있는 것은 아닙니다.

위의 예시와 같이 가독성이 상당히 나쁜편입니다.
일반적으로 사용되는 문자열이 아닌 meta문자열을 사용하기 때문에 기본적으로 가독성이 떨어질 수 밖에 없습니다.

또한, 정규표현식 엔진은 대부분 백트래킹을 사용하기 때문에 최악의 경우 시간복잡도 O(2^n)이 나오게 됩니다.
따라서 일반 내장 메서드나 for문보다 성능상 떨어질 수 있습니다.

따라서 간단한 로직은 내장 메서드를 활용해서 함수 분리를 하는 것이 성능이나 가독성 면에서 더 유리하고, 복잡하고 복합적인 로직을 검사하고 싶을때는 정규표현식이 유용하게 사용됩니다.



📌 참고자료

https://www.redhat.com/en/topics/api/what-are-application-programming-interfaces
https://www.youtube.com/watch?v=We8JKbNQeLo
https://velog.io/@surim014/use-maps-more-and-objects-less
https://codediver.tistory.com/49
https://www.youtube.com/watch?v=8u-ofGrlhQU
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Regular_Expressions
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/RegExp
https://www.youtube.com/watch?v=_eEZqTx5N7s&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=53
https://www.youtube.com/watch?v=CjoDIgDOHA4&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=193
https://blogpack.tistory.com/1131
https://velog.io/@semnil5202/%EC%A0%95%EA%B7%9C%ED%91%9C%ED%98%84%EC%8B%9D
https://kim6394.tistory.com/100
https://velog.io/@ssoon-m/JS-Array.includes-%EC%99%80-Set.has
https://velog.io/@100pearlcent/Map-Set-WeakMap-WeakSet

profile
성장에는 성장통이 있기 마련이다.

0개의 댓글