제너레이터란?
제너레이터는 실행을 일시중지하고 재개할 수 있는 특징을 가진 함수이다. 일반 함수와 제너레이터의 차이는 다음과 같다
- 제너레이터 함수는 함수 호출자에게 함수 실행의 제어권을 양도할 수 있다.
- 함수 제어권을 함수가 독점하는 것이 아니라 호출자에게 양도
- 제너레이터 함수는 함수 호출자와 함수의 상태를 주고 받을 수 있다.
- 제너레이터함수는 함수 호출자에게 상태를 전달할 수 있고 함수 호출자로부터 상태를 전달 받을 수 도있다.
- 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
- 제너레이터 함수를 호출하면 함수 코드를 실행하는 것이 아니라 이터러블이면서 동시에 이터레이터인 제너레이터 객체 반환
제너레이터 함수의 정의
제너레이터 함수는 function* 키워드로 선언한다. 그리고 하나 이상의 yield 표현식을 포함한다.
이것을 제외하면 일반함수와 선언하는 방식이 같다.
function* genFunc() {
yield 1;
}
const genFunc = function* () {
yield 1;
}
const obj = {
* genObjMethod() {
yield 1;
}
}
class MyClass {
* genClsMethod() {
yield 1;
}
}
애스터리스크(*)의 위치는 function 키워드와 함수 이름 사이라면 이디든지 상관없다. 다음 예제의 제너레이터 함수는 모두 유효하다.
function * genFunction() { yield 1; }
function* genFunction() { yield 1; }
function *genFunction() { yield 1; }
function*genFunction() { yield 1; }
제너레이터는 화살표 함수로 선언이 불가하며, 생성자 함수로 사용할 수 없다.
const genArrowFunc = * () => {
yield 1;
}
// Syntax Error
const genFunc = * () => {
yield 1;
}
new genFunc();
// genFunc is not a constructor
제너레이터 객체
제너레이터 함수를 호출하면 코드 블록을 실행하는 것이 아니라 제너레이터 객체를 생성해서 반환한다. 이 객체는 이터러블이면서 이터레이터이다.
즉, 제너레이터 객체는 Symbol.iterator 메서드를 상속 받는 이터러블이면서 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환하는 next 메서드를 가진 이터레이터이다.
따라서 별로도 Symbol.iterator를 호출해서 이터레이터를 생성할 필요가 없다.
function* genFunc() {
yield ;1;
yield ;2;
yield ;3;
}
const generator = genFunc();
console.log(Symbol.iterator in generator); // true
console.log('next' in generator); // true
제너레이터 객체는 next 메서드를 갖는 이터레이터이지만 이터레이터에는 없는 return, throw 메서를 갖는다.
return 메서드를 호출하면 이수로 전달 받은 값을 value프로퍼티 값으로, true를 done 프로퍼티 값으로 가지는 리절트 객체를 반환한다.
function* genFunc() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.error(e);
}
}
const generator = genFunc();
console.log(generator.next()); // {value: 1, done: false}
console.log(generator.return('End!')); // {value: End, done: true}
throw 메서드를 호출하면 인수로 전달받은 에러를 발생시키고 undefined를 value 프로퍼티의 값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다.
function* genFunc() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.error(e);
}
}
const generator = genFunc();
console.log(generator.next()); // {value: 1, done: false}
console.log(generator.error('Error!')); // {value: undefined, done: true}
제너레이터 일시중지와 재개
제너레이터는 yield키워드와 next메서드를 통해 실행을 일시 중지했다가 필요한 시점에 다시 재개할 수 있다. 일반 함수는 호출된 이후 제어권을 독점하지만 제너레이터는 함수 호출자에게 제어권을 양도하여 필요하면 다시 실행을 재개한다.
제너레이터는 일반 함수처럼 코드를 일괄로 실행하는 것이 아니라 yield 키워드를 만날 때까지만 실행하고 일시 중지 시키고 yield 뒤에 오는 값을 value 프로퍼티에 담아 리절트 객체를 함수 호출자에게 반환한다.
function* genFunc() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.error(e);
}
}
const generator = genFunc();
next 메서드를 인자 전달과 함께 호출하면 전달된 인수를 yield 표현식에 할당된다.
function* genFunc() {
const x = yield 1;
consoel.log('x', x);
const y = yield (x + 10);
console.log('y', y);
console.log('x + y', x + y);
return x + y;
}
const generator = genFunc(); // next 메서드를 가진 이터레이터를 반환
// next를 호출하면 yield까지만 코드를 실행하고 yield 뒤에 오는 값을 반환 코드 실행을 중지한다.
console.log(generator.next()); // {value: 1, done: false}
console.log(generator.next(10)); // {value: 20, done: false}
console.log(generator.next(20)); // {value: 30, done: false}
// next를 계속 호출하다가 함수가 끝까지 실행되었으면 done 프로퍼티가 true가 된다.
console.log(generator.next()); // {value: undefined, done: true}
제너레이터 활용
이터러블 구현
제너레이터 함수를 사용하면 이테레이션 프로토콜을 준수해 이터러블을 생성하는 방식 보다 간단히 이터러블을 구현할 수 있다.
const infiniteFibonacci = (function() {
let [pre, cur] = [0, 1];
return {
next() {
[pre, cur] = [cur, pre + cur];
return {value: cur}
};
};
})();
for (const num of ininfiniteFibonacci) {
if (num > 10000) break;
console.log(num); // 1 2 3 5 8 ...6765
}
이번에는 제너레이터 함수 사용으로 위 방식보다 간단하게 구현해보자
const infiniteFibonacci = (function* () {
let [pre, cur] = [0, 1];
while (true) {
[pre, cur] = [curm pre + cur];
yield cur;
}
})();
for (const num of ininfiniteFibonacci) {
if (num > 10000) break;
console.log(num); // 1 2 3 5 8 ...6765
}
비동기 처리
제니레이터 함수는 next 메서드와 yield 표현식을 통해 함수 호출자와 함수의 상태를 주고 받을 수 있다. 이 특성을 활용하면 프로미스를 사용한 비동기 처리를 동기 처럼 구현할 수 있다.
즉, 프로미스의 후속 처리 메서드 then/catch/finally 비동기 처리 결과를 반환할 수 있다.
const fetch = require('node-fetch');
const async = generatorFunc = {
const generator = generatorFunc(); // 2
const onResolved = arg => {
const result = generator.next(arg); // 5
return result.done
? result.value
: result.value.then(res => onResolved(res)); // 7
}
return onResolved; // 3
}
(async(function* fetchTodo() { // 1
const url = '<https://examples.com>';
const response = yield fetch(url); // 6
const todo = yield responose.json(); // 8
console.log(todo);
})());
위 예제의 제너레이터 함수를 실행하는 제너레이터 실행기인 async 함수는 이해를 돕기 위해 간략화한 예제는 완전하지 않다. async/awiat를 사용하면 async와 같은 제너레이터 실행기를 사용할 필요가 없지만 필요하다면 co 라이브러리를 사용하는 것을 추천한다.
async/await
제너레이터를 활용하여 비동기 처리를 동기처리처럼 동작하도록 구현했지만 코드의 가독성이 나쁘다. 그래서 ES8부터 간단하게 비동기 처리를 동기 처리 동작 시킬 수 있는 async/await가 도입되었다.
async/await는 프로미스를 기반으로 동작한다. promise의 then/catch/finally 사용없이 동기 처리처럼 프로미스의 결과를 반환하도록 할 수 있다.
const fetch = require('node-fetch');
async function fetchTodo() {
const url = '<https://examples.com>';
const response = await fetch(url);
const todo = await responose.json();
console.log(todo);
}
async 함수
await 키워드는 반드시 async 함수 내에서 사용해야 하며, async함수는 반드시 async 키워드와 함께 선언된다. 또한 async 함수는 암묵적으로 반환값을 resolve하는 프로미스를 반환한다.
async function foo(n) {return n}
foo(1).then(v => console.log(v)); // 1;
const bar = function foo(n) {return n}
bar(1).then(v => console.log(v)); // 1;
const baz = async n => n;
baz(1).then(v => console.log(v)); // 1;
const obj = {
async foo(n) {return n}
}
obj.foo(1).then(v => console.log(v)); // 1;
class MyClass {
async bar(n) {return n}
}
const myClass = new MyClass();
myClass.foo(1).then(v => console.log(v)); // 1;
위 처럼 대부분의 함수 선언을 통해 async 함수 사용이 가능하지만 클래스의 constructor로는 사용이 불가하다.
생성자 함수는 항상 인스턴스를 반환해야 하지만, async함수는 프로미스를 반환한다.
await 키워드
await는 프로미스가 settled 상태(비동기 처리가 수행된 상태)까지 기다렸다가 settled 상태가 되면 프로미스가 resolve한 처리를 반환한다. await키워드는 반드시 프로미스 앞에 있어야 한다.
const fetch = require('node-fetch');
const getGithubUserName = async id => {
const res = await fetch('<https://api.github.com/users/${id}>'); // 1
const { name } = await res.json();
console.log(name);
}
getGithubUserName('ungmo2');
await 키워드는 프로미스가 settled 상태가 될 때까지 대기한다. 1의 fetch함수가 수행한 HTTP요청에 대한 서버 응답이 도착해서 반환한 프로미스가 settled상태가 될 때까지 1은 대기한다. 이후 프로미스가 settled 상태가 되면 프로미스가 resolve한 처리 결과가 res 변수에 할당된다. 이처럼 await는 다음 실행을 일시 중지했다가 프로미스가 settled 되면 다시 재개한다.
async function foo() {
const a = await new Promise(resolve => setTimeot(() => resolve(1), 3000));
const b = await new Promise(resolve => setTimeot(() => resolve(2), 2000));
const c = await new Promise(resolve => setTimeot(() => resolve(3), 1000));
}
foo(); // 6초 소요
await는 다음 프로미스가 settled될 때까지 실행을 일시 중지 하기 때문에 a,b,c는 순차적으로 결과를 받게된다. 따라서, 3초를 대기하다 재개하고 2초를 대기하다 재개하고 1초를 대기하다 c는 6초뒤에 결과를 받을 수 있는 것이다.
async function bar(n) {
const a = await new Promise(resolve => setTimeot(() => resolve(n), 3000));
const b = await new Promise(resolve => setTimeot(() => resolve(a + 1), 2000));
const c = await new Promise(resolve => setTimeot(() => resolve(b + 2), 1000));
}
bar(1);
위 예제를 실행해 보면 b를 a를 필요로하고, c는 b를 필요로 하는 코드이다. 이 코드를 실행해 보면 순차적으로 실행되는 걸 볼 수 있다.
에러 처리
비동기 처리를 위한 콜백 패턴의 단점 중 하나는 에러 처리가 곤란하다는 것이다. 에러는 호출자 방향으로 전파된다. 즉, 콜 스택의 아래 방향, 실행 중인 컨텍스트가 푸시되기 직전에 푸시된 실행 컨텍스트 방향으로 전파된다. 하지만 비동기 함수의 콜백 함수(다른 컨텍스트로 옮겨짐)를 호출하는 것은 비동기 함수가 아니기 때문에 try…catch문을 사용해 에러를 잡을 수 없다.
try {
setTimeout(() => { throw new Error('Error'); }, 1000);
} catch(e) {
console.error('캐치한 에러', e);
}
async/await에서 에러 처리는 try…catch문을 사용할 수 있다. 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확하다.
const fetch = require('node-fetch');
const foo = async () => {
try {
const wrongUrl = '<https://wrong.url>';
const res = await fetch(wrongUrl);
const data = await response.json();
} catch(err) {
console.log(err);
}
}
foo();
async 함수 내에서 catch문을 사용해서 에러 처리를 하지 않으면 async 함수는 발생한 에러를 reject하는 프로미스를 반환한다. 따라서 async함수를 호출하고 Promise.prototype.catch 후속 처리 메서드를 사용할 수도 있다.
const fetch = require('node-fetch');
const foo = async () => {
try {
const wrongUrl = '<https://wrong.url>';
const res = await fetch(wrongUrl);
const data = await response.json();
} catch(err) {
console.log(err);
}
}
foo()
.then(console.log)
.catch(console.error);
'Tech Books & Lectures > Javascript_Modern JS Deep dive' 카테고리의 다른 글
48. 모듈 (0) | 2023.02.25 |
---|---|
47. 에러 처리 (0) | 2023.02.25 |
45. 프로미스 (2) | 2023.02.25 |
44. REST API (0) | 2023.02.25 |
43. Ajax (0) | 2023.02.25 |