## Generator & Iterator 자바스크립트는 매년 버전을 올리고 있으며 이를 ES 버전 또는 ECMAScript 버전 식으로 표기를 하고 있다. 2020년 3월 기준 현재 최신 버전은 ES10 또는 ECMAScript 2019 버전 이다. 6월 쯤 ECMAScript 2020 또는 ES11이 나올 예정으로 [여기](https://tc39.es/ecma262/)서 스펙들을 미리 확인해 볼 수도 있다. 자바스크립트가 단순히 브라우저의 인터렉티브한 동작을 담당하는 역활에서 프로그래밍 언어로서 인정받기 까지는 이러한 버전관리와 매년 추가되는 기능으로 인해서인데 ES6 버전이 나올때 쯤 언어로서의 갖추어야 할 기능들이 많이 추가되었다고 볼 수 있다. (그래서 보통 자바스크립트에 대한 이해도를 나타 낼때 ES6 이후 부터인 모던 자바스크립트를 이해한다는 의미로 ES6+ 라고 표기한다.) 그 중 중요한 개념이 몇가지가 있는데, Class 나 Promise 등과 더불어 **제너레이터 와 이터레이터** 개념이 도입되었다. ### 이터레이터 이터레이터는 자바스크립트 뿐만 아니라 다른 언어들에도 있는 개념으로 일종의 루프를 생각하면 이해하기 쉽다. Array는 대표적인 **이터러블 객체**로서 이터레이터를 사용 할 수 있다. 이터레이터는 단순히 루프를 도는 것이 아니라 현재 어디를 돌고 있는지 알 수 있다. 간단한 예로 이터레이터를 생성해 보도록 하겠다. ``` javascript const numbers = [1, 2, 3]; // 배열 생성 const it = numbers.values() // 이터레이터 생성 ``` `values` 메서드를 사용하여 이터레이터를 생성했다. `console.log`로 브라우저에 찍어보면 아래와 같이 뜬다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1584275387663-image.png) Iterator 로서 `next`라는 메서드를 가지고 있다. `next`를 numbers 개수만큼 사용하면 아래와 같다. ``` javascript it.next(); // {value: 1, done: false} it.next(); // {value: 1, done: false} it.next(); // {value: 3, done: false} it.next(); // {value: undefined, done: true} it.next(); // {value: undefined, done: true} ``` `next`를 사용할 때 마다 그 다음 값으로 넘어가며 모든 값을 돌고 나면 `done`이 `true`로 나오며 끝난다. 한번 끝난 이터레이터는 다시 돌아가지 않으며 `value`가 `undefined`로 리턴한다. 이때문에 `for of` 루프가 가능한 것이며 일반 오브젝트가 루프를 돌 수 없는 이유이기도 하다. 실제로 오브젝트를 `for of`로 루프를 돌려고 하면 아래와 같은 에러가 나온다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1584275936360-image.png) `for of`문을 흉내내면 아래와 같다. ``` javascript const numbers = [1, 2, 3]; const it = numbers.values(); let i = it.next(); while(!i.done) { console.log(i.value); i = it.next(); } for (let i of number) { console.log(i); } ``` 위에서 `while`문과 `for of`문은 동일한 동작을 한다. 즉 이터러블 오브젝트란 반복 가능한 오브젝트를 의미하여 이터레이터란 이러한 반복을 정의한 규약이라고 할 수 있다. 이 두가지를 포함한 개념을 **이터레이션 프로토콜** 이라 한다. 이터레이터란 프로토콜의 하나이므로 일반 오브젝트를 이터레이터 프로토콜을 적용하면 이터레이블 오브젝트로 만들 수 있다. 이때 사용되는 것이 `Symbol.iterator`이다. 이터러블 오브젝트로 만들려면 `Symbol.iterator`와 value, done가 들어있는 오브젝트를 반환하는 next 메서드를 가진 객체를 반환하기만 하면 된다. 일반 Array와 이터러블 오브젝트를 비교해 보자. ``` javascript const arr = [1, 2, 3]; const iterableObj = arr[Symbol.iterator](); ``` `arr`에는 일반적인 배열을 할당하였고 `iterableObj`에는 `Symbol.iterator`를 호출하여 이터레이터 오브젝트를 생성하였다. 위 두개의 차이점은 console로 찍어보면 확연히 차이가 난다. 우선 arr를 찍으면 아래와 같이 prototype이 Array로 나온다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1584363888851-image.png) iterableObj는 Array Iterator라는 이터레이터 오브젝트가 뜬다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1584363928236-image.png) 이번에는 좀 더 실용적으로 활용해 보도록 하겠다. 마라톤 대회에서 순서대로 우승한 사람들을 Class를 통해 기록하도록 만들었다. 이 Class를 루프를 돌아서 순위를 보고 싶은데 Class는 루프를 돌 수 없어 순위를 찍을 수가 없다. Array에 다시 집어넣거나 값에 직접 접근해야 할까? 이럴때 활용 할 수 있는 것이 이터레이션 프로토콜이다. 우선 `Symbol.iterrator` 메서드를 추가하고 `{ value, done }`을 리턴하는 `next` 메서드를 리턴하도록 만들면 된다. 랭킹을 기록하는 Class는 아래와 같다. ``` javascript class Rank { constructor() { this.ranking = []; } add(winner) { const rank = this.ranking.length + 1; this.ranking.push({ winner, rank }); } [Symbol.iterator]() { return this.ranking.values(); } } ``` 이 Rank Class를 활용한 예는 아래와 같다. ``` javascript const marathonRanking = new Rank(); marathonRanking.add("John"); marathonRanking.add("Lion"); marathonRanking.add("Gill"); marathonRanking.add("Muzi"); for (let ranking of marathonRanking) { console.log(`${ranking.rank}st winner is ${ranking.winner}`); } ``` 해당 콘솔을 찍어보면 순서대로 돌면서 우승자를 찍어준다. ``` shell 1st winner is John 2st winner is Lion 3st winner is Gill 4st winner is Muzi ``` 이 외에도 로그를 저장하고 순서대로 보거나 재귀를 활용하는 등 데이터를 다루는 곳에서는 이터레이터는 매우 유용하게 쓰일 수 있다. ### 제너레이터 다양한 곳에서 이터레이터를 사용할 수 있지만 아마도 이터레이터를 가장 효율적으로 사용하는 방식이 바로 **제너레이터**일 것이다. 제너레이터는 이터레이터를 사용해 실행을 제어하는 함수이다. 일반적인 함수는 호출하고 나면 제어권을 함수에게 빼앗긴다. 함수가 종료될 때까지 해당 함수를 핸들링 할 수 있는 방법이 없단 말이다. 그러나 제너레이터 함수를 이용하면 함수의 실행을 조절할 수 있다. 일시정지하거나 다시 시작하거나 하는 등 함수의 실행을 제어할 수 있는 것이다! 마치 파이프 라인 같이 단계가 나뉘어 진다고 보면 된다. 제너레이터 함수는 일반 함수와는 두가지 다른 점이 있다. 첫째 `function` 키워드 뒤에 `*`를 붙여준다. 둘째 `return` 외에도 `yield`를 사용할 수 있다. 간단한 카운트 다운 제너레이터 함수를 보자. ``` javascript function* countDown() { yield 5; yield 4; yield 3; yield 2; yield 1; } const it = countDown(); ``` `console.log`를 찍어보면 아래와 같은 결과가 나온다. ``` javascript console.log(it.next()); // { value: 5, done: false } console.log(it.next()); // { value: 4, done: false } console.log(it.next()); // { value: 3, done: false } console.log(it.next()); // { value: 2, done: false } console.log(it.next()); // { value: 1, done: false } console.log(it.next()); // { value: undefined, done: true } ``` 함수의 실행이 순차적으로 진행되었다. 물론 next를 호출하지 않으면 실행 중간에 멈추게 된다. 우리는 위에서 이터레이터를 보면서 어떻게 이렇게 동작 할 수 있는지 보았다. 그렇다! 제너레이터 함수는 **제너레이터 오브젝트를 생성**하는 함수로서 위의 예에서는 `countDown` 함수는 **제너레이터 함수** 이며 `it`은 **제너레이터 오브젝트**이다. 제너레이터 함수를 선언하는 방법은 3가지가 있다. - function* 선언문 - function* 표현식 - GeneratorFunction 선언문은 `countDown`과 같이 생성하는 것이고 표현식은 일반 함수와 같다. ``` javascript // Generator 표현식 const count = function*() { yield 1; } const it = count(); ``` GeneratorFunction 으로 생성하는 방식은 일반적인 방식은 아니므로 생략하도록 하겠다. new 연산자로 직접 생성은 불가능 하나 빈 제너레이터 함수를 생성하여 contructor를 통해 생성하는 방식이나 이런 방식이 있다 정도로만 이해하고 실무에서는 위 두가지 정도로 선언하면 충분하다. 제너레이터 함수는 화살표 함수로는 선언이 안되며 반드시 위 3가지로만 선언하여야 한다. vscode에서 제너레이터 오브젝트에 메서드를 확인하면 아래와 같다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1584365768647-image.png) 제너레이터 오브젝트가 가지고 있는 위 3가지 메서드와 `yield` 키워드에 대해 더 알아보도록 하겠다. ### yield `yield`는 제너레이터를 멈추거나 다시 실행하는데 사용된다. `yield`는 표현식(expression)으로도 작성할 수도 있는데 표현식을 작성할 경우 왼쪽에 결과값을 할당한다. ``` javascript const job = yield "What do you do?"; ``` 표현식을 작성하지 않을 경우 undefined를 반환하며 할당하는 순간은 `next()`를 호출하여 전달한 파라미터 값이 할당된다. 이러한 방식을 통해 함수가 실행 되고 있는 도중에 외부와 소통이 가능하다. 이 부분은 `next` 메서드와 같이 더 알아보도록 하겠다. 제너레이터 함수에서는 `yield` 키워드가 있을때 까지 실행되므로 `yield` 이전에 있는 평가문들의 실행을 멈출수는 없다. ``` javascript function* catchMe() { console.log("You can't catch me"); console.log("I'm working"); console.log("Can you catch me?"); yield "I catch you"; console.log("I'm not working"); } const it = catchMe(); it.next(); ``` 위의 예제에서 `console`을 확인해 보면 `yield` 키워드가 위치한 이전까지 실행된 것을 확인할 수 있다. `yield` 이외에도 `yield*` 키워드도 있는데, `yield`를 일괄 처리해준다고 생각하면 된다. ``` javascript function* generateAll() { yield* ["Call", "Me"]; } const it = generateAll(); console.log(it.next()); // { value: 'Call', done: false } console.log(it.next()); // { value: 'Me', done: false } ``` ### next 이터레이터에서 활용된 next 를 제너레이터에서 동일한 개념으로 사용할 수 있다. 한가지 달라진 점은 `next`를 통해 값을 제너레이터로 전달할 수 있다는 점이다. ``` javascript function* call() { const phoneNumber = yield "What is your phone number?"; return `I'm calling to ${phoneNumber}`; } const it = call(); console.log(it.next()); console.log(it.next("123-12345")); ``` 해당 함수를 실행하면 아래와 같이 `console`로 찍힌다. ``` shell { value: 'What is your phone number?', done: false } { value: "I'm calling to 123-12345", done: true } ``` 위의 실행을 단계별로 보자면, - 첫째, it 변수에 call 제너레이터 함수를 실행하면 이터레이터를 반환 하고 멈춤 상태가 된다. - 둘째, `next`를 호출하고 파라미터로 `undefined`를 넘겨주면 제너레이터는 "What is your phone number?"을 넘기고 멈춤 상태가 된다. - 셋째, `next`를 호출하고 파라미터로 "123-12345"를 넘겨주면 제너레이터는 해당 값을 할당 후 `return`한다. 여기서 알 수 있는 것은 `next` 다음에 전달한 값을 변수에 할당하고 그 다음 순서로 실행한다는 점이다. 그 이유는 처음 `next`를 호출 할 경우 첫 행을 실행하지만 `yield`가 있으므로 다시 제어권을 넘겨주기 때문이다. 첫 행을 완료하기 위해선 다시 `next`를 호출 해주어야 하며 이때 전달된 값을 할당한다. ### return `yield` 문은 `return`을 사용하지 않으면 제너레이터를 끝내지 않는다. 그 말은 `return`을 사용할 경우 `yield` 문이 남아있더라도 제너레이터를 종료하고 값을 반환한다는 뜻이다. ``` javascript function* finish() { const a = yield; const b = yield a; const c = yield b; return c; } const it = finish(); console.log(it.next()); console.log(it.next(10)); console.log(it.return(30)); console.log(it.next(20)); ``` 해당 결과는 아래와 같다. ``` shell { value: undefined, done: false } { value: 10, done: false } { value: 30, done: true } { value: undefined, done: true } ``` 만약 `return` 메서드를 활용하지 않고 `next`를 사용할 경우는 아래와 같은 결과가 나온다. ``` javascript console.log(it.next()); // { value: undefined, done: false } console.log(it.next(10)); // { value: 10, done: false } console.log(it.next(30)); // { value: 30, done: false } console.log(it.next(20)); // { value: 20, done: false } ``` ### throw throw는 return과 마찬가지로 실행 즉시 종료 시키며 다른 점은 Error를 발생시킨다는 점이다. ``` javascript function* warning() { try { yield 10; yield 20; yield 30; } catch (error) { console.error(error); } } const it = warning(); console.log(it.next()); // { value: 10, done: false } console.log(it.throw("Whoops..")); // Whoops.. console.log(it.next()); // { value: undefined, done: true } console.log(it.next()); // { value: undefined, done: true } ``` 제너레이터를 사용할때 `try...catch`문으로 에러를 잘 핸들링 하는 것이 중요하다. 여기까지가 제너레이터와 이터레이터에 대한 기본 개념과 기초적인 예제이다. 제너레이터는 이러한 기본 개념을 바탕으로 다양한 패턴들을 만들 수 있으며 기존 라이브러리 중에서 제너레이터를 가장 잘 활용하는 것 중 하나가 `Redux-Saga`가 아닐까 싶다. 이런 개념들을 바탕으로 Saga를 쓰다보면 해당 이펙트 들이 무슨 역활을 하고 어떻게 동작하며 어떤 조합으로 사용 할 수 있는지 한 눈에 들어와 활용도를 좀 더 높일 수 있다.