Node.js 에서 비동기를 처리하는 방법

정하람·2025년 1월 18일

Node.js (TS)

목록 보기
3/3
post-thumbnail

동기(Synchronous) VS 비동기(Asynchronous)

동기 프로그래밍

  • 작업이 차례로 실행되며 작업이 완료될 때까지 중단될 수 없음.
  • 모든 작업은 이전 작업의 실행이 완료될 때까지 기다려야 함.

비동기 프로그래밍

  • 임의의 순서, 즉 동시에 작업이 실행될 수 있음.
  • Node.js 는 이벤트 기반 아키텍처임.

JS 의 비동기 처리

싱글스레드로 동작하지만 이벤트 기반 아키텍처 <콜스택 / Node.js API / 이벤트 루프 / 테스크 큐> 로 설계된 Node.js는 비동기 처리가 가능하디.

Callback Function / Promise Object / async await 를 사용하면 js 에서 비동기 처리를 할 수 있다.

  1. Callback 함수는 함수의 파라미터로 함수를 전달하며, 비동기 처리가 끝났을 때 전달된 함수를 실행한다.
    콜백은 가독서이 좋지 못하여 유지보수 / 디버깅이 힘들다는 단점이 있다.

  2. Promise 객체는 콜백 대신 사용할 수 있는 방법으로 비동기 작업이 완료되면 결과를 반환하는 객체이다.
    프로미스 객체는 상태를 가지고 있으며, 처음엔 대기 상태이고 작업이 완료되면 성공 / 실패 상태가 된다.
    .then / .catch / .finally 메서드를 사용하여 이를 제어할 수 있다.

  3. async, await 키워드는 프로미스를 사용하는 비동기 작업을 동기적으로 처리하는 것처럼 코드를 작성하게 해준다.
    async가 붙어 있는 함수를 실행할 때, await 키워드를 사용하여 비동기 작업이 완료될 때까지 기다릴 수 있다.

Call Back Function (콜백함수)

비동기는 현재 코드의 실행 결과를 받지 않고 이후에 코드를 수행하는 기법이다.

컴퓨팅 자원을 효율적으로 사용하는 기법이지만, 정확한 순서를 지켜 수행해야 하는지를 고려하여 처리해야 한다.
비동기 코드를 순서대로 실행하는 가장 일반적인 방안으로 콜백(callback)이 있다.

콜백은 실행 가능한 함수를 인자로 전달하여, 특정 상황이 발생할 때 호출되게 하는 방식이다.

예시 코드 작성.

회원가입 함수에 3가지 동작을 비동기 처리로 작성해보겠다.

  1. 데이터베이스에 저장
  2. 이메일을 보냄
  3. 성공 메시지 반환

예시 코드

const db = [];

// 첫 번째 동작
const saveDB = (user, callback) => { // DB save 동작 후 콜백 함수 실행.
  db.push(user);
  console.log(`save ${user.name} to DB`);
  return callback(user);
}

// 두 번째 동작
const sendEmail = (user, callback) => { // Email send 동작 후 콜백 함수 실행.
  console.log(`email to ${user.email}`);
  return callback(user);
}

// 세 번째 동작
const getResult = (user) => { // 결과 반환
  return `success register ${user.name}`;
}

// 회원가입 함수
const register = (user) => {
  return saveDB(user, (user) => { // 콜백
    return sendEmail(user, (user) => { // 콜백
      return getResult(user);
    });
  });
}

const result = register({ 
	email: "04harams77@gmail.com", 
	password: "1234", 
	name: "haram jeong"
})

console.log(result);

실행 결과

save haram jeong to DB
email to 04harams77@gmail.com
success register haram jeong

문제점

예상대로 잘 동작하지만, 매우 간단한 코드인데도 콜백을 사용하는 경우 코드가 복잡해 보인다. 현재 3단계로 콜백을 중첩시켰지만, 실제 개발에선 10단계 그 이상도 있을 수 있다.

그러면 코드의 깊이가 계속 깊어져서 가독성이 매우 떨어질 것이다.
만일 콜백의 깊은 곳에서 에러가 발생한다면 추적이 어려워진다.

이러한 콜백의 문제를 해결할 목적으로 프로미스(Promise) 객체가 도입되었다.

Promise 객체

Promise는 자바스크립트에서 비동기 실행을 동기화하는 구문으로 사용한다.

Promise의 뜻인 약속을 떠올리면 프로미스의 개념을 이해하기가 편하다.
Promise은 이행 / 불이행 / 대기 이렇게 3가지 상태를 가질 수 있다.

Promise 는 객체이므로 new 연산자로 인스턴스를 생성할 수 있다.

우선적으로 Promise 객체가 생성되면 대기 상태가 된다

resolve() 함수가 실행되면 이행 상태로 변경되고

reject() 함수가 실행되면 거절(불이행) 상태로 변경된다.

예시 코드

const db = [];
  
const saveDB = user => {
  const oldDBSize = db.length;
  db.push(user);
  console.log(`save ${user.name} to DB`);
  return new Promise((resolve, reject) => { // 콜백 대신 Promise 객체 반환
    if (db.length > oldDBSize) {
      resolve(user); // 이행 시 user 객체를 다음 동작으로 반환
    }
    else {
      reject(new Error("Save DB Error!")); // 불이행 시 Error 발생
    }
  })
}

 const sendEmail = user => {
   console.log(`email to ${user.name}`);
   return new Promise(resolve => { // 콜백 대신 Promise 객체 반환
     resolve(user); // 이행 시 user 객체를 다음 동작으로 반환
   })
 }

const getResult = user => {
  return new Promise((resolve) => { // 콜백 대신 Promise 객체 반환
    resolve(`success register ${user.name}`);// 이행 시 결과를 반환
  })
}

const registerByPromise = user => {
	// 순서를 지켜서 실행
  const result = saveDB(user).then(sendEmail).then(getResult);
  console.log(result) // 아직 비동기 작업 중이므로 Promise 객체 Pending 상태.
  return result;
}

const myUser = { email: "04harams77@gmail.com", password: "1234", name: "haram"};
const result = registerByPromise(myUser);
result.then(console.log); // result 가 Promise 객체이므로 .then 메소드로 결과값 확인.

실행 결과

save haram to DB
Promise { <pending> } // 아직 실행이 완료되지 않았는데 result를 출력함.
email to haram
success register haram

추가적으로 Promise 객체의 예외처리를 하고 싶다면

.then()

  • 매개변수로 <함수를 이행 / 거절 시, 실행할 함수>를 넣어줌.

.catch()

  • 매개 변수로 <에러 핸들링 구문>

.finally()

  • 매개 변수로 <비동기 처리의 성공 / 실패 여부와 관계없이 실행되는 구문>

위 메소드를 사용하면 된다.

// 코드 수정
const registerByPromise = user => {
  const result = saveDB(user)
								  .then(sendEmail)
									.then(getResult)
									.catch(err => new Error(err)) // 에러 핸들링
									.finally(() => console.log("complete")); // 완료 메시지 출력
  console.log(result)
  return result;
}

추가적으로 동시에 여러 Promise 객체를 호출하고 싶다면

Promise.all([Promise1, Promise2,])

처럼 쓰면 된다. 그러면 나열된 순서와 상관 없이 동시에 실행되며, 결과는 배열로 반환된다.

// 코드 수정
const myUser = { email: "04harams77@gmail.com", password: "1234", name: "haram"};
allResult = Promise.all([saveDB(myUser), sendEmail(myUser), getResult(myUser)]);
allResult.then(console.log);

결과

save haram to DB
email to haram
[
  { email: '04harams77@gmail.com', password: '1234', name: 'haram' },
  { email: '04harams77@gmail.com', password: '1234', name: 'haram' },
  'success register haram'
]

문제점.

자바스크립트에서 비동기 처리를 하는 데 사용하는 Promise 객체는 콜백함수보단 확실히 편리하다.

하지만 .then() / .catch() / .finally() 함수를 연결하는 체이닝 방식을 사용하기가 쉽지 않다.

추가적으로 복잡한 로직을 넣고 예외 처리까지 해야한다면 코드가 가독성이 매우 떨어질 것이다.

자바스크립트가 발전하면서 사용하기 까다로운 Promise를 사용이 간편한 async / await 으로 발전시켰다.

async / await 구문을 사용하면 동기 방식의 코드처럼 코드를 작성하면서 비동기 코드를 순서대로 실행할 수 있다.

async / await 구문

async와 await은 자바스크립트에 가장 최근 도입된 비동기 처리 방식이다. 기존의 비동기 처리 방식의 단점을 보완하며 가독성 높은 코드를 작성할 수 있다.

async

async 는 함수 앞에 붙이는 키워드 이다. (asynchronous 비동기) 라는 의미.

즉, async 가 붙은 함수는 프로미스를 반환한다.

const myName = async () => "haram";

console.log(myName());
Promise { 'haram' }

await

await는 기다린다는 의미 그대로이다.

즉, await은 성공 / 실패로 Promise 객체의 실행이 완료되기를 기다린다.

따라서 await의 뒤에는 Promise 객체가 온다.

await은 async 키워드를 사용한 함수 안에서만 사용할 수 있다.

const myName = async () => "haram";

const showName = async () => {
  const name = await myName();
  console.log(name);
  return "complete"
}
console.log(showName())
Promise { <pending> }
haram

결과해석

우선 showName()async 함수이므로 Promise 객체를 반환한다. 하지만 await 키워드가 없으므로 기다리지 않고 바로 출력하므로 Promise { <pending> } 출력된다.

myName()async 함수이므로 Promise 객체를 반환하는데, await 키워드가 있어서 반환될 때까지 기다린다. 이후 name을 출력하여 haram 이 출력된다.

pending 되지 않고 출력이 잘 되게 하기 위해서 수정한다면,

const myName = async () => "haram";

const showName = async () => {
  const name = await myName();
  console.log(name);
  return "complete"
}

const a = async () => console.log(await showName())

a()
//haram
//complete

위와 같이 작성할 수 있다.

추가적인 예시 코드

const timeTicking = async (msg) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(msg),1000) // 특정 시간 이후 코드를 실행하는 func
  })
}

const main = async () => {
  for (let i = 1; i < 11; i++){
    let result = await timeTicking(`${i}초 지났습니다.`)
    console.log(result)
  }
}

main()
1초 지났습니다.
2초 지났습니다.
3초 지났습니다.
4초 지났습니다.
5초 지났습니다.
6초 지났습니다.
7초 지났습니다.
8초 지났습니다.
9초 지났습니다.
10초 지났습니다.

0개의 댓글