TIL

비동기 프로그래밍 (Callback, Promise, Async/Await)

팝삐 2024. 8. 16. 16:01

비동기 프로그래밍이란

자바스크립트는 싱글 스레드 언어이기 때문에 한 번에 하나의 작업만을 수행한다.

이를 동기적이라고 한다. 동기 방식은 간단하고 직관적이지만 장기 실행 함수의 경우 응답이 늦어져 전체적인 성능이 저하 된다.

동기적인 프로그래밍의 문제점

: 실행한 동기적 프로그래밍의 결과값이 나오기 전까지는 아무 작업도 시작할 수 없다.

그러면 이를 해결하기 위한 비동기 프로그래밍은 무엇을 해줄 수 있을까

  1. 함수를 호춤함으로써 장기적으로 실행되는 작업을 시작한다.
  2. 이 함수로 작업을 시작하고 즉시 복귀하여 다른 이벤트에 계속 응답할 수 있게 한다.
  3. 작업이 완료되면 결과를 알려준다.

즉, 비동기 함수 실행 시 응답이 오는 것에 상관없이 다른 작업을 이어나가 병렬적으로 작업을 처리할 수 있게 된다. 이는 총 코드 실행 시간을 감소할 수 있게 한다.

비동기 처리의 처리 원리

호출 스택이벤트 루프

  • 비동기 함수의 콜백 함수가 이벤트 루프에 의해 callback queue에 담기고 다시 싱글 스레드인 call stack에 담겨서 콜백 함수가 실행되는 동작 원리

자바스크립트는 싱글 스레드 언어라고 했으면서 어떻게 병렬 처리가 가능한가요

  • 자바 스크립트를 실행하는 Call Stack : 싱글 스레드
  • 서버에게 리소스를 요청하거나 파일 입출력 혹은 타이머 대기 작업을 실행하는 Web APIs : 멀티 스레드

즉, 브라우저가 멀티 스레드이기 때문에 메인 자바스크립트의 싱글 스레드를 차단하지 않고 다른 스레드를 사용하여 Web API의 작업을 처리할 수 있기 때문에 비동기 처리가 가능한 것이다.

비동기 처리의 문제점

  • 만일 그 다음 실행할 작업이 이전에 요청한 작업의 결과가 반드시 필요할 경우 문제가 생긴다.
  • 서버로부터 데이터를 받을 때 비동기 함수의 결과가 동기적으로 실행되는 코드에 영향을 줄 때도 문제가 된다.

작업의 순서를 맞추는 것이 필수 불가결일 경우에 문제가 생긴다.

이를 해결하기 위한 몇 가지 기법 중 가장 대표적인 것은 콜백 함수 기법이다.

콜백 함수

  • 콜백 함수는 자바스크립트의 일급 객체 특성을 이용해 함수의 매개변수에 함수 자체를 넘겨 함수 내에서 매개변수 함수를 실행하는 기법을 말한다.
    • 콜백 함수를 이용함으로써 간접적으로 작업 순서를 끼워 맞출 수 있다.
  • 콜백 함수는 비동기 함수에서 작업 결과를 전달받아 처리하는데 사용되어 작업 순서를 맞출 수 있다.

⇒ 언뜻 문제가 해결되어 보이지만 콜백 함수는 비동기를 순차적으로 처리하기 위한 하나의 조치일 뿐 공식적인 해결 방식은 아니다.

Promise란 무엇인가

  • Promise 객체는 이러한 한계점 극복을 위해 비동기 처리를 위한 전용 객체로서 탄생했다.
  • Promise는 비동기 처리를 위한 전용 객체이다.
    • Promise는 비동기 작업의 성공과 실패를 나눠 그 결과에 따른 코드를 실행할 수 있게 한다.
    ⇒ Promise를 통해 비동기 작업을 쉽고 깔끔하게 연결할 수 있다.
  • const myPromise = new Promise((resolve, reject) => { setTimeout(() => { const success = true; if (success) { resolve("작업 성공!"); } else { reject("작업 실패."); } }, 2000); }); myPromise .then(result => { console.log(result); }) .catch(error => { console.error(error); });

Promise와 Callback

Promise와 Callback의 차이

  • 에러 처리
    • Callback은 단순히 비동기 작업의 결과를 처리하는 데 사용되지만
    • Promise는 비동기 작업의 결과를 완료 / 실패로 나타내어 각 결과에 맞는 코드를 실행할 수 있다.
  • 중첩 문제 해결
    • Callback은 구조가 단순해 중첩이 쉬워 이른바 ~~‘콜백 지옥’~~을 발생시킬 수 있다.
    • Promise는 중첩 문제에서 자유롭고 콜백보다 높은 가독성을 제공한다.
      • Promise 체이닝을 통해 여러 비동기 작업을 간단하게 연결할 수 있으나
      • 작업이 너무 복잡해질 경우 코드가 어려워질 수 있다.
      ⇒ 이 경우 async/await으로 보완이 가능하다.

Async, Await

  • ES8에 해당하는 문법 async / await
  • Callback처럼 Promise에도 Promise Hell이 존재한다. (주로 지나친 then 남용으로 일어난다.)
  • 이를 해결하기 위한 async / await은 프로미스를 기반으로 하지만 마치 동기 코드처럼 쉽게 작성할 수 있게 해준다.

개념

  • async 함수
    • 함수 선언 앞에 async 키워드를 붙이면 해당 함수는 자동으로 Promise를 반환한다.
    • async 함수 내부에서 reuturn 된 값은 자동으로 Promise.resolve()로 감싸져 반환된다.
  • await 키워드
    • await은 async 함수 내부에서만 사용 가능하며, Promise가 해결 될 때까지 함수 실행을 일시 정지한다.
    • await 뒤에 오는 Promise가 해결되면 그 결과값을 반환하고 await은 거부 이유를 throw하여 예외를 발생시킬 수 있다.

예시 코드

async function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const data = "데이터 로드 완료";
            resolve(data);
        }, 2000);
    });
}

async function main() {
    try {
        const result = await fetchData();
        console.log(result);
    } catch (error) {
        console.error("에러 발생:", error);
    }
}

main();

  • 비동기 코드를 동기식 코드처럼 작성할 수 있어 가독성이 좋다.
  • try/catch 구문으로 오류를 직관적이고 일관되게 처리할 수 있다.
  • 여러 개의 Promise를 더 간단하게 작성할 수 있다.

+) 웹 워커

  • 웹 워커는 브라우저 환경에서 메인 스레드와 독립적으로 백그라운드에서 자바스크립트를 실행할 수 있게 하는 비동기 작업의 일환이다.
  • 무거운 작업을 처리하면서도 UI의 변동 사항이 없기 때문에 사용자 경험을 개선할 수 있다.

예시 코드

// main.js (메인 스레드)
const worker = new Worker('worker.js');
worker.postMessage(1000000000);

worker.onmessage = function(event) {
    console.log('결과:', event.data);
    worker.terminate();
};

// worker.js (웹 워커)
self.onmessage = function(event) {
    let result = 0;
    for (let i = 0; i < event.data; i++) {
        result += i;
    }
    self.postMessage(result);
};

장점

  • UI 반응성 유지
  • 성능 향상
  • 독립적인 환경
    • 웹 워커는 독립된 실행 환경에서 동작하므로 메인 스레드와 충돌 없이 작업을 수행할 수 있다.

출처

https://developer.mozilla.org/ko/docs/Learn/JavaScript/Asynchronous/Introducing

https://developer.mozilla.org/ko/docs/Learn/JavaScript/Asynchronous