[우아한테크코스 6기 프리코스] 비동기 확실하게 이해하기

재오·2023년 10월 25일
3
post-thumbnail

우아한테크코스 6기 프리코스 1주차 문제를 풀어보면서 정리한 글입니다.
아직 부족한 부분이 많기에 내용에 오류가 있다면 알려주시면 감사하겠습니다


🤷🏻 비동기...동기... 그게 뭔데?

비동기 를 쉽게 이해하기 위해서는 동기 와 비교하면서 어떤 차이가 있는지 살펴보는 것이 좋을 것 같다.

동기 는 영어로 synchronous이다. 동시에 발생, 즉 직렬적으로 작동된다고 생각하면 되고, 반대로 비동기는 병렬적으로 작동된다. 해당 내용을 코드를 통해 쉽게 이해해보자.

function makeBread() {
  let start = new Date().getTime();
  while (new Date().getTime() < start + 1000) {
  }
}

console.log("Start Three Fish!");
makeBread();
makeBread();
makeBread();
console.log("Finish Three Fish!");

빵을 만드는 작업을 하는 함수 하나를 만들었다. 빵 하나를 만드는데 1초가 걸린다고 가정을 해보자. 빵 3개를 만들기 위해서는 함수를 세번 호출해야 하기 때문에 총 3초가 소요된다. 위와 같은 방법을 동기 라고 한다.

그렇다면 이번에는 내가 직접 빵을 만드는 것이 아닌 다른 사람이 빵을 만드는 것을 기다린다고 생각해보자. 나는 빵을 주문하고 기다리기만 하면 된다. setTimeout 함수를 사용하여 아래와 같이 코드를 작성하였다.

console.log("빵 3개 주세요~");

setTimeout(() => {
  console.log("A: 빵 하나 완성이오.");
}, 1000);

setTimeout(() => {
  console.log("B: 빵 하나 완성이오.");
}, 1000);

setTimeout(() => {
  console.log("C: 빵 하나 완성이오.");
}, 1000);

빵 3개를 주문하고 1초만에 빵 3개를 받을 수 있게 된다. 3초가 걸렸던 일을 setTimeout 함수로 단 1초만에 해결할 수 있게 된 것이다. 만약 모두 1초가 아닌 1초, 3초, 5초로 만든다면 최종적으로 5초가 걸리게 되고, 각자 독립적으로 실행이 된다. 여기서 이 독립이 정말 중요하다. 비동기는 독립적으로 돌아간다.

🤦🏻 비동기의 단점

흐름의 예측이 힘들다

예를 들어 사람 10명이 각자의 일을 정해진 순서에 맞게 하는 코드를 작성한다고 가정한다면 매우 복잡한 코드를 작성할 확률이 높다. 똑같은 함수를 여러개 작성하는 동기적인 코드가 오히려 흐름을 예측하는데 더 큰 도움이 될 것이다.

콜백의 늪에 빠지게 된다

위와 비슷한 이유이긴 하지만 각각 비동기 작업이 끝났을 때 뒤에 이어지는 작업을 미리 부여하는 방식은 자바스크립트에서 많이 표현하는 콜백 지옥 에 빠지게 된다.

이런 콜백의 늪에서 빠지기 위해서 Promise 가 등장했다고 하지만 이 Promise 역시 콜백을 통해 다음 할 일을 정하긴 한다. 하지만 코드가 조금 지저분하게 되는 것을 조금 방지하는 것 뿐이다.

이제 본격적으로 Promise 에 대해 알아보자.

📂 Promise

Promise 는 비동기 작업의 단위를 의미한다. 사용하는 방법은 여러가지가 있지만 가장 정석적이고, 공식문서에도 설명되어 있는 것을 이용하는 것이 좋은 것 같다.
(아직 많이 사용해보지 않아서 모르지만 기본적인 방법을 사용하고 후에 다시 업데이트 해야지...)

Promise 사용법

const promise = new Promise((resolve, reject) => {
  // 비동기 작업 {...}
});
  • new Promise()Promise 객체를 새롭게 만들었다. 셍성자는 특별한 함수 하나를 인자로 받는다. 이 특별한 함수를 공식 문서에는 executor 라는 이름으로 불린다.
  • executor 는 첫번째 인수로 resolve, 두번째 인수로 reject 를 받는다.
  • resolveexecutor 내에서 호출할 수 있는 또 다른 함수이다. resolve 를 호출한다면 해당 비동기 작업은 성공한 것을 의미한다.
  • rejectexecutor 내에서 호출할 수 있는 또 다른 함수이다. reject 를 호출한다면 해당 비동기 작업은 실패한 것을 의미한다.

new Promise() 하는 순간 여기에 할당된 비동기 작업은 바로 시작된다. 함수는 정의하는 시점과 호출하는 시점이 다르지만, new Promise는 기다리지 않고 바로 호출한다. Promise가 끝나고 난 다음의 동작을 우리가 설정해줄 수 있는데, 그것이 바로 then 메서드와 catch 메서드이다.

  • then 메서드는 해당 Promise가 성공했을 때의 동작을 지정한다. 인자로 함수를 받는다.
  • catch 메서드는 해당 Promise가 실패했을 때의 동작을 지정한다. 인자로 역시 함수를 받는다.

자 그럼 이제 위에서 배운 내용을 이번 우아한테크코스 6기 프리코스 문제 를 통해 확인해보자.

숫자 야구 미션 문제에 적용

  static readLineAsync(query) {
    return new Promise((resolve, reject) => {
      if (arguments.length !== 1) {
        reject(new Error("arguments must be 1"));
      }
      {...}
    })
  }
  async readUserNumber(callback) {
    await Console.readLineAsync(GuideMessage.INPUT_NUMBER).then((input) => {
      callback(input);
    });
  }

일단 위에 있는 asyncawait 은 신경쓰지 말자. 뒤에서 더 자세하게 다룰 예정이다. Console API를 사용했기 때문에 API에 쓰인 코드 일부를 가져온 것이 맨 위 코드이다. 앞에서 배운 것과 마찬가지로 new Promise 를 사용하여 비동기 작업이 시작되었다. input 값을 받고 그 값이 성공적이라면 바로 아래에 작성된 코드와 같이 그 값에 대해 then 연산자가 진행되었다.

전반적인 구성은 위와 같다.

Promise 사용시 유의사항

executor 를 만들 때 아래와 같은 부분을 고려해야 한다.

  • executor 내부에서 throw 된다면 해당 에러로 reject 가 수행된다.
  • executor 의 리턴 값은 무시된다.
  • 첫번째 reject 혹은 resolve만 유효하다.

📂 async

async 사용법

async 키워드는 함수를 선언할 때 앞에 붙인다.
async 함수는 Promise 와 굉장히 밀접한 연관을 가지고 있는데, 기존에 작성하던 executor 로부터 몇 가지 규칙만 적용한다면 new Promise(…) 를 리턴하는 함수를 async 함수로 손쉽게 변환할 수 있습니다.
간단하게 정리하자면 아래와 같다.

  • 함수에 async 키워드를 붙인다.
  • new Promise() 부분을 없애고 executor 본문 내용만 남긴다.
  • resolve(value) 부분을 return value로 변경한다.
  • reject(new Error) 부분을 throw new Error(...) 로 수정한다.

예시를 확인해보자. 위의 예시는 모듈과의 연결, API 사용때문에 적합한 예시 코드는 아니지만 컨트롤러에 있는 함수 하나를 가져와서 확인해보자.

숫자 야구 미션 문제에 적용


  async inputRestartNumber() {
    await InputView.readRestartNumber((input) => {
      InputValidator.validateRestartNumber(input);
      if (input === "1") {
        this.resetGame();
      }
      if (input === "2") return;
    });
  }

async 를 함수 앞에 붙여주었고, new Promise() 부분이 생략되었고, return도 찾아볼 수 있다. 마치 평소 함수와 같이 사용된 것을 확인할 수 있다.

async 함수의 리턴 값은 Promise 이다

async 함수는 일반 함수와 다르다. 그리고 함수처럼 사용할 수 없다. 예를 들어 문자열을 리턴하였다고 하더라도 promise 는 문자열이 아니다. 앞으로 무조건 async 함수를 실행시킨 뒤 thencatch 를 활용하여 흐름을 제어해야 한다.

익숙하지 않는 작업을 하게 되는데 다행인건 async 함수 안에는 await 함수를 사용할 수 있다는 점이다.

📂 await

await은 기다리라는 뜻을 가지고 있을 것이라고 추측을 할 수 있다. 실제로도 그런 역할을 한다.

await 사용법

  • awaitPromise 가 완료될 때까지 기다린다. 따라서 executor 에서 resolve 함수가 호출될 때까지 기다린다.
  • awaitPromiseresolve 한 값을 내놓는다.
  • 해당 Promise 에서 reject 가 발생한다면 예외가 발생한다. 이 예외 처리를 하기 위해 try-catch 구문을 사용했다. 이로써 익숙한 에러 처리 흐름을 진행할 수 있다.

awaitthencatch 의 동작을 모두 자기 나름대로 처리하기 때문에 async 함수 내에서 then , catch 메서드의 존재를 잊게 할 수 있다는 장점이 있다.

숫자 야구 미션 문제에 적용


  async inputRestartNumber() {
    await InputView.readRestartNumber((input) => {
      InputValidator.validateRestartNumber(input);
      if (input === "1") {
        this.resetGame();
      }
      if (input === "2") return;
    });
  }

async 함수 안에 await 을 가져왔다. 따라서 InputView.readRestartNumber() 함수는 연결이 되어있는 모듈에 있는 Promise 시행이 끝나기를 기다리고 resolve한 값을 처리한다. 마치 일반 함수와 같아진 것 같아 보인다.

async 안에서만 await을 사용할 수 있는 이유

어떤 블로그 글을 봤는데 이 비유가 매우 찰떡이었던 것 같아서 그대로 인용하겠습니다.(아래 링크 참조)

비동기 작업으로부터 파생된 모든 작업은 비동기 작업으로 간주할 수 있습니다. 어느 항구 마을에서 커다란 고기잡이 배를 바다로 떠나보낸다고 가정합시다. 커다란 고기잡이 배는 비동기 작업의 시작입니다. 동이 틀 무렵 고기잡이 배는 떠났고, 그 고기잡이 배는 나름 열심히 일할 겁니다. 고기잡이 배에서 다른 소형 배를 다시 내보내든 그물을 준비하는 작업을 하든 큰 배를 떠나보낸 항구 입장에서는 신경쓸 일이 없습니다. 배 안에서 일어나는 게 비동기 작업이든 동기 작업이든, 항구 입장에서는 모두 비동기 작업입니다.

동기 환경에서 비동기 작업을 기다리는 것은 의미가 없다. 다른 작업을 수행할 수도 있는 시간인데 아무것도 하지 않고 기다리는 것은 비동기 작업의 의의가 없다.

반면 비동기 환경에서 비동기 작업을 기다리는 것은 의미가 있다. 기다린다는 것은 동기 작업처럼 동작을 한다는 의미이고 종종 유용하다. 어떤 일의 과정이 있어야 결과가 나오는 것처럼 기다리는 것이 정답일 때가 있다. 그때 await 을 사용하는 것이다.

비동기는 동작의 특성상 실제 작업과 작업의 후속조치를 따로 분리하였는데(try, catch) 이것을 asyncawait을 사용하여 하나의 흐름 속에서 코딩을 할 수 있게 해주었다. 실제 작업이 끝난 다음 후속조치를 하는 것이 아닌 실제 작업이 끝난 것을 기다린 다음 다음 코드를 수행하는 느낌으로 코딩을 할 수 있게 해준다.

기다린다는 것은 동기 코드를 쓸 때 했던 것이지만 asyncawait 은 우리가 예전에 동기 코드를 작성했던 익숙한 느낌으로 비동기 작업을 할 수 있게 도와준다.

🔚 마무리

아직 비동기 코드를 많이 작성해보지 않아서 다양한 예를 들 수 없었다.
앞으로 코딩을 하면서 부족한 내용이나 내가 잘못 알았던 내용은 새롭게 표시할 예정이다.

부족한 점이 있으면 말씀 해주시면 감사하겠습니다. 🙇🏻‍♂️

📑 참고 자료

Promise 공식 문서
봄가을 블로그 - 비동기 Promise, async, await
khy226님 블로그 : 동기, 비동기란? (+Promise, async/await 개념)
동기 & 비동기 설명 이미지

profile
블로그 이전했습니다

0개의 댓글