Leo Jeong
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 }