JS 비동기 정복하기

밍글·2025년 1월 30일
0

FE스터디

목록 보기
1/4

티니핑 캐릭터들로 무거운 포스팅을 환기하고 싶

시작하기 전에

이전에 비동기 처리 방식에 대한 질문이 왔었고 그 때 당시에는 어물쩡하게 답했지만 제대로 알 필요가 있다고 생각하여 포스팅을 오랜만에 하게 되었다.

해당 글은 1/5에 노션으로 작성한 걸 옮긴 것입니다.

비동기 함수

  • JS는 기본적으로 단일 스레드이기 때문에 동기적으로 문제를 해결
  • 비동기 처리란 특정 코드가 끝날때 까지 코드의 실행을 멈추지 않고 다음 코드들을 먼저 실행하는 것

비동기 로직

콜 스택 (Call Stack)

함수의 호출을 스택방식으로 기록하는 자료구조, 한 번에 하나의 Task만 처리 가능

마이크로태스크 큐 (Microtask Queue)

Promise, async/await와 같은 비동기 호출의 Callback함수들은 해당 스택에 담기게 된다.

Eventloop는 현재 실행중인 Task가 있는지, 큐들에 적재된 Task가 있는지 주기적으로 확인하고 만약 실행중인 Task가 콜 스택에 없다면 큐에서 꺼내와 콜 스택에 올리고 실행시키는 역할을 한다.

즉, Promise를 반환하면 비동기로 실행된다고 해서 병렬로 실행된다는 아니라는 것

Promise의 thencatchfinally로 전달되는 Callback 함수가 비동기로 실행되는 것!

Callback 함수

  • 나중에 호출하는 함수
  • 대표적인 방식은 setTimeout으로, 후순위로 밀려나고 callback함수를 사용해서 동기적으로 코드가 작성하는 것처럼 보여줄 수 있다.
  • 예제코드(한 번 실행 예시를 먼저 생각해 볼 것) 문제 1
// 코드 실행이 어떻게 될까요? 🙃
function processOrder(orderNumber) {
    if (!orderNumber) {
        return console.log("주문번호가 없습니다");
    }
    console.log(`주문 ${orderNumber}번 처리중...`); 
    startCooking(); 
}

function checkInventory(item, callback) {
    console.log("재고 확인중..."); 
    console.log(`${item} 찾는중...`);
    
    setTimeout(() => {
        const stock = {
            '파스타': { quantity: 5, orderNum: 'A123' },
            '피자': { quantity: 0, orderNum: 'B456' },
            '샐러드': { quantity: 3, orderNum: 'C789' }
        };
        callback(stock[item]?.orderNum);
    }, 2000);
}

function startCooking() {
    setTimeout(() => {
        console.log("조리 시작!"); 
        setTimeout(() => {
            console.log("완성되었습니다!"); 
        }, 1000);
    }, 1500);
}

function orderFood(menu, callback) {
    console.log(`${menu} 주문이 들어왔습니다`);
    checkInventory(menu, callback);
}

orderFood('파스타', processOrder);
  • 코드 실행 답
    // 실행 결과
    // "파스타 주문이 들어왔습니다" 
    // "재고 확인중..." 
    // "파스타 찾는중..." 
    // (2초 후) "주문 A123번 처리중..."
    // (1.5초 후)"조리 시작!" 
    // (1초 후)"완성되었습니다!" 
  • 다만 비동기 처리 로직을 위해 콜백 함수를 연속해서 사용하면 콜백지옥이 발생한다.
    • 이를 해결하고 싶으면 Promise나 Async를 사용하면 되지만 만약 코딩으로만 해결하고 싶다면 콜백 함수를 분리하면 된다.

Promise

  • 비동기 처리에 사용되는 객체
  • Promise의 상태(처리 과정)
    • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
      • new Promise() 메서드를 호출하면 해당 상태가 되는데 콜백 함수를 선언할 수 있고, 콜백 함수의 인자는 resolve, reject
    • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
      • 콜백 함수의 인자 resolve가 실행하면 해당 상태가 된다.
      • 이행 상태가 되면 then()을 이용하여 처리 결과 값을 받을 수 있다.
    • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태
      • reject가 실행하면 해당 상태가 되고 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있다.
  • 여러 개 프로미스 연결하기 (Promise Chaining)
    • then() 메서드를 호출하고 나면 새로운 프로미스 객체가 반환 (약간 아래와 같은 방식으로)

      function getData() {
        return new Promise({
          // ...
        });
      }
      
      // then() 으로 여러 개의 프로미스를 연결한 형식
      getData()
        .then(function(data) {
          // ...
        })
        .then(function() {
          // ...
        })
        .then(function() {
          // ...
        });
      

async&await

  • async function를 실행하면 Promise 객체가 반환
  • async function의 return 값은 promise의 resolve값이 된다.
  • async await의 오류는 try catch로 잡아낼 수 있다.
  • await는 promise 상태가 다 진행될 때까지 기다렸다가 다음으로 진행
    • 그러므로 await 위치가 promise 객체를 생성하는 함수 앞에 놓이는 것
💡

Promise.all

async function concat(){
  const a = await one();
  const b = await two();
  return a+b;
}
  • 원래면 one이 실행될때까지 기다리고, 함수 two가 실행
  • 굳이 순차적으로 실행시키지 않고 한번에 전부 돌려버릴수도 있는데 그게 바로 Promise.all
function concat(){
  return Promise.all([one(),two()]).then(res=>
    res[0]+res[1]);
}

concat().then(console.log); 

사용은 Promise.all의 인자로 병렬로 실행시킬 함수들을 배열로 넣어주면 된다.

특징

  • 실패 처리
    • 배열의 Promise 중 하나라도 reject 되면 전체가 reject → 즉시 에러 반환
    • 다른 Promise들은 계속 실행되지만 결과는 무시된다.
  • 순서 보장 → 실행 완료 순서와 관계없이 입력한 순서대로 결과 반환
  • 서로 독립적인 비동기 작업을 동시에 실행하므로 성능이 최적화된다. → 다만 각각 순서상으로 상관없는 작업을 진행할 때 유용하다

WebAPI

웹 코드를 작성하는데 필요한 작업들을 모아둔 API들

브라우저나 nodeJS와 같은 런타임에 탑재되어 있다.

여기에 있는 API는 대표적으로 fetch, setTimeout, setInterval 등이 있다.

비동기 처리의 핵심

💡 요약
  1. async function은 결국 new Promise 객체 (자동 resolve 기능이 포함된)!
    async function을 호출할 경우 new Promise(() ⇒ {async function 내부 코드})가 실행된다.
  2. 비동기 함수가 호출되면 await 키워드 부분을 만날 때까지는 콜스택에서 머무르며 동기적인 동작을 처리한다.
  3. web api 비동기 함수 또는 await 키워드를 만났을 경우 해당 함수가 실행될 수 있는 위치로 이동하고 콜스택에서 없어진다.
  4. web api 비동기 함수 또는 await 키워드 뒤에 있는 함수가 실행되고 만약 거기에 Promise.then 객체가 있다면 마이크로태스크 큐로 전달된다. (즉, promise와 관련된 then, catch, finally 등이 전달된다는 것)
  5. 만약 처음 실행됐던 비동기 함수의 뒷 부분이 남았다면, 해당 부분도 마이크로태스크 큐에 추가된다.(4번이 실행된 다음에 마이크로태스크 큐에 추가되는 것)
  6. 마이크로태스크 큐와 태스크 큐가 비어있는지 확인하는데, 이때 마이크로태스크가 우선순위로 처리된다. 마이크로태스크 큐가 전부 비워지고 나서 태스크 큐를 비운다. 태스크 큐는 한번에 콜백 함수 하나씩을 처리한다. 즉, 하나가 실행되는 동안 새로운 태스크가 추가되면 다음 이벤트 루프 사이클까지 기다려야 함
  7. 올려진 콜백 함수는 다시 콜스택에서 동기적으로 실행된다.

코드 예제

실행 방식을 제대로 이해해보기
velog는 아쉽게도 토글 기능을 지원하지 않기 때문에 안 보고 최대한 해보고 정답을 보기

async function asyncProcess() {
 console.log('Process Start');
 await new Promise((resolve) => {
   console.log('Inner Works');
   resolve('done');
 }).then((data) => {
   console.log('First Complete');
 });
 console.log('Process End');
}

console.log('Initialize');
asyncProcess().then(() => {
 console.log('All Done');
});
console.log('Keep Going');
  • 코드 정답
    /* 실행 순서:
    "Initialize"
    "Process Start"
    "Inner Works"
    "Keep Going"
    "First Complete"
    "Process End"
    "All Done"
    */
async function asyncFunction() {
    console.log('First Step');
    
    new Promise(resolve => {
        console.log('Inner Promise');
        resolve('data');
    }).then(data => {
        console.log('Promise Resolved');
    });
    
    Promise.resolve().then(() => {
        console.log('Quick Promise');
    });
    
    console.log('Last Step');
}

console.log('Start Process');
asyncFunction().then(() => {
    console.log('All Complete');
});
console.log('End Process');
  • 코드 정답
    /* 실행 순서:
    "Start Process"
    "First Step"
    "Inner Promise"
    "Last Step"
    "End Process"
    "Promise Resolved"
    "Quick Promise"
    "All Complete"
    */

출처

[Javascript]비동기 처리(callback, promise, async/await)

Javascript 비동기 함수의 동작원리 (feat. EventLoop)

Promise.all() - JavaScript | MDN

자바스크립트 Promise 쉽게 이해하기

profile
예비 초보 개발자의 기록일지

0개의 댓글

관련 채용 정보