What is Asynchronous?
JavaScript 언어 자체는 single thread 언어이지만, JS 엔진의 Event Loop와 Runtime Environment(코드가 실행되는 환경)의 협업을 통해, 한 번에 여러 줄의 코드를 실행하는 것이 가능한 것처럼 동작했다. setTimeout과 같은 비동기적(asynchronous)인 일을 수행해야 할 때 이런 협업이 필요하다. 비동기적인 일이란 정확히 무엇일까?
Asynchronous meaning in CS is a specific operation begins upon receipt of an indication (signal) that the preceding operation has been completed.
=> 이전 작업이 완료되었다는 신호를 받았을 때 실행하는 코드를 비동기적이라고 한다.
예를 들어 어떤 사용자가 웹사이트에서 로그인을 한다고 해보자. 이 때 사용자가 입력한 아이디와 패스워드가 서버에 저장된 데이터와 일치하는지를 확인해야 한다. 아이디와 패스워드가 모두 일치해서 로그인을 성공할 수도 있다. 또는, 아이디가 존재하지 않을 수도 있고, 아이디는 있는데 패스워드는 틀렸을 수도 있다. 때때로 사용자의 네트워크 연결이 끊겨서 서버로부터 데이터를 받아오는 작업 자체를 실패할 수도 있다. 어쨌든 이 모든 상황을 처리해야 하는 코드는 사용자가 로그인을 시도한 다음에야 실행되는 코드이다. 즉, 비동기적인 코드이다.
Callback function
searchMovieAPI("query", ()=> {
// if searching works, run this:
saveMovieToDB(result, ()=> {
// if saving works, run this:
}, ()=> {
// if saving doesn't work , run this:
});
}, ()=> {
// if searching doesn't work , run this:
throwError();
})
영화를 검색하는 api에는 두 개의 콜백 함수가 전달되어 있다. 하나는 영화를 검색해서 데이터를 받아오는 것이 성공적으로 끝나면, 그 결과를 데이터 베이스에 저장하는 함수이고, 다른 하나는 데이터를 받아오는 것을 실패 했을 때 에러를 보여주는 함수이다.
만약 데이터를 잘 받아왔다면, 그 데이터를 저장하는 함수가 실행되는데, 이 함수에도 다시 두 개의 콜백함수가 전달된다. 즉 콜백 함수 안에 콜백 함수가 중첩되어 있는 상태이다.
많은 콜백 함수를 전달하고 있거나, 여러 콜백 함수가 중첩된 상태에서는 코드의 가독성이 떨어지고, 코드를 이해하기 어려워진다(callback hell). 콜백 함수의 이런 문제점을 해결하기 위해 등장한 새로운 syntax가 promise이다.
what is Promise?
new Promise(( resolve, reject ) => {
/* asynchronous thing */
})
/*
* [[Prototype]]: Promise
* [[PromiseState]]: "pending"
* [[PromiseResult]]: undefined
*/
Promise는 프로퍼티와 메서드를 가지고 있는 객체로, 생성자 함수를 실행하면 Promise 객체가 반환된다.
Property | state & result
promise 객체가 가지고 있는 가장 중요한 프로퍼티는 state이다. state는 promise를 생성할 때 작성한 비동기적인 일을 성공적으로 수행했는지, 아닌지 등에 대한 데이터를 담고있으며, 세 가지 value를 가질 수 있다.
- pending | state의 초기값으로, 어떤 일을 성공하지도, 실패하지도 않았을 때를 가리킨다. 예를 들어 콜백함수 안에서 setTimeout을 실행한다면, 설정해둔 시간이 끝나기 전까지 state는 pending이 된다.
- fullfilled | 비동기 코드의 실행을 성공한 상태
- rejected | 비동기 코드의 실행을 실패한 상태
result는 state에 따라 프로그래머가 전달할 수 있는 값이다. state가 "fullfilled" 일 때 전달할 값과, "rejected" 일 때 전달할 값을 각각 설정할 수 있다. resolve 함수와 reject 함수가 그 일을 하는데, 예제 코드를 통해 더 자세히 살펴보자.
function runInDelay(seconds) {
return new Promise((resolve, reject) => {
setTimeout(()=> {
if(!seconds || seconds > 3000) {
reject("Connection Timeout!");
} else {
resolve("Connection Success");
}
}, seconds);
});
}
예제 코드의 runInDelay는 Promise 객체를 반환하는 함수이다. promise의 생성자 함수는 콜백함수를 인자로 받는데, 이 콜백함수는 다시 resolve와 reject 함수를 인자로 받아 비동기적인 일(setTimeOut)을 실행하는 함수이다.
만약 값을 전달받지 않거나, 3000보다 큰 값을 전달받았다면, 연결 시간이 초과되었다는 string을 인자로 받는 reject 함수를, 그 외의 경우에는 성공적으로 연결되었다는 string을 인자로 받는 resolve 함수를 실행할 수 있다. 이 때 resolve 함수와 reject 함수는 단순히 state를 fullfilled 또는 rejected로 결정하고, result를 전달하기 위한 껍데기 함수이다. 중요한 것은 state에 따라 우리가 원하는 일을 수행하는 것이다.
methods
Instance Level | then, catch, finally
runInDelay(2000)
.then((msg)=>console.log(msg)); // 실행⭕ Connection Success
.catch((msg)=>console.log(msg)); // 실행❌ Connection Timeout!
runInDelay(2000)를 실행하면 seconds === 2000이므로, 1) resolve 함수를 실행한 뒤에 2) state가 "fullfilled", 3) result가 "connection success"인 promise 객체를 반환한다.
promise 객체에는 이런 property 외에도, then과 catch라는 내장 method를 가지고 있다. then은 state가 "fullfilled"일 때, catch는 "rejected"일 때 전달받은 콜백함수를 실행하는 method이다.
- seconds === 2000 이므로 resolve 함수가 실행된다.
- state가 fullfilled, result가 "connection success"로 업데이트 된다.
- then 함수가 실행된다(resolve에 의해 state가 fullfilled로 업데이트 되었으므로).
- then 함수는 result(msg === connection success)를 인자로 받아서, 전달 받은 콜백함수(console.log)를 실행한다.
이 때 then method의 콜백 함수가 다시 promise 객체를 return하게 만들 수도 있다. 즉, promise 객체에 내장되어있는 then이나 catch method를 연결하는(chaining) 코드를 작성할 수 있다.
runInDelay(2000)
.then((msg)=>{
console.log(msg, "first request"); // ⭕
return runInDelay(2500);
});
.then((msg)=>{
console.log(msg, "second request"); // ⭕
return runInDelay(5500);
});
.then((msg)=>{
console.log(msg, "third request"); // ❌, ⬇️
});
.catch((err) => console.log(err));
반면 seconds에 3000보다 큰 값을 전달하면, reject 함수를 실행하고, state와 result를 각각 rejected와 "connection Timeout!"로 업데이트 한 뒤에, catch method를 실행할 것이다.
Static Level | mdn reference
const getBanana = () => new Promise(resolve => setTimeout(() => resolve('🍌'), 1000));
const getApple = () => new Promise(resolve => setTimeout(() => resolve('🍎'), 2000));
const getOrange = () => Promise.reject(new Error('오렌지는 없어!'));
// 1. Promise.reject
// state가 rejected인 promise를 만든다
getOrange();
/*
* [[Prototype]]: Promise
* [[PromiseState]]: "rejected"
* [[PromiseResult]]: Error: 오렌지는 없어!
*/
// 2. Promise.all(iterable)
Promise.all( [ getBanana(), getApple() ] )
.then((fruits) => console.log(fruits, '나는 동시에 바나나, 사과 promise를 실행해서 2초면 끝나!'));
// ['🍌', '🍎'], 나는...
// 3. Promise.race(iterable)
Promise.race( [ getBanana(), getApple() ] )
.then( win => console.log('나는 더 빠르게 끝나는 promise를 반환 해!', win) );
// 나는..., 🍌
// 4. Promise.allSettled(iterable)
Promise.allSettled([getBanana(), getApple(), getOrange()])
.then((fruits) => console.log(fruits, 'promise의 state와 value data를 알려줄거야'))
async & await
+) 외부에서 async 함수를 호출하면, await 키워드를 사용했더라도 항상 promise를 반환한다.
async와 await 키워드를 이용하면 promise를 더 쉽고, 깔끔하게 작성할 수 있다.
async function rainbow() {
await delayedColorChange("red", "1000"); // (*)
delayedColorChange("orange", "1000"); //(*)
}
/*
await 키워드를 사용하지 않았다면 *과 **가 함께 실행되어,
1000ms 뒤 배경색은 orange가 된다
*/
- async | function 키워드 앞에 작성할 수 있으며, async function은 자동으로 promise를 반환한다.
- async function이 어떤 값을 return 한다면, state와 result를 각각 fullfilled와 해당 값으로 업데이트 한다.
- async function이 어떤 Error 객체를 throw 한다면, state와 result를 각각 rejected와 에러 객체로 업데이트 된다.
- await | async function 내부에서 promise 또는 promise를 반환하는 코드 앞에 작성할 수 있다
- await 키워드는 해당 promise가 성공적으로 끝날 때 까지 다음 코드를 실행하지 않고 기다린다.
- 해당 promise가 가지고 있는 result 값을 반환하기 때문에, result를 변수에 저장하면, then을 이용해서 결과를 따로 받아올 필요가 없다.
Try... Catch
비동기 함수를 사용하다보면 예기치 않은 오류가 발생하는 상황이 자주 생긴다. 이럴 때 유용하게 사용할 수 있는 것이 try... catch 구문인데 다음과 같이 사용할 수 있다.
async function makeTwoDelays() {
try {
const first = await runInDelay(2500); // resolve
console.log(first); // prints out "connection success"
const second = await runInDelay(5500); // reject
console.log(second); // second reject => won't execute this
} catch(e) {
console.log(e); // second => prints out "connection timeout"
}
}
- try 구문 안에는 실행할 코드를 작성한다.
- catch 구문 안에는 위에서 작성한 코드에 에러가 발생했을 때 실행할 코드를 작성한다.
- try에서 에러가 발생하더라도, app이 crash되지 않고 다음 코드를 실행한다.
'Study > JavaScript' 카테고리의 다른 글
OOP | Prototype, Class (0) | 2022.04.05 |
---|---|
Scope, Execution context, Closure (0) | 2022.04.04 |
Call stack & Task que (0) | 2022.04.01 |
Modularization (0) | 2022.03.31 |
Iterable (0) | 2022.03.30 |