HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🤩
개발
/
NodeJs
NodeJs
/
🩹
제너레이터와 async/await
🩹

제너레이터와 async/await

제너레이터란?제너레이터와 일반 함수의 차이제너레이터의 일시 중지와 재개이터레이터의 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 표현식의 평과 결과가 할당되지 않는다는점 주의!
  • 함수 호출자는 next 메서드를 통해 yield 표현식까지 함수를 실행시켜 제너레이터 객체가 관리하는 상태(yield된 값)를 꺼내올 수 있고
  • next 메서드에 인수를 전달해서 제너레이터 객체에 상태(yield 표현식을 할당받는 변수)를 밀어넣을 수 있음

async/await ⇒ 콜백을 쓸 필요가 없게됨

  • ES8에서는 제너레이터보다 간단하고 가독성 좋게 비동기 처리를 동기 처리처럼 동작하도록 구현할 수 있는 async/await가 도입되었음
  • async/await 는 프로미스를 기반으로 동작함. async/await를 사용하면 프로미스의 후속 처리 메서드에 콜백 함수를 전달해서 비동기 처리 결과를 후속 처리할 필요 없이 마치 동기 처리처럼 프로미스를 사용할 수 있다

async 함수

  • await 키워드는 async 함수 내부에서 사용해야 함
  • async 함수는 async 키워드를 사용해 정의하며 언제나 프로미스를 반환함
  • 명시적으로 프로미스를 반환하지 않더라도 async 함수는 암묵적으로 반환값을 resolve 하는 프로미스를 반환함

await 키워드

  • await 키워드는 프로미스가 settled 상태(비동기 처리가 수행된 상태)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve 한 처리 결과를 반환함
  • await 키워드는 반드시 프로미스 앞에서 사용해야 함

에러 처리

  • 비동기 처리를 위한 콜백 패턴의 단점 중 가장 심각한 것은 에러 처리가 곤란하다는 것임
  • 비동기 함수의 콜백함수를 호출한 것이 비동기 함수가 아니기에, 에러 전파가 안됨
  • 그러나 async/await는 try.. catch 문을 사용해서 에러 잡아낼 수 있음
  • async 함수 내에서 catch 문을 사용해서 에러 처리를 하지 않으면 async 함수는 발생한 에러를 reject 하는 프로미스를 반환함

사용 예시

  • 여기서 elapsed time이 5000ms 가 나와야 할 것 같지만, 그렇게 되지 않고 test1과 test2는 태스크 큐로 보내짐
  • 그래서 console.log가 제일 먼저 뜨고 콜스택이 비워지고 나서 태스크 큐에서 이벤트 루프가 test1과 test2를 실행하게 됨
  • async를 쓰는 이유는 결국 그 안에서 await를 사용하기 위함. 근본적으로 async/await는 Promise를 더 편리하게 쓰기 위한것이고 Promise는 콜백 헬을 개선하기 위한 것이니까, 콜백 패턴을 이쁘게 쓰기 위해서 async/await를 쓰는 것이 아닐까
  • async를 쓴다고 비동기고 안쓴다고 동기가 아님 자바스크립트에서는. async를 쓰면 Promise를 조금 더 이쁘게 쓸 수 있는 것이지, async function아니더라도 안에서 Promise를 쓰면 비동기로 돌아감
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}
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); });
제너레이터와 co 라이브러리(제너레이터 실행기)를 활용한 비동기코드
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 함수 선언문 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));
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 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 부분은 태스크 큐에 들어가서 콜스택이 비워지기를 기다리는 것이고.(아마도?) */
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 */