JavaScript

비동기 프로그래밍(Callback, Promise, Async&await)과 AJAX - 2

마닐라 2022. 3. 18. 23:40

비동기 프로그래밍에 대해 학습하던 중 Callback, Promise, Async&await, Ajax, XMLHttpRequest, fetch API에 대한 개념을 보다 명확하게 정리하고자 작성하는 글입니다.

 

1편에서 Callback 지옥을 해결 해주는 것들이 Promise와 async&await라고 했습니다.

저것들의 개념은 무엇인지 어떤 방식으로 문제점을 해결해주는지에 대해서 알아보겠습니다.

 

Promise

1편에서 대표적인 비동기 함수에는 setTimeout가 있다고 했습니다.

그렇다면 Promise는 무엇일까요? Promise는 자바스크립트에 내장되어 있는 대표적인 비동기 '객체' 입니다.

 

비동기로 음성 파일을 생성해주는 createAudioFileAsync() 라는 함수가 있다고 가정해보겠습니다.

해당 함수는 첫번째 인자로 음성 설정에 대한 정보를 받고, 두번째로는 성공에 대한 콜백 함수, 세번째로는 실패에 대한 콜백 함수를 전달한다고 해보겠습니다.

 

function successCallback(result) {
  console.log("Audio file ready at URL: " + result);
}

function failureCallback(error) {
  console.log("Error generating audio file: " + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback);

 

그러면 코드는 위와 같이 작성될 것이고 함수의 인자로 콜백을 전달하지 않고 .then() 으로 붙여 사용할 수 있게 하는 것이 Promise 입니다.

then() 메서드는 두 개의 콜백 함수를 인자로 받습니다.

하나는 Promise가 작업을 성공(이행)했을 때 다른 하나를 실패(거절)했을 때를 위한 콜백 함수입니다.

따라서 createAudioFileAsync() 함수가 Promise 객체로 리턴이 된다면 아래와 같이 사용될 수 있습니다.

const promise = createAudioFileAsync(audioSettings);
promise.then(successCallback, failureCallback);

 

then()을 여러번 사용하여 여러개의 콜백을 추가할 수 있으며 각각의 콜백은 주어진 순서대로 하나하나 실행되게 됩니다.

Promise의 가장 뛰어난 장점 중에 하나는 'chaining' 입니다.

 

보통 하나나 두 개 이상의 비동기 작업을 순차적(동기적)으로 실행해야 하는 상황을 흔히 보게됩니다.

이것은 이전 단계 비동기 작업이 성공하고 나서 그 결과값을 이용하여 다음 비동기 작업을 실행해야 하는 경우를 의미합니다. Callback 함수는 1편에서 봤듯이 가독성이 매우 떨어지는 Callback 지옥이 펼쳐지게됩니다.

이러한 상황에서 promise chain을 이용하여 해결할 수 있습니다.

 

then() 함수는 값을 반환할 수도 있지만 새로운 promise를 반환합니다.

처음에 만들었던 promise와는 다른 새로운 promise입니다.

const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

또는

const promise2 = doSomething().then(successCallback, failureCallback);

 

successCallback or failureCallback 또한 promise를 반환하는 비동기 함수일 수도 있습니다.

이 경우 promise2에 추가 된 콜백은 successCallback또는 failureCallback에 의해 반환된 promise 뒤에 대기합니다.

 

Promise를 사용하지 않았을 때의 콜백 함수는 아래와 같이 작성해야 했습니다.

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

 

하지만 Promise를 이용한다면 위의 코드는 아래와 같이 바뀌게 됩니다.

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

또는

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

 

 

Promise는 3가지 상태 중 하나를 가집니다.

- 대기(Pending) : 이행하지도 거부하지도 않은 초기 상태

- 이행(Fulfilled) : 연산이 성공적으로 완료됨

- 거부(Rejected) : 연산이 실패함

 

Promise 동작 과정

 

 

대기(Pending) 상태는 Promise 객체를 호출한 상태입니다.

new Promise();

 

new Promise()의 인자로 콜백 함수를 2개 넣을 수 있고 그 콜백 함수의 첫번째 인자는 이행(성공)을 뜻하는 resolve를 두번째 인자는 거절(실패)을 뜻하는 reject입니다.

 

new Promise(function(resolve, reject) {
  // ...
});

 

콜백 함수의 인자 resolve를 아래와 같이 실행하면 이행 상태가 됩니다.

new Promise(function(resolve, reject) {
  resolve();
});

 

그리고 이행 상태가 되면 then을 이용하여 처리 결과 값을 받을 수 있습니다.

new Promise(function(resolve, reject) {
  resolve("성공");
})
.then(function(onFulfilled) {
	console.log(onFulfilled);
});
// 성공

 

콜백 함수의 인자 reject를 아래와 같이 실행하면 실패 상태가 됩니다.

new Promise(function(resolve, reject) {
  reject();
});

 

그리고 실패 상태가 되면 실패의 이유를 catch() 메서드를 통해 받을 수 있습니다.

new Promise(function(resolve, reject) {
  reject("거부");
})
.catch(function(onRejected) {
	console.log(onRejected);
});
// 거부

또는

new Promise(function(resolve, reject) {
        reject("거부");
    })
        .then(function(onFulfilled) {}
    		, function(onRejected) {
         console.log(onRejected);
    });
// 거부

then() 메서드에서 거절 상태의 콜백함수를 두번째 인자로 받는다고 했으니 위와 같이 작성해도 데이터를 받아올 수 있습니다.

 

let myFirstPromise = new Promise((resolve, reject) => {
  setTimeout( function() {
    resolve("성공!")  // 와! 문제 없음!
  }, 250)
})

myFirstPromise.then((successMessage) => {
  // successMessage는 위에서 resolve(...) 호출에 제공한 값입니다.
  // 문자열이어야 하는 법은 없지만, 위에서 문자열을 줬으니 아마 문자열일 것입니다.
  console.log("와! " + successMessage)
});

위의 코드는 수행한 비동기 작업이 성공한 경우 resolve(...)를 호출하고, 실패한 경우 reject(...)를 호출합니다.

위의 코드는 '와! 성공!' 이라는 로그를 출력하게됩니다.

 

다른 예제를 보겠습니다.

new Promise((resolve, reject) => {
    console.log('Initial');
    reject("거부");
    resolve("이행");
})
.then((resolve, reject) => {
    throw new Error('Something failed');
    console.log('Do this');
})
.catch((error) => {
    console.log(error)
    console.log('Do that');
})
.then(() => {
    console.log('Do this, whatever happened before');
});
// 출력
// Initial
// 거부
// Do that
// Do this, whatever happend

 

위의 코드는 성공시 "이행" 이라는 문자열을 실패시 "거부" 라는 문자열을 인자로 전달하게끔 했습니다.

그 다음 then() 메서드에서 의도적으로 Error를 발생시키고나서의 상황을 예측한 겁니다.

catch는 에러가 났기에 "거부"라는 문자열이 출력되고 그 다음의 then() 메서드로 다음 동작을 이어 가게됩니다. 

 

다음글

비동기 프로그래밍(Callback, Promise, Async&await)과 AJAX - 3

 

참고

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise

https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Using_promises

https://joshua1988.github.io/web-development/javascript/promise-for-beginners/