iterator, generator, symbol 삼총사

YEONGHUN KO·2023년 12월 5일
0

JAVASCRIPT - BASICS

목록 보기
27/27
post-thumbnail

이번 글에서는 요 삼총사에 대해서 알아보려고 한다. 요 삼총사는 서로 연결되어있고 친하다. 우선 Symbol에 대해서 알아보장

Symbol

이 친구는 나만 알고 싶은 비밀의 방을 만들때 사용된다.

바로 코드로 보자

const person = {
  firstName: "John",
  lastName: "Doe",
  age: 50,
  eyeColor: "blue"
};

let id = Symbol('id');
const id2 = Symbol('id')

person[id] = 140353;
person.id // undefined

id === id2 // false

요런식으로 쓰인다. id객체는 id 변수를 통해서만 접근 가능하다.

어떤 사람은 아래와 같이 설명했다.

It represents a unique "hidden" identifier that no other code can accidentally access.
For instance, if different coders want to add a person.id property to a person object belonging to a third-party code, they could mix each others values.
Using Symbol() to create a unique identifiers, solves this problem:

일단 요정도로 Symbol에 대해서 개념을 잡자. 그럼 iterator랑 무슨 상관??

Symbol객체 안에 iterator라는 녀석이 있는데 이 녀석을 사용해야만 iterator를 만들 수 있다 그럼 iterator를 만나러 슝~

iterator

말그대로 반복가능한 어떤 녀셕(iterable)을 만든다.

바로 코드로 보자

const wellFormedIterable = {
  [Symbol.iterator]:()=> {
    let i = 1;
    return {
      next() {
        return i > 3 ? { done: true } : { value: i++, done: false };
      },
      
    };
  },
};

const iterable = wellFormedIterable[Symbol.iterator]()
iterable.next() // {value : 1, done : false}

for(const element of wellFormedIterable) {console.log(element) // 1 2 3}

그렇다 그래!! [Symbol.iterator]라는 key를 가지고 있는 함수인데, 이 함수가 next라는 함수가 담긴 객체를 리턴한다면 이는 iterable한 아이를 만들어내는 iterator이다 그리고 이러한 조건을 갖춘 함수를 iteration protocol을 따른다고 한다.

단, next함수가 value, done을 리턴하지 않고 다른걸 리턴한다 했을때, iterator[Symbol.iterator]().next() 라고 하면 문제가 없지만 for문으로 돌릴 경우 무한 undefined를 출력하니 조심!

for문은 자동으로 {value:..., done:....}을 찾는 것 같은 느낌이다.

그럼 이.. 다소 복잡해 보이는 녀석이 generator 문법으로 너무나 쉽게 간소화 할 수 있게 된다. 자 그럼 멋쟁이 generator를 만나러 가보자

generator

쉽게 말해서 iterator를 쉽게 만들기 위해 존재하는 syntatic sugar라고 생각하면 된다.

바로 코드를 보자

function* countFruit(phrase) { 
 const fruits = ["apple", "banana", "peach"];

 let curIndex = 0;
   while (curIndex < fruits.length) {
     yield phrase + fruits[curIndex];
     curIndex += 1;
  }
}
 
const fruitIterator = countFruit("A nice: ");

console.log(fruitIterator.next()); // A nice apple... 
console.log(fruitIterator.next()); // A nice banana...
console.log(fruitIterator.next()); // A nice peach...

array를 순회하는 generator를 더 쉽게 만들 수 있다.

function* generator(array) {
  for(let i = 0; I < array.length ; I++) {
  yield array[I]
  }
}
 
// 아래처럼 바뀔 수 있음. 
function* generator(array) {
    yield* array;
}

자 그럼 일반 array를 순회하는 것이랑 iterator를 이용해서 필요할때 필요한 만큼 순회하는 것이랑 어떤 차이가 있는지 살펴보자.

function range(lim) {
  let i = -1;
  let res = [];
  while (++i < lim) {
    console.log('range');
    res.push(i);
  }
  return res;
}

const L = {};

L.range = function* (lim) {
  let i = -1;
  while (++i < lim) {
    console.log('L.range');
    yield i;
  }
};

let rangeIterator = L.range(5)
console.log(rangeIterator)// L.range {<suspended>} // 이름 자체에 이미 지연이라고 나온다ㅋㅋㅋㅋ

rangeIterator.next()

// iterable 을 lim 길이만큼 자르는 함수
const take = (lim, iter) => {
  let res = [];
  for (const a of iter) {
    res.push(a);
    if (res.length === lim) return res;
  }

  return res;
};

console.time('');
take(5, range(1000));
console.timeEnd('');

console.time('');
take(5, L.range(1000));
console.timeEnd('');

// 1000 range
// 53.947265625 ms

// 5 L.range
// 0.185791015625 ms

이미 1000개의 요소를 미리 만들어서 array에 담아두는 것 하고, 필요한 만큼 만드는 것 하고는 차이가 있다.

이 개념을 그대로 서버 통신에 적용가능하다. 사용자가 필요한 만큼 통신을하고 담아두었다가 보여주는 것!

바로 async generator라는게 있다.

async generator

지금 만들려고 하는 비동기 제너레이터는 flicker라는 페이지에서 tag에 해당하는 image를 필요한만큼 받은 다음 url로 파싱해서 리턴하는 함수이다.

바로 코드를 보도록 하자.

function flickrTagSearch(tag) {
// Returns a promise that resolves after _seconds_ 
  const delay = seconds => new Promise(resolve => setTimeout(resolve, seconds * 1000))
  
  // Returs a Promise that resolves to an array of urls to images that represents a (paged) result of a tag search on Flickr.
  
  function flickrTagSearch (tag, page) {
    const apiKey = '' // api.flickr.com에서 직접 apiKey를 받아와라.
    return fetch(
      'https://api.flickr.com/services/rest/' + 
      '?method=flickr.photos.search' +
      '&api_key=' + apiKey +
      '&page=' + page +
      '&tags=' + tag + 
      '&format=json' + 
      '&nojsoncallback=1'   
    )
    .then(response => response.json())
    .then(body => body.photos.photo)
    .then(photos => photos.map(photo =>
      `https://farm${photo.farm}.staticflickr.com/` +
      `${photo.server}/${photo.id}_${photo.secret}_q.jpg`               
    ))
  }
  
  return {
    [Symbol.asyncIterator]: async function*() {
      let pageIndex = 1
      while(true) { 
        const pageData = await flickrTagSearch(tag, pageIndex)
        for (const url of pageData) {
          await delay(2)
          yield url
        }
        pageIndex = pageIndex + 1
      }
    }
  }
}

const getImageByTag = flickrTagSearch('bird')
const firstPageImageUrl = getImageByTag.next()

요런식으로 사용하면 서버통신을 매번하지않고 page에 해당되는 image를 일단 서버에서 다운받은후 한개씩 필요할때마다 image url을 yield한다.

한 페이지에 해당하는 모든 url이 yield되면 pageIndex = pageIndex + 1 로 넘어가면서 pageIndex가 하나 올라가고 while(true) 에 의해서 다시
const pageData = await flickrTagSearch(tag, pageIndex) 로 넘어가면서 그 다음 페이지에 해당하는 이미지를 서버에서 불러오고 그 이미지가 pageData라는 변수에 담긴다.

즉, generator는 새로운 페이지에 대해서 서버에서 받은 이미지들을 다시 yield 준비가 된것이다.

이로써 iterator를 이용해서 서버 통신 횟수를 최소화 할 수 있게 되었다.

<출처>

  1. https://www.youtube.com/watch?v=wrI-Jb0oFyk&t=1402s
  2. https://observablehq.com/@mpj/code-for-using-async-generators-to-stream-data-in-javascrip
profile
'과연 이게 최선일까?' 끊임없이 생각하기

0개의 댓글