1. Well-Known Symbols
스펙에 정의된 로직으로부터 명시적으로 참조되어 사용되는 Symbol values
ECMA Specification(링크)의 6.1.5.1절을보면 Well-Known Symbols를 살펴보면 스펙(Specification)에서 알고리즘에 이름을 부여하고 이름(혹은 프로퍼티)으로 참조하기위한 빌트인 Symbol 값이라고 합니다.
즉, match()라는 메소드를 실행하면 디폴트로 @@match가 실행되는 것입니다.
위 이미지는 ECMA Specification문서의 Well-known Symbols Table1의 @@match부분인데 코드에서 String.prototype.match가 호출되면 우선 개발자 코드에서 "Symbol.match"를 찾는데 성공한다면 해당 함수를 실행합니다. 하지만, 찾지 못했을 경우 엔진에서 @@match를 실행합니다.
원래, 기존에는 "Symbol.XXXX"가 따로 제공되지 않았기에 바로 엔진에서 @@XXXX알고리즘이 호출되어 실행되었습니다. 하지만 이제는 개발자가 운용할 수 있도록 오픈되었습니다. 그리고 이것이 Symbol.property의 목적입니다.
이렇게 개발자 코드로 디폴트 기능의 필요한 부분을 오버라이딩해서 사용할 수 있게 해주는 것이 일반적인 방법과 Well-known Symbols의 차이점입니다.
제공되는 Well-Known Symbols
위 표에 나와있는 항목들이 Well-Known Symbols인데, 여기서 Symbol항목이 개발자가 작성하는 코드들이고 대응 부분의 함수및 알고리즘을 실행하면 우선 Symbol 에 관련 내용이 오버라이드 되어있으면 그 코드를 실행하고 없으면 엔진의 기본 알고리즘을 호출합니다.
2. Symbol.toStringTag
Object.prototype.toString()의 확장
기본적으로 Object에서 비롯된 모든 객체에 연결되는 메소드인 toString()은 호출 시 [object type]을 반환합니다. 여기서 type은 Object Type을 의미하는데, 기본적인 toString()을 호출하면 [object Object] 를 반환합니다. 이래서는 여러 클래스나 오브젝트 인스턴스의 타입을 명확하게 구하기가 힘듭니다. 여기서 Symbol.toStringTag를 이용해 오버라이드해서 구분이 가능하게 할 수 있는데, 반환 될 값의 두 번째에 표시될 문자열을 작성할 수 있습니다.
const Book = function(){};
const obj new Book();
console.log(obj.toString());//[object Object]
Book.prototype.toStringTag[Symbol.toStringTag] = "Book";
console.log(obj.toString());
JavaScript
복사
[실행 결과]
[object Object]
[object Book]
1.
첫 번째 console.log(obj.toString());
⇒ Object의 toString()메소드의 디폴트 로직은(@@toStringTag)이 호출되어 function으로 만들었는데도 Object가 반환됩니다.
2.
Book.prototype.toStringTag[Symbol.toStringTag] = "Book";
⇒ prototype에 Symbol.toStringTag를 연결해 두 번째료 표시될 문자를 Book으로 변경했습니다.
3.
두 번째 console.log(obj.toString());
⇒ 첫 번째와 동일하게 toString()을 호출하지만 오버라이드 된 toStringTag가 호출되어 두 번째 문자로 적용한 'Book'이 붙어 출력됩니다.
3. Symbol.isConcatSpreadable
개요
isConcatSpreadable은 Array.prototype.concat()에 대응하는 Well-Known Symbols입니다.
기본적으로 isConcatSpreadable을 설정하지 않는다면 배열(혹은 Array-like)을 concat으로 연결하면배열의 끝에 다른 값 혹은 배열이 연결됩니다. 여기서 해당 Symbol을 어떻게 설정하느냐에 따라 연결 방식이 달라집니다.
1. [Symbol.isConcatSpreadable] = true
const one = [10, 20], two = ["A", "B"];
const show = () =>{ console.log(one.concat(two));};
two[Symbol.isConcatSpreadable] = true;
show();//[10, 20, A, B]
JavaScript
복사
해당 값을 true로 설정하면 기존 배열의 끝에 새로운 값을 하나씩연결하며 배열값인 경우 전개해서 연결합니다.
2. [Symbol.isConcatSpreadable] = false
const one = [10, 20], two = ["A", "B"];
const show = () =>{ console.log(one.concat(two));};
two[Symbol.isConcatSpreadable] = false;
show();//[10, 20, [A, B]]
JavaScript
복사
배열을 전개하지않고 two 배열 자체를 연결합니다.
여기서 주의할점이 있는데 Array-like 오브젝트의 경우에는 기존 배열과는 반대로 기본값이 전개를 하지 않는 것입니다.
3. Array-like 전개
const one = [10, 20];
const like = {0: "A", 1:"B", length: 2};
const show = () =>{ console.log(one.concat(like));};
show();//[10, 20, {0:A, 1:B, length:2}]
like[Symbol.isConcatSpreadable] = true;
show();//[10, 20, A, B]
like[Symbol.isConcatSpreadable] = false;
show();//[10, 20, {0:A, 1:B, length:2}]
JavaScript
복사
1.
첫 번째 show();
⇒ 기본적으로 Array-Like일 경우에는 전개하지 않습니다. 그렇기에 Object자체가 배열의 하나의 엘리먼트 요소로 연결됩니다.
2.
like[Symbol.isConcatSpreadable] = true; show();
⇒ 기존 배열에서 isConcatSpreadable를 true로 설정할 경우 Array-like의 값만 꺼내 전개합니다.
3.
like[Symbol.isConcatSpreadable] = false; show();
⇒ false로 설정하면 원래 동일하게 전개하지 않고 그대로 연결합니다.
정리
concat()메소드의 대상의 전개여부를 설정할 수 있으며, Array-Like 오브젝트의 경우 값만 전개되고 설정 여부에 따라 전개여부가 일반 배열과는 반대입니다.
4. Symbol.species
개요
constructor를 반환하여 실행하기에 인스턴스를 생성하여 반환하는 Symbol.
배열 오브젝트를 사용하다보면 slice를 이용해 잘라내기도하고 concat을 이용해 연결짓기도합니다.
근데 여기서 arr이라는 배열 인스턴스가 있다고 할 때 이 인스턴스에서 slice로 조작한 뒤 반환받은 결과는 동일한 배열 인스턴스가 아닌 새로 생성된 인스턴스입니다. 이게 Symbol.species때문인데, 이를 오버라이드하면 다른, 인스턴스를 반환할수도 있습니다.
코드 분석
const obj = [1, 2, 3];
const one = obj.slice(1, 3);
const two = one.slice(1, 2);
JavaScript
복사
1.
const obj = [1, 2, 3];
: 빌트인 오브젝트 Array으로 Array 오브젝트를 생성하여 obj에 할당합니다. prototype은 없고 __proto__만 있기에 obj는 빌트인 Array 오브젝트가 아닌 인스턴스인것입니다.
2.
const one = obj.slice(1, 3);
⇒slice(1, 3)메소드를 실행 후 결과(one)의 구조를 보면 obj와 차이가 없으며 값만 다릅니다. 이를 통해 인스턴스에 있는 메소드를 호출하면 메소드 실행 결과값을 반환하는게 아닌 결과값이 설정된 인스턴스를 반환한다는 점을 알 수 있습니다.
3. const two = one.slice(1, 2);
⇒ obj.slice(1, 3)의 결과 값인 one에서도 slice(1, 2)메소드가 호출 가능하다는것은 one이 인스턴스라는 것을 시사합니다. 또한, slice(1, 2)를 실행하면 2번과 동일하게 결과 값이 설정된 인스턴스를 반환합니다.
Symbol.species 기능
class Sports extends Array{};
const obj = new Sports(10, 20, 30);
const one = obj.slice(1, 2);
console.log(one);
JavaScript
복사
[실행 결과]
Sports [20]
⇒ Sports라는 클래스는 빌트인 Array오브젝트를 상속(확장, 연결)받습니다. 이 클래스의 인스턴스를 new 를 이용해 생성하고 생성된 인스턴스 obj의 slice()메소드를 호출했을 때 처리 결과를 인스턴스에 설정하여 인스턴스를 반환합니다.
이처럼 인스턴스의 메소드를 호출했을 때 인스턴스를 반환하도록 하는 것이 Symbol.species기능입니다.
정리
기존에 알고있는 객체의 인스턴스 생성방법은 생성자를 호출하여 생성하는 것이였습니다. 예를들어 Sports라는 객체의 인스턴스를 만들고싶다면 new연산자를 이용하여 new Sports()를 호출하는 식으로 말이죠. 하지만, Symbol.species의 기능을 이용해 인스턴스 내의 메소드를 호출하면 constructor가 반환되며 인스턴스가 설정되어 반환되는 것입니다. 심지어 인스턴스 내부에는 생성자(constructor)가 없는데도 말이죠!
5. Symbol.species 오버라이드
개요
•
Symbol.species는 static access property이며 getter만 있고 setter는 없습니다.
•
Symbol.species를 사용할 수 있는 빌트인 오브젝트는
◦
Array, Map, Set, RegExp, Promise, ArrayBuffer, TypedArray
•
빌트인 오브젝트를 상속받은 class에 Symbol.species를 작성하면 빌트인 오브젝트의 @@species가 오버라이드 됩니다.
코드 분석
class Sports extends Array{
static get [Symbol.species](){
return Array;
}
};
const one = new Sports(10, 20, 30);
console.log(one instanceof Sports);
const two = one.slice(1, 2);
console.log(two instanceof Array);
console.log(two instanceof Sports);
JavaScript
복사
[실행 결과]
true
true
false
1.
static get [Symbol.species](){ return Array; }
⇒ 빌트인 Array 오브젝트의 @@species를 오버라이드하며 반환 값을 Array로 했기때문에 이제 Sports 객체 인스턴스의 메소드를 호출하면 인스턴스가 바뀌어 반환됩니다.
2.
console.log(one instanceof Sports);
⇒ Sports로 one을 만들었으니 true입니다.
3.
const two = one.slice(1, 2);
⇒ Sports는 Array오브젝트를 상속받았기에 one인스턴스로 slice()메소드 호출이 가능하며, 메소드 호출시 객체 생성시 준 값(10, 20, 30)을 가지고 slice(1, 2)를 실행하며 반환되는 인스턴스의 결과값을 설정합니다.
⇒ Symbol.species()로 오버라이드 되었기 때문에 static get [Symbol.species]()이 호출되어 Sports 타입이아닌 Array타입의 인스턴스를 반환합니다.
4.
two instanceof Array
⇒ two 인스턴스에는 Array인스턴스가 할당되어 있기에 true를 출력합니다.
5.
two instanceof Sports
⇒ two 인스턴스는 Sports 타입의 one 인스턴스의 slice()를 호출하여 결과값을 인스턴스로 설정해 반환하지만, Symbol.species()가 오버라이드 되어 Array 인스턴스를 반환하게 했기 때문에
false입니다.
6. Symbol.toPrimitive
오브젝트를 대응하는 Primitive 값으로 변환
코딩을 하다보면 내가원하는 값이 잘 설정되었는지 출력하기위해 콘솔창에 변수값을 넣어보기도하고 템플릿에 변수를 대입해서 문장을 완성시키기도 합니다. 하지만, 이 때 값을 알기위해 매번 toString()메소드를 호출하거나 valueOf()메소드를 호출하지 않습니다. 설정된 값이 100인 변수 point 를 출력하고 싶으면 console.log(point)이면되지 point.valueOf()를 호출하진 않습니다.
엔진에서 toPrimitive를 대응,기대하는 타입은 number, string, default 가 있습니다.
스펙(링크)을 잘 살펴보면 Symbol.toPrimitive의 초기값은 는 well-known symbol인 @@toPrimitive라고 정의하고 있습니다. 이 프로퍼티는 { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }. 으로 작성, 열거, 설정이 모두불가능합니다.
코드 분석
1. 오브젝트를 문자열에 대응
const point = {bonus: 100};
console.log(point.toString());
const book ={
toString(){
return "책";
}
};
console.log(`${book}`);
JavaScript
복사
[실행 결과]
[object Object]
책
1.
point.toString()
⇒ point를 문자열로 변환하기위해 toString()메소드를 호출하는데 결과를 보면 [object Object]입니다. 이는 toString이 Object.prototype.toString() 이 호출되었기 때문인데 Object에 연결된 toString()메소드는 object의 타입을 두번째 값으로 출력합니다.
2.
console.log(`${book}`);
⇒ 엔진이 오브젝트내에서 toString()메소드를 찾습니다. book에는 toString()에 정의되어있기 때문에 해당 함수가 호출되고 책 이 출력됩니다. 만일 이 book 오브젝트에 toString()이 정의되어있지 않았다면 __proto__의 toString이 호출되고 point와 동일하기 [object Object]가 출력될 것입니다.
2. 오브젝트를 숫자에 대응
const point = {bonus: 100};
console.log(point.valueOf());
const book ={
toString(){
return 70;
},
valueOf(){
return 30;
}
};
console.log(book * 20);
JavaScript
복사
[실행 결과]
{bonus: 100}
600
1.
point.valueOf()
⇒Object.prototype.valueOf()가 호출되어 {bonus: 100}이 출력됩니다.
2.
console.log(book * 20)
⇒ 얼핏보면 Object 타입 * Number 타입으로 성립될 수 없는 코드지만, 이는 Object타입이 Number타입의 Primitive 값으로 변환되기를 의도하는 코드이며 엔진에서는 오브젝트내에 valueOf() 메소드를 찾습니다. 만약 valueOf()가 없다면 toString()을 찾고 그 다음 Object의 prototype에 연결된 메소드를 호출합니다. 위 코드내에서는 valueOf() 메소드가 정의되어 있기에 호출하여 30을 반환받고 30 * 20 이 실행되어 600이 출력됩니다.
타입에따라 toString()혹은 valueOf()를 정의해서 로직을 구현할 수도 있습니다. 하지만 Symbol.toPrimitive()를 사용하면 조금 더 간단하게 사용할 수가 있는데 이는 위에서도 얘기했던 매개
변수 hint 를 이용하면 편리합니다.
Symbol.toPrimitive()
const obj = {
[Symbol.toPrimitive](hint){
console.log(hint);
return hint==="number" ? 30:
hint === "string" ? "책" : "default";
}
};
console.log(20 * obj);
console.log(`${obj}` + 100);
console.log(obj + 50);
console.log("default" == obj);
JavaScript
복사
[실행 결과]
600
책100
default50
false
1.
[Symbol.toPrimitive](hint){...}
⇒ Symbol.toPrimitive값을 오버라이드해서 사용하며 hint는 해당 오브젝트를 호출하는 상황에 따라 맞게 들어옵니다. hint의 타입으로는
•
곱셈(*)이나 나눗셈(/)등 수학적 연산처리의 경우 "number"
•
템플릿(`${obj}`)은 힌트에 "string"
•
기본적인 덧셈은 힌트에 "default"
•
== 비교 역시 힌트에 "default"가 설정됩니다.
정리
Symbol.toPrimitive는 오브젝트를 Primitive 값으로 변환하는데 있어서 기본엔진보다 편의성읖 높힐 수 있게 해줍니다. hint 를 이용하면 하나의 메소드로 반환값을 유연하게 지정해줄 수 있습니다.
7. Symbol.iterator, Array.prototype[@@iterator], Object 이터레이션
iterator
이터레이터(iterator)/이터러블(iterable)에 대해서는 이전시간에 포스팅을 한 적이 있습니다.
이터레이터 이터러블은 하나의 프로토콜(규약)으로써 개발자들이 공통된 명세를 통해 관리와 사용을 편하게 해줍니다. 요즘 나오는 전자기기의 충전코드는 대부분 usb-c 타입으로 해당 케이블하나만있으면 대다수의 많은 최신 전자기기가 충전가능한것처럼 많은 오브젝트들 역시 이터레이터 프로토콜만 제대로 따라준다면 for-of뿐 아니라 Symbol.iterator 호출을 통해 next()메소드를 호출해 반복도 가능합니다.
JS의 엔진에서 디폴트 값인 @@iterator가 있는 빌트인 오브젝트는
•
String, Array, Map, Set, TypedArray
위와같이 존재하며 이터레이터 프로토콜에 따라 [Symbol.iterator]()로 이터레이터 오브젝트를 꺼낼수도 있고 for-of문으로 쉽게 내부를 순회할 수 있습니다.
그런데 잘 보면 @@iterator가 빌트인 Object에는 없다는걸 확인할 수 있습니다. 그렇다면 빌트인 Object에서는 이터레이터 반복이 불가능할까요? Symbol.iterator 오버라이드로 충분히 반복이 가능하게 할 수 있으며 아래에서 알아봅니다.
Array.prototype[@@iterator]
Array 오브젝트의 [Symbol.iterator]()를 호출하면 이터레이터 오브젝트를 반환하는데 next()메소드를 호출해 배열 엘리먼트 값을 하나씩 구할 수 있습니다.
const list = [10 , 20];
const obj = list[Symbol.iterator]();
console.log(obj.next());
console.log(obj.next());
console.log(obj.next());
JavaScript
복사
[실행 결과
{value: 10, done: false}
{value: 20, done: false}
{value: undefined, done: true}
String.prototype[@@iterator]
String오브젝트의 [Symbol.iterator]()를 호출하면 이터레이터 오브젝트를 반환하는데 next()메소드를 호출해 배열 엘리먼트 값을 하나씩 구할 수 있습니다.
const str = "ABC";
const iter = str[Symbol.iterator]();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
JavaScript
복사
[실행 결과
{value: "A", done: false}
{value: "B", done: false}
{value: "C", done: false}
{value: undefined, done: true}
Object 이터레이션
빌트인 Object에는 Symbol.iterator가 없습니다. 하지만 Object에 이터레이터 프로토콜을 따르는 구조에 Symbol.iterator를 작성하면 빌트인 Object도 반복이 가능해집니다.
const obj = {
[Symbol.iterator](){
return {
count: 0,
maxCount: this.maxCount,
next(){
if(this.count< this.maxCount){
return {value: this.count++, done: false};
}
return {value: undefined, done: true};
}
};
}
};
obj.maxCount = 2;
for(const value of obj){
console.log(value);
};
JavaScript
복사
[실행 결과]
0
1
1.
[Symbol.iterator](){return {...}}
⇒빌트인 Object인 obj는 [Symbol.iterator]가 정의되어있지 않기때문에 사용자 정의 이터레이터를 정의합니다. 이터레이터 오브젝트를 사용할때 호출하는 메소드인 next()를 구현하여 로직에 맞게끔 특정 조건까지 반환 오브젝트의 done을 false로 해 반복하게끔 하고, 반복이 종료되면 done:true를 반환해 이터레이터의 반복이 끝내도록 로직을 구현했습니다. 위 코드에서는 maxCount를 설정해 maxCount까지 초기값이 0인 count가 도달할 때까지 순회하도록 반복하는 로직입니다.
2.
obj.maxCount = 2;
⇒ obj의 반복 도달점은 maxCount에 의해 설정되는데 2로 설정해주었습니다.
3.
for(const value of obj){...}
⇒ 엔진은 for-of문을 시작하면 먼저 대상 오브젝트에서 [Symbol.iterator]를 검색합니다. 그래서 기존의 빌트인 오브젝트는 [Symbol.iterator]가 정의되지 않아서 for-of를 사용못했지만 위 코드에서는 정의를 해줬습니다.
⇒ 처음 obj의 [Symbol.iterator]()가 호출되어 오브젝트를 반환하는데 이때 obj.maxCount는 2번항목에서 2로 반복횟수를 정의해줬습니다.
8. Symbol.iterator에 제너레이터 함수 연결
generator 함수 연결
Symbol.iterator는 Iterator 오브젝트를 반환하여 반복가능하게 해주고 generator는 yield를 수행해 호출할 때마다 값을 반환해줍니다. 코드를 통해 오브젝트{}에 [Symbol.iterator] 에 generator를 연결해보겠습니다.
const obj = {};
obj[Symbol.iterator] = function*(){
yield 1;
yield 2;
yield 3;
};
console.log([...obj]);
JavaScript
복사
[실행 결과]
[1, 2, 3]
1.
obj[Symbol.iterator] = function*(){...}
⇒ obj의 Symbol.iterator에 제너레이터 함수를 연결했습니다.
2.
[...obj]
⇒ obj에서 [Symbol.itertor]를 검색합니다. 검색 결과 연결된 함수를 실행하며 이터레이터 오브젝트를 생성해 반환합니다.
⇒ yield가 끝날 때까지(done:true) 반복하며 yield에서 반환된 값을 배열에 첨부합니다.
⇒ ...obj가 대괄호로 감싸져있지 않는다면 마지막 값(3)만 반환되어 3만 출력됩니다
연결 구조
Symbol.iterator의 __proto__에 제너레이터 오브젝트가 있는 구조입니다.
Symbol.iterator안의 __proto__를 살펴보자.
할당 연산자 = 때문에 obj의 Symbol.iterator가 제너레이터로 대체되는 것 같지만 그렇지는 않고, 위 이미지의 전개내용처럼 __proto__에 제너레이터 오브젝트에 연결되는 구조로 이렇게 두개가 존재해야 iterator와 yield처리가 둘 다 가능한 것입니다.
또한, 제너레이터 오브젝트에 이터레이터 오브젝트를 대체하는게 아닌 연결하는 것이기에 값은 따로 설정되는게 아닌 공유되는 형태입니다.
const gen = function*(){
yield 10;
yield 20;
};
const genObj = gen();
console.log(genObj.next());
const obj = genObj[Symbol.iterator]();
console.log(obj.next());
JavaScript
복사
[실행 결과]
{value: 10, done: false}
{value: 20, done: false}
1.
const genObj = gen();
⇒ gen() 제너레이터 함수를 호출하면 제너레이터 오브젝트가 반환되는데 이는 곧 이터레이터이기도해서 next()메소드를 호출하면 첫 번째 yield가 실행되어 {value: 10, done: false}이 반환됩니다.
2.
const obj = genObj[Symbol.iterator]();
⇒ 제너레이터 오브젝트의 Symbol.iterator()를 호출하여 이터레이터 오브젝트를 반환합니다.
3.
console.log(obj.next());
⇒ obj의 첫 번째 next()호출이기에 첫번째 yield인 10이 반환될 것 같지만 실제로는 {value: 20, done: false}이 반환되어 출력됩니다. 이는 제너레이터 함수에서 수행했던 과정부터 시작하여 두 번째 yield가 수행되어 값을 반환되었다는것이고 즉, 이터레이터 오브젝트에서 yield 처리를 공유한다는 점을 알 수 있습니다.
9. Symbol.match
Well-Known Symbol을 지원하는 String 메소드로는
•
match(), replace(), search(), split()
정도가 있습니다. 기본 값으로는String.prototype.메소드명으로 연결되어있습니다.
match()는 String.prototype.match()에 연결되어 있으며 문자열에 패턴을 매치하고 매치된 결과를 배열로 반환합니다.
const result = "Sports".match(/s/);
console.log(result);
JavaScript
복사
[실행 결과]
["s", index: 5, input: "Sports", groups: undefined]
Symbol.match()
Symbol.match()는 코드를 함수 블록에 오버라이딩하여 String.prototype.match()대신 동작하도록 할 수 있습니다.
const sports = {
base: "ball",
[Symbol.match](value){
return this.base.indexOf(value)<0?"없음":"있음";
}
}
console.log("al".match(sports));
JavaScript
복사
[실행 결과]
있음
⇒ base로 설정된 'ball'에 'al'이 있으면 '있음' 없으면 '없음' 을 반환하는 로직인데 엔진은 sports 오브젝트에서 Symbol.match가 오버라이드 되있는지 확인합니다. 없다면 디폴트 값인 String.prototype.match()를 호출하고 있으면 Symbol.match()를 호출합니다. 이때 value가 될 매개변수는 "al"이 됩니다.
⇒ 이때 Symbol.match메소드를 오버라이드하는 것이기에 메소드의 시맨틱은 유지해야 합니다.
여기서 match를 할 value에 가 /param/ 이런 패턴이면 파라미터를 패턴으로 처리합니다. 이럴 경우 정규 표현식을 사용할 수 없으므로 에러가 발생하는데 이를 Symbol.match = false 로 설정해서 해결할 수 있습니다.
try{
"/book/".startsWith(/book/);
}catch{
console.log("에러 발생");
};
let check = /book/;
check[Symbol.match] = false;
console.log("/book/".startsWith(check));
JavaScript
복사
[실행 결과]
에러 발생
true
1.
"/book/".startsWith(/book/);
⇒파라미터 /book/을 패턴으로 처리하기에 정규표현식을 사용할 수 없기에 에러가 발생합니다.
2.
check[Symbol.match] = false;
⇒ 파라미터를 정규표현식으로 인식하지 않도록 설정합니다.
3.
"/book/".startsWith(check);
⇒ 첫번째와 동일하게 파라미터가 check의 값인"/book/"이지만 Symbol.match를 false로 설정해줬기에 check값을 문자열로 인식합니다. 그렇기 때문에 에러가 발생하지 않고 true가 출력됩니다.