[Node.js] Node.js의 비동기 처리 - Promise / Async&await

Node.js는 왜 비동기인가?

Node.js는 단일 스레드(Single Thread) 기반 런타임입니다.
이는 기본적으로 한 번에 하나의 JS 코드만 실행된다는 말인데요.

그런데도 동시에 많은 요청을 처리할 수 있는 이유는 바로 비동기(Asynchronous) + 논블로킹(Non-blocking) I/O 이기 때문이라고 할 수 있습니다. 

 

 

우선, 동기와 비동기에 대해 알아보겠습니다.

동기 vs 비동기

- 동기 (Synchronous)

: 코드가 순서대로 실행되고, 이전 작업이 끝날 때까지 다음 작업이 기다리는 방식 

const data = fs.readFileSync('file.txt');
console.log(data);
 
  • 파일 읽는 동안 프로그램은 멈춤
  • 다른 요청 처리 불가 👉 블로킹(작업이 끝날 때까지 이벤트 루프가 멈춤)

- 비동기 (Asynchronous) 

작업을 맡겨두고, 끝나면 나중에 알려달라는 방식 -> 실행되는 코드가 기다려야 하는 시간이 생김

ex) setTimeOut(), setIntervel(), query()

fs.readFile('file.txt', (err, data) => {
	console.log(data);
});
 
  • 파일 읽는 동안 다른 코드 실행 가능
  • 완료되면 콜백 실행 👉 논블로킹(작업은 OS가, Node는 계속 다른 일) 

Node.js의 이벤트 루프 

Call Stack
↓
Web API (또는 libuv)
↓
Callback Queue
↓
Event Loop

동작 흐름

JS 코드 실행 (Call Stack)
비동기 작업 발견
OS(libuv)에 맡김
완료되면 Callback Queue에 넣음
Call Stack이 비면 Event Loop가 콜백 실행

비동기 발생 -> 비동기 처리

여기서 제가 말씀드리는 발생과 처리는 당연하게도 다른 이야기인데요,

일단 이전 시간이 오래 걸리면, 안 기다려주고 다음 코드를 무작정 실행하는 비동기가 발생을 하면,

이를 '동기처럼' 순서를 맞춰 이전 코드들의 시간을 다 기다려주고 실행하기 위해, 비동기를 처리해줄 수 있다는 것입니다.

 

1. 콜백 

: 완료 시점에 실행될 함수를 전달

  • 제어권을 Node에게 넘김
  • 완료되면 이벤트 루프가 콜백을 실행
  • 중첩되면 콜백 지옥이 발생할 수 있음
fs.readFile('a.txt', (err, data) => {
  console.log(data);
});

 

2. Promise (reseolve, reject)

: 비동기 작업의 상태를 객체로 모델링

const p = new Promise((resolve, reject) => {
  resolve("성공");
});

resolve -> 성공

reject -> 실패

 

3. then & catch : Promise의 사용 문법 

//Promise "객체" : 무조건 약속을 지키는 친구

let promise = new Promise((resolve, reject)=>{
    //executor : 친구가 해야할 일
    setTimeout(() => resolve('성공!'), 3000); //3초 후에 resolve라는 콜백함수 호출하면서 '성공!'이라는 결과 전달
    
    //resolve(결과) : executor가 성공 했을 때 
    //reject(에러) : 실패했을 때 
    //할 일 다하면 무조건 콜백함수 resolve() or reject() 둘 중 하나 호출
}); //친구 소환, 매개변수로 함수를 받음

// promise의 기본 메소드 : promise가 일 다 하고 (= 약속 다 지키고) 호출해야하는 함수
promise.then(
    function(result){
        console.log(result); // 성공! 출력
    }, //첫번째 매개변수 : resolve이 호출됐을 때 실행할 함수
    function(error){} //두번째 매개변수 : reject이 호출됐을 때 실행할 함수
);

 

Promise 체이닝 

let promise = new Promise((resolve, reject)=>{
    setTimeout(() => resolve('성공'), 3000); 
}).then(
    function(result){
        console.log(result);
        return result + "!!!!!";
    }, 
    function(error){} 
).then(
    function(result){
        console.log(result);
        return result + "?????";
    }, 
    function(error){}
).then(
    function(result){
        console.log(result);
    }, 
    function(error){}
);

return으로 다음 then을 실행하도록 할 수 있고, return을 하지 않으면 그 이후의 then에 undefined가 전달되어 항상 새로운 Promise를 반환하게 됩니다. 

 

4. ES2017 promise를 쉽게 쓰기 위한 방법 => async & await

async-await : Promise를 좀 더 간결하게 사용할 수 있도록 도와주는 문법 -> 즉 비동기 처리가 쉽다

- async

  • async 함수 : async function f() {} 
  • async 함수는 항상 Promise 객체를 반환한다.
  • 만약 반환값이 Promise가 아니라면, 반환값을 Promise.resolve()로 감싸서 반환한다.
async function f() {}

// = 동일 = 

function f() {
  return new Promise(...)
}

 

async function f() {
    return 7;
}

f().then(
    function(result){
        console.log("promise resolve : ", result);
    }, 
    function(error){
        console.log("promise reject : ", error);
    }
);

 

- wait

  • async-await : Promise를 좀 더 간결하게 사용할 수 있도록 도와주는 문법 -> 즉 비동기 처리가 쉽다
  • await :  async 함수 안에서만 동작, Promise 객체.then() 메소드를 좀 더 쉽게 사용할 수 있도록
  • async 의 두번째 기능 : Promise 객체가 일이 끝날 때까지 기다릴 수 있는 공간을 제공
async function f() {
    let promise = new Promise((resolve, reject)=>{
        setTimeout(() => resolve('성공'), 3000); 
    });

    let result = await promise; // await : Promise 객체가 resolve될 때까지 기다렸다가, resolve된 값을 result에 할당
    console.log(result);
}

f();

 

직렬 실행 

: Promise가 끝난 후 다음 Promise를 생성 

// async-await : Promise를 좀 더 간결하게 사용할 수 있도록 도와주는 문법 -> 즉 비동기 처리가 쉽다

// await :  async 함수 안에서만 동작
// Promise 객체.then() 메소드를 좀 더 쉽게 사용할 수 있도록

// async 의 두번째 기능 : Promise 객체가 일이 끝날 때까지 기다릴 수 있는 공간을 제공

async function f() {

    // promise 객체 한개당 => query 하나당 => await 하나씩 
    let promise1 = new Promise((resolve, reject)=>{
        setTimeout(() => resolve('첫번째 쿼리'), 3000); 
    });

    let result1 = await promise1; 
    // promise 객체가 일 다 할때까지 기다려줌

    console.log(result1);

    let promise2 = new Promise((resolve, reject)=>{
        setTimeout(() => resolve('두번째 쿼리 with ' + result1 ), 3000); 
    });

    let result2 = await promise2;
    console.log(result2);

    let promise3 = new Promise((resolve, reject)=>{
        setTimeout(() => resolve('세번째 쿼리 with ' + result2 ), 3000); 
    });

    let result3 = await promise3;  
    console.log(result3);
}

f();