컴퓨터 공부/🕸️ Web

Javascript - Callbacks, ES6 Promise, Async, Await

letzgorats 2023. 12. 18. 14:24

< Callbacks, Promise 그리고 Async, Await >

자바스크립트는 싱글스레드이다.

그래서 하나의 일을 할 때, 하나밖에 못하는데, 만약 그 하나의 일이 오래 걸리는 일이면 어떻게 될까?

그 하나의 일이 오래걸리기에 다른 직업들은 그 하나의 일이 끝날때 까지 기다려야 한다.

 

→ 이러한 문제점을 해결하기 위해서 비동기로 어떠한 일을 수행하게 된다.

 

동기 요청
비동기 요청

 

※ 만약 비동기 요청이 여러 개 있을 때, 하나의 요청이 다른 요청의 결과에 의존한다면?

(예를 들어, 요청1의 응답을 이용해서 요청2를 수행해야 한다면?)

 

아래 코드에서처럼 둘 다 비동기 요청을 보내는데, 두 번째 요청이 첫 번째 요청의 결과가 필요할 수가 있다. 하지만, 둘 다 병렬적으로 요청을 보내기 때문에, response1을 가지기 전에 두 번째 요청이 보내지게 된다.

// 1st request

const response1 = request('http://abc.com');

// 2nd request

const response2 = request('http://bcd.com',response1);

 

이런 부분은 어떻게 처리해줘야 하는 것일까?

 

→ 위와 같은 문제를 해결하는 3가지 방법

Callback 함수, Promise, Async Await 을 이용하는 것이다!

비동기 요청을 해결하는 3가지 방법

 

[ 콜백 함수 ]

: 콜백함수는 특정 함수에 매개변수로 전달된 함수를 의미한다. 그리고 그 콜백 함수는 함수를 전달받은 함수 안에서 호출된다.

function firstFunction(parameters, callback){
    // do something
    const response1 = request(`http://abc.com?id=${parameters.id}`);
    callback(response1);
}

function secondFunction(response1, callback){
    const response2 = request('http://bcd.com',response1);
    callback();
}

firstFunction(para, function (response1){
    secondFunction(response1, function () {
        thirdFunction(para, function () {
            // ...
        })
    })
})

 

- 콜백 사용의 단점 - 

1. 위에서 볼 수 있듯이 소스코드를 보는데, 가독성이 떨어진다.

2. 에러 처리를 한다면, 모든 콜백에서 각각 에러 핸들링을 해줘야 한다.

 

[ Promise ]

: Promise 객체는 new 키워드와 생성자를 사용해 만든다. 생성자는 매개변수로 "실행 함수"를 받는다.

: 이 함수는 매개변수로 두 가지 함수를 받아야 하는데, 첫 번째 함수(resolve)는 비동기 작업을 성공적으로 완료해 결과를 값으로 반환할 때 호출해야 한다.

: 두 번째 함수(reject)는 작업이 실패하여 오류의 원인을 반환할 때 호출하면 된다.두 번째 함수는 주로 오류 객체를 받는다.

const myFirstPromise = new Promise((resolve, reject) => {
  // do something asynchronous which eventually calls either:
  //
  //   resolve(someValue)        // fulfilled
  // or
  //   reject("failure reason")  // rejected
});

 

즉, 프로미스는 자바스크립트 비동기 처리에 사용되는 객체로 Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.

function fetchData() {
    return new Promise((resolve, reject) => {
        // 비동기 요청
        const success = true;
        if(success) {
            resolve('성공');
        }
        else {
            reject('실패');
        }
    })
}

fetchData()
    .then((response)) => {
        // '성공'이 나온다.(Promise에서 success가 true니까 resolve반환값이 나옴)
        console.log(response);  
    })
    .cathch((error)) => {
        console.log(error);
    })

or

function fetchData() {
    // 함수에서 반환하는 것이 Promise 객체
    return new Promise((resolve, reject) => {
        const success = true;
        if (success){
            resolve('성공'); 
        }
        else{
            reject('실패');
        }
    })
}

fetchData()
    .then(function(result){
        console.log(result);
    })
    .catch(function(error){
        console.log(error);
    })

 

Promise 객체의 반환 값을 .then을 이용해서 함수 인자로 이용한다.

결과

Promise는 다음 중 하나의 상태를 가진다.

  • 대기(pending) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • 이행(fulfilled) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • 거부(rejected) : 비동기 처리가 실패하거나 오류가 발생한 상태

 

대기 중인 프로미스는 값과 함께 이행할 수도, 어떤 이유(오류)로 인해 거부될 수도 있다.

이행이나 거부될 때, 프로미스의 then 메서드에 의해 대기열(큐)에 추가된 처리기들이 호출된다.

이미 이행했거나 거부된 프로미스에 처리기를 연결해도 호출되므로, 비동기 연산과 처리기 연결 사이에 경합 조건은 없다.

myPromise
    .then((result)=>{
        console.log(result);
    })     // resolve 값 여기로
    
    .catch((error)=>{
        console.log(error);
    })    // reject 값 여기로
    
    .finally(()=>{
        console.log('모든작업끝');
    })    // 이행이든, 거부든 모두 마지막엔 여기로

 

(ex) 실제로 비동기 요청 두 개를 보내기

fetch('http://jsonplaceholder.typicode.com/todos/1')
    .then(response1 => console.log(response1))

 

내장함수 fetch를 사용해서 비동기요청을 보내본다. fetch는 promise를 지원하기 때문에 따로 new promise 생성자를 사용 안해줘도 된다.

그래서 바로, .then으로 비동기 요청 반환값을 받아 올 수 있다.

 

그대로 response를 출력

 

해당 반환 값을 json 형태로 출력하려면 .json()을 사용하면 된다.

fetch('http://jsonplaceholder.typicode.com/todos/1')
    .then(response1 => response1.json())
    .then(json1 => console.log(json1))

json() 형태로 출력

 

다음에 또 어떤 작업을 해주고 싶으면, 바로 요청을 보내면 된다.

fetch('http://jsonplaceholder.typicode.com/todos/1')
    .then(response1 => response1.json())
    .then(json1 => console.log(json1))
    .then(() => fetch('http://jsonplaceholder.typicode.com/todos/2'))
    .then(response2 => response2.json())
    .then(json2 => console.log(json2))

바로 또 fetch()

 

그럼, 에러도 받아보자. 에러는 catch를 통해서 받는다고 했다.

두 번째 fetch에서 요청을 아무렇게나 해보고, 결과를 살펴보면, 에러가 뜰 것이다.

fetch('http://jsonplaceholder.typicode.com/todos/1')
    .then(response1 => response1.json())
    .then(json1 => console.log(json1))
    .then(() => fetch('http://xxxxxxxx.ypicode.com/todos/2'))
    .then(response2 => response2.json())
    .then(json2 => console.log(json2))
    .catch((error) => {
        console.log(error);
    })

catch((error))

첫 번째 요청은 잘 이행됐지만, 두 번째 요청은 거부되고 error 를 반환한다. ( Failed to fetch ) 

 

그럼, finally 도 살펴보자. 이는 요청이 이행되든 거부되든 실행된다.

fetch('http://jsonplaceholder.typicode.com/todos/1')
    .then(response1 => response1.json())
    .then(json1 => console.log(json1))
    .then(() => fetch('http://xxxxxxxx.ypicode.com/todos/2'))
    .then(response2 => response2.json())
    .then(json2 => console.log(json2))
    .catch((error) => {
        console.log(error);
    })
    .finally(() => {
        console.log("작업 완료");
    })

finally() 작업완료


[ Promise.all() ]

 

Promise.all() 메서드는 순회 가능한 객체에 주어진 모든 프로미스가 이행한 후, 혹은 프로미스가 주어지지 않았을 때 이행하는 Promise를 반환한다.

주어진 프로미스 중 하나가 거부하는 경우,  첫 번째로 거절한 프로미스의 이유를 사용해 자신도 거부한다.

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve,reject) => {
    setTimeout(resolve, 3000, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
    console.log(values);    // [3,42,'foo']
});

세 promise 다 이행 -> 3초 후 결과 나옴.

 

즉, 모든 프로미스가 이행되어야 fulfill 이다. 안 그러면 reject 다.

catch를 통해 실패한 이유가 나온다.

const promise1 = Promise.reject('failed reason');
const promise2 = 42;
const promise3 = new Promise((resolve,reject)=>{
    setTimeout(resolve, 3000, 'foo');
});

Promise.all([promise1, promise2, promise3])
    .then((values) => {
    console.log(values);
    })
    .catch((error)=> {console.log(error)})    // 'failed reason'

모든 promise가 이행되지 않으면 reject가 되어서 거부된 이유가 출력된다.

 

[ Promise.race() ]

 

Promise.race() 메서드는 Promise 객체를 반환한다. 이 프로미스 객체는 iterable 안에 있는 프로미스 중에 가장 먼저 완료된 것의 결과값으로 그대로 이행하거나 거부한다.

const promise1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'two');
});

Promise.race([promise1,promise2]).then((value) => {
    console.log(value); // 'two'
    // Both resolve, but promise2 is faster
});

two가 먼저 출력된다.

프로미스 중에서 가장 먼저 완료된 객체의 반환 값을 출력한다. 

가장 먼저 완료된 객체가 이행될 수도 있고 거부될 수도 있다. 

 

[ Async, Await ]

아까 Promise 파트에서 내장함수 fetch를 사용한 코드를 한 번 살펴보자.

fetch('http://jsonplaceholder.typicode.com/todos/1')
    .then(response1 => response1.json())
    .then(json1 => console.log(json1))
    .then(() => fetch('http://xxxxxxxx.ypicode.com/todos/2'))
    .then(response2 => response2.json())
    .then(json2 => console.log(json2))
    .catch((error) => {
        console.log(error);
    })
    .finally(() => {
        console.log("작업 완료");
    })

 

해당 코드를 Async, Await를 이용해서 가독성 좋게 바꿀 수 있다.

await는 기다리다는 뜻인데, 이를 활용해서 아래와 같이 코드를 작성할 수 있다.

async function makeRequests() {
    try {
        const response1 = await fetch('http://jsonplaceholder.typicode.com/todos/1');
        const jsonResponse1 = await response1.json();
        console.log('jsonResponse1', jsonResponse1);
        
        const response1 = await fetch('http://jsonplaceholder.typicode.com/todos/2');
        const jsonResponse2 = await response2.json();
        console.log('jsonResponse2', jsonResponse2);
    }    catch (error) {
        console.log(error);
    }    fianlly {
        console.log("작업 완료");
    }
}

makeRequests();

 

fetch를 통해 해당 요청에 대한 응답이 끝날 때까지 기다린 다음(await)에 그 반환 값을 response1 에 넣어주고,

response1.json() 에 대한 작업요청이 끝날 때까지 기다린 다음(await)에 그 반환 값을 jsonRespons1에 넣어주고,

...

이런식으로 비동기요청이 진행된다.

 

비동기 요청인데, 흡사 동기요청 처럼 보인다.

결과

 

최근에는 async, await 형식을 사용해서 비동기 요청 처리를 많이 한다.

  • 비동기 코들르 마치 동기 코드처럼 보이게 할 수 있다.
  • Promise에 then 메서드를 체인 형식으로 호출하는 것보다 가독성이 좋다.
  • await는 async 내부 함수에서만 사용할 수 있다.
  • 동기식 코드에서 쓰는 try...catch 구문을 async/await 구조에서 사용할 수 있다.

※ 비동기 함수를 작성할 때, 자주 나올 수 있는 상황에 대한 해결책을 정리한 글도 한번 확인하면 좋다!

 

Javascript 에서 forEach 함수는 비동기함수를 기다리지 않아요!

🚨 문제 상황 배열에 담긴 요소들을 하나씩 뽑아내는 작업을 할 때 아래와 같은 코드가 있다고 해보자. const allu = new Array(10).fill('dachshund'); const result = [] const asyncFunction = (i) => { return new Promise(reso

letzgorats.tistory.com


참고 자료

따라하며 배우는 자바스크립트 A-Z

반응형