컴퓨터 공부/🕸️ Web

Javascript - Symbol(), Iterator, Generator

letzgorats 2023. 12. 20. 13:26

< Symbol >

Symbol Type이란 2015년 ES6에서 새로 추가된 원시 타입이며, 이 타입의 목적은 유니크한 식별자를 만들기 위해서 사용된다.

 

Symbol Type값을 생성하는 방법은?

→ 여러가지 타입의 값을 생성할 때를 우선 살펴보자.

symbol은 shortcut이 없다.

 

Symbol은 Shortcut이 없고 반드시 Symbol() 을 통해서만 값을 줄 수 있다.

 

1) Symbol Type유니크한 식별자를 위해 사용한다고 했는데, 값은 보이는게 같더라도 내부에서는 다른 값을 가진다.

const sym1 = Symbol();
const sym2 = Symbol();

console.log(sym1 == sym2); // false

 

2) Symbol description을 줄 수 있다.

: symbol에 매개변수로 넣어주면 된다.

description 은 스트링, 숫자, undefined 가능

const sym3 = Symbol("Allu");

console.log(sym3.description);

description을 해야 값이 나온다.

 

매개변수 없이 그냥 Symbol()을 주면, 어떤 값을 가지는지 파악하기 힘들다. 

즉, description은 이 Symbol 값이 어떠한 심볼인지 알 수 있게 해주며, 이 덕분에 디버깅할 때 사용할 수 있다. description이 없다면 어떠한 심볼인지 알기 어렵다는 뜻이다.

const sym1 = Symbol();
const sym2 = Symbol();

console.log(sym1); // Symbol()
console.log(sym2); // Symbol()

Symbol() 은 그냥 Symbol() 이 나옴

[ ※ 그럼 언제 Symbol 을 사용할 할까? ]

예를 들어보자.

let petA = {
    id : 1,
    name : 'Allu',
    species : 'dachshund',
    color : 'black'
}

petA.id = 300;

console.log(petA)

 

petA 에 어떤 속성이 들어가 있는지 몰라서, petA.id 를 300으로 줬다고 가정해보자. 

 

 

 

유니크한 값을 넣어주기 위해서 petA.id = 300 으로 값을 300을 줬기 때문에, 기존 id 값이 1에서 300으로 오버라이드 되어버렸다.

해당 객체에 어떠한 것이 있는지 모르고 그냥 유니크한 값만 주려고 했는데, 이게 원래 있던 속성의 값을 바꿔버리는 현상이 일어난 것이다.

 

이럴 때, Symbol() 을 이용해서 유니크한 값을 줄 수가 있다.

let petA = {
    id : 1,
    name : 'Allu',
    species : 'dachshund',
    color : 'black'
}

const idSym = Symbol('id');
petA[idSym] = 300;
console.log(petA)

 

이렇게 하면, 객체 내의 속성 id는 고유의 값을 보존하고, idSym이 Symbol('id')이므로 Symbol(id):300 이라는 속성이 새로 만들어진다.

Symbol() 을 사용하면 항상 유니크하게 될 수밖에 없다.

 

Symbol(id) : 300

해당 방법 말고 아래처럼 해도 결과는 동일하다.

const idSym = Symbol('id');

let petA = {
    id : 1,
    name : 'Allu',
    species : 'dachshund',
    color : 'black',
    [idSym] : 300,
}

console.log(petA)

 

대괄호를 사용해 Symbol 형 키로 만들어야 한다.

Symbol(id) : 300

 

[ Symbol 은 for...in 과 getOwnPropertyNames 에서 제외가 된다. ]

: 심볼을 이용하면 기본적으로 Property가 숨겨지게 된다.(물론, 찾을 수 있는 방법도 있다.- getOwnPropertySymbols)

: 그래서  for...ingetOwnPropertyNames를 이용할 때 Symbol로 만든 프로퍼티는 안보인다.

 

ex) getOwnPropertyNames

: 객체의 모든 속성(심볼을 사용하는 속성을 제외한 열거할 수 없는 속성 포함) 들을 배열로 반환한다.

const idSym = Symbol('id');

let petA = {
    id : 1,
    name : 'Allu',
    species : 'dachshund',
    color : 'black',
    [idSym] : 300,
}

console.log(Object.getOwnPropertyNames(petA));

Symbol 속성이 안보인다.

 

getOwnPropertySymbols 를 활용하면 Symbol을 찾을 수 있다.

console.log(Object.getOwnPropertySymbols(petA));

[Symbol(id)]

 

ex) for ... in

: 객체의 반복에서 사용

const idSym = Symbol('id');

let petA = {
    id : 1,
    name : 'Allu',
    species : 'dachshund',
    color : 'black',
    [idSym] : 300,
}

for(const key in petA) {
    console.log(key);
}

Symbol 속성이 안보인다.

 

※ Symbol.for() 을 이용한 전역 심볼 ]

: 원래는 심볼로 값을 생성하면 심볼의 description이 같더라도 다 다른 값을 가지게 된다.

: 하지만, Symbol.for()을 이용하면 같은 description을 가졌을 때 같은 값을 가지게 된다.

Symbol('id') === Symbol('id')    // false
Symbol.for('id') === Symbol.for('id')    // true

 

※ Symbol.keyFor() ]

: Symbol.for 을 이용해서 전역 심볼을 만들 때(찾을 때) 사용하는 description을 얻을 수 있는게 Symbol.keyFor() 이다. 

// description을 이용해 심볼을 찾음

let sym1 = Symbol.for("name");
let sym2 = Symbol.for("id");

// 심볼을 이용해 description을 얻음

let sym1key = Symbol.keyFor(sym1);    // name
let sym2key = Symbol.keyFor(sym2);    // id

console.log(sym1key);
console.log(sym2key);

 

Symbol.for() 와 Symbol.keyFor() 의 차이

 

Symbol.for() 에는 description 이 인자로 들어가서 심볼을 찾는 것이고,

Symbol.keyFor() 에는 심볼 변수가 인자로 들어가서 해당 심볼에 대한 description을 찾는 것이다.

 

그럼, 이제 심볼이 어떻게 사용되는지 사용 예시를 살펴보자.

(ex 1)

const RED = 'red';
const ORANGE = 'orange';
const YELLOW = 'yellow';
const BLACK = 'black';
const dog = 'black';

function getImportantLevel(color) {
    switch (color) {
        case RED:
            return 'very important';
        case ORANGE:
            return 'important';
        case YELLOW:
            return 'little important';
        case BLACK:
            return 'not important';
        default:
            console.log(`$(color) not included`);
    }
}

console.log(getImportantLevel(BLACK));    // not important
console.log(getImportantLevel(dog));     // not important

 

console.log(getImportantLevel(BLACK)); 을 하면 'not important' 가 나오고,

console.log(getImportantLevel(dog)); 을 하면 default로 빠질 것이라고 예상될 수 있다.

 

not important

하지만, 결과는 둘다 'not important'가 출력된다.

console.log(getImportantLevel(BLACK)); 을 하면 'not important' 가 나오는 것은 이해가 가지만, 

console.log(getImportantLevel(dog)); 을 하면 default 분기로 안 빠지고, 또 'not important' 가 나오게 된다.

이런 실수를 방지하기 위해서 Symbol을 사용한다.

const RED = Symbol('red');
const ORANGE = Symbol('orange');
const YELLOW = Symbol('yellow');
const BLACK = Symbol('black');
const dog = 'black';

function getImportantLevel(color) {
    switch (color) {
        case RED:
            return 'very important';
        case ORANGE:
            return 'important';
        case YELLOW:
            return 'little important';
        case BLACK:
            return 'not important';
        default:
            console.log(`${color} not included`);
    }
}

console.log(getImportantLevel(BLACK));    // not important
console.log(getImportantLevel(dog));     // black not important

 

해당 코드처럼 Symbol을 사용하면 BLACK 에 해당하는 black 과 dog 에 해당하는 black 이 달리 인식되어

console.log(getImportantLevel(dog)); 을 하면 default 분기로 빠져서 'black not important'가 나오게 된다.

 

(ex 2)

class Dog {
    constructor(){
        this.length = 0;
    }
    
    add(species, name) {
        this[species] = name;
        this.length ++;
    }
}

let myDogs = new Dog();
myDogs.add('dachshund','Maru');
myDogs.add('beagle','Aloo');
myDogs.add('golden retriever','Goldang');

for (const dog in myDogs) {
    console.log(dog, myDogs[dog]);
}

결과

 

결과가 아래와 같이 나왔는데, length 3은 인스턴스를 3개 추가해줬으니까 0에서 3이 더해진 3이 나온 것이다.

length 3
dachshund Maru
beagle Aloo
golden retriever Goldang

 

근데 여기서, length 3 부분을 출력해주고 싶지 않을 때, 이럴 때도 Symbol 을 사용할 수 있다.

const length = Symbol('length');
class Dog {
    constructor(){
        this[length] = 0;
    }
    
    add(species, name) {
        this[species] = name;
        this[length] ++;
    }
}

let myDogs = new Dog();
myDogs.add('dachshund','Maru');
myDogs.add('beagle','Aloo');
myDogs.add('golden retriever','Goldang');

for (const dog in myDogs) {
    console.log(dog, myDogs[dog]);
}

 

Symbol 로 만들면, property에서 빠지기 때문에, 심볼로 만든 심볼 값을 length로 넣어주면, 결과는 아래와 같이 나온다.

length를 symbol로 줌

결과에는 아까처럼 'length 3'  이 나오지 않는다.

for...in 으로 하나씩 나열할 때 symbol 값은 포함이 안 되기 때문이다.

getOwnPropertyNames 를 통해 심볼을 사용하는 속성을 제외한 모든 프로퍼티가 객체배열로 나오는 것을 볼 수 있다. getOwnPropertySymbols 를 통해서는 숨겨진 심볼을 찾을 수 있다.

 

 

결국, 심볼타입은 유니크한 식별자를 만들기 위해서 사용을 한다고 생각을 해주면 된다.


< Iterator(반복기) >

→ Iterable

    - 배열은 반복가능한 객체이며, 반복이 가능하다는 것을 Iterable 이라고 부른다. 

    - for...of 를 이용할 수 있거나 [Symbol.iterator]() 이 값을 가지면 Iterable 한 것이다.

 

→ Iterator

    - 반복자는 next() 를 호출해서 {value: , done: } 두개의 속성을 가지는 객체를 반환하는 객체이다.

 

그럼, Iterator를 직접 생성해보자.

function makeIterator(numbers) {
    let nextIndex = 0;
    
    return {
        next: function () {
            return nextIndex < numbers.length ?
                { value : numbers[nextIndex++], done : false } : 
                { value : undefined, done : true}
        }
    }
}

// 숫자 배열 생성
const numbersArray = [1,2,3];
const numbersIterator = makeIterator(numbersArray);

console.log(numbersIterator.next());
console.log(numbersIterator.next());
console.log(numbersIterator.next());
console.log(numbersIterator.next());

직접 함수를 만들어서 iterator 생성

iterable 한 값을 iterator로 반복기로 만들기 위해서 makeIterator 함수를 임의로 만들어서 사용했다.

하지만, Symbol.iterator 를 이용해서도 Iterator 를 만들 수 있다.

function makeIterator(numbers) {
    let nextIndex = 0;
    
    return {
        next: function () {
            return nextIndex < numbers.length ?
                { value : numbers[nextIndex++], done : false } : 
                { value : undefined, done : true}
        }
    }
}

// 숫자 배열 생성
const numbersArray = [1,2,3];

const numbersIterator = numbersArray[Symbol.iterator]();

console.log(numbersIterator.next());
console.log(numbersIterator.next());
console.log(numbersIterator.next());
console.log(numbersIterator.next());

 

Symbol.iterator 를 이용해서도 Iterator 를 만들 수 있다

 

makeIterator 를 사용하지 않고, [Symbol.iterator]() 를 사용해서 Iterator를 만들었다. 결과는 똑같이 나온다.

즉, [Symbol.iterator]() 를 이용하면 반복가능한 값을 반복기로 생성가능하다.

 

iterable 한지 알아보는 방법은 이외에도 아래와 같은 방식이 있다.

const numbersIterable = [1,2,3];
const numbersNotIterable = { a : 1, b : 2};
console.log(typeof numbersIterable);    // object
console.log(typeof numbersNotIterable);    // object

for (const n of numbersNotIterable) {
    console.log(n);
}

 

type은 둘 다 object 가 나온다. 

이 때, for ... of 가 사용 가능하면 iterable 하다고 했는데, console.log(n)을 시도해보면, 아래 사진처럼 에러가 뜬다

 

 

numbersNotIterable은 iterable 하지 않다.

 

 배열은 iterable 한 반면, numbersNotIterable 객체는 iterable 하지 않기 때문이다.

for ... of 말고도 [Symbol.iterator()] 를 사용함으로써 iterable 한지 안한지 알아볼 수 있다.

const set = new Set([1,2,3,4]);
console.log('set', set);
const map = new Map([['a',1],['b',2]]);
console.log('map', map);

console.log(set[Symbol.iterator]().next());
console.log(map[Symbol.iterator]().next());

 

set 과 map 둘 다 iterable 한 것을 볼 수 있다.

set, map

 

map[Symbol.iterator()]() 는 iterable 한 객체 iterator 니까 next() 를 사용함으로써 {vlaue:_ , done:_} 객체를 반환한다.


< Generator(생성기) >

생성자 함수 Generator Function 은 사용자의 요구에 따라 다른 시간 간격으로 여러 값을 반환할 수 있다.

  • 일반 함수 → 단 한 번의 실행으로 함수 끝까지 실행된다.
  • 제너레이터 함수 → 사용자의 요구에 따라 일시적으로 정지될 수도 있고, 다시 시작될 수도 있다.

그럼, 먼저 Generator 를 직접 생성해보자.

// Generator Example

function* SayNumbers() {
    yield 1;
    yield 2;
    yield 3;
}

// 제너레이터 함수의 반환이 제너레이터
const number = SayNumbers();

console.log(number.next().value);
console.log(number.next().value);
console.log(number.next().value);

 

'*'를 사용해서 제너레이터 함수를 만든다. 여기서 yield 는 제너레이터 함수의 실행을 일시적으로 정지시킨다.

즉, 일반 함수의 return 과 매우 유사하다.

(제너레이터 사용한 라이브러리는 Node.js 의 프레임워크 중 하나인 Koa, 비동기 처리를 위한 Redux 미들웨어인 Redux Saga 등이 있다.)

 

generator 함수

 

generator 함수를 통해서 '제너레이터'를 생성할 수 있는데, 그것은 제너레이터 함수를 그냥 호출해서 반환하면 된다.

생성한 generator 에 next() 를 사용함으로써 {vlaue:_ , done:_} 객체를 반환한다.

 

또, generator 는 generator의 iterable 에서 반환하는 iterator 자기 자신과 같다.

function* generatorFunction() {
    yield 1;
}

const generator = generatorFunction();

// generator = generator[Symbol.iterator]();

console.log(generator.next());

 

아까 Iterator를 만들 때, 어떠한 이터러블한 값에다가 [Symbol.iterator]() 해서 반환한게 Iterator 였다.

마찬가지로, 이 제너레이터도 제너레이터에 이터러블해서 반환하는 이터레이터 자기 자신과 같다.

그래서, next() 를 사용할 수 있는 셈이다.

generator 는 generator의 iterable 에서 반환하는 iterator 자기 자신

 

그럼, 이제 제너레이터를 주로 어떻게 사용을 하는지 사용 예시를 살펴보자.

(ex 1)

// ID Creator
function* createIds() {
    let index = 1;
    
    while(true) {
        yield index++;
    }
}

const gen = createIds();

console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.return(10));

 

create id 를 하는데 필요할 때만 생성을 하는 것이다. 제너레이터필요할 때마다 사용자의 요구에 따라 일시적으로 정지할 수 있고 다시 시작할 수 있다고 했다. 아이디를 사용자가 원하는 때만 생성할 수가 있는 셈이다.

 

※ Lazy Evaluation

: 계산의 결과값이 필요할 때까지 계산을 늦춰서 필요한 데이터를 필요한 순간에 생성을 하는게 레이지 이벨루에이션이다. 

 

지금 하고 있는 부분이 바로 Lazy Evaluation 이다. 

next, return

 

next() 함수 말고, return 함수를 넣을 수도 있다. 위와 같이 return 10을 하면, 억지로 value에다가 10 넣는다.

 

제너레이터 함수를 만들 때, 함수 내부에서  yield* 형태로 배열 형식(iterable)으로도 줄 수 있다.

function* generatorFunction() {
    yield* [1,2,3];
}

const generator = generatorFunction();

for (const number of generator){
    console.log(number);
}

 

for...of 를 이용해서 해당 generator 를 돌면서 찍어보면, 하나씩 순회하며 값이 나오는 것을 알 수 있다.

 

yield *[]


참고 자료

따라하며 배우는 자바스크립트 A-Z

반응형