이 카테고리는 Modern JavaScript Deep Dive 책을 공부하면서 정리한 글을 올리는 영역입니다.
46.1 제너레이터란?
생성기는 코드 블록의 실행을 일시 중지하고 필요할 때 다시 시작할 수 있는 특수 함수입니다.
- 제너레이터 함수는 함수 실행 제어를 함수 호출자에게 양도할 수 있습니다.
- 일반 함수를 호출할 때 함수 호출자는 호출 후 함수 실행을 제어할 수 없습니다.
- 생성기 함수를 사용하면 함수 호출자가 함수 실행을 일시 중지하거나 다시 시작할 수 있습니다. 이는 함수에 의해 독점되는 대신 함수 제어가 함수 호출자에게 위임될 수 있음을 의미합니다.
- 제너레이터 함수는 호출자로부터 함수 상태를 보내고 받을 수 있습니다.
- 일반 함수가 호출되면 매개변수를 통해 함수 외부에서 값이 삽입되고 함수 코드가 일괄적으로 실행되며 그 결과 값이 함수 외부로 반환됩니다. 즉, 함수가 실행되는 동안 함수 외부에서 함수 내부의 함수로 값을 전달하여 함수의 상태를 변경할 수 없습니다.
- 생성기 함수는 함수 호출자를 사용하여 양방향으로 함수 상태를 보내고 받을 수 있습니다. 생성기 함수는 함수 호출자와 상태를 전달할 수 있습니다.
- 제너레이터 함수를 호출하면 제너레이터 객체가 반환됩니다.
- 일반 함수를 호출하면 함수 코드가 일괄적으로 실행되고 값이 반환됩니다.
- 제너레이터 함수를 호출하면 함수 코드가 실행되지 않지만 반복 가능하고 이터레이터인 제너레이터 객체가 반환됩니다.
46.2 생성기 함수의 정의
제너레이터 함수는 function* 키워드로 선언됩니다. 그리고 하나 이상의 yield 표현식을 포함합니다.
// 제너레이터 함수 선언문
function* genDecFunc() {
yield 1;
}
// 제너레이터 함수 표현식
const getExpFunc = function* () {
yield 1;
};
// 제너레이터 메서드
const obj = {
* genObjMethod() {
yield 1;
}
};
// 제너레이터 클래스 메서드
class MyClass {
* genmClsMethod() {
yield 1;
}
}
별표
function 키워드와 함수 이름 사이 아무 곳에나 배치할 수 있습니다. 그러나 일관성을 위해 function 키워드 바로 뒤에 배치하는 것이 좋습니다.
생성기 함수는 화살표 함수로 정의할 수 없으며 new 연산자를 사용하여 생성자 함수로 호출할 수 없습니다.
46.3 제너레이터 객체
제너레이터 함수를 호출하면 일반 함수처럼 함수 코드 블록을 실행하는 대신 제너레이터 객체를 생성하고 반환합니다.
제너레이터 함수가 반환하는 제너레이터 객체는 이터러블이자 이터레이터입니다.
// 제너레이터 함수
function* getFunc() {
yield 1;
yield 2;
yield 3;
)
// 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
const generator = getFunc();
// 제너레이터 객체는 이터러블이면서 동시에 이터레이터다.
// 이터러블은 Symbol.iterator 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속받는 객체다.
console.log(Symbol.iterator in generator); // true
// 이터레이터는 next 메서드를 갖는다.
console.log('next' in generator); // true
제너레이터 객체는 Symbol.iterator 메서드와 value 및 done 속성이 있는 반복자 결과 객체를 반환하는 next 메서드가 있는 반복자를 상속하는 반복 가능 객체입니다.
next 메서드를 호출하면 제너레이터 함수의 yield 표현식까지 코드 블록을 실행하고, value 속성 값으로 yieldd 값을, done 속성 값으로 false 를 가지는 iterator 결과 객체를 반환합니다.
return 메서드를 호출하면 value 속성 값으로 인수로 전달된 값과 done 속성 값으로 true를 포함하는 반복기 결과 개체가 반환됩니다.
function* getFunc() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.error(e);
}
}
const generator = getFunc();
console.log(generator.next()); // {value: 1, done: false}
console.log(generator.return('End!')); // {value: "End!", done: true}
console.log(generator.throw('Error!')); / {value: undefined, done: true}
throw 메서드가 호출되면 인수로 전달된 오류가 발생하고 반복자 결과 객체가 반환됩니다. undefined는 value 속성 값이고 true는 done 속성 값입니다.
46.4 제너레이터 중지 및 재개
생성기는 yield 키워드와 next 메서드를 사용하여 원하는 시간에 중지 및 재개할 수 있습니다.
제너레이터 객체의 next 메서드를 호출하면 제너레이터 함수의 코드 블록이 실행됩니다.
그러나 일반 함수처럼 코드 블록의 모든 코드를 한 번에 실행하는 대신 yield 표현식만 실행합니다.
yield 키워드는 제너레이터 함수의 실행을 중단하거나 yield 키워드 이후의 표현식 평가 결과를 제너레이터 함수 호출자에게 반환합니다.
제너레이터 객체의 next 메서드를 호출하면 yield 표현식까지 실행한 후 멈춥니다. 이 시점에서 함수의 제어는 호출자에게 이전됩니다. 이 시점에서 next 메서드는 value 및 done 속성이 있는 반복자 결과 개체를 반환합니다.
yield 표현식에서 반환되는 값(yield 키워드 뒤의 값)은 next 메서드가 반환하는 반복자 결과 객체의 value 속성에 할당되고, 제너레이터 함수가 완료되었는지 여부를 나타내는 부울 값은 finished 속성에 할당됩니다.
function* genFunc() {
// 처음 next 메서드를 호출하면 첫 번째 yield 표현식까지 실행되고 일시 중지된다.
// 이때 yield된 값 1은 next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티에 할당된다.
// x 변수에는 아직 아무것도 할당되지 않았다. x 변수의 값은 next 메서드가 두 번째 호출될 때 결정된다.
const x = yield 1;
// 두 번째 next 메서드를 호출할 때 전달한 인수 10은 첫 번째 yield 표현식을 할당받는 x 변수에 할당된다.
// 즉, const x = yield 1은 두 번째 next 메서드를 호출했을 때 완료된다.
// 두 번째 next 메서드를 호출하면 두 번째 yield 표현식까지 실행되고 일시 중지된다.
// 이때 yield된 값 x + 10은 next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티에 할당된다.
const y = yield (x+10);
// 세 번째 next 메서드를 호출할 때 전달한 인수 20은 두 번째 yield 표현식을 할당받는 y 변수에 할당된다.
// 즉, const y = yield(x+10);는 세 번째 next 메서드를 호출했을 때 완료된다.
// 세 번째 next 메서드를 호출하면 함수 끝까지 실행된다.
// 이때 제너레이터 함수의 반환값 x + y는 next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티에 할당된다.
// 일반적으로 제너레이터의 반환값은 의미가 없다.
// 따라서 제너레이터에서는 값을 반환할 필요가 없고 return은 종료의 의미로만 사용해야 한다.
return x + y;
}
// 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
// 이터러블이며 동시에 이터레이터인 제너레이터 객체는 next 메서드를 갖는다.
const generator = genFunc(0);
// 처음 호출하는 next 메서드에는 인수를 전달하지 않는다.
// 만약 처음 호출하는 next 메서드에 인수를 전달하면 무시된다.
// next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티에는 첫 번째 yield된 값 1이 할당된다.
let res = generator.next();
console.log(res); // {value: 1, done: false}
// next 메서드에 인수로 전달한 10은 genFunc 함수의 x 변수에 할당된다.
// next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티에는 두 번째 yield된 값 20이 할당된다.
res = generator.next(10);
console.log(res); // {value: 20, done: false}
// next 메서드에 인수로 전달한 20은 genFunc 함수의 y 변수에 할당된다.
// next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티에는 제너레이터 함수의 반환값 30이 할당된다.
res = generator.next(20);
console.log(res); // {value:30, done:true}
생성기 개체의 next 메서드에 인수를 전달할 수 있습니다. 생성기 객체의 next 메서드에 전달된 인수는 생성기 함수의 yield 표현식을 받는 변수에 할당됩니다.
46.5 생성기 사용
// 무한 이터러블을 생성하는 제너레이터 함수
const infiniteFibonacci = (function* () {
let (pre, cur) = (0,1);
while(true) {
(pre, cur) = (cur, pre + cur);
yield cur;
}
}());
// infiniteFibonacci는 무한 이터러블이다.
for (const num of infiniteFibonacci) {
if(num > 10000) break;
console.log(num); // 1 2 3 5 8 ... 2584 4181 6765
}
반복 가능한 구현
비동기 처리
Generator 함수는 next 메서드와 yield 표현식을 통해 함수 호출자에게 함수의 상태를 반환할 수 있습니다.
// node-fetch는 Node.js 환경에서 window.fetch 함수를 사용하기 위한 패키지다.
// 브라우저 환경에서 이 예제를 실행한다면 아래 코드는 필요 없다.
// https://github.com/node-fetch/node-fetch
const fetch = require('node-fetch');
// 제너레이터 실행기
const async = generatorFunc => {
const generator = generatorFunc();
const onResolve = arg => {
const result = generator.next(arg);
return result.done
? result.value
: result.value.then(res => onResolved(res));
};
return onResolved;
};
(async(function* fetchTodo() {
const url="https://jsonplaceholder.typicode.com/todos/1";
const response = yield fetch(url);
const todo = yield response.json();
console.log(todo);
// {userId: 1, title: 'delectus aut autem', completed: false}
})());
이러한 속성을 사용하여 Promise의 후속 처리 방법 없이 비동기 처리 결과를 반환하도록 구현할 수 있습니다.
46.6 비동기/대기
ES8은 비동기 처리가 제너레이터보다 더 간단하고 읽기 쉬운 방식으로 동기 처리처럼 작동하도록 구현할 수 있는 async/await를 도입했습니다.
Async/await는 약속을 기반으로 작동합니다. async/await를 사용하면 약속의 then/catch/finally 후속 프로세스 메서드에 콜백 함수를 전달하여 비동기 프로세스를 추적할 필요 없이 동기 프로세스와 같은 약속을 사용할 수 있습니다.
즉, Promise의 후속 처리 방법 없이 동기적으로 처리된 것처럼 처리 결과를 반환하도록 Promise를 구현할 수 있습니다.
비동기 함수
await 키워드는 비동기 함수 내에서 사용해야 합니다. async 함수는 async 키워드로 정의되며 항상 약속을 반환합니다. 비동기 함수가 명시적으로 약속을 반환하지 않더라도 비동기 함수는 암시적으로 반환 값을 확인하는 약속을 반환합니다.
클래스 생성자 메서드는 비동기 메서드일 수 없습니다. 클래스의 생성자 메서드는 인스턴스를 반환해야 하지만 비동기 함수는 항상 약속을 반환해야 합니다.
대기 키워드
const fetch = require('node-fetch');
const getGithubUserName = async id => {
const res = await fetch(`https://api.github.com/users/${id}`);
const {name} = await res.json();
console.log(name); // Seonju Yoo
};
getGithubUserName('yousunzoo')
await 키워드는 약속이 충족될 때까지 기다렸다가 해결된 약속의 결과를 반환합니다. 약속 앞에 awiat 키워드를 사용해야 합니다.
가져오기 대기는 가져오기 기능의 HTTP 요청에 대한 서버 응답이 도착하고 가져오기 기능에서 반환된 약속이 완료될 때까지 기다립니다. 이후 Promise가 결정되면 Res 변수에 Promise 해결 결과를 대입합니다.
오류 처리
비동기 함수의 콜백 함수 호출은 비동기 함수가 아니므로 try … catch 문을 사용하여 오류를 잡을 수 없습니다.
const fetch = require('node-fetch');
const foo = async () => {
try {
const wrongUrl="https://wrong.url";
const response = await fetch(wrongUrl);
const data = await response.json();
console.log(data);
} catch (err) {
console.log(err); // TypeError: Failed to fetch
}
};
foo();
async/await의 오류 처리는 try … catch 문을 사용할 수 있습니다. 콜백 함수를 인자로 받는 비동기 함수와 달리 프라미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있어 호출자에게 명확해진다.
비동기 함수 내에서 catch 문을 사용하여 오류를 처리하지 않으면 비동기 함수는 발생한 오류를 거부하는 약속을 반환합니다. 따라서 비동기 함수를 호출하고 후속 메서드 Promise.prototype.catch로 오류를 잡을 수도 있습니다.
const fetch = require('node-fetch');
const foo = async () => {
cosnt wrongUrl="https://wrong.url";
const response = await fetch(wrongUrl);
const data = await response.json();
return data;
};
foo()
.then(console.log)
.catch(console.error); // TypeError: Failed to fetch
