서론
자바스크립트는 기본적으로 실행 컨텍스트를 기반으로 하여 함수 제어권을 현재 실행 컨텍스트 스택의 최상단에 위치한 함수에 제어권을 부여합니다. 하지만 마치 무전기처럼 외부함수와 티키타카로 정보를 주고받으며 실행할 수 있는 방법이 존재합니다. 그 방법이 바로 generator
입니다. 지금부터 이 제네레이터에 대해서 알아보도록 하겠습니다.
Generator 란?
제네레이터는 ES6에서 도입된 함수의 형태입니다. 간단하게 말하자면 ‘코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 함수’입니다.
일반 함수와의 차이점으로는 다음과 같습니다.
- 제네레이터 함수는 함수 ‘호출자’에게 함수 실행의 제어권을 양도할 수 있습니다. 일반 함수는 호출 시 제어권이 호출된 함수에게 넘어가고 함수 코드를 멈추지 않고 일괄 실행합니다. 이는 호출자가 함수 호출이후 함수 실행을 제어할 수 없다는 것을 의미합니다. 이에 반해 제네레이터 함수는 함수 실행을 함수 호출자가 제어할 수 있습니다. 함수 제어권을 독점의 개념이 아닌 호출자에게도 양도(yield)할 수 있는 것입니다.
- 제네레이터 함수는 호출자와 함수의 ‘상태’를 주고 받을 수 있습니다. 일반 함수를 호출하면 매개변수를 통해 함수 외부에서 값을 전달받고 함수 코드를 일괄 실행한 뒤 결과값을 함수 외부로 반환(return)합니다. 호출자가 함수 외부에서 함수 내부로 값을 전달하여 함수의 상태를 변경할 수 없는 것입니다. 하지만 제네레이터 함수는 호출자와 양방향으로 함수의 상태를 주고받을 수 있습니다.
- 제네레이터 함수는 반환 값으로 제네레이터 객체를 가집니다. 일반 함수는 호출 시 명시된 return 값을 가지는 데 반해 제네레이터는 함수 호출 시 함수 코드를 실행하는 것이 아니라 이터러블이자 이터레이터인, 즉 순회가능한 객체인 제네레이터 객체를 반환합니다.
제네레이터 함수 정의 방법
제네레이터 함수의 정의 방법은 간단합니다. 함수 선언 키워드에 애스터리스크*
를 넣어주면 됩니다. 또한 하나 이상의 yield
표현식을 포함해야 합니다. 이를 제외하면 일반 함수 정의방법과 동일합니다.
function* generatorFunc() {
yield 'generator';
}
const generatorFunc = function* () {
yield 'generator';
}
const obj = {
* generatorFunc() {
yield 'generator';
}
}
여기서 애스터리스크*
의 위치는 function 키워드와 함수 이름 사이라면 어디든 상관없습니다. 일관성만 유지하면 되는 것이지요. 다만 화살표 함수로 정의할 수 없고 new
연산자와 함께 생성자 함수로 호출 불가하다는 점만 기억하면 될 것 같습니다.
제네레이터 객체
앞서 제네레이터 함수는 호출 시 코드 블록을 실행하는 것이 아니라 제네레이터 객체를 생성하여 반환한다고 언급하였습니다. 언급하면서 이터러블이자 이터레이터인 객체라고 말하였는데 이를 달리 말하면 next
메서드를 소유하는 객체임을 의미합니다. 다만 제네레이터 객체는 여기서 더 나아가 이터레이터에는 없는 return
, throw
메서드를 가집니다. 이 장에서는 제네레이터 객체가 가지는 메서드들에 대해서 소개하겠습니다.
우선 next
메서드는 호출 시 제네레이터 함수의 yield
표현식까지 코드 블록을 실행합니다. 여기서 yield
된 값, 즉 표현식 뒤로 오는 값을 value 프로퍼티로, false를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환합니다.
return
메서드는 인수로 전달 받은 값을 value 프로퍼티로, true를 done 프로퍼티 값으로 가지는 이터레이터 리절트 객체를 반환합니다.
throw
는 보통의 throw
와 비슷하게 인수로 전달받은 에러를 발생시킵니다. 다른 점으로는 undefined를 value 프로퍼티로 true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다는 것입니다.
일시 중지와 재게
next
메서드에 대해서 조금 더 자세하게 알아보겠습니다. 제네레이터 함수는 yield
키워드와 next
메서드를 통해 실행을 일시 중지했다가 필요해졌을 때 다시 재개할 수 있습니다. 제네레이터 함수는 호출 시 코드 블럭을 실행하는 것이 아니라 제네레이터 객체를 반환한다고 했었지요. 함수 호출자가 next
메서드를 실행하면 그제서야 제네레이터 함수의 코드 블럭을 실행합니다. 단 여기서도 주의해야 할 점은 코드 블럭을 전부 실행하는 것이 아니라 yield
표현식까지 실행되고 일시 중지 된다는 점입니다.
yield
표현식을 만나 일시 중지된 뒤에는 함수 제어권이 다시 호출자로 양도됩니다. 이후 필요 시점에 다시 next
메서드를 호출하면 일시 중지됐던 부분부터 제네레이터 함수가 실행되는 것입니다. 제네레이터 함수가 전부 실행되었는지는 제네레이터 객체가 반환하는 이터레이터 리절트 객체의 done 프로퍼티를 통해 알 수 있습니다. 한마디로 generator.next()
→ yield
→ generator.next()
→ yield
→ … → generator.next()
→ return
의 사이클을 도는 것입니다.
또 중요한 점은 이터레이터의 next
메서드와 달리 제네레이터 객체의 next
메서드에는 인수를 전달할 수 있습니다. 제네레이터 객체의 next
메서드에 전달한 인수는 제네레이터 함수의 yield
표현식을 할당받는 변수에 할당합니다. yield
표현식을 할당받는 변수에 yield
표현식의 평가 결과가 할당되지 않음에 유의해야 합니다.
const generatorFunc = function* () {
const first = yield 1;
const second = yield first + 2;
return first + second;
};
const generator = generatorFunc(0); // 여기서 전달된 인수 0은 아무 의미 없습니다.
let res = generator.next();
console.log(res); // {value: 1, done: false} 2번째 줄 yield 1 에서 전달된 값과 아직 끝나지 않음을 의미하는 done: false 인 이터레이터 객체가 반환되었습니다.
res = generator.next(10); // 여기서 전달된 10은 first에 할당됩니다.
console.log(res); // {value: 12, done: false} first에 10이 할당되어 (10 + 2)가 반환됩니다.
res = generator.next(100); // 여기서 전달된 100은 second에 할당됩니다.
console.log(res); // {value: 112, done: true} (100 + 12)인 값과 함께 제네레이터 함수 블록이 모두 실행되었음을 의미하는 done: true 값을 가진 이터레이터 객체가 반환되었습니다.
결론
지금까지 함수를 일시 중지시켰다가 재개할 수 있으며, 호출자와 양방향 통신이 가능한 제네레이터 함수에 대해 알아봤습니다. 문득 보면 ‘이걸 어디에 쓸 수 있을까?’ 하는 의문이 들 수도 있습니다. 하지만 다음번에 설명드릴 async/await
의 원리에 대해서 설명드릴 때 이 제네레이터 함수는 뗄래야 뗄 수 없는 관계였기 때문에 제네레이터 객체를 먼저 설명드렸습니다. 또한 일시 중지와 재게 장에서 예시로 보여드린 코드 또한 후에 async/await
를 설명 드릴 때 좀 더 직관적으로 다가올 것이라 예상합니다. 지금까지 글 읽어주셔서 감사하며 다음에 async/await
로 다시 찾아뵙겠습니다!