프론트엔드 개발 블로그

자바스크립트에서 비동기 프로그래밍

by heeji_

싱글스레드.

자바스크립트는 싱글 스레드 언어입니다. 스레드는 뭘까요? 스레드는 프로세스 내에서 실행되는 가장 작은 실행 단위를 말합니다.

싱글 스레드 방식은 스레드가 한 개란 뜻으로, 한 번에 하나의 작업만 수행할 수 있다는 특징을 가지고 있습니다. 그래서 하나의 작업이 오래 걸리면 다음 작업이 오래 대기하게 되어 전체 실행 시간이 길어지는 문제가 있습니다.

아래 예시에서 볼 수 있듯이 중간에 정말 많은 연산을 진행하는 코드가 존재한다면 end가 콘솔에 찍히기까지 시간이 오래걸릴 것입니다.

console.log("start");

for (let i = 0; i < 10000000000000; i++) {
  // 많은 연산량을 가지는 코드
}

console.log("end");

자바스크립트가 실행되는 런타임인 브라우저에서는 동시다발적인 요청이 많이 들어옵니다. 그런데 한 번에 한 가지 작업만 수행할 수 있다면 웹 페이지가 정말 느릴 것 같은데 그렇지 않습니다. 자바스크립트는 이 단점을 비동기 방식으로 극복했습니다.

 

비동기 프로그래밍.

비동기 방식은 이전 작업이 끝날 때까지 기다리지 않고 다음 코드를 실행합니다. 요즘 우리가 카페에서 줄을 서서 기다리지 않고 키오스크로 주문을 해놓고 음료가 나올 때까지 다른 것을 하고 있는 것 처럼요!

비동기 요청의 예로 setTimeout, fetch 등이 있습니다. 그런데 만약 setTimeout으로 5초 뒤에 A함수를 실행해라고 명령하려면 어떻게 해야할까요?

 

콜백 함수

위에 예시로 든 setTimeout이 대표적인 비동기 작업입니다. setTimeout 이벤트 처리가 종료되었을 때 어떤 함수를 실행하라고 콜백 함수를 넘겨주어 비동기 처리를 할 수 있습니다.

const fetchData = (callback) => {
  setTimeout(() => {
    const data = { name: "John", age: 30 };
    callback(data);
  }, 3000);
};

fetchData((data) => {
  console.log(data);
});

위 코드에는 callback을 인자로 받고 3초 뒤에 callback의 인자에 data를 넘겨주는 함수입니다. data라는 인자를 받으면 콘솔에 찍어줘라고 코딩한 것입니다.

그런데 문제가 있습니다. 만약 콜백이 여러 번 중첩되는 경우 코드를 알아보기 힘들어 유지보수하기 어려워지겠죠. 일명 콜백 지옥이라고도 합니다.

getData(function (a) {
  getMoreData(a, function (b) {
    getEvenMoreData(b, function (c) {
      getEvenEvenMoreData(c, function (d) {
        getFinalData(d, function (finalData) {
          console.log(finalData);
        });
      });
    });
  });
});

 

Promise

이런 콜백지옥을 피하기 위해 자바스크립트에 등장한 것이 promise입니다. 비동기 작업을 구조화시켜 관리하기 쉽게 만들어줍니다.

Promise는 세 가지 상태를 가질 수 있습니다.

  1. 대기 (pending): 작업이 완료되지 않은 상태
  2. 이행 (fullfilled): 작업이 성공적으로 완료되어 결과 값이 있는 상태
  3. 거부 (rejected): 작업이 실패하고 실패 이유가 있는 상태

promise는 아래와 같이 만들고 사용할 수 있습니다. resolve함수 인자에 비동기 작업의 결과물을 넣어주고 reject함수 인자에 에러를 넘겨줍니다. 이렇게 되면 then문에서 결과값을 catch문에서 에러를 전달받아 처리할 수 있는 것입니다!

const promise = new Promise((resolve, reject) => {
  // 비동기 작업 수행
  resolve(비동기_작업의_결과물);
  reject(비동기_작업_에러);
});

promise
  .then((result) => {
    // 작업이 성공한 경우 처리
  })
  .catch((error) => {
    // 작업이 실패한 경우 처리
  });

then문을 체인 형태로 연결해 비동기 작업을 연속적으로 처리할 수도 있습니다. 이렇게 해서 콜백 지옥을 피하고 코드 가독성을 높일 수 있었습니다.

 

async/await

promise를 사용할 때, then문이 많이 중첩되고 실행하는 것이 많다면 결과값이 어떻게 흘러가서 어떻게 처리되는지 알아보기 어려워지는 문제가 있었습니다.

fetchData(() => {
    const result = fetch() // 비동기 작업
  return result
})
  .then((result) => {
        // result를 처리하는 매우 긴 코드
      const newResult = result..

    return newResult
  })
  .then((result) => {
        // result를 처리하는 매우 긴 코드
        const newResult = result..

    return newResult
  })
  .catch((e) => {
    console.log(e)
  })

이 문제를 해결하기 위해 async/await 문법을 도입했습니다. promise를 더 직관적으로 사용할 수 있고 비동기 작업을 동기로 작성한 것처럼 보이게 만들어주는 녀석입니다!

async 키워드를 function 앞에 붙이면 비동기 함수로 만들어줍니다. 이 함수 내부에서는 await 키워드를 사용할 수 있습니다. async 키워드를 붙인 함수의 결과값은 promise가 아니더라도 promise로 감싸 반환됩니다.

await 키워드는 키워드 뒤의 비동기 작업이 이행될 때까지 코드 실행을 잠시 중단합니다. 작업이 완료되면 다시 실행됩니다.

function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Data fetched"), 1000);
  });
}

async function getData() {
  try {
    const result = await fetchData();
    console.log(result); // "Data fetched"
  } catch (error) {
    console.error(error);
  }
}

getData();

위의 코드에서 getData함수는 async로 감싸져 비동기 함수로 정의되었습니다. await 키워드를 사용해 fetchData의 실행 결과를 기다리고 result에 결과를 반환합니다. 만약 에러가 발생한다면 catch문에서 에러를 처리할 수 있습니다.

async/await 문법은 promise의 체이닝을 대체해 코드의 가독성을 높이고 비동기 작업을 동기로 다룰 수 있게 해주는 강력한 도구입니다!

 

참고 문서

'공부기록 > JavaScript' 카테고리의 다른 글

일급함수 & 콜백함수, 고차함수  (0) 2024.03.22
이벤트 루프란?  (0) 2024.03.22
자바스크립트 - Event  (0) 2021.07.16
자바스크립트 - DOM  (0) 2021.07.16
자바스크립트 - 문법  (0) 2021.07.16

블로그의 정보

아자아자

heeji_

활동하기