[Javacript] 비동기 처리하기 Callback, Promise

wrld_worthy·2024년 1월 2일

JavaScript

목록 보기
20/21

1. 비동기

동기 Synchronous 프로그래밍에서 작업은 차례로 실행되며 이전 작업이 끝날 때까지 중단할 수 없다. 모든 작업은 이전 작업의 실핼이 완료될 때까지 기다려야만 한다.

반면, 비동기 Asynchronous 프로그래밍에서는 임의의 순서로 똔느 동시에 작업이 실행될 수 있다.

자바스크립트는 런타임(Node.js)에서 싱글 스레드로 작동한다. 싱글 스레드로 동작 한다는 것은 한번에 하나의 작업만 처리한다는 뜻이다. 그럼 동기 프로그램으로 동작한다면 실행이 완료될 때까지는 매우 긴 시간이 걸리게되고 매우 비효율적으로 동작하게 될 것이다.

어떻게 효율적으로 처리해야 할 까?
비동기 처리를 하면된다. 비동기처리는 이전 작업이 끝나지 않아도 다른 작업을 수행 할 수 있게 해준다.

비동기로 처리하기 위해서는 Callback, Promise, asyn, await 방법을 사용하면 자바스크립트에서 비동기로 처리가 가능하다.

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

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

  • async, await : 어싱크 어웨이트는 프로미스를 사용하는 비동기 작업을 동기적으로 처리하는 것처럼 코드를 작성할 수 있게 해준다. async가 붙어있는 함수를 실행할 때 await 키워드를 사용하여 비동기 작업이 완료될 때까지 기다릴 수 있다. asyn와 await 두 키워드는 반드시 붙어 다닌다. async인 함수에만 await을 사용할 수 있다.

2. Callback function

비동기는 현재 코드의 실행 결과를 받지 않고 이후 코드를 수행하는 기법이다. 컴퓨팅 자원을 효율적으로 사용하는 기법이긴 하자민 정확한 순서를 지켜 수행해야 하는지를 고려해서 처리해야한다.

비동기 코드를 순서대로 싱행하는 가장 일반적인 방안으로 콜백이 있다. 콜백은 실행 가능한 함수를 인자로 전달하여, 특정 상황이 발생할 때 호출되게 하는 방식이다.

콜백은 현실 세계에서도 발견할 수 있는데, 커피숍에 가서 점원에게 커피를 먼저 주문하고 다른 것을 하고 있으면, 커피 제조가 끝난 후에 손님을 호출(callback)하는 상황으로 콜백을 볼 수 있다.

콜백 함수를 작성해 보자.

const DB = []

// 회원 가입 API 함수
// 콜백이 3중으로 중첩된 함수
function register(user) {
  return saveDB(user, function(user){
   return sendEmail(user, function(user){
     return getResult(user);
   });
  });
}

// DB에 저장후 콜백 실행
function saveDB(user, callback){
  DB.push(user);
  console.log(`save ${user.name} to DB`);
  return callback(user);
}

// 이메일 발송 로그만 남기는 코드 실행 후 콜백
function sendEmail(user, callback){
  console.log(`email to ${user.email}`);
  return callback(user);
}

// 결과를 반환하는 함수
function getResult(user){
  return `success register ${user.name}`;
}

const result = register({ email:"andy@test.com", password: "1234", name: "andy"});
console.log(result);

3단계로 회원 가입 API를 실행하는 1. register함수는 2.saveDB, 3.sendEamil, 4. getResult 함수를 각각 차례로 호출하여 콜백을 사용했다.

여기서 보장하는 것은 함수의 실행 순서이다.

결과

예상한 순서대로 잘 작동하는 것을 볼 수 있따. 하지만 매우 간단한 코드 인데도 콜백을 사용하니 코드가 매우 복잡해보여 가독성이 금방 매우 떨어지는 것을 볼 수 있다.

콜백을 3번만 사용해도 tab이 3번을 사용해야한다. 좀 더 복잡하고 무거운 프로그램을 만들어야 한다면? 10단, 20단 콜백문을 볼 수 있을지도 모른다.

그렇다면 지금은 짧아서 괜찮더라도 점점 알아보기가 힘든 상황이 된다. 심지어 콜백에 에러가 발생한다면? 찾기는 매우 힘들 것이다. 생각도 하기 싫다.

이러한 문제를 해결하기 위해서 Promise 객체가 2015년 ES6 버전에서 등장했다.

Promise 객체

Promise객체는 자바스크립트에서 비동기 실행을 동기화하는 구문으로 사용한다. Promise의 뜻인 약속을 떠올리면 프로미스 개념을 이해하기 좀 쉽다.

현실 세계에서 약속은 미래에 어떤 것을 할 거라고 정하는 것이다. 약속은 이행, 거절, 대기 세가지 상태를 가질 수 있다.

자바스크립트에서 '이 코드는 어느 시점에서 실행할 거야'라고 약속하는 객체로 Promise를 사용한다.

Promise는 각각 이행, 거절, 대기 세가지 상태를 가질 수 있다. Promise는 객체로 new 연산자로 인스턴스 생성이 가능하다.

Promise 객체가 생성되면 대기 (pending)상태가 된다. resolve()함수가 실행되면 이행으로 변경되고
실패해 reject()함수가 실행되면 거절로 변한다.

const DB = []

// DB에 저장후 콜백 실행
function saveDB(user, callback){
  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)	// 성공 시 유저 정보 반환
    } else {
      reject(new Error("Save DB Error!")); // 실패 시 에러처리
    }
  });
}

// 이메일 발송 로그만 남기는 코드 실행 후 콜백
function sendEmail(user, callback){
  console.log(`email to ${user.email}`);
  return new Promise((resolve)=>{ // Promise 객체 반환, 실패 처리 없음
    resolve(user);
  });
}

// 결과를 반환하는 함수
function getResult(user){
  return new Promise((resolve, reject)=>{
    resolve(`success register ${user.name}`);
  });
}

function registerByPromise(user){
  //비동기 호출이지만 순서를 지켜서 실행
  const result = svaeDB(user).then(sendEmail).then(getResult);
  // 아직 완료되지 않아서 지연(pending)상태이다.
  
  console.log(result);
  return result
}

const myUser = { email:"andy@test.com", password: "1234", name: "andy"};
const result = registerByPromise(myuser);
result.then(console.log);

결과

각각의 함수에 있던 콜백 함수를 Promise 객체로 바꿨다. Promise는 객체이므로 new 생성자로 생성할 수 있고 그 안에 resolve, reject 함수가 있다.

성공하면 resolve를 실패하면 reject를 실행시킨다. 또한 Promise는 then(promise) 메서드가 있어서 비동기 호출이지만 Promise1.then(Promise2).then(Promise3) 이런 식으로 순서대로 호출할 수 있다.

then은 Promise 객체에만 사용할 수 있다.

then의 사용법

then(onFulfilled)
then(onFulfilled, onRejected)

then(
  value=>{/*Fulfilled handler*/},
  reason=>{/*rejection handler*/}
)

then은 성공했을 때, catch는 주로 실패했을 때 사용하지만
then의 파라미터를 잘 이요하면 2번째 파라미터에 실패시의 처리를 해줄 수 있다.

동시에 여러 Promise 객체 호출하기.

동시에 여러 Promise 객체를 호출해 결괏값을 받고 싶을 때는 어떻게 해야할까??

Promise.all([Promise1, Promise2, ...])처럼 사용하면 된다.

그러면 나열된 순서 상관없이 동시에 실행할 수 있다. 결과 값은 배열로 반환된다.

// 위 코드 그대로...

const myUser = { email:...}
allResult = Promise.all([saveDB(myUser), sendEmail(myUser), getResult(myUser)]);
allResult.then(console.log);

결과

이와 같은 배열의 결과 값을 얻을 수 있다.


Promise 예외 처리

saveDB() 함수의 로직을 약간 변경해서 에러를 발생시켜보자.

const DB = []

// DB에 저장후 콜백 실행
function saveDB(user, callback){
  const oldDBSize = DB.length+1; // 이 부분 수정하여 에러발생
  DB.push(user);
  console.log(`save ${user.name} to DB`);
  return new Promise((resolve, reject)=>{ // 콜백 대신 Promise 객체 반환
    if (DB.length > oldDBSize){
      resolve(user)	// 성공 시 유저 정보 반환
    } else {
      reject(new Error("Save DB Error!")); // 실패 시 에러처리
    }
  });
}

// 이메일 발송 로그만 남기는 코드 실행 후 콜백
function sendEmail(user, callback){
  console.log(`email to ${user.email}`);
  return new Promise((resolve)=>{ // Promise 객체 반환, 실패 처리 없음
    resolve(user);
  });
}

// 결과를 반환하는 함수
function getResult(user){
  return new Promise((resolve, reject)=>{
    resolve(`success register ${user.name}`);
  });
}

function registerByPromise(user){
  //비동기 호출이지만 순서를 지켜서 실행
  const result = svaeDB(user).then(sendEmail).then(getResult);
  // 아직 완료되지 않아서 지연(pending)상태이다.
  
  console.log(result);
  return result
}

const myUser = { email:"andy@test.com", password: "1234", name: "andy"};
const result = registerByPromise(myuser);
result.then(console.log);

DB.length보다 oldDBSize의 크기가 커야한다고 명시했는데, 일부러 oldDBSize를 더 크게 해주면 Promise 로직이 실패하게 된다.

function registerByPromise(user){
  //비동기 호출이지만 순서를 지켜서 실행
  const result = saveDB(user)
  	.then(sendEmail)
  	.then(getResult)
  	.catch(error=>new Error(error));
  // 아직 완료되지 않아서 지연(pending)상태이다.
  console.log(result);
  return result
}

위와 같이 수정해주면

좀 다르고 보기 쉬운 에러를 얻을 수 있다.

finally 메서드도 봐보자.

finally 메서드는 비동기 처리의 성공, 실패 여부와 상관 없이 마지막에 실행해주는 메서드다.

function registerByPromise(user){
  //비동기 호출이지만 순서를 지켜서 실행
  const result = saveDB(user)
    .then(sendEmail)
    .then(getResult)
    .catch(error=>new Error(error))
    .finally(()=> console.log("완료"));
  // 아직 완료되지 않아서 지연(pending)상태이다.
  console.log(result);
  return result
}

위와 같이 수정해주면

완료가 잘 찍혀있는 것을 확인할 수 있다.

Promise의 문제점

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

하지만 then()과 catch() 함수를 연결하는 체이닝 방식을 사용하기가 만만하지는 않다. 거기에 더 복잡한 로직을 추가하고 예외 처리까지 해야되는 상호아이라면 더욱 힘들어진다.

이상적으로 깔끔한 코드를 작성할 수는 있겠지만 그런 코드는 실전에서는 보기가 힘들다.

실전은 더욱 복잡하고 까다로운 로직을 수행해야하는 경우가 많기 때문이다.

Promise는 콜백보다는 깔끔하지만 잘못 사용될 수 있는 여지가 남아있따.

  1. 프로미스의 then 함수에 성공 시와 실패 시 처리할 함수를 둘 다 넘기는 경우.
    둘다 넘기면 프로미스는 장식에 불과하고 기존 콜백 함수에 결과와 에러를 동시에 넘기는 형태와 다를 바가 없다.

  2. 프로미스를 중첩해서 사용하는 경우이다. 콜백에는 익숙한데 프로미스에 익숙하지 않은 때 이런 실수를 많이 한다.

이러한 문제를 해결하기 위해서 async, await이 만들어 졌다!!

다음에 알아보자.

0개의 댓글