동기 비동기

younghyun·2022년 9월 3일
0

동기

요청 후 응답을 받아야 다음 동작을 실행하는 방식이다.

비동기

요청을 보낸 후 응답과 관계없이 다음 동작을 실행하는 방식이다.

잘못된 비동기 방식 예제

setTimeout 함수는 비동기 방식으로 처리되는 함수 중 하나
따라서 setTimeout 함수를 통해 1초 뒤에 작업을 수행하면 완료될 때까지 기다렸다가 다음 작업을 수행하는 것이 아니라 다음 작업이 먼저 실행이 되는 현상이 발생.

예제로 학교에 가서 공부하는 과정을 메시지로 출력하는 3가지 함수를 작성.
학교에 도착하는 것은 1초 뒤라고 가정하여 setTimeout 함수를 적용.

3가지 함수를 차례로 실행.

역시 올바르지 못한 결과가 나옴.

학교에 도착하기도 전에 열심히 공부를 하는 사태가 발생.
그리고 1초 뒤에 학교에 도착했다는 메시지가 출력 됨.

자바스크립트에서 동기와 비동기

자바스크립트는 단일 스레드 프로그래밍 언어로 단일 호출 스택이 있어 한 번에 하나의 일을 처리할 수 있습니다.
그러므로 자바스크립트는 동기 방식으로 진행이 됩니다. 하나의 호출 스택만 있기 때문에 하나의 함수를 처리하는데 매우 오랜 시간이 걸린다면 다음 실행해야할 함수에 지장을 줄 수 있다는 문제가 발생합니다.
예를 들어 웹 페이지를 사용자에게 보여줄 때 해당 웹 페이지에 있는 모든 데이터(사진, 글 등)를 받고 나서야 화면이 보여진다고 가정.
서버에서 데이터를 모두 받아올 때까지 시간이 오래 걸릴 수 있으며 사용자 입장에서 웹 페이지를 보는데 너무 느려서 답답할 수 있습니다.
그러나 데이터를 받아오는 일을 하는 도중에 우선 웹 페이지의 기본 레이아웃을 보여주고 보여줄 수 있는 것들을 우선 보여주는 것이 더 바람직할 것이다. 마치 세탁기가 돌아가는 도중에 라면을 끓이는 것처럼!
이것이 비동기의 필요성이다. 그리고 아래와 같은 방법으로 이 문제를 해결할 수 있습니다

  • 비동기적 callback 함수 사용
  • ES6 Promise
  • ES8 async await

이전의 비동기 구현 패턴이 가졌던 단점을 극복하기 위해 새로운 문법이 등장한 순서대로 나열한 것입니다. 단일 호출 스택을 가진 자바스크립트에서 비동기를 구현할 수 있게 한 위 세가지 방법을 차례대로 알아봅니다.

call back

콜백 함수란 다른 함수의 인자로 이용되는 함수이며 어떤 이벤트에 의해 호출되는 함수.
콜백 함수는 아래 코드와 같이 동기적으로 사용될 수도 있습니다.
primtImmediately 라는 이벤트 함수가 인자로 함수를 받는 코드라는 점을 주목합니다.

function printImmediately(callBackFunction) {
  callBackFunction()
}
printImmediately(()=>console.log('synchronous callback'))

단순히 인자로서 함수를 받아 그 함수를 실행합니다. 일반적인 자바스크립트 코드이기에 당연히 함수 호출 스택에 따라 동기적으로 실행됩니다.

우리가 구현하고자 하는 비동기적 과정은 여기서 일어나지 않습니다.
우리는 비동기적 callback 함수를 사용해야 합니다.
아래는 비동기적인 콜백 함수Asynchronous callback 예제
입니다

function printWithDelay(callback, sec){
  setTimeout(callback, sec*1000)
}
printWithDelay(()=>console.log("async callback"), 2)
console.log("hello")

동기적인 방식을 따른다면 printWithDelay() 가 모두 완료한 뒤 "hello"를 출력해야 합니다. 하지만
setTimeout 을 사용해 2초 뒤에 "async callback"이 비동기적으로 출력됩니다. 따라서 "hello"가 먼저 출력
이 되고 2초 뒤에 "async callback"이 출력이 됩니다.


현재는 데이터 값을 받고 싶은데, API요청해서 결과값을 받고 나서 내가 그 다음 내용을 진행해야 함.

다른 예시로 사용자가 어떤 버튼을 클릭했을 때 실행할 함수도 비동기 콜백 함수입니다.

addEventListener는 함수. click이 첫 번째 인자. 두 번째는 함수. click이 되고 나서 함수가 실행이 되어야 함.
click이 끝나고 실행이 되어야 함. 저기 안에 addEventListenr안에가 어떻게 구성되어있는지는 몰라도
click이 끝난 다음 function(){alert(‘a’)}를 돌려준다는 것을 알 수 있음.
그 이전까진 실행하지 않다가 클릭이라는 이벤트가 발생했을 때 콜백 함수를 실행하는 것입니다.

어떻게 비동기가 작동하나

앞서 자바스크립트는 하나의 호출스택을 가진 단일 스레드 프로그래밍 언어라고 말했다. 따라서 혼자서 비동기를 구현할 수 없다. 위 코드에서 어떻게 비동기가 구현된 것일까?

자바스크립트 엔진만으로는 비동기적으로 구현할 수 없으므로 자바스크립트 실행 환경(Runtime)은 브라우저에서 제공하는 Web API를 사용하여 비동기를 구현하게 됩니다. DOM 이벤트, setTimeout과 같은 비동기 함수는 web API를 호출하여 콜백 함수를 콜백 큐에 넣습니다. 콜백 함수들이 담긴 큐는 특정 시점에서 콜백을 실행시키는 방식입니다.
출처 및 자세한 내용: https://new93helloworld.tistory.com/361

콜백 지옥

비동기 구현을 위한 첫 번째 방법인만큼 큰 문제가 있습니다.
콜백 함수가 콜백 함수를 부르고, 그 콜백 함수가 또 다른 콜백함수를 부르는 이른바 콜백 지옥이 발생하는 것입니다.

많은 중첩함수가 생겨 가독성과 유지보수면에 끔찍한 코드가 발생한다. 이를 해결하기 위해 Promise가 나오게
됩니다.

Promise

자바스크립트 비동기 처리(특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 자바스크립트의 특성)에 사용되는 객체.

Promise의 3가지 상태 및 처리 흐름

pending(대기) : 처리가 완료되지 않은 상태
fulfilled(이행) : 성공적으로 처리가 완료된 상태
rejected(거부) : 처리가 실패로 끝난 상태

Promise 객체가 비동기 함수의 처리 상태를 보고 완료되었는지 판단하여 성공 여부에 따라 다음 처리를 다르게 수행할 수 있도록 함.

Promise 생성

프로미스는 Promise 생성자 함수를 통해 인스턴스화. Promise 생성자 함수는 비동기 작업을 수행할 콜백 함수를 인자로 전달받는데 이 콜백 함수는 resolve와 reject 함수를 인자로 전달받음.

// Promise 객체의 생성
const promise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행한다.

  if (/* 비동기 작업 수행 성공 */) {
    resolve('result');
  }
  else { /* 비동기 작업 수행 실패 */
    reject('failure reason');
  }
});

Promise는 비동기 처리가 성공(fulfilled)하였는지 또는 실패(rejected)하였는지 등의 상태(state) 정보를 가짐.

Promise 생성자 함수가 인자로 전달받은 콜백 함수는 내부에서 비동기 처리 작업을 수행. 이때 비동기 처리가 성공하면 콜백 함수의 인자로 전달받은 resolve 함수를 호출. 이때 프로미스는 ‘fulfilled’ 상태가 됨. 비동기 처리가 실패하면 reject 함수를 호출. 이때 프로미스는 ‘rejected’ 상태가 됨.

const promiseAjax = (method, url, payload) => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.setRequestHeader('Content-type', 'application/json');
    xhr.send(JSON.stringify(payload));

    xhr.onreadystatechange = function () {
      // 서버 응답 완료가 아니면 무시
      if (xhr.readyState !== XMLHttpRequest.DONE) return;

      if (xhr.status >= 200 && xhr.status < 400) {
        // resolve 메소드를 호출하면서 처리 결과를 전달
        resolve(xhr.response); // Success!
      } else {
        // reject 메소드를 호출하면서 에러 메시지를 전달
        reject(new Error(xhr.status)); // Failed...
      }
    };
  });
};

위 예제처럼 비동기 함수 내에서 Promise 객체를 생성하고 그 내부에서 비동기 처리를 구현.
이때 비동기 처리에 성공하면 resolve 메소드를 호출. 이때 resolve 메소드의 인자로 비동기 처리 결과를 전달. 이 처리 결과는 Promise 객체의 후속 처리 메소드로 전달. 만약 비동기 처리에 실패하면 reject 메소드를 호출. 이때 reject 메소드의 인자로 에러 메시지를 전달. 이 에러 메시지는 Promise 객체의 후속 처리 메소드로 전달.

Promise 기본 예제


구조를 살펴보면 사실 콜백 함수를 사용한 방법과 유사한 형태에서 Promise 객체를 적용하여 리턴한 정도.

이 함수를 다음과 같이 호출하여 사용할 수 있음.

일단 학교에 가는 함수는 동일하게 호출하면 되구요.

arriveAtSchool_tobe 함수는 Promise 객체를 리턴하는데 Promise는 then이라는 메서드를 가지고 있고 그 메서드 파라미터에 콜백 함수를 대입하면 앞서 resolve()라고 정의했던 구문이 실행이 되는 구조.
구현의 차이는 있지만 아직까진 직접 콜백 함수를 작성한 것과 의미상으로 큰 차이는 없어 보임.
일단 결과를 확인해 보면 다음과 같이 정상적으로 수행된 것을 확인할 수 있음.

학교에 도착했다는 메시지는 1초 뒤에 출력이 되고 그 이후에 열심히 공부한다는 메시지가 출력.

Promise 후속 처리 메서드

Promise로 구현된 비동기 함수는 Promise 객체를 반환하여야 한다. Promise로 구현된 비동기 함수를 호출하는 측(promise consumer)에서는 Promise 객체의 후속 처리 메소드(then, catch)를 통해 비동기 처리 결과 또는 에러 메시지를 전달받아 처리. Promise 객체는 상태를 갖는다고 하였다. 이 상태에 따라 후속 처리 메소드를 체이닝 방식으로 호출. Promise의 후속 처리 메소드는 아래와 같음.

앞에서 프로미스로 정의한 비동기 함수 get을 사용. get 함수는 XMLHttpRequest 객체를 통해 Ajax 요청을 수행하므로 브라우저에서 실행

<!DOCTYPE html>
<html>
<body>
<!DOCTYPE html>
<html>
<body>
  <pre class="result"></pre>
  <script>
    const $result = document.querySelector('.result');
    const render = content => { $result.textContent = JSON.stringify(content, null, 2); };

    const promiseAjax = (method, url, payload) => {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open(method, url);
        xhr.setRequestHeader('Content-type', 'application/json');
        xhr.send(JSON.stringify(payload));

        xhr.onreadystatechange = function () {
          if (xhr.readyState !== XMLHttpRequest.DONE) return;

          if (xhr.status >= 200 && xhr.status < 400) {
            resolve(xhr.response); // Success!
          } else {
            reject(new Error(xhr.status)); // Failed...
          }
        };
      });
    };

    /*
      비동기 함수 promiseAjax은 Promise 객체를 반환한다.
      Promise 객체의 후속 메소드를 사용하여 비동기 처리 결과에 대한 후속 처리를 수행한다.
    */
    promiseAjax('GET', 'http://jsonplaceholder.typicode.com/posts/1')
      .then(JSON.parse)
      .then(
        // 첫 번째 콜백 함수는 성공(fulfilled, resolve 함수가 호출된 상태) 시 호출된다.
        render,
        // 두 번째 함수는 실패(rejected, reject 함수가 호출된 상태) 시 호출된다.
        console.error
      );
  </script>
</body>
</html>

Promise 고급 예제

기본 예제에서는 전부 다루지 않았지만 Promise 객체는 2가지의 콜백 함수를 가짐.
하나는 앞서 언급한 fulfilled 상태에서 실행되는 resolve 함수( 비동기 처리 결과값이 성공이면 resolve함수 인자로 들어감. resolve함수는 결과값을 Promise로 Wrapping. 이 Promise는 이행 프로미스가 되고 인스턴스 메서드/후속 처리 메서드로 then을 가짐), rejected 상태일 경우 실행되는 reject 함수.
( 비동기 처리 결과값이 성공이면 reject함수 인자로 들어감. reject함수는 결과값을 Promise로 Wrapping. 이 Promise는 인스턴스 메서드/후속 처리 메서드로 catch를 가짐 )

랜덤 함수를 이용하여 숫자 0 또는 1을 status 변수에 담은 후에 1일 경우 성공, 0일 경우 실패라고 가정하고 각각의 메시지를 콜백 함수로 넘겨 줌.
또한 실패일 경우 처리해 줄 cure 함수를 하나 작성.

작성한 함수를 다음과 같이 수행해봄.

먼저 마찬가지로 학교에 가는 함수를 실행.
학교에 도착하는 함수는 성공 또는 실패하는 두 가지 경우를 만듦.
성공 시에는 then 메서드가 실행되어 resolve 함수를 통해 넘겨준 문장을 실행.
만약 실패하여 reject 함수가 실행되는 경우는 catch 메서드를 통해 reject 콜백 함수를 수행하게 됨.
reject일 때는 then 메서드가 실행되지 않으므로 성공, 실패를 각각 분리해서 처리할 수 있게 되는 것.
콜백 함수 작성 방법에서도 콜백 함수를 2개 넘겨주면 비슷하게 처리가 가능.

두 가지 경우의 결과를 비교.

각각의 경우 다른 처리를 해 줄 수 있다는 것이 명확하게 보임.
사실 reject인 경우는 명시적으로 에러 객체를 만들어 넘겨주는 것이 더 일반적이고 올바른 방법.

catch구문으로 예외 처리를 해주지 않으면 이런 식으로 에러가 발생.

이렇게 되면 프로그램 완성도가 떨어져 보일 뿐만 아니라 빨간 글씨는 좀 거부감이 듦.
따라서 catch 구문으로 에러를 인지하여 처리를 해 주는 것이 좋음.

에러 관련 정보를 보여주고 적절한 처리를 할 수 있게 됨.
성공, 실패 콜백 함수를 다음과 같이 호출할 수도 있음.

단일 호출에서는 별 문제가 없지만 아래에서 설명할 체이닝 기법 사용 시에는 catch 메서드 사용을 권장

Promise의 장점

Promise 연결하기(체이닝)

비동기로 연산이 이루어지는 Promise 객체가 적용된 add 함수가 있다고 가정

then 메서드에서 값을 return 키워드를 사용하면 결과 값이 기본 자료형이 아닌 Promise 객체로 반환되기 때문에 이와같은 체인 형식이 가능하게 됨.

then 메서드 내에서 직접 Promise를 return 할 수도 있음.

각각의 함수가 Promise 객체를 리턴하는 비동기 작업이라고 가정한다면 위와 같이 then 메서드를 연속적으로 사용하여 순차적인 작업을 할 수도 있음.
체이닝 기법을 활용함으로써 콜백 함수 사용 시 발생할 수 있는 콜백 지옥에서 탈출이 가능.

Promise 예외(에러) 처리


학교에 가는 도중에 다치거나 공부를 하다가 열이 나거나 점심을 먹다가 체하는 일이 생기더라도 마지막 catch 구문에서 조퇴하는 것으로 한 번에 처리가 가능하다는 의미.
이 경우 최초 발생하는 rejected 상태의 작업만 처리하고 구문을 빠져 나오게 됨.

비동기 처리 시에 발생한 에러는 then 메서드의 두 번째 콜백 함수로 처리할 수 있음.

const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';

// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseAjax(wrongUrl)
  .then(res => console.log(res), err => console.error(err)); // Error: 404

비동기 처리에서 발생한 에러는 Promise 객체의 후속 처리 메서드 catch를 사용해서 처리할 수도 있음.

onst wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';

// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseAjax(wrongUrl)
  .then(res => console.log(res))
  .catch(err => console.error(err)); // Error: 404

catch 메서드를 호출하면 내부적으로 then(undefined, onRejected)을 호출. 위 예제는 내부적으로 다음과 같이 처리

const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';

// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseAjax(wrongUrl)
  .then(res => console.log(res))
  .then(undefined, err => console.error(err)); // Error: 404

단, then 메서드의 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못하고 코드가 복잡해져서 가독성이 좋지 않음.

promiseAjax('https://jsonplaceholder.typicode.com/todos/1')
  .then(res => console.xxx(res), err => console.error(err));
  // 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못한다.

catch 메서드를 모든 then 메서드를 호출한 이후에 호출하면 비동기 처리에서 발생한 에러(reject 함수가 호출된 상태)뿐만 아니라 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있음.

promiseAjax('https://jsonplaceholder.typicode.com/todos/1')
  .then(res => console.xxx(res))
  .catch(err => console.error(err)); // TypeError: console.xxx is not a function

또한 then 메서드에 두 번째 콜백 함수를 전달하는 것보다 catch 메서드를 사용하는 것이 가독성이 좋고 명확. 따라서 에러 처리는 then 메서드에서 하지 말고 catch 메서드를 사용하는 것을 권장.

Promise 정적 메소드

7.1 Promise.resolve/Promise.reject

Promise.resolve와 Promise.reject 메소드는 존재하는 값을 Promise로 래핑하기 위해 사용.
정적 메소드 Promise.resolve 메소드는 인자로 전달된 값을 resolve하는 Promise를 생성.

const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [ 1, 2, 3 ]

위 예제는 아래 예제와 동일하게 동작

const resolvedPromise = new Promise(resolve => resolve([1, 2, 3]));
resolvedPromise.then(console.log); // [ 1, 2, 3 ]

Promise.reject 메소드는 인자로 전달된 값을 reject하는 프로미스를 생성.

const rejectedPromise = Promise.reject(new Error('Error!'));
rejectedPromise.catch(console.log); // Error: Error!

위 예제는 아래 예제와 동일하게 동작.

const rejectedPromise = new Promise((resolve, reject) => reject(new Error('Error!')));
rejectedPromise.catch(console.log); // Error: Error!

7.2 Promise.all

Promise.all 메소드는 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받음. 그리고 전달받은 모든 프로미스를 병렬로 처리하고 그 처리 결과를 resolve하는 새로운 프로미스를 반환.

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(console.log) // [ 1, 2, 3 ]
  .catch(console.log);

Promise.all 메소드는 3개의 프로미스를 담은 배열을 전달받았다. 각각의 프로미스는 아래와 같이 동작.

첫번째 프로미스는 3초 후에 1을 resolve하여 처리 결과를 반환.
두번째 프로미스는 2초 후에 2을 resolve하여 처리 결과를 반환.
세번째 프로미스는 1초 후에 3을 resolve하여 처리 결과를 반환.
Promise.all 메소드는 전달받은 모든 프로미스를 병렬로 처리. 이때 모든 프로미스의 처리가 종료될 때까지 기다린 후 아래와 모든 처리 결과를 resolve 또는 reject.

모든 프로미스의 처리가 성공하면 각각의 프로미스가 resolve한 처리 결과를 배열에 담아 resolve하는 새로운 프로미스를 반환. 이때 첫번째 프로미스가 가장 나중에 처리되어도 Promise.all 메소드가 반환하는 프로미스는 첫번째 프로미스가 resolve한 처리 결과부터 차례대로 배열에 담아 그 배열을 resolve하는 새로운 프로미스를 반환. 즉, 처리 순서가 보장됨.

프로미스의 처리가 하나라도 실패하면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환.

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 1!')), 3000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 2!')), 2000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 3!')), 1000))
]).then(console.log)
  .catch(console.log); // Error: Error 3!

위 예제의 경우, 세번째 프로미스가 가장 먼저 실패하므로 세번째 프로미스가 reject한 에러가 catch 메소드로 전달.

Promise.all 메소드는 전달 받은 이터러블의 요소가 프로미스가 아닌 경우, Promise.resolve 메소드를 통해 프로미스로 래핑

Promise.all([
  1, // => Promise.resolve(1)
  2, // => Promise.resolve(2)
  3  // => Promise.resolve(3)
]).then(console.log) // [1, 2, 3]
  .catch(console.log);

아래는 github id로 github 사용자 이름을 취득하는 예제

const githubIds = ['jeresig', 'ahejlsberg', 'ungmo2'];

Promise.all(githubIds.map(id => fetch(`https://api.github.com/users/${id}`)))
  // [Response, Response, Response] => Promise
  .then(responses => Promise.all(responses.map(res => res.json())))
  // [user, user, user] => Promise
  .then(users => users.map(user => user.name))
  // [ 'John Resig', 'Anders Hejlsberg', 'Ungmo Lee' ]
  .then(console.log)
  .catch(console.log);

위 예제의 Promise.all 메소드는 fetch 함수가 반환한 3개의 프로미스의 배열을 인수로 전달받고 이 프로미스들을 병렬 처리.
모든 프로미스의 처리가 성공하면 Promise.all 메소드는 각각의 프로미스가 resolve한 3개의 Response 객체가 담긴 배열을 resolve하는 새로운 프로미스를 반환하고 후속 처리 메소드 then에는 3개의 Response 객체가 담긴 배열이 전달. 이때 json 메소드는 프로미스를 반환하므로 한번 더 Promise.all 메소드를 호출해야 하는 것에 주의.
두번째 호출한 Promise.all 메소드는 github로 부터 취득한 3개의 사용자 정보 객체가 담긴 배열을 resolve하는 프로미스를 반환하고 후속 처리 메소드 then에는 3개의 사용자 정보 객체가 담긴 배열이 전달

7.3 Promise.race

Promise.race 메소드는 Promise.all 메소드와 동일하게 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받음.
그리고 Promise.race 메소드는 Promise.all 메소드처럼 모든 프로미스를 병렬 처리하는 것이 아니라 가장 먼저 처리된 프로미스가 resolve한 처리 결과를 resolve하는 새로운 프로미스를 반환.

Promise.race([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(console.log) // 3
  .catch(console.log);

에러가 발생한 경우는 Promise.all 메소드와 동일하게 처리. 즉, Promise.race 메소드에 전달된 프로미스 처리가 하나라도 실패하면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환.

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 1!')), 3000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 2!')), 2000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 3!')), 1000))
]).then(console.log)
  .catch(console.log); // Error: Error 3!

async, await

async await 는 비동기처리의 최신문법입니다.
기존의 promise와 다른 것은 아니고, syntatic sugar일 뿐이다.

promise를 사용할 경우에 callback처럼 chaining이 일어나는 것은 마찬가지입니다.
따라서 콜백 지옥의 문제가 어느정도 나타날 수 있다는 것입니다.

then안에서 Promise가 있는 함수를 부르면 또 옆으로 길어집니다.
물론 일반적인 콜백 지옥 보단 짧지만
하지만 async await을 사용하면 promise를 '깔끔한 스타일'로 작성할 수 있습니다.
그러나 무조건 async await이 절대적으로 깔끔한 방법은 아니고 상황에 따라 적절한 것을 선택하면 됩니다.

async 기본 예제


function 앞에 async 키워드만 붙여주면 됨. async 함수에서는 Promise가 아닌 값을 리턴하더라도 resolved promise가 반환됨.

위와 같이 hello가 출력됨.

비교를 위해 greet 함수 리턴 값 자체를 찍어보면 명확하게 이해가 가능함.

위와 같이 Promise 객체가 반환되는 것을 알 수 있음. async 키워드가 없었다면 바로 hello가 출력.

명시적으로 resolved promise 반환도 가능.

마찬가지로 hello가 출력.

추가 설명

function 앞에 async를 선언한 함수는 항상 Promise 객체를 반환. Promise가 아닌 값을 리턴하더라도 내부적으로 Promise로 감싸서 리턴. 이어서 설명할 async 함수 안에서만 사용할 수 있는 await 키워드는 매우 유용.

await 기본 예제

예시 1

앞에 async / await 키워드만 붙여주면 비동기 작업의 순차 처리가 일반 순차 프로그래밍과 동일하게 가능함.

1초 후 hello가 출력.

async 못 붙임. 직접 만든 함수가 아니어서, fetch 이제 동기로 실행 됨.

예시 2

const getTodo = async (id) => {
  const todoResponse = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
  const todo = await todoResponse.json()
  return todo;
}

getTodo(172).then(console.log)

fetch 함수에서 사용한 API는 무료 가짜 데이터 REST API를 제공하는 API 주소입니다.
간단한게 데이터를 가져오는 과정을 async await 문법을 사용한 코드입니다. async 키워드는 함수 앞에 붙이는 키워드이며 await 키워드는 async 키워드가 붙어 있는 함수 내부에서만 사용할 수 있습니다. await 라는 코드로 의도한 순서대로 코드의 흐름을 제어하고 있습니다.
일반적인 동기 코드 처리와 동일한 흐름으로 코드를 작성할 수 있기에 코드 읽기가 수월 해집니다.
한 가지 주의할 것은 async 함수를 호출할 때 명시적으로 promise 객체를 생성하여 리턴 하지 않아도 promise 객체가 리턴 됩니다. 따라서 호출하는 코드를 보면 then() 매서드를 사용하여 결과값을 출력하고 있다.
async await의 또다른 장점은 동기와 비동기 구분없이 try catch 로 일관된 예외 처리를 할 수 있다는 점입니다.

const myFunction = async (postId) => {
  const postResponse = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${postId}`)
  const post = await postResponse.json()
  const userId = post.userId

  try {
    const userResponse = await fetch(
      // 1번 주소: `https://jsonplaceholder.typico`
      // 2번 주소: `https://jsonplaceholder.typicode.com/users/${userId}`
    )
    const user = await userResponse.json()
    return user.name || "no data"
  } catch (err) {
    console.log(err)
    return "Unknown"
  }
}

myFunction(15).then(console.log)

위 코드에서 2번 주소가 정상적으로 작동하는 코드입니다. postId 를 주면 해당 포스트의 userId 로부터 다시 user 의 이름을 얻어오는 코드입니다. 2번 주소일 때, postId 혹은 userId 가 DB에 없을 경우 빈 객체가 반환되는데 그 때는 "no data" 를 반환하도록 하였습니다. 데이터가 존재할 경우 user.name 을 정상적으로 반환합니다.
그리고 1번 주소의 경우 에러 상황으로 err를 출력하고 (위 코드에서는 Error: 'Failed to fetch' 라는 에러 발생) "Unknown"을 반환하게 됩니다.

참고
https://joshua1988.github.io/web-development/javascript/promise-for-beginners/
https://sangminem.tistory.com/284
https://velog.io/@open_h/javascriptasync
https://poiemaweb.com/es6-promise
https://joshua1988.github.io/web-development/javascript/js-async-await/

profile
선명한 기억보다 흐릿한 메모

0개의 댓글