우리가 인터넷에 정보를 검색하면 그 결과를 기다리고, 결과를 로딩하면 화면에 뷰가 출력되는 것을 경험해본 적이 있을 것이다.
앞의 과정에 종속되어서 결과를 기다리고, 뒤의 과정이 실행되는 것은 비일비재하다.
즉 위와 같은 일이 빈번하다는 것이다.
급식을 먹으려면 줄을서야하고, 내 앞의 사람이 다 사라지만 식판을 꺼내들고, 식판을 들었으면 배식받고...
하지만, 굳이 종속적이지 않다면 동시에 실행하면 어떨까?
이렇게 말이다. 숨을 쉬면서, 벨로그 글을 쓰고 이 두가지일은 종속성이 없기때문에 동시에 실행이 가능하다.
그러면 실행시간도 단축되고 얼마나 편할까? 그래서 자바스크립트에서는 비동기라는 개념을 도입하였다.
맞다. 자바스크립트는 싱글스레드 언어이다. 하나의 시간대에 한 종류의 일을 처리한다는 말이다.
수강신청 사이트를 예로 들면, 버튼을 누르고 이에 대한 결과를 기다리고, 화면을 갱신해야한다. 그런데 현재 시간은 계속 1초마다 흐르는 것을 표현해야한다. 두 개를 동시에 어떻게 하는 것일까?
어떻게 이를 가능하게 한 것일까?
자바스크립트는 요청이 들어오면 콜스택에 저장한다. 그리고 그 스택에 있는 요청을 Pop 하여 처리한다. 싱글쓰레드이기때문에, 콜스택은 1개이다.
이벤트가 발생할 때 실행될 콜백 함수가 저장되는 공간
콜백 함수 중 process.nextTick()
, Promise객체의 callback
, async function
, queueMicroTask
에 해당하는 것들이 적재된다.
콜백 함수 중 setTimeout()
특정 시간 이후 실행, setInterval()
특정 시간을 주기로 반복 실행 , setImmediate()
이거는 아직 잘 모르겠다...
이 3가지에 대한 것을 매크로태스크 큐에 저장한다고 한다.
위의 콜스택과 2가지의 태스크 큐를 합친 총 3개를 감시한다.
감시하다가 콜스택이 비어있으면 태스크 큐의 일을 콜스택으로 넘겨준다.
function fifth() { }
function fourth() { fifth() }
function third() { fourth() }
function second() { third() }
function first() { second() }
first();
function promiseFunc(){
return new Promise(function(res, rej)
{// Doing something!
res(1);
});
}
console.log("2");
promiseFunc().then(console.log);
console.log("3")
이런 비동기 작업을 처리하기 위해 Promise를 만들었다.
const promise = new Promise((resolve, reject) => {
if (/* 비동기 성공 시 */) {
resolve('resolve');
}
else { /* 비동기 실패 시 */
reject('reject');
}
});
pending: 비동기 처리가 아직 수행되지 않은 상태
fulfilled: 비동기 처리가 수행된 상태 (성공)
rejected: 비동기 처리가 수행된 상태 (실패)
settled: 비동기 처리가 수행된 상태 (성공 또는 실패)
단순히 프로미스만으로 로직을 구현하면, 이후에 어떤 행동을 해야할지 정의할 필요가 있다.
예를 들어, https://api.test.io/index.html 에 GET 요청을 한다고 하자.
const req = new Promise((resolve, reject) => {
var url = "https://api.test.io/index.html";
var res = 요청하는함수(url);
if (res == 성공) {
resolve('resolve');
}
else {
reject('reject');
}
});
여기서 성공을 했을 때와, 실패를 했을 때의 핸들링을 프로미스 안에서 할 수도 있겠지만, 후속 처리 메소드를 통해도 구현이 가능하다.
then 메소드는 두 개의 콜백 함수를 파라미터로 가진다.
첫 번째 콜백 함수는 성공(fulfilled, resolve 함수가 호출된 경우)시에 실행된다.
두 번째 콜백 함수는 실패(rejected, reject 함수가 호출된 경우)시에 실행된다.
then 메소드는 기본적으로 프로미스를 반환한다.
catch 메소드는 비동기 처리 혹은 then 메소드 실행 중 발생한 에러(예외)가 발생하면 호출된다.
catch 메소드 역시 프로미스를 반환한다.
const req = new Promise((resolve, reject) => {
var url = "https://api.test.io/index.html";
var res = 요청하는함수(url);
if (res == 성공) {
resolve('resolve');
}
else {
reject('reject');
}
});
req.then(
(data) => {
console.log('성공: ', data);
},
(error) => {
console.log('실패: ', error);
}
)
.catch((error) => {
console.error('에러 핸들링: ', error);
});
then에는 2개의 함수가 들어갈 수도, 1개가 들어갈 수도 있다. 1개라면 성공에 대한 메시지(Promise가 resolve), 2개라면 실패에 대한 메시지(Promise가 reject)가 담길 수 있다.
위 코드에서 요청하는함수(url)
에서 어떠한 에러가 throw 된다면, catch 메소드에서 해당 오류를 잡아 핸들링을 할 수 있다.
개발자들이라면 자주본 짤...
아도겐을 처맞은 코드이다. 그런데 이를 Promise로 해결할 수 있다고하지만, 사실 Promise를 사용해도 then을 남용하면.....
function asyncOperation(value) {
return new Promise((resolve) => {
// 간단한 비동기 작업 시뮬레이션
setTimeout(() => {
console.log('작업 완료:', value);
resolve(value + 1);
}, 1000);
});
}
// 무한정 실행되는 프로미스 지옥...
asyncOperation(1)
.then((result) => {
return asyncOperation(result);
})
.then((result) => {
return asyncOperation(result);
})
.then((result) => {
return asyncOperation(result);
})
.then((result) => {
// 이어서 계속해서 then을 쌓을 수 있음
return asyncOperation(result);
})
// ...
// 이어지는 then 계속 쌓는 부분은 생략
가독성이 엄청 떨어진다.... 이를 해결하기 위해 async/await가 등장했다.
참고로 위는 callback hell의 중 하나인 promise hell이라고 부른다.
async는 태스크큐에 작업이 올라가는 코드가 존재하는 함수에 키워드로 사용한다.
await는 해당 작업을 기다리는 역할을 한다.
또한 async로 선언한 함수는 자동으로 Promise 객체를 반환한다.
async function f() {
return 1;
}
f().then(alert); // 1
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
위 두 코드는 똑같이 Promise 객체를 반환하기 때문에 후속 처리 메소드인 then의 사용이 가능하다.
// 비동기로 데이터를 가져오는 함수
function fetchData(url) {
return new Promise((resolve, reject) => {
// 간단한 비동기 작업 시뮬레이션
setTimeout(() => {
const data = `Data from ${url}`;
resolve(data);
}, 1000);
});
}
// 비동기 함수를 사용하는 함수
async function fetchDataAndPrint() {
try {
// 비동기로 데이터를 가져오기 위해 await 사용
const result1 = await fetchData('https://example.com/api/data1');
console.log(result1);
// 두 번째 비동기 작업, 여기서도 await 사용
const result2 = await fetchData('https://example.com/api/data2');
console.log(result2);
// 세 번째 비동기 작업
const result3 = await fetchData('https://example.com/api/data3');
console.log(result3);
// 이어지는 작업들...
} catch (error) {
console.error('에러 발생: ', error);
}
}
// fetchDataAndPrint 함수 호출
fetchDataAndPrint();
요청을 하는 fetchData(url) 함수를 async로 선언된 fetchDataAndPrint() 함수가 await를 하며 요청하는 것을 볼 수 있다.
이렇게 되면, then이나 catch없이도 await 만으로 표현이 가능하다.
화살표 함수로 괄호가 더 생겨서 못생겨진 코드를 안 봐도 된다는 말이다!!
그렇지만 적재적소에 async/await와 then/catch를 사용해야한다.