본문 바로가기

cs/react

[JS] Promise & async & await

목차

1. JS의 비동기 처리 문제점

2. Promise

3. async

4. await

5. Promise.all


JS의 비동기 처리 문제점

자바스크립트는 단일 스레드이지만 브라우저의 Web APIs와 microstack queue, macrostack queue 그리고 이벤트 루프의 동작으로 인해 한번에 다양한 동작이 가능한것처럼 보입니다.

 

JS의 비동기 동작에 대해서는 해당 글에서 알 수 있습니다.

 

비동기는 끝나는 시점을 예측하기 어렵고 비동기가 완료된 이후 어떻게 처리해줘야할지.. 즉, 비동기의 흐름을 예상하기 어렵습니다.

 

아래는 JS에서 비동기 작업시 겪을 수 있는 어려움의 예시입니다.

function fantasticWorking() {...}

function doMyWork() {
    
    //비동기 작업의 완료 시점 부정확
    let energe = 100
    
    fantasticWorking(energe).then(() => 
        energe = takeARest();
    )
    
    fantasticWorking2(energe).then(() => 
        energe = takeARest();
    )
    
    //콜백 지옥
    otherWorking(time).then(() => 
        goOnHoliday().then(() => 
            otherDelayWorking().then(() => 
                otherDelayWorking1(...)
            )
        );
    );
}

doMyWork라는 함수에서 비동기 함수인 fantasticWorking함수가 호출되었습니다. 이 함수가 끝나면 takeARest라는 함수가 호출될 수 있습니다. 하지만, fantasticWorking함수가 언제 끝날지 모르고 그 다음에 진행되어야할 fantasticWorking2함수에 영향을 미치기에 좋지 못한 코드입니다.

그리고 otherWorking이라는 함수가 끝나면 끝도 없는 콜백 함수로 코드가 지저분해져서 유지보수가 어려울수도 있다는 단점을 가지고 있습니다.

 

이렇게 복잡한 비동기를 처리하기 위해 나온 개념이 Promise는 아니지만, 비동기의 단점을 개선해주기 위해 필수적으로 알아야하는 개념인 Promise부터 Promise 내부 비동기 처리를 도와주는 async 그리고 await까지 순차적으로 알아보겠습니다.


Promise

promise는 비동기 작업의 단위입니다.

 

const promiseTask = new Promise((resolve, reject) => {
    if(doYourWork()) {
        resolve();  //비동기 작업 성공시 resolve()함수 호출
    }
    else {
        reject();  //비동기 작업 실패시 reject()함수 호출
    }
})

promiseTask
  .then(() => {
      console.log("promise 성공 후 resolve 호출 이후 작업");
  })
  .catch(() => {
      console.log("promise 실패 후 reject 호출 이후 작업");
  })

promise로 관리할 비동기 작업을 만들기 위해서는 Promise 객체를 먼저 생성하고 그 안에서 동작시켜야합니다.

Promise객체 생성의 가장 정석적인 방법은 new Promise를 이용해서 객체를 만드는 방법입니다.

 

promise에는 resolve, reject 두 개의 인지값이 있습니다.

resolve는 promise 내부에서 동작한 작업이 성공했을 경우 호출하는 함수입니다.

reject는 promise 내부에서 동작한 작업이 실패했을 경우 호출하는 함수입니다.

 

promise의 인지가 호출된 경우 promise 객체가 반환되는데, 이후 체인 메서드를 통해 이후 동작을 관리할 수 있습니다.

 

resolve 호출시 then 실행
reject 호출시 catch 실행

resolve를 호출한 경우 then의 콜백 함수가 실행되고 reject을 호출한 경우 catch의 콜백 함수가 실행됩니다.

then의 콜백은 promise의 내부 작업(비동기 작업)이 성공한 이후에 진행되는 흐름입니다.

catch의 콜백은 작업이 실패한 이후에 진행되는 흐름입니다.

 

참고로, promise객체 내부에서 resolve, reject 인자 중 어느것도 호출하지 않는 경우 promise객체 반환 후 then, catch 메서드 작업은 실행되지 않습니다.

const promise = new Promise((resolve, reject) => {
        setTimeout(() => {console.log("비동기 작업")}, 1000)
    })

promise
    .then(() => console.log("성공 이후 작업"))
    .catch(() => console.log("실패 이후 작업"))

promise인지 호출 안한 결과

 

 


Promise의 상태

https://springfall.cc/article/2022-11/easy-promise-async-await

promise에는 대기(pending), 완료(fulfilled), 오류(rejected) 상태가 존재합니다.

promise 내부의 비동기 작업이 완료시(fulfilled) then 메서드 호출, 실패시(rejected) catch를 호출함으로써 후속 작업을 지정해주게 됩니다.

 

promise 상태를 관리하는 개체는 브라우저나 Node.js에서 처리해준다고 합니다.


Promise까지 정리..

 

promise의 사용으로 비동기 작업의 시작(new Promise(...))과 작업 이후의 동작(then, catch)를 구분할 수 있게되어 비동기 작업을 좀 더 유연하게 제어할 수 있게 되었습니다.

 

하지만 promise라는 비동기 작업을 시작하고 내부 비동기 작업의 성공여부를 모두 지정(resolve, reject)해줘야하는 것도 번거로운 일입니다.

이러한 번거로운 작업을 개선해주는 기능이 바로 async입니다.


async

async는 함수를 비동기 작업을 하는 함수라고 정의해버리는 기능을 합니다.

 

아래는 일반 함수의 반환결과와 Promise 객체 반환결과를 보여주는 예시입니다.

const promise = new Promise((resolve) => {
    const resultOfWork = doMyWork();

    console.log("resultOfWork : ",resultOfWork)
})

console.log("promise result : ",promise);

function doMyWork() {
    return "쉬고싶어..."
}

doMyWork라는 함수가 있습니다. doMyWork함수의 반환값은 단순한 문자열이 출력됩니다.

그렇기에 doMyWork함수는 단순한 일반 함수입니다.

 

일반 함수에 async를 적용해보면 어떻게 되는지 예시에서 확인해보겠습니다.

const promise = new Promise((resolve) => {
    const resultOfWork = doMyWork();

    console.log("resultOfWork : ",resultOfWork)
})

console.log("promise result : ",promise);

async function doMyWork() {
    return "쉬고싶어..."
}

일반 함수였던 doMyWork의 반환 결과가 일반 문자열이 아닌 Promise객체에 감싸져 반환되는것을 확인할 수 있습니다.

즉, doMyWork는 async로 인해 비동기 작업 단위인 Promise를 감싸서 반환하는 비동기 함수가 되었습니다.

 

게다가 resolve를 호출하지 않았음에도 상태는 완료(fulfilled)로 반환되어있습니다.

 

만약 비동기 함수(async 적용 함수) 내부에서 오류가 발생하는 경우에는 어떻게 되어있을까요?

const promise = new Promise((resolve) => {
    const resultOfWork = doMyWork();

    console.log("resultOfWork : ",resultOfWork)
})

console.log("promise result : ",promise);

async function doMyWork() {
    throw new Error("넌 못쉰다.");
    // return "쉬고싶어..."
}

 

결과는 실패(rejected) 상태를 가진 Promise 객체를 반환합니다.

 

결국 async 함수를 사용함으로써 비동기 동작의 결과 성공여부를 개발자가 관여하지 않아도 되게 되었습니다.

 


async까지 정리..

 

promise라는 비동기 단위로 비동기의 시작과 이후 동작을 유연하게 처리할 수 있게 되었고, async 함수를 사용함으로써 비동기 작업 상태 처리에 관여하지 않아도 되었습니다.

 

하지만, 비동기의 단점 중 하나인 콜백 지옥은 여전히 빠져나올 수 없습니다. 이를 해결하기 위한 기능이 바로 await 입니다.


await

await는 비동기 작업(promise)의 결과 반환을 기댜려주는 역할을 하는 기능입니다.

 

const promise1 = await asyncFunction();
const promise2 = await asyncFunction2();

syncFunction(promise1, promise3);

await 사용은 then을 사용하는 것과 동일한 결과를 보여주지만, 코드 레벨상으로는 동기 흐름처럼 보여줍니다.

 

await는 promise를 반환하는 함수 앞에 선언해줍니다. 그리고 await가 선언된 함수의 결과는 promise로 감싸져있지 않고 결과만(object, 문자열 등..)이 반환되기 때문에 사용성이 편리해집니다.

 

그리고 await 사용시 주의할 점은 async함수 내부에서만 await 선언이 가능하다는 것입니다.

 

 

아래는 파라미터로 받은 delay만큼 대기 후 promise를 반환하는 longTermWork함수를 호출하고, 걸린시간을 반환하는 예시입니다.

longTermWorks();

function longTermWorks() {
    const startTime = new Date().getTime();
    
    longTermWork(1000)
      .then(() => {
          longTermWork(1500)
            .then(()=>{
              longTermWork(2000)
                .then(()=>{
                    const endTime = new Date().getTime();
                    console.log("걸린시간 : ",(endTime-startTime)/1000);
                })
            })
    })
}

function longTermWork(delay:number) {
    return new Promise((resolve) => setTimeout(resolve, delay));
}

 

보기만해도 콜백이 잔뜩 쌓여있어 가독성이 좋지 못합니다.

 

하지만 await를 사용해서 promise결과가 반환될때까지 기다렸다가 다음 함수를 호출하는 방법으로 구현한다면 then절의 콜백 없이 아래와 같이 코드레벨에서 가독성이 훨씬 좋아지게 됩니다.

 

await사용 예시입니다.

longTermWorks();

async function longTermWorks() {
    const startTime = new Date().getTime();

    await longTermWork(1000);
    await longTermWork(1500);
    await longTermWork(2000);

    const endTime = new Date().getTime();
    console.log("걸린시간 : ",(endTime-startTime)/1000);
}

function longTermWork(delay:number) {
    return new Promise((resolve) => setTimeout(resolve, delay));
}

 

콜백 지옥에서 벗어나 순차적인 비즈니스 로직 흐름이 보입니다.

 

 

await는 왜 async 함수 내부에서만 사용이 가능할까?

https://springfall.cc/article/2022-11/easy-promise-async-await

 

동기라는 것은 정의되어있는 작업을 순차적으로 하나씩 해결해 나가는 것입니다.

중간에 작업 하나가 오래걸리면 그 작업이 끝날때까지 다른 작업을 처리할 수 없습니다.

 

비동기라는 것은 정의되어있는 작업을 따로 실행하면서 다른 작업을 실행해 나가는 것입니다.

비동기 작업이 동시에 이루어질때 작업 하나가 오래걸리더라도 나머지 작업들은 영향을 마치지 않고 자신의 일만 처리합니다.

 

await라는 것은 비동기 작업이 끝날때까지 기다리는 기능을 하는데, JS의 동작(동기)에서 비동기 작업을 기다리는 것은 무의미하고 비효율적인 일입니다.

하지만 비동기 작업은 다른 작업에 영향을 미치지 않기 때문에 필요한 비동기 작업을 기다리는 것(await)은 무의미 하지 않을 수 있습니다.

 

그렇기 때문에 비동기 작업의 단위인 promise를 반환하는 async함수에서 await사용은 유의미한 동작이라 허용하지만, 동기 동작인 일반 함수에서 await은 무의미하고 비효율적이기에 async 함수에서만 await 사용이 가능합니다.

 

(JS는 단일 스레드로 이론상 하나의 일을 순차적으로 처리해나갑니다. 하지만 브라우저나 Node.js를 통해 여러 작업이 동시다발적으로 동작할 수 있습니다.)


Promise.all

Promise.all은 다량의 비동기 작업을 하나의 promise객체로 새로 만들어주는 기능입니다.

 

 

아래는 반복문을 돌면서 비동기 작업을 호출한 후 처리된 결과의 평균값을 구하는 예시입니다.

longTermWorks();


async function longTermWorks() {
    const startTime = new Date().getTime();

    let promiseArr = [];

    for(let i=0;i<10;i++) {
        promiseArr.push(await fetchAge(1000))
    }

    const endTime = new Date().getTime();
    console.log("걸린시간 : ",(endTime-startTime)/1000);

    console.log("평균값 : ", promiseArr.reduce((prev:number, current:number) => prev+current,0)/promiseArr.length);
}

async function fetchAge(delay:number) {
    await setTimePromise(delay);
    return Math.random()*20 + 25;
}

async function setTimePromise(delay:number) {
    return new Promise((resolve) => {setTimeout(resolve, delay)})
}

결과 반환 10초

 

반복문마다 await로 인해 비동기 작업 완료를 기다렸다가 처리하려고하니 시간이 굉장히 오래걸립니다.

 

이런 비효율을 처리할 수있는 방법이 Promise.all 사용입니다.

 

 

아래는 기존의 비동기 작업에서 await를 제거하고 Promise.all을 적용해서 성능 최적화를 하는 예시입니다.

 longTermWorks();


async function longTermWorks() {
    const startTime = new Date().getTime();

    let promiseArr = [];

    for(let i=0;i<10;i++) {
        promiseArr.push(fetchAge(1000))  //await 제거
    }

    promiseArr = await Promise.All(promiseArr)  //Promise.All 추가

    const endTime = new Date().getTime();
    console.log("걸린시간 : ",(endTime-startTime)/1000);

    console.log("평균값 : ", promiseArr.reduce((prev:number, current:number) => prev+current,0)/promiseArr.length);
}

async function fetchAge(delay:number) {
    await setTimePromise(delay);
    return Math.random()*20 + 25;
}

async function setTimePromise(delay:number) {
    return new Promise((resolve) => {setTimeout(resolve, delay)})
}

결과 반환 1초

 

Promise.all을 사용함으로써 반복마다 비동기 작업이 끝날때까지 기다리는 것이 아니라 비동기 작업을 병렬로 처리시키고 한 곳(Promise.all 적용 코드)에서 모든 작업이 끝날때까지 대기했다가 결과를 promise 객체로 반환시켜줍니다.

 

참고로 Promise.all에서 관리하는 promise는 모든 비동기 작업이 성공했다면 내부적으로 resolve를 호출하고 하나라도 작업이 실패한다면 reject을 호출합니다.


정리

  • promise
    • 비동기 작업 단위
    • 비동기의 시작(new Promise(...))과 이후 처리(then, catch)를 분리하여 비동기 작업 흐름을 유연하게 처리 가능
  • async
    • 비동기 함수로 정의 (promise 객체 반환)
    • 비동기 작업의 성공 여부(resolve, reject)판단과 promise객체화 작업을 내부적으로 처리해서 비동기 작업 처리를 효율적으로 가능
  • await
    • 비동기 작업 완료 대기
    • promise를 반환하는 비동기 작업이 완료될때까지 대기하였다가 비동기 함수의 반환 결과를 그대로 반환함으로써 then절과 같은 콜백 지옥을 방지하고 코드 레벨에서 가독성 향상

참고

https://springfall.cc/article/2022-11/easy-promise-async-await

 

[Javascript] 비동기, Promise, async, await 확실하게 이해하기

초보자 입장에서 헷갈리기 쉬운 자바스크립트의 Promise 에 대해 낱낱이 파헤칩니다. 더 나아가 async와 await을 올바르게 사용하는 법까지 소개합니다.

springfall.cc

'cs > react' 카테고리의 다른 글

[React] Debounce  (0) 2026.01.08
[JS] 화살표 함수 반환 규칙  (0) 2026.01.03
[JS] JS의 비동기와 콜백 큐  (1) 2025.12.08
[React] React Router  (0) 2025.12.05
[React] Zustand  (0) 2025.12.04