제너레이터란?제너레이터와 일반 함수의 차이제너레이터의 일시 중지와 재개이터레이터의 next 메서드와 달리 제너레이터 객체의 next 메서드에는 인수 전달이 가능함async/await ⇒ 콜백을 쓸 필요가 없게됨async 함수await 키워드에러 처리사용 예시
제너레이터란?
- ES6 에서 도입된 제너레이터는 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수임
제너레이터와 일반 함수의 차이
제너레이터 함수는 함수 호출자(caller
)에게 함수 실행의 제어권을 양도할 수 있다.
- 일반 함수는 호출하면 제어권이 함수에게 넘어가고 함수 코드를 일괄 실행함. 즉,
caller
는 함수를 호출한 이후 함수 실행 제어할 수 없음
- 제너레이터 함수는 함수 실행을 함수 호출자가 제어할 수 있음. 즉, 함수 호출자가 함수 실행을 일시 중지시키거나 재개시킬 수 있다
- 이는 함수의 제어권을 함수가 독점하는 것이 아니라
caller
에게yield
할 수 있다는 것을 의미함
제너레이터 함수는 함수 호출자(caller
)와 함수의 상태를 주고 받을 수 있다.
- 일반 함수를 호출하면 매개변수를 통해 함수 외부에서 값을 주입받고 함수 코드를 일괄 실행하여 결과값을 함수 외부로 반환함. 즉, 함수가 실행되고 있는 동안에는 함수 외부에서 함수 내부로 값을 전달하여 함수의 상태를 변경할 수 없다
- 제너레이터 함수는 함수 호출자(
caller
)에게 상태를 전달할 수 있고 함수 호출자(caller
)로부터 상태를 전달받을 수도 있다.
제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
- 일반 함수 호출 시, 함수 코드 일괄 실행 후 값을 반환함
- 제너레이터 함수를 호출하면 함수 코드를 실행하는 것이 아니라 이터러블이면서 동시에 이터레이터인 제너레이터 객체를 반환함
제너레이터의 일시 중지와 재개
- 제너레이터는
yield
키워드와next
메서드를 통해 실행을 일시 중지했다가 필요한 시점에 다시 재개할 수 있음
- 제네레이터 함수를 호출하면 제너레이터 함수의 코드 블록이 실행되는 것이 아니라 제너레이터 객체를 반환
- 그 후,
next
메서드가 불리면 다음yield
표현식 까지만 실행함 - 제너레이터 객체의
next
메서드를 호출하면yield
표현식까지 실행되고 일시 중지됨. 이 때 함수의 제어권이 호출자로 양도(yield
)됨 next
메서드는value
,done
프로퍼티를 갖는iterator result
객체를 반환함
이터레이터의 next 메서드와 달리 제너레이터 객체의 next 메서드에는 인수 전달이 가능함
- 제너레이터 객체의 next 메서드에 전달한 인수는 제너레이터 함수의 yield 표현식을 할당받는 변수에 할당됨
- yield 표현식을 할당받는 변수에 yield 표현식의 평과 결과가 할당되지 않는다는점 주의!
function* genFunc() { // 첫번째 next 호출시에는 x에는 아무 값도 할당 안됨 // yield 1 까지만 실행하고 중단되기 때문에 const x = yield 1; // 두 번째 next 호출 시 10을 인자로 넘겨주었고, 그 값이 x에 저장됨 // 그 후, yield (x + 10) - 20의 값이 반환 const y = yield (x + 10); // 세 번째 next 호출 시 20을 인자로 넘겨주었고, 그 값이 y에 저장 // 세 번쨰 next. 호출 하면 함수 끝까지 실행됨 // 일반적으로 제너레이터의 반환값은 의미가 없기에, 제너레이터에서는 값을 반환할 필요가 없고 // return은 종료의 의미로만 사용해야 함 return x + y; } const generator = genFunc(); let res = generator.next(); // 첫번째 next 호출 시, yield 1 까지만 실행됨 console.log(res); // {value : 1, done: false} res = generator.next(10); console.log(res); // {value : 20, done: false} res = generator.next(20); console.log(res); // {value : 30, done: true}
- 함수 호출자는 next 메서드를 통해 yield 표현식까지 함수를 실행시켜 제너레이터 객체가 관리하는 상태(yield된 값)를 꺼내올 수 있고
- next 메서드에 인수를 전달해서 제너레이터 객체에 상태(yield 표현식을 할당받는 변수)를 밀어넣을 수 있음
async/await ⇒ 콜백을 쓸 필요가 없게됨
- ES8에서는 제너레이터보다 간단하고 가독성 좋게 비동기 처리를 동기 처리처럼 동작하도록 구현할 수 있는 async/await가 도입되었음
- async/await 는 프로미스를 기반으로 동작함. async/await를 사용하면 프로미스의 후속 처리 메서드에 콜백 함수를 전달해서 비동기 처리 결과를 후속 처리할 필요 없이 마치 동기 처리처럼 프로미스를 사용할 수 있다
const fetch = require('node-fetch'); const co = require('co'); co(function* fetchTodo() { const url = 'https://jsonplaceholder.typicode.com/todos/1'; const response = yield fetch(url); const todo = yield response.json(); console.log(todo); });
const fetch = require('node-fetch'); async function fetchTodo() { const url = 'https://jsonplaceholder.typicode.com/todos/1'; const response = await fetch(url); const todo = await response.json(); console.log(todo); } fetchTodo();
async 함수
await
키워드는async
함수 내부에서 사용해야 함
async
함수는async
키워드를 사용해 정의하며 언제나 프로미스를 반환함
- 명시적으로 프로미스를 반환하지 않더라도
async
함수는 암묵적으로 반환값을resolve
하는 프로미스를 반환함
// async 함수 선언문 async function foo(n) { return n; } foo(1).then(v => console.log(v)); // async 함수 표현식 const bar = async function (n) { return n; }; bar(2).then(v => console.log(v));
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}`); const { name } = await res.json(); console.log(name); }; getGithubUserName('ungmo2');
에러 처리
- 비동기 처리를 위한 콜백 패턴의 단점 중 가장 심각한 것은 에러 처리가 곤란하다는 것임
- 비동기 함수의 콜백함수를 호출한 것이 비동기 함수가 아니기에, 에러 전파가 안됨
- 그러나 async/await는 try.. catch 문을 사용해서 에러 잡아낼 수 있음
async
함수 내에서catch
문을 사용해서 에러 처리를 하지 않으면async
함수는 발생한 에러를reject
하는 프로미스를 반환함
사용 예시
async function test1(){ console.log(`test1 start : ${Date.now()}`); return await new Promise(resolve => setTimeout(() => resolve(1), 2000)); console.log(`test1 end : ${Date.now()}`); }; async function test2(){ console.log(`test2 start : ${Date.now()}`); return await new Promise(resolve => setTimeout(() => resolve(2), 3000)); console.log(`test2 end : ${Date.now()}`); } function sleep(ms) { const wakeUpTime = Date.now() + ms; while (Date.now() < wakeUpTime) {} } const start = new Date(); // const [a, b] = Promise.all([test1(), test2()]); // test1().then(console.log); // test2().then(console.log); test2(); test1(); const end = new Date(); console.log(`elapsed time : ${end - start}`); /* 실행 결과 test2 start : 1666351186637 test1 start : 1666351186640 elapsed time : 3 .. elapsed time이 제일 먼저 나올 줄 알았는데 아니네?? 아 하긴, await 부분이 비동기로 도는 부분이지, 그 전 부분은 아니니까 콜스택에서 실행이 되는부분인거 같다! await 부분은 태스크 큐에 들어가서 콜스택이 비워지기를 기다리는 것이고.(아마도?) */
- 여기서 elapsed time이 5000ms 가 나와야 할 것 같지만, 그렇게 되지 않고 test1과 test2는 태스크 큐로 보내짐
- 그래서 console.log가 제일 먼저 뜨고 콜스택이 비워지고 나서 태스크 큐에서 이벤트 루프가 test1과 test2를 실행하게 됨
- async를 쓰는 이유는 결국 그 안에서 await를 사용하기 위함. 근본적으로 async/await는 Promise를 더 편리하게 쓰기 위한것이고 Promise는 콜백 헬을 개선하기 위한 것이니까, 콜백 패턴을 이쁘게 쓰기 위해서 async/await를 쓰는 것이 아닐까
- async를 쓴다고 비동기고 안쓴다고 동기가 아님 자바스크립트에서는. async를 쓰면 Promise를 조금 더 이쁘게 쓸 수 있는 것이지, async function아니더라도 안에서 Promise를 쓰면 비동기로 돌아감
router.get('/ping1', (req, res) => { console.log(`ping1 start : ${Date.now()}`); for(let i=0; i<10; i++){ fetch('https://jsonplaceholder.typicode.com/todos/1') .finally(() => console.log(`ping1 : ${i}`)); } console.log(`ping1 end : ${Date.now()}`); res.send('pong1'); }); /* 아래 출력을 보면, fetch도 Promise를 반환하고, 그래서 비동기로 돌기 때문에 순서가 보장이 안되고 마찬가지로 태스크 큐에 들어가서 콜스택이 비워지고 실행되기 때문에 ping1 end가 끝나고 나서 실행이 됨 ping1 start : 1666491022728 ping1 end : 1666491022756 GET /ping1 304 30.653 ms - - ping1 : 0 ping1 : 7 ping1 : 9 ping1 : 5 ping1 : 4 ping1 : 6 ping1 : 8 ping1 : 3 ping1 : 2 ping1 : 1 */