우아한테크코스 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
는 비동기 작업의 단위를 의미한다. 사용하는 방법은 여러가지가 있지만 가장 정석적이고, 공식문서에도 설명되어 있는 것을 이용하는 것이 좋은 것 같다.
(아직 많이 사용해보지 않아서 모르지만 기본적인 방법을 사용하고 후에 다시 업데이트 해야지...)
const promise = new Promise((resolve, reject) => {
// 비동기 작업 {...}
});
new Promise()
로 Promise
객체를 새롭게 만들었다. 셍성자는 특별한 함수 하나를 인자로 받는다. 이 특별한 함수를 공식 문서에는 executor
라는 이름으로 불린다.executor
는 첫번째 인수로 resolve
, 두번째 인수로 reject
를 받는다.resolve
는 executor
내에서 호출할 수 있는 또 다른 함수이다. resolve
를 호출한다면 해당 비동기 작업
은 성공한 것을 의미한다.reject
는 executor
내에서 호출할 수 있는 또 다른 함수이다. 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);
});
}
일단 위에 있는 async
와 await
은 신경쓰지 말자. 뒤에서 더 자세하게 다룰 예정이다. Console
API를 사용했기 때문에 API에 쓰인 코드 일부를 가져온 것이 맨 위 코드이다. 앞에서 배운 것과 마찬가지로 new Promise
를 사용하여 비동기 작업이 시작되었다. input
값을 받고 그 값이 성공적이라면 바로 아래에 작성된 코드와 같이 그 값에 대해 then
연산자가 진행되었다.
전반적인 구성은 위와 같다.
executor
를 만들 때 아래와 같은 부분을 고려해야 한다.
executor
내부에서 throw
된다면 해당 에러로 reject
가 수행된다.executor
의 리턴 값은 무시된다.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
함수를 실행시킨 뒤 then
과 catch
를 활용하여 흐름을 제어해야 한다.
익숙하지 않는 작업을 하게 되는데 다행인건 async
함수 안에는 await
함수를 사용할 수 있다는 점이다.
await
은 기다리라는 뜻을 가지고 있을 것이라고 추측을 할 수 있다. 실제로도 그런 역할을 한다.
await
은 Promise
가 완료될 때까지 기다린다. 따라서 executor
에서 resolve
함수가 호출될 때까지 기다린다.await
은 Promise
가 resolve
한 값을 내놓는다.Promise
에서 reject
가 발생한다면 예외가 발생한다. 이 예외 처리를 하기 위해 try-catch
구문을 사용했다. 이로써 익숙한 에러 처리 흐름을 진행할 수 있다. await
은 then
과 catch
의 동작을 모두 자기 나름대로 처리하기 때문에 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한 값을 처리한다. 마치 일반 함수와 같아진 것 같아 보인다.
어떤 블로그 글을 봤는데 이 비유가 매우 찰떡이었던 것 같아서 그대로 인용하겠습니다.(아래 링크 참조)
비동기 작업으로부터 파생된 모든 작업은 비동기 작업으로 간주할 수 있습니다. 어느 항구 마을에서 커다란 고기잡이 배를 바다로 떠나보낸다고 가정합시다. 커다란 고기잡이 배는 비동기 작업의 시작입니다. 동이 틀 무렵 고기잡이 배는 떠났고, 그 고기잡이 배는 나름 열심히 일할 겁니다. 고기잡이 배에서 다른 소형 배를 다시 내보내든 그물을 준비하는 작업을 하든 큰 배를 떠나보낸 항구 입장에서는 신경쓸 일이 없습니다. 배 안에서 일어나는 게 비동기 작업이든 동기 작업이든, 항구 입장에서는 모두 비동기 작업입니다.
동기 환경에서 비동기 작업을 기다리는 것은 의미가 없다. 다른 작업을 수행할 수도 있는 시간인데 아무것도 하지 않고 기다리는 것은 비동기 작업의 의의가 없다.
반면 비동기 환경에서 비동기 작업을 기다리는 것은 의미가 있다. 기다린다는 것은 동기 작업처럼 동작을 한다는 의미이고 종종 유용하다. 어떤 일의 과정이 있어야 결과가 나오는 것처럼 기다리는 것이 정답일 때가 있다. 그때 await
을 사용하는 것이다.
비동기는 동작의 특성상 실제 작업과 작업의 후속조치를 따로 분리하였는데(try
, catch
) 이것을 async
와 await
을 사용하여 하나의 흐름 속에서 코딩을 할 수 있게 해주었다. 실제 작업이 끝난 다음 후속조치를 하는 것이 아닌 실제 작업이 끝난 것을 기다린 다음 다음 코드를 수행하는 느낌으로 코딩을 할 수 있게 해준다.
기다린다는 것은 동기 코드를 쓸 때 했던 것이지만 async
와 await
은 우리가 예전에 동기 코드를 작성했던 익숙한 느낌으로 비동기 작업을 할 수 있게 도와준다.
아직 비동기 코드를 많이 작성해보지 않아서 다양한 예를 들 수 없었다.
앞으로 코딩을 하면서 부족한 내용이나 내가 잘못 알았던 내용은 새롭게 표시할 예정이다.
부족한 점이 있으면 말씀 해주시면 감사하겠습니다. 🙇🏻♂️
Promise 공식 문서
봄가을 블로그 - 비동기 Promise, async, await
khy226님 블로그 : 동기, 비동기란? (+Promise, async/await 개념)
동기 & 비동기 설명 이미지