비동기 처리를 위한 문법의 "역사"
이런 말이 맞을 지 모르겠지만 거창하게 역사 라는 말을 붙여보았다..ㅋㅋ비동기 처리를 위해 ES6에서 여러 문법이 추가되는 과정을 공부하고 나니 괜히 하나의 "역사"를 본 것 같아서ㅎㅎ
Promises
ES6의 새로운 문법
- javascript에서 비동기를 처리할 때 주로 사용되는 문법
- 콜백 지옥(callback hell, 프로미스가 나오기 전에는 콜백을 사용해서 비동기를 처리하였는데, 콜백이 점점 쌓이면서 깊이가 점점 깊어지는 문제)을 해결하였다!!
용어 정리
비동기 처리
동기와 비동기에 대해 먼저 알아야 한다.
- 동기 (synchronus : 동시에 일어나는) 동시에 일어난다. 요청과 동시에 응답이 주어져야한다.
- 즉, 요청을 보냈을 때 응답을 받아야지만 다음 동작이 이루어지는 직렬 방식이다.
- 장점 : 직관적 | 단점 : 응답이 올 때까지 대기해야 한다.
- 비동기 (asynchronous : 동시에 일어나지 않는) 동시에 일어나지 않는다. 요청과 응답이 동시에 일어나지 않을 수 있다.
- 병렬 방식이다.
- 장점 : 응답이 올 때까지 효율적으로 다른 작업을 처리할 수 있다. | 단점 : 복잡함
비동기 처리는 동기적으로 발생하는 일을 비동기처럼 처리하는 것이라고 할 수 있다.
그리고 이를 처리해주기 위해 Call Back 함수를 사용했다.
Call Back 함수
콜백 함수는 특정 함수에 매개변수로 전달된 함수를 의미한다. 그 콜백 함수는 전달받은 함수 안에서 호출된다.
- 예시로 "버스를 타는 과정" 을 생각해보겠다.
- 버스를 기다린다 (isBusHere)
- 버스가 정류장에 도착한다 (isBusStop)
- 버스 문이 열린다 (isDoorOpen)
- 지갑을 찾는다 (findWallet)
- 지갑에 교통카드를 찾는다 (findCard)
- 교통카드로 버스 요금을 낸다 (payBusFare)
1번을 해야 2번을 할 수 있고, 2번을 해야 3번을 할 수 있는 동기적인 과정이다. 이를 비동기적으로 처리하기 위해서 콜백을 이용해야했다.
isBusHere(function(isBusHereResult) {
isBusStop(isBUsHereResult, function(isBusStopResult) {
isDoorOpen(isBusStopResult, function(isDoorOpenResult) {
findWallet(isDoorOpenResult, function(isWalletHere) {
findCard(isWalletHere, function(isCardHere) {
payBusFare(isCardHere, function () {
console.log('결제 완료');
}, failureCallback);
}, failureCallback);
}, failureCallback);
}, failureCallback);
}, failureCallback);
}, failureCallback);
→ 버스를 기다리는 함수가 실행되고, callback 함수로 isBusHereResult 응답이 올 때까지 기다린다. callback 함수가 잘 실행이 되면 (응답이 오면) 다음 함수를 실행한다. x 반복하다보면 depth가 매우 깊어진다. → 콜백 지옥!!!
이를 해결한게 Promise이다. Promise로 작성해볼까?
isBusHere()
.then(isBusHereResult) {
return isBusStop(isBusHereResult) //callback이 실행됐을 때 수행
})
.then(function(isBusStopResult) { //.then : 위 함수실행이 끝나면 이걸 해라
return isDoorOpen(isBusStopResult)
})
.then(function(isDoorOpenResult) {
return findWallet(isDoorOpenResult)
})
.then(function(isWalletHere) {
return findCard(isWalletHere)
})
.then(function(isCardHere) {
return payBusFare(isCardHere)
})
.catch( failureCallback ); //.catch는 Promise의 try-catch 메서드이다.
추가로, 이전 글에서 언급한 Arrow Function을 써주면?! 더 간결해진다.
isBusHere()
.then(isBusHereResult => isBusStop(isBusHereResult))
.then(isBusStopResult => isDoorOpen(isBusStopResult))
.then(isDoorOpenResult => findWallet(isDoorOpenResult))
.then(isWalletHere => findCard(isWalletHere))
.then(isCardHere => payBusFare(isCardHere))
.catch(failureCallback);
그럼 비동기 처리는 왜 필요한가?
데이터를 서버로 받아와서 보여주는 앱이라고 가정했을 때, 서버는 데이터를 먼저 받아와야 데이터를 뿌려줄 수 있기 때문에 맨 처음에 서버로부터 데이터를 받아오는 코드를 실행시킬 것이다. 그렇다고 서버가 데이터를 받아오고 나서 앱을 실행시키면 ? 데이터를 받아오기까지 앱이 대기하는 상태가 발생한다.
이를 방지하기 위해 데이터 수신과 페이지 표시는 비동기적으로 처리해야한다.
Promise의 상태
Promise의 상태는 총 3가지가 있다.
- Pending(대기) : Promise가 처음 생성되면 대기 상태
const promise = new Promise((resolve, reject) => { //함수 정의 });
// Promise { <pending> } 출력
2. Fulfilled(이행) : Promise에서 resolve를 실행하면 상태는 이행(Fulfilled)이 된다. 다르게 생각하면, Promise에서 실행이 끝나면 rsolve를 실행시킨다.
const promise = new Promise((resolve, reject) => { //함수 정의 resolve(); });
console.log(promise);
// Promise { undefined } 출력 -> 특정값 넘겨준 상태
3. Rejected(실패) : Promise에서 reject를 실행하면 상태는 실패(Rejected)가 된다.
const promise = new Promise((resolve, reject) => { //함수 정의 reject(); });
console.log(promise);
//Promise { <rejected> undefined } 출력
이 세가지 상태에 대해서 조건문을 통해 다뤄보면
const isReady = true;
// 1. Producer
const promise = new Promise((resolve, reject) => { //함수 정의
console.log("pending 상태");
if (isReady) {
resolve("It's ready"); //이행이 잘 되면
} else {
reject("Not ready");
}
});
// 2. Consumer
promise //promise 함수 실행
// promise에서 resolve가 잘 이행된 경우, then으로 넘어옴
.then(messsage => {
console.log(messsage); // 인자로 넘어온 string을 실행
})
// promise에서 reject(에러 발생)가 된 경우. isReady가 false인 경우
.catch(error => {
console.error(error);
})
// true의 출력
pending 상태
It's ready
// false의 출력
pending 상태
Not ready
그럼 위에서 Producer와 Consumer는 뭘까?
Producer
- Promise를 처음 생성할 때 Promise의 내부 코드블럭이 실행(executor라고 함)되는데 이 실행 결과에 따라 resolve 혹은 reject를 부르는 역할!
Consumer
- Promise의 결과에 따라 후처리를 하는 역할.
- 정상적으로 실행되면(resolve가 되었으면) then을 통해 후처리.
- reject가 되었으면 catch를 통해 후처리.
<정리>
콜백이 많아질 수록 depth가 깊어지는 콜백 지옥 문제를 해결하고자 Promise가 등장하였고, 이는 콜백을 내부에 추가하되 then 메서드를 이용함으로써 복잡성을 줄였다.
async, await
Promise를 더 편하게 사용하기 위한 문법
async
- 일반 함수 앞에 async 를 붙이면 해당 함수는 항상 Promise를 반환하게 된다.
await
- async 함수 내부에서 쓰인다.
- await 키워드를 만나면 promise가 끝날 때까지 기다린 후에 실행된다. 기존에 then을 사용할 때는 코드가 위에서부터 실행되다가 promise가 실행되면 분기가 이루어지면서 한쪽에서는 코드가 계속 실행되고 한쪽에서는 then으로 한 depth 들어가는 셈이었는데 await를 사용하게 되면 일렬로 실행되는 것이다.
코드로 다시 얘기하자면
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"),1000) //1초 후에 string 전달
});
console.log("then 전");
promise
.then(message => {
console.log(message);
})
.catch(error => {
console.log(error);
})
console.log("code end");
promise를 반환하는 분기와 쭉 코드를 아래로 실행시키는 분기가 나눠지면서 실행이 된다. (한 줄로 쭉 실행되는 게 아닌!)
- promise를 반환하는 분기는 promise에서 resolve가 이행이 되면서 then으로 넘어가고
- 쭉 코드를 아래로 실행시키는 분기는 계속내려가면서 console을 찍는다.
출력은 어떻게 될까?
쭉 내려온 분기의 실행이 끝난 (code end) 후에야 "It's ready"가 출력된다.
then 전
code end
It's ready // Promise 이행
이제 async, await를 이용한 함수로 고쳐보겠다.
- Prmise와 달리 한 줄로 쭉 실행된다고 언급했다!
- 출력은 순서대로 진행되서 아래와 같이 출력된다. beore await after await (1초 후에 출력됨) 완료!
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"),1000) //1초 후에 string 전달
});
console.log("before await");
let result = await promise; // 프라미스가 이행될 때까지 기다림 (*) string이 전달되면 result에 저장하고 넘어감.
console.log("after await");
console.log(result); // "완료!"
}
f();
//"before await" 출력되고 1초 후에 after await과 "완료!" 가 찍힘.
비동기적 처리가 어디서 쓰일까?
서버에서 외부로 데이터를 요청했을 때 응답을 받고나서 실행이 돼야 할 때 쓰인다.
그래서 http call을 요청한 응답이 result에 저장되는 식으로 진행된다. 위의 코드를 조금만 변경하면 된다.
서버에있는 JSON 파일을 불러오는 행위를 간단히 봐보자.
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
먼저 fetch 를 이용해서 user.json 파일을 가져올 때까지 기다린다(await).
그다음 response를 json화할 때까지 또 기다리고(await), user에 데이터를 저장한다.
그다음 github를 통해 사용자 정보를 가져온다.
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
이제 설명은 생략하고 비동기 처리를 한 개 더 추가해보겠다.
이제는 await 키워드를 본 순간 비동기 처리라는 것을 알 수 있어야 한다!
async function showAvatar() {
//...
//추가
// 아바타 보여주기
let img = document.createElement('img'); // 요소 생성
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// 3초 대기(비동기 처리)
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAbatar();
<결론>
asyn, await는 call back을 여러 번 해야 될 것을 다음과 같이 가독성 좋게 해결하였다.
비동기 처리의 역사를 봤다!
비동기 처리를 해결하기 위해 콜백함수를 사용했고..
콜백 지옥 문제를 해결하기 위해 Promise를 사용했고..
Promise를 편하게 사용하기 위해 async와 await를 사용한다.
공부하면서 정리한 내용이라 잘못된 내용이 있을 수 있습니다.
따뜻한 피드백은 환영입니다♥
'언어 > node.js' 카테고리의 다른 글
Node.js를 이용한 크롤링 (2) | 2021.10.12 |
---|---|
html의 유용한 기능 (0) | 2021.08.31 |
[npm] 필요한 모듈 한번에 설치하기 (0) | 2021.07.28 |
[개념] Routing (0) | 2021.07.20 |
[개념] JS와 ES6 문법 (0) | 2021.07.20 |