JS: Generator
redux saga 를 공부하다보니 Generator 를 공부해야 할 필요성을 느껴서, 글을 쓰기로 했다.
Generator Function
Generator func 는 일반적인 함수와 비슷하다. 이 함수는 generator 를 리턴한다. 선언 문법은 다음과 같다.
function* someGenerator() {
//do something
}
으레 함수를 선언하듯 선언하면 되는데, 중요한 \점은 function 선언자 뒤에 * 를 찍어줘야 한다. 이 함수는 일반적인 함수와 거의 똑같이 작동한다. 다만 실행 시에 약간의 차이점이 있다.
function* someFunc() {
console.log("hello, world");
}
const func = someFunc();
func.next(); //hello, world
Generator function 은 그 자체로 실행할 수 있는 함수는 아니다. 우선 generator function 을 실행시켜서 generator 를 받아내고, 해당 generator 를 실행시켜야 비로소 우리가 선언한 코드가 작동한다. 위의 코드에서는 someFunc 로 generator function 을 선언하고, func 에 generator 를 받아서 실행시켰다. next() 를 사용한 것에 유의하자.
yield
generator 안에서 쓸 수 있는 재밌는 키워드가 있는데, 바로 yield 이다. yield 는 일종의 breakpoint 처럼 생각하면 된다. generator 는 yield 키워드를 만나면 해당 지점에서 실행을 멈추고, expression 의 리턴값을 반환한다.
function* gen() {
yield 1;
yield 2;
}
const gengen = gen();
gengen.next(); // 1
gengen.next(); // 2
Line 2 에서 yield 1 을 실행했다. generator 는 1을 반환하고 실행을 멈춘다. 해당 지점이 기억되고 있기 때문에, 이후에 다시 gengen.next() 를 실행하면 그 다음 구문이 실행된다. 이것이 generator 의 기본적인 사용법이다. yield 를 만나서 멈춘 generator 를 다시 실행시키고 싶다면 next 를 쓰면 된다.
return, throw
일반적인 함수처럼, generator 도 return 과 throw 를 쓸 수 있다. 다만 generator 함수 안에서 쓰는 return과 밖에서 쓰는 return 을 구분해야 한다. throw 도 마찬가지.
안에서 쓸 때
return 은 yield 와 거의 비슷한데, done 을 true 로 설정한다는 점이 다르다. done 이 true 로 설정되면 generator 는 더 이상 돌지 않는다. 마지막으로 반환된 IteratorResult 객체를 몇번을 호출하든 계속 반환한다.
throw 는 알고 있듯 에러를 만들때 쓴다. 큰 차이점은 없다.
밖에서 쓸 때
밖에서 쓸 때는 조금 다르다. generator.return(arg) 로 호출이 되면 value arg, done true 의 IteratorResult 가 반환된다. 다만 특이하게도 return 하는 시점에서 generator 가 이전에 실행한 yield 가 try / finally 블럭 안에 있다면, generator는 종료되지 않고 finally 블럭 안쪽의 구문을 실행한다.
next
next 는 위에서 보았듯, generator 를 실행시킬 때 쓴다. generator 는 가장 최근에 만난 yield 의 지점을 기억하고 있다가, next 로 실행되면 그 다음 구문부터 작업을 재개한다. 이때 next 에 인자를 넣어줄 수 있는데, 이 인자는 이 전에 불렸던 yield 의 반환값이 된다. 다음을 보자.
function* gen() {
while (true) {
var value = yield null;
console.log(value);
}
}
var g = gen();
g.next(1);
// { value: null, done: false }
g.next(2);
// { value: null, done: false }
// 2
위의 코드는 MDN 의 Generator.prototype.next 문서에 있는 예시 코드이다. 하나씩 보자.
우선, g.next(1) 을 통해 generator 를 호출했다. 이때 generator 는 yield null 구문을 만나고, 거기서 멈춘다. yield 뒤의 expression 의 리턴 값이 null 이었기 때문에 next 는 value null, done false 의 IteratorResult 객체를 반환한다. 여기서 next 에 인자로 전달된 1 은 아무런 의미가 없다. generator 가 기억하는 가장 최근의 yield (내 표현대로 하자면 breakpoint) 가 없기 때문이다.
다음 g.next(2) 를 통해 다시 한 번 generator 를 호출해 보았다. 여기서부터 next 에 전달한 인자가 의미를 갖기 시작한다. 우선, 가장 최근의 yield 는 Line 3 의 yield 이다. next 로 숫자 2가 전달되었기 때문에, 가장 최근의 yield 의 리턴값은 2 가 된다. 여기서 yield 의 반환값이 변수 value 에 할당되었으므로, value 의 값은 2가 된다. 그 다음 console.log 를 만났으므로 value 2 를 반환하고, while loop 를 돌다 다시 yield 를 만난다. 여기서 yield 의 표현식 반환값이 null 이었으므로 결과적으로 g.next(2) 는 value null, done false 의 IteratorResult 객체를 반환한다.
내가 가장 헷갈렸던 부분이라 좀 자세하게 썼다. 만약 잘 이해가 되었다면 다음 코드의 결과를 예측해보자.
function* gen() {
let a = 1;
yield a;
const b = yield (a = 2);
yield a;
yield b;
}
const gengen = gen();
gengen.next(10);
gengen.next(20);
gengen.next(30);
gengen.next(40);
gengen.next(50);
정답은 아래와 같다.
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 2, done: false }
{ value: 30, done: false }
{ value: undefined, done: true }