javascript

비동기(Asynchronous) - 콜백지옥, Promise, async/await

Bittersweet- 2022. 8. 30. 14:01
728x90

자바스크립트의 비동기 처리는 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드 먼저 실행하는 자바스크립트의 특성을 의미한다.

 

 

 

 

비동기 처리 사례

1. Ajax Web API 요청 : 서버쪽에서 데이터를 받아와야 하는 경우

function getData() {
  var tableData;
  $.get('https://domain.com/products/1', function(response) {
    tableData = response;
  })
  return tableData;
}

console.log(getData()); // undefined

$.get()은 jquery의 ajax 통신 부분으로 url로 HTTP GET 요청을 날려 1번에 대한 정보를 요청하는 코드이다. 이렇게 서버에서 받아온 데이터는 response 인자에 담기고 tableData에 저장합니다.

그런데 자세히 보면(실제로도) getData()의 호출값이 undefined가 반환된다.

 

그 이유는 데이터를 요청하고 받아올 때까지 기다리지 않고 다음 코드인 return tableData를 실행했기 때문이다. 초기값을 설정하지 않았던 tableData는 undefined이 되었다.

 

2. 작업 예약 : setTimeout을 사용하여 비동기 처리하는 경우

// 1
console.log("Hello");
// 2
setTimeout(function() {
  console.log("Bye");
}, 3000);
// 3
console.log("Hello Again");

setTimeout()은 Web API의 한 종류로 코드를 바로 실행하지 않고 지정된 시간만큼 기다렸다가 로직을 실행하는 코드이다.

위 코드에 대한 처리 결과 값은 아래와 같이 Hello, Hello Again이 출력된 후 3초 뒤에 Bye가 출력된다.

Hello
Hello Again
Bye

setTimeout() 역시 비동기 방식으로 실행되기 때문에 3초 기다렸다가 다음 코드를 수행하는 것이 아니라 setTimeout()이 실행되고 나서 바로 다음 코드인 console.log("Hello Again")을 실행했다.

 

 

3. 콜백 함수로 비동기 처리 문제점 해결하기

1번과 2번의 방식으로 비동기를 처리할 경우, 예상했던 것과 다른 결과값이 도출될 수 있다. 이러한 문제를 개선하는 방법으로 콜백을 알아보도록 하자.

다음은 1번에서 살펴보았던 ajax 통신 코드를 콜백 함수로 개선한 코드이다.

function getData(callbackFunc) {
  $.get('https://domain.com/products/1', function(response) {
    // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
    callbackFunc(response);
  });
}

getData(function(tableData) {
  // $.get()의 response 값이 tableData에 전달
  console.log(tableData);
});

이렇게 콜백함수를 사용하면 특정 로직이 끝났을 때 원하는 동작을 실행시킬 수 있다.

 

콜백 지옥(Callback hell)

(콜백 함수 : 함수 안에서 어떤 특정한 시점에 호출되는 함수. 보통 함수의 매개 변수로 전달하여 특정 시점에서 콜백 함수를 호출함.)

콜백 지옥은 비동기 처리를 위해 콜백함수를 연속해서 사용할 때 발생하는 것을 말한다. 콜백 안에 콜백함수가 계속 중첩되는 구조를 하는데 이러한 코드 구조로 인해 가독성이 떨어지고 수정 시 로직을 변경하기도 어렵다는 단점이 있다.

 

콜백지옥 예시.

$.get('url', function(response) {
  parseValue(response, function(id) {
    auth(id, function(result) {
      display(result, function(text) {
        console.log(text);
      });
    });
  });
});

콜백함수 분리 예시.

function parseValueDone(id) {
  auth(id, authDone);
}

function authDone(result) {
  display(result, displayDone);
}

function displayDone(text) {
  console.log(text);
}

$.get('url', function(response) {
  parseValue(response, parseValueDone);
});

1. ajax 통신으로 받은 데이터를 parseValue() 메서드로 파싱한다.

2. parseValueDone()에 파싱한 결과값인 id가 전달되고 auth() 메서드가 실행된다.

3. auth() 메서드로 인증을 거치고 나면 콜백함수 authDone()이 실행된다.

4. 인증 결과 값인 result로 display()를 호출하면 마지막으로 displayDone() 메서드가 수행되면서 text가 콘솔에 출력된다.

 

콜백 지옥을 해결하는 방법으로 Promise와  Async/await를 사용할 수 있다.

 

 

 

 

 


 

 

 

Promise (ES6에 도입된 기능)

const promise = new Promise ((resolve, reject) => {
    //executor 실행자, 실행함수..
})

*executor의 인자 (resolve, reject) 는 자바스크립트가 자체적으로 제공하는 콜백

  • resolve : 작업이 성공적으로 끝난 경우, 그 결과를 나타내는 value와 함께 호출한다.
  • reject : 에러 발생 시 에러 객체를 나타내는 error와 함께 호출한다.

executor는 자동으로 실행되는데 여기서 원하는 일이 처리되며, 처리가 끝나면 성공 여부에 따라 resolve나 reject가 호출됨.

 

 

promise의 3가지 상태(states)

promise는 new Promise()로 프로미스를 생성하고 종료할 때까지 3가지 상태를 가진다.

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태
const myPromise = new Promise((resolve, reject) => {
    setTimeout (() => {
        resolve(1);
    }, 1000);
);

myPromise.then(n => {
   console.log(n);
});

작업이 완료되고 또 다른 작업을 하고자 할 때 .then(...)를 붙여서 사용

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(new Error());
    }, 1000);
});

myPromise.then(n => {
    console.log(n);
})
.catch(error => {
    console.log(error);
});

성공 - .then() 메소드가 실행

실패 - reject() 사용

error - .catch() 메소드 사용

promise flow - 출처: MDN

 

Promise 코드 예제.

function getData() {
  return new Promise(function(resolve, reject) {
     $.get('url', function(response) {
       if(response) {
         resolve(response);
       }
       reject(new Error("Request is failed");
     });
   });
 }
 
 // $.get() 호출 결과에 따라 'response' 또는 Error'를 출력
getData().then(function(data) {
  console.log(data); // response 값 출력
}).catch(function(err) {
  console.error(err); // Error 출력
});

 

 

Promise 체이닝

Promise는 여러 개의 Promise를 연결해서 사용할 수 있다는 특징을 가진다. then() 메서드를 호출하고 나면 새로운 Promise 객체가 반환되어 다음과 같이 가능하다.

new Promise(function(resolve,reject) {
  setTimeout(function() {
    resolve(1);
  }, 2000);
})
.then(function(result) {
  console.log(result); // 1;
  return result + 10;
})
.then(function(result) {
  console.log(result); // 11
  return result + 20;
})
.then(function(result) {
  console.log(result); // 30
});

 

Promise 실무 예제(Promise 체이닝)

사용자 인증 로직 프로미스

var userInfo = {
  id: 'test@abc.com',
  pw: '****'
};

function parseValue() {
  return new Promise({
    // ...
  });
}

function auth() {
  return new Promise({
    // ...
  });
}

function display() {
  return new Promise({
    // ...
  });
}

getData(userInfo)
  .then(parseValue)
  .then(auth)
  .then(display);

 

 

Promise 에러처리

실제 서버를 구현하다보면 네트워크 연결 또는 서버 등의 문제로 오류가 발생할 수 있다. 이런 상황에 Promise의 에러 처리 방법은 다음과 같이 2가지가 있다.

 

1. then()의 두번째 인자로 에러를 처리한다.

getData().then(
  handleSuccess,
  handleError
);

2. catch()를 사용한다.

getData().then().catch();

2가지 방법 모두 Promise의 reject() 메서드가 호출되어 실패 상태가 된 경우에 실행된다.

function getData() {
  return new Promise(function(resolve, reject) {
    reject('failed');
  });
}

// 1. then()의 두번째 인자로 에러 처리
getData().then(function() {
  // ...
}, function(err) {
  console.log(err);
});

// 2. catch()로 예외 처리
getData().then().catch(function(err) {
  console.log(err);
});

더 많은 예외 처리 상황을 위해 가급적 catch() 사용 권장한다.

 

 

 

 


 

 

 

Async / await (ES8)

async function logName() {
  var user = await fetchUser('url');
  if(user.id == 1) {
    console.log(user.name);
  }
}

서버에서 사용자 데이터를 불러와 변수에 담고 사용자 아이디가 1이면 사용자 이름을 출력한다.

 

async는 함수 앞에 asyn를 붙이며 항상 promise를 반환한다. await은 HTTP 통신을 하는 비동기 처리 코드 앞에 붙인다. 여기서 주의해야할 점은 비동기 처리 메서드가 꼭 promise 객체를 반환해야 await이 의도한대로 동작한다. (async 함수가 아닌데 await를 사용하면 문법 에러 발생함)

일반적으로 await의 대상이 되는 비동기 처리 코드는 Axios 등 프로미스를 반환하는 API 호출함수이다.

 

예제.

funcion fetchItetms() {
  return new Promise(function(resolve, reject) {
    var items = [1, 2, 3];
    resolve(items);
  });
}

async function logItems() {
  var resultItems = await fetchItem();
  console.log(resultItems); // [1, 2, 3]
}

fetchItems()는 프로미스 객체를 반환하는 함수로 실행 시 프로미스가 이행(fullfilled)되면서 결과값(resolve)은 Items 배열이 된다.

logItems() 실행 시 fetchItems()함수의 결과 값인 Items배열이 resultItems 변수에 담긴다. 그래서 [1, 2, 3]이 콘솔에 출력된다.

 

await을 사용하지 않았다면 데이터를 받아온 시점에 콘솔을 출력할 수 있게 콜백함수나 .then() 등을 사용해야 했을 것이다. 하지만 async await 문법 덕택에 비동기에 대한 사고를 하지 않아도 된다.

 

실무 예제.

function fetchUser() {
  var url = 'url';
  return fetch(url).then(function(response) {
    return response.json();
  });
}

function fetchTodo() {
  var url ='url';
  return fetch(url).then(function(response) {
    return response.json();
  });
}

* fetch() api는 크롬과 같은 최신 브라우저에서만 동작한다. 다음 링크에서 브라우저 지원 여부 확인 - fetch API 브라우저 지원표

 

각각 사용자 정보와 할일 정보가 담긴 프로미스 객체를 반환한다.

두 함수를 이용해 할일 제목을 출력해보자. 살펴볼 코드의 로직은 아래와 같다.

 

1. fetchUser()로 사용자 정보 호출

2. 받아온 사용자의 ID가 1이면 할 일 정보 호출

3. 받아온 할 일 정보의 제목을 콘솔에 출력

 

async function logoTodoTitle() {
  var user = await fetchUser();
  if(user.id === 1) {
    var todo = await fetchTodo();
    console.log(todo.title);
  }
}

이 코드를 만약 콜백이나 프로미스로 작성했다면 코드가 훨씬 더 길어졌을 것이고 가독성이 좋지 않았을 것이다.

 

async/await 예외처리

async/await에서 예외를 처리하는 방법은 try catch 이다.

프로미스에서 에러처리를 위해 .catch()를 사용했던 것처럼 async 에서는 catch{} 사용하면된다.

async function logTodoTitle() {
    try {
        var user = await fetchUser();
        if(user.id === 1) {
            var todo = await fetchTodo();
            console.log(todo.title);
        }
    } catch (error) {
        console.log(error);
    }
}

위의 코드를 실행하다가 발생하는 네트워크 통신 오류 뿐 아니라 간단한 타입오류 등의 일반적인 오류까지도 catch로 잡아낼 수 있다. 발견된 error는 객체에 담기기 때문에 에러의 유형에 맞게 에러 코드를 처리해주면 된다.