자바스크립트 13. 비동기의 꽃 JavaScript async 와 await 그리고 유용한 Promise APIs | 프론트엔드 개발자 입문편 (JavaScript ES6)
자바스크립트를 비동기 언어라고 오해를 하는 경우도 있는데, 이는 우리가 Javascript를 비동기적으로 동작하도록 조작할 수 있기 때문이다.
비동기적으로 동작하는 경우는 WebAPI의 ajax, setTimeout(), Event handler, DOM 이 있다. 즉 브라우저엔진에서 작동할 때 비동기적으로 처리가 된다.
비동기로 동작하는 핵심요소는 자바스크립트 언어가 아니라 브라우저가 가지고 있다.
브라우저는 Web APIs, Event Table, Callback Queue, Event Loop 등으로 구성된다.
자바스크립트 코드가 실행될때 브라우저 동작은 아래그림을 통해 참고할 수 있다.
아래의 동작 순서를 이해하고나면, 자바스크립트 언어자체가 비동기 특성을 제공하는 것이 아니라, 브라우저의 구성 요소들이 제공한다는 것을 알 수 있다.
const cb = () => {
console.log('second');
};
console.log('first');
setTimeout(() => cb(), 1000);
console.log('third');
console.log(‘first’)가 Call Stack에 추가(push)
console.log(‘first’)가 실행되어 화면에 출력한 뒤, Call Stack에서 제거(pop)
3.setTimeout(function cb() {..}) 이 Call Stack에 추가
setTimeout 함수가 실행되면서 Browser가 제공하는 timer Web API 를 호출. 그 후 Call Stack에서 제거.
console.log(‘third’)가 Call Stack에 추가
console.log(‘third’)가 실행되어 화면에 출력되고 Call Stack에서 제거
setTimeout 함수에 전달한 1000ms 시간이 지난뒤 Callback으로 전달한 cb 함수가 Callback Queue에 추가.
8. Event Loop는 Call Stack이 비어있는 것을 확인하고 Callback Queue를 살펴본다. cb를 발견한 Event Loop는 Call Stack에 cb를 추가
cb 함수가 실행 되고 내부의 console.log(‘second’)가 Call Stack에 추가
console.log(‘second’)가 화면에 출력되고 Call Stack에서 제거
cb가 Call Stack에서 제거
이런식으로 자바스크립트는 브라우저엔진을 통해 비동기적으로 동작하게 된다.
개발을 진행하다보면 비동기적으로 동작하는 코드를 쓰게되는데 이를 동기적으로 실행하게 해주는 대표적인 방식으로는 Promise , async/await가 있다.
비동기 로직을 마치 동기처럼 사용할 수 있게 해주는 객체.
const promise = new Promise((resolve, reject) => {
// code
});
resolve
: 성공했을때 실행되는 함수
reject
: 실패했을때 실행되는 함수
callback
함수 : 어떤일이 완료되고 나서 실행되는 함수
new Promise 가 반환하는 객체
state : pending (대기)
result : undefined
state : fulfilled (이행)
result : value // resolve 함수로 전달된 값
state : rejected (거부됨)
result : error // reject 함수로 전달된 error
const delivery = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('OK')
}, 3000)
});
위 코드는 3초 후에 state: pending , result: undefined 상태에서 state :fufilled, result:'OK' 상태로 바뀐다.
const delivery = new Promise((resolve, reject) => {
setTimeout(()=>{
reject(new Error('error'))
}, 3000)
});
위 코드는 3초 후에 state: pending , result: undefined 상태에서 state :rejected, result: error 상태로 바뀐다.
Promise 객체를 통해 value값을 얻기 위해서는 then 을 이용해주어야합니다.
const delivery = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('OK')
}, 3000)
});
delivery.then(
function(result){
console.log(
result + '의 주문이 완료되었습니다.'
);
}
).catch(
function(err){
console.log('다시 주문해주세요');
}
)
promise를 사용하면서 콜백지옥이라고하는 가독성이 안좋은 현상을 피할 수 있습니다.
아래 코드는 callback으로 구현한 동기 작업입니다.
const f1 = (callback) => {
setTimeout(function () {
console.log("1번 주문 완료");
callback();
}, 1000);
};
const f2 = (callback) => {
setTimeout(function () {
console.log("2번 주문 완료");
callback();
}, 3000);
};
const f3 = (callback) => {
setTimeout(function () {
console.log("3번 주문 완료");
callback();
}, 2000);
};
console.log("시작");
f1(function() {
f2(function() {
f3(function() {
console.log("끝");
});
});
});
const f1 = () => {
return new Promise((res, rej) => {
setTimeout(() => {
res("1번 주문 완료");
}, 1000);
});
};
const f2 = (message) => {
console.log(message);
return new Promise((res, rej) => {
setTimeout(() => {
res("2번 주문 완료");
}, 3000);
});
};
const f3 = (message) => {
console.log(message);
return new Promise((res, rej) => {
setTimeout(() => {
res("3번 주문 완료");
}, 2000);
});
};
console.log('시작');
f1()
.then((res) => f2(res))
.then((res) => f3(res))
.then((res) => console.log(res))
.catch(console.log) // 에러 처리
.finally(() => {
console.log('끝');
});
위의 방법대로하면 총 6초라는 시간이 걸림, 이를 줄이기위해 한꺼번에 코드를 실행하고 결과를 도출하는 방법, promise에서 제공하는 API
Promise.all([f1(), f2(), f3()])
.then(res => {
console.log(res);
});
// 세 작업이 모두 완료 후 then 이 실행된다.
> ["1번 주문 완료", "2번 주문 완료", "3번 주문 완료"]
Promise.race([f1(), f2(), f3()])
.then(res => {
console.log(res);
});
// 1번 주문 함수가 가장 빨리 끝나기때문에, 1번 주문 후 Promise 종료되는것을 확인할 수 있음
> "1번 주문 완료"
Promise를 깔끔한 스타일로 더 쉽게 사용할 수 있도록 해주는 ES8 문법
async를 이용하면 promise의 then method를 이용하는 것 보다 가독성이 좋아진다.
await는 반드시 함수안에서 실행되어야 하며, await를 포함한 함수는 async 키워드를 추가해줘야한다.
const something = async() => {
return "Tech";
};
console.log(something());
> Promise {} // async 함수를 호출하면 promise 객체가 출력된다.
something().then((title) => {
console.log(title);
});
> Tech
const something = async (title) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(title);
}, 1000);
});
}
const getSomething = async () => {
const result = await something("Tech");
console.log(result);
}
console.log("시작");
getSomething();
> 시작
> Tech // 1초 후에 찍힘
const order = async() => {
try { // 에러 핸들링
const result1 = await f1();
const result2 = await f2(result1);
const result3 = await f3(result2);
console.log(result3);
} catch (e) {
console.log(e);
}
}
order()
> "1번 주문 완료"
> "2번 주문 완료"
> "3번 주문 완료"
// fruitPicker() 라는 Promise를 반환하는 함수가 있다고 가정
fruitPicker.create()
.then((category) => {
fruitPicker.create(category.id)
.then((fruit) => {
fruit.send(fruitList);
});
fruitPicker.create(category.id)
.then((fruit) => {
fruit.send(fruitList);
});
});
// async/await 적용
const category = fruitPicker.create();
const sendFruit = async (category) => {
const fruit1 = await fruitPicker.create(category.id);
await fruit1.send(fruitList);
const fruit2 = await fruitPicker.create(category.id);
await fruit2.send(fruitList);
}
const delay = (time) => {
setTimeout(
()=>{}, time)
};
async function getTitle() {
await delay(1000);
return 'Title';
}
async function getBody() {
await delay(1000);
return 'Body';
}
async function getAll() {
const titlePromise = getTitle();
const bodyPromise = getBody();
const title = await titlePromise;
const body = await bodyPromise;
return `${title} ${body} here`
}
getAll().then(console.log);
> 'Title Body' // 1초만에 실행되는것을 확인할 수 있다.
// 하지만 이방법으로 쓰게되면 코드가 길어지기때문에 위에서 다뤘던 Promise.all()을 사용하는것이 좋다.
useEffect(() => {
async () => {
const getData = await axios.get("https://dailyfunding.com/users/1");
setMemberInfo(getData.data);
}
}, []);
사용자의 데이터를 얻어온 후 memberInfo 값을 셋팅해주기 위해서 API와 통신하는 부분에 await를 선언해 동기적으로 실행하게 만들어준다.