Notice
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

코린이 탈출기

[JavaScript] Callback, Promise, Async/Await 본문

FE 공부

[JavaScript] Callback, Promise, Async/Await

명란파스타 2020. 11. 5. 20:55

저번 시간에 자바스크립트의 비동기 흐름에 대해 알아보았습니다.

이번에는 자바스크립트에서 비동기 흐름을 제어할 수 있는 여러 방법에 대해 알아보도록 하겠습니다 ~

 

Callback

자바스크립트는 이벤트 중심의 언어입니다.

즉, 자바스크립트는 이벤트 값이 반환될 때까지 기다리지 않고 다음 이벤트를 계속 실행하게 되죠.

function func1(){
	console.log('first');
    func2();
}

function func2(){
	setTimeout(function(){
    	console.log('second');
    }, 0);
    func3();
}

function func3(){
	console.log('third');
}

func1();

//출력값
// first -> third -> second

저번시간에 본 예시를 다시 보자면, 자바스크립트는 func2의 응답을 기다린 후에 func3를 실행하는 게 아니라는 것을 알 수 있죠.

 

따라서, 어떠한 작업의 수행을 마친 뒤 원하는 다음 작업을 수행할 수 있게 하도록 해주는 것이 콜백함수의 역할입니다.

 

"콜백", 이름의 의미

다른 함수가 실행을 끝낸 뒤 실행되는 (call back) 함수를 말합니다.

var f1 = function(arg, callback) {
    $.ajax({
        success: function(data) {
            callback(data);
        }
    });
}
// 데이터를 성공적으로 받으면 파라미터로 넘겨준 콜백함수에 값이 넘어가 실행됨
f1(arg, function(data) { 
           var a = f2(data);
           alert(a);
        }
 );

이렇게 콜백을 이용해서 특정 로직이 끝났을 때 원하는 함수를 실행시킬 수 있습니다.

 

문제점: 콜백지옥

func1(function(err, result){
	func2(function(err, result){
    		func3(function(err, result){
        		func4(function(err, result){
            			func5(function(err, result){
                			//do something
                		});
           		});
     		});
 	});
});

콜백함수에서는 만약 함수의 return 값을 받은 후 다음 함수를 실행하고, 그 함수의 return 값을 또 다음 함수에서 사용해야 한다면 call back 함수를 계속 중첩으로 사용해야 합니다.

이러한 코드를 콜백지옥이라고 하며, 이는 가독성이 떨어지고 코드 유지보수를 어렵게 만듭니다.

이러한 콜백 지옥을 해결하기 위해서 등장한것이 ES6의 Promise 입니다!

 

Promise

Promise란 어떤 작업의 중간 상태를 나타내는 오브젝트입니다.

미래에 어떤 종류의 결과가 반환됨을 약속해주는 오브젝트라고 볼 수 있습니다.

Promise 객체는 비동기 작업이 맞이할 미래의 완료/실패 정보와 그 결과값을 나타내는데요,

크게 3가지 상태값이 있습니다.

- Pending(대기): 비동기 처리 로직이 아직 완료되지 않은 상태

- Fulfilled(이행): 비동기 처리가 완료되어 프로미스가 결과값을 반환해준 상태

- Rejected(실패): 비동기 처리가 실패하거나 오류가 발생한 상태

 

그럼, 이 Promise 객체는 어떻게 생성할까요?

Promise도 보통 인스턴스 생성처럼 new 키워드를 통해 하나의 객체를 생성합니다.Promise는 하나의 콜백함수를 인자로 받습니다.새로운 Promise가 생성되는 즉시, 인자로 받아지는 함수도 실행되며 이 함수를 executor, 실행자 함수라고 부릅니다.해당 실행자 함수는 다시 2개의 함수(resolve, reject)를 인자로 받습니다.

 

실행자 함수가 실행되면 함수 내부에서 비동기 작업이 이루어지고, 만약 비동기 작업이 성공하면 그 성공값을 인자로 resolve() 함수를 호출하고만약 비동기 작업이 실패하면 그 실패값을 인자로 reject()함수를 호출하게 됩니다.

 

그리고, 이 Promise의 에러핸들링은 then, catch를 통해 매우 간결하게 할 수 있습니다.

function getData() {
  return new Promise(function(resolve, reject) {
    $.get('url 주소/products/1', 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 출력
});

 

어떤 작업 이후에 수행되어야 할 작업들이 순서대로 많다면, then 구문을 이어붙여서 쉽게 구현할 수 있습니다. 아래의 예제처럼 Promise에서 하는 이러한 처리를 Promise Chaining이라고 합니다.then 메소드와 catch 메소드의 반환값이 또 다른 프로미스 객체이기 때문에 서로 Chaining이 가능해졌습니다.

 

const successPromise = new Promise((resolve, reject) => {
  setTimeout(function () {
    resolve("Success");
  }, 3000);
});

const anotherPromise = (value) => {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve(`${value} not`);
    }, 1000);
  });
};

successPromise
  .then((value) => `${value} is`) //  `${value} is`를 결과 값으로 가진 Promise 객체 생성
  .then((secondValue) => anotherPromise(secondValue)) // 다른 프로미스가 처리될 때까지 기다리다가 처리가 완료되면 그 결과를 받음.
  .then((thirdValue) => console.log(thirdValue + " impossible"))
  .catch((error) => {
    errorHandling(error);
    return "again?"; // catch 메소드 이후에도 체이닝 가능.
  })
  .then((lastValue) => console.log(lastValue));

// 약 4초 후에 "Success is not impossible"을 출력

위 코드의 실행 순서를 알아봅시다.

1. successPromise와 anotherPromise가 생성됩니다.(실행되는 것은 아닙니다)

2. successPromise를 실행합니다.

2-1. new Promise()의 인스턴스가 리턴되며 콜백함수로 resolve, reject를 인수로 가지는 익명함수가 호출됩니다.

      이때 Promise의 상태값은 Pending입니다.

2-2. 콜백 함수의 인자 resolve를 실행하면 Fulfilled 상태가 됩니다.

2-3. resolve의 결과값("Success")은 then()을 이용하여 받을 수 있습니다.

3. 첫번째 then이 완료되면 이 반환값으로 `Success is`를 결과값으로 가지는 Promise가 생깁니다. 

4. 'Success is'를 인자로 하는 anotherPromise가 실행됩니다.

5. 1초 뒤에 resolve()를 통해 'Success is not'이라는 결과값을 then()에서 받을 수 있습니다.

6. 마지막 then()에서 "Success is not impossible"을 출력합니다.

 

결과값

 

Error Handling 예제도 볼까요?

successPromise
            .then((value) => `${value} is`) //  `${value} is`를 결과 값으로 가진 Promise 객체 생성
            .then((secondValue) => anotherPromise(secondValue)) // 다른 프로미스가 처리될 때까지 기다리다가 처리가 완료되면 그 결과를 받음.
            .then((thirdValue) => {throw new Error( thirdValue + " possible")})
            .catch((error) => {
                console.log(error)
                return "again?"; // catch 메소드 이후에도 체이닝 가능.
            })
            .then((lastValue) => console.log(lastValue));

결과값

정리

콜백함수를 통한 비동기 처리시 발생하는 콜백 지옥을 해결할 수 있습니다.

Return Value를 통한 Chaining이 가능합니다.

Error Handling이 동기식 코드와 유사하게 쓰일 수 있습니다.(try... catch문)

 

-> Promise를 통해 비동기 흐름을 동기적 흐름과 유사하게 만들 수 있습니다.

 

Async/Await

자바스크립트 ES8에서는 Async/Await가 등장합니다.

Promise의 결과값을 then, catch로 다루는 것이 아니라 변수에 담아서 동기적 코드처럼 작성할 수 있다는 점에서 편리함을 제공합니다.

 

함수에 async를 붙여주고 비동기로 처리되는 곳에 await를 추가합니다.

await 뒷부분은 반드시 promise를 반환해야하며, async 자체도 promise를 반환해야 합니다.

 

function doubleAfter2Seconds(x) {
  console.log('value:' + x)
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x * 2);
    }, 2000);
  });
}

async function addAsync(x) {
  const a = await doubleAfter2Seconds(10);
  const b = await doubleAfter2Seconds(20);
  const c = await doubleAfter2Seconds(30);
  return x + a + b + c;
}

addAsync(10).then((sum) => {
  console.log('result:' + sum);
});

위의 코드를 봅시다.

async 키워드가 붙은 addAsync 함수가 비동기 처리를 할 함수입니다.

async에서 값을 리턴하면 promise는 그 값을 받아서 resolve됩니다.

async 함수는 await 표현식을 포함하고 있으며, async 함수에 promise 값이 전달되기 전까지 실행을 지연시킵니다.

 

await 키워드를 사용함으로써, 기존에는 실행 순서가 예측 불가능했던 비동기 작동 방식이 동기적으로 실행되는 코드처럼 예측 가능해졌습니다.

console.log(1);

    const promise = function () {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(3);
                resolve("2");
            }, 3000);
        });
    };

    const promise2 = function () {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(4);
                resolve("1");
            }, 1000);
        });
    };

    console.log(2);

    async function foo() {
        const result = await promise(); // 프라미스가 이행될 때까지 아래 코드로 넘어가지 않음..
        const result2 = await promise2(); // 위 코드의 프로미스가 반환될 때까지 대기...

        console.log(result)
        console.log(result2); // 완료 되면 하단의 코드가 이어서 실행됨

        const Parallel1= promise(); // 위 아래 타이머는 동시에 시작됨.
        const Parallel2 = promise2(); // 해당 프로미스 이행 값이 먼저 반환됨.(약 1초)

        console.log(await Parallel1);
        console.log(await Parallel2); // 먼저 프로미스 객체가 반환되었지만 위 함수가 먼저 실행되어야 실행됨.
    }

    foo(); // 콘솔에 찍히는 값은 순서대로 1 2 3 4 2 1 4 3 2 1

 

에러처리는 try ...  catch문을 통해서 합니다.

async function getName() {
  try {
    const user = await Promise.reject(new Error("Error!!"));
    const name = user.name;  // 아래 코드는 실행되지 않음
    if (name === "downy") {
      return name;
    }
  } catch (error) {
    console.log(error); // "Error!!"를 출력
  }
}

try문 내의 어느 곳이든지 에러가 발생하면 제어 흐름이 catch 블록으로 넘어갑니다.

이 또한 마찬가지로, 동기식 코드에서 에러 핸들링을 하는 것과 유사하다는 점에서 장점을 발휘합니다.

 

 

이렇게 자바스크립트에서 비동기 제어를 위해 사용하는 Callback, Promise, Async/Await에 대해서 알아보았습니다.

자바스크립트에서는 비동기 프로그래밍이 핵심인 만큼 세가지의 사용법과 차이점을 잘 알아두어야겠습니다 !