JS_11. 비동기와 프로미스

Seoyong Lee·2021년 5월 17일
0

JavaScript / TypeScript

목록 보기
12/25
post-thumbnail
post-custom-banner

동기와 비동기

우리는 주로 여러 가지 일을 동시에 처리할 수 있을 때 '멀티 태스킹'이 가능하다고 말한다. 프로그래밍 언어의 처리 방식도 이러한 동시 처리 여부에 따라 '동기'와 '비동기'로 나눌 수 있다.

동기식 처리 모델(Synchronous processing model)은 한 번에 한 태스크(task)를 수행한다. 이러한 과정은 직렬적으로, 한 태스크의 종료 전까지 이후의 태스크들은 블로킹(blocking) 된다.

이와 달리 비동기식 처리 모델(Asynchronous processing model)은 병렬적으로 태스크를 수행한다. 즉, 태스크가 종료되지 않은 상태라 하더라도 대기하지 않고(Non-Blocking) 다음 태스크를 실행한다. 이러한 모델은 자바스크립트의 대부분의 DOM 이벤트 핸들러와 Timer 함수(setTimeout, setInterval), Ajax 요청에 적용된다.

그렇다면 자바스크립트 자체는 동기식일까? 비동기식일까?

놀랍게도 자바스크립트 엔진 자체에는 비동기란 개념이 없으며, 단지 주어진 대로 하나씩 처리할 뿐이다(싱글 스레드). 그렇다면 위에서 언급했던 Timer 함수 등은 어떻게 비동기적(멀티 스레드)으로 작동하는 것일까? 바로 엔진을 감싸고 있는 웹 브라우저(호스팅 환경)에서 이벤트 루프가 동시성(Concurrency)을 지원하기 때문이다. 이에 대해선 이벤트 루프에서 더욱 자세히 다룰 예정이다.

그렇다면 자바스크립트에서 이러한 비동기 처리가 필요한 이유는 무엇일까?

만약 어떠한 웹페이지가 동기 처리로만 동작한다고 생각해보자. 우리가 어떠한 버튼을 클릭하면 화면에서 서버로 데이터를 요청하면 응답을 기다리기 전까진 다른 동작은 블로킹 될 것이다. 만약 이러한 요청이 10개라면 어떨까? 100개라면? 아마 오랫동안 기다릴 수밖에 없을 것이다.

비동기 처리는 이러한 문제를 해결하고, 나아가 만족스러운 사용자 경험을 위해서 필수적이다. 그렇다면 실제 자바스크립트 내에서 비동기를 구현하는 가장 간편한 방법은 무엇일까? 바로 콜백(Callback)을 이용하는 방법이다.

Callback과 Callback Hell

다음은 문자열을 출력하는 함수이다.

const printString = (string) => {
  console.log(string);
}

이러한 함수는 다음과 같이 순차적으로 실행된다.

printString("A");
printString("B");
printString("C");

// "A" "B" "C"

그러나 같이 특정 시간 이후에 함수를 실행시키는 setTimeout이 사용된다면 이러한 출력은 작성된 순서와 달라질 수 있다.

const print = (string) => {
  setTimeout(
    () => {
      console.log(string);
    }, 500);
}

printString("A"); // "A"
print("B"); // "B"
printString("C"); // "C"

// "A" "C" "B"

극단적인 예시이지만 다음과 같이 무작위로 만들어진 숫자만큼 실행이 지연된다면?

const printString = (string) => {
  setTimeout(
    () => {
      console.log(string)
    },
    Math.floor(Math.random() * 100 ) + 1
  )
}

const printAll = () => {
  printString("A")
  printString("B")
  printString("C")
}
printAll()

// "A" "C" "B"
// "B" "C" "A"
// ...

이러한 상황에서 콜백을 이용하면 실행순서를 정해줄 수 있다.

const printString = (string, callback) => {
  setTimeout(
    () => {
      console.log(string)
      callback()
    },
    Math.floor(Math.random() * 100 ) + 1
  )
}

const printAll = () => {
  printString("A", () => {
    printString("B", () => {
      printString("C", () => {})
      // Callback Hell...
    })
  })
}
printAll()

// "A" "B" "C"

위와 달리 printString의 인자로 callback을 추가로 받는 것을 볼 수 있다. 그러나 이러한 방식은 콜백이 꼬리를 물며 반복되는 콜백 헬(Callback Hell)을 만들 수 있다. 이러한 콜백 헬은 가독성을 해치고 유지보수를 어렵게 만든다. 이러한 문제를 해결하기 위해 나온 것이 '프로미스(Promise)'이다.

Promise와 async/await

'프로미스(Promise)'는 ES6에서 추가된 비동기 처리를 위한 객체로, 다음과 같은 3가지 상태(state)를 가진다.

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

프로미스를 사용해서 위의 코드를 바꾸면 다음과 같다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        console.log(string);
        if (something) {
          resolve();
        } else {
          reject(new Error("Request is failed"));
        }
      },
      Math.floor(Math.random() * 100 ) + 1
    )
  });
}

const printAll = () => {
  printString("A");
  .then(() => {
    return printString("B");
  });
  .then(() => {
    return printString("C");
  });
  .catch((err) => {
    console.log(err);
  });
}
printAll()

// 마지막 체이닝에서 .catch()를 통해 error handling 가능
// callback은 처리 시마다 error 처리 함수 필요

new Promise를 통해 프로미스 메소드를 호출하면 대기(Pending) 상태가 된다. 그 뒤 함수의 인자 resolve를 실행시키면 이러한 상태는 이행(Fulfilled)으로 변하며, .then을 통해 처리 결괏값을 받을 수 있다. 만약 reject를 실행시키면 상태는 실패(Rejected)로 바뀌고 이러한 실패가 일어난 이유를 .catch를 통해 확인할 수 있다.

프로미스를 사용하니 콜백에 비해 가독성이 많이 높아졌다. 그러나 .then의 반복은 다시 프로미스 헬에 빠지게 될 수 있다. 이러한 반복을 막기 위해 도입된 개념이 바로 async/await이다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        console.log(string);
        if (something) {
          resolve();
        } else {
          reject(new Error("Request is failed"));
        }
      },
      Math.floor(Math.random() * 100 ) + 1
    )
  });
}

const printAll = async () => {
  try {
    const printA = await printString("A");
    const printB = await printString("B");
    const printC = await printString("C");
  } catch (err) {
    console.log(err);
  }
}
printAll()

위와 같이 async/await을 사용하면 조금 더 직관적인 코드 작성이 가능하다.

더 알아볼 내용

  • 마이크로 큐
  • 이벤트 루프

참고
You Don't Know JS
MDN - Introducing asynchronous JavaScript
poiemaweb.com - 동기식 처리 모델 vs 비동기식 처리 모델
캡틴판교 - 자바스크립트 Promise 쉽게 이해하기

profile
코드를 디자인하다
post-custom-banner

0개의 댓글