자바스크립트 제너레이터의 재미

Jooyung Han (한주영)
5 min readDec 1, 2016

--

JavaScript의 Generator는 재미있는 녀석이다. 함수이면서 함수와는 다른 동작 방식 때문에 잘만 사용한다면 의외로 쓸모가 많을 녀석이다.

먼저 일반적인 Generator부터 살펴보자. 제너레이터는 쉽게 말해 Iterator의 반대쪽이다. 어떤 의미의 반대쪽이냐면 Iterator가 값을 읽어오기 위한 인터페이스라면 Generator는 값을 쓰기 위한 인터페이스란 측면 때문이다.

function* foo() {
yield 1
yield 2
yield 3
}
// foo()로 생성된 제너레이터를 순회하며 값을 읽어간다.
for (let i of foo()) {
console.log(i)
}

JavaScript의 제너레이터는 function* 으로 정의된 제너레이터 함수를 호출하여 얻어지는 객체를 의미한다. 이 객체는 이터러블(iterable)이면서 이터레이터(iterator)이기도 하다. (for..of 에 사용할 수도 있으면서 직접 next()를 호출할 수도 있다)

제너레이터가 이터레이터로 동작한다는 특징 덕분에 복잡한 자료구조의 이터레이터를 좀더 쉽게 만들 수 있다. 예를 들어 이진트리를 inorder로 순환하는 이터레이터를 제너레이터로 구현해보자.

function* inorder(t) {
if (!t) return
yield* inorder(t.left)
yield t
yield* inorder(t.right)
}

그럼 제너레이터가 왜 재미있는 녀석인지 조금 더 들여다보자.

제너레이터 함수는 동작이 특이하다. 일반적인 함수와 다르다. 호출하면 제너레이터 객체만 생성하고 반환한다. 제너레이터 객체의 next()가 호출되면(for..of에 사용된 경우도 마찬가지) 그제서야 실행을 시작하여 처음 만나는 yield 에서 호출자에 값을 전달하면서 실행을 멈춘다(pause/suspend). 다시 next()가 호출되면 아까 멈춘 위치에서 실행을 시작(resume)하여 다음 yield까지 실행되고 또 멈춘다. Java의 멀티쓰레딩 환경이라면 모를까 함수가 실행 중에 멈췄다가 그 위치에서 실행을 계속하다니…

게다가 일반적인 이터레이터의 next() 와 다르게 제너레이터 객체의 next()는 인자를 받을 수도 있다. next() 로 전달한 인자는 제너레이터 함수가 yield 문의 결과로 받아갈 수 있다.

function* foo() {
console.log(yield)
console.log(yield)
console.log(yield)
}
let g = foo()
g.next() // start generator
g.next(1)
g.next(2)
g.next(3)

처음 본 예제와 달리 데이터가 흘러가는 방향이 바뀌었다!

여기서 잠깐! 제너레이터 함수가 값을 쓰고 제너레이터 객체의 이터레이터 인터페이스로 값을 읽는 것뿐만 아니라, 제너레이터 객체에 값을 쓰고 제너레이터 함수가 값을 읽어갈 수도 있다고?

제너레이터의 이런 특성은 쓰레드를 사용하지 않으면서 자바스크립트에서 동시성 프로그래밍을 가능하게 해준다. 이른바 “협력적 멀티태스킹(cooperative multitasking)"!

아래 코드는 producer가 100 ms마다 값을 쓰고, consumer가 값을 읽으면서 출력하는 코드다.

go(function* producer() {
for (let i=0; i<10; i++) {
yield write(i)
yield sleep(100) // -- sleep이 가능해진다!
}
})
go(function* consumer() {
let v
while (typeof (v = yield read()) !== "undefined") {
console.log("read:", v)
}
})

go() 라는 실행 함수를 통해 제너레이터가 마치 별도의 쓰레드에서 실행되는 것처럼 보인다. 두 제너레이터 함수는 입출력을 공유하고 있는데, Channel 객체를 도입한다면 Go나 Clojure의 CSP(Communicating Sequential Processes)를 구현한 js-csp 라이브러리와 같아진다.[소개글] (go, sleep, read, write를 직접 구현해보시길!)

아쉬운 점도 있다. 자바스크립트의 다른 기능들이 이터러블/이터레이터에 대한 지원이 미비하다보니 제너레이터의 쓰임새가 제한된다는 점이다. 예를 들어 Math.min 같은 간단한 유틸리티 함수도 가변 길이 인자를 지원할 뿐 이터러블을 입력으로 받지 않는다. ... 같은 펼침 연산자를 사용하면 되지만 제너레이터의 효율성을 유지하지 못한다. (제너레이터로부터 모든 값을 얻어내 배열 인자로 매핑되어야 한다)

let min = Math.min(...foo())

“개미 수열을 푸는 10가지 방법”에는 제너레이터를 이용하여 개미 수열을 구현한 예가 있다.

--

--

Jooyung Han (한주영)

가끔 함수형 프로그래밍 관련 글을 쓰거나 번역합니다. “개미 수열을 푸는 10가지 방법"이란 책을 썼습니다. https://leanpub.com/programming-look-and-say