동기적 처리는 입력과 출력 사이에 또 다른 일이 발생하지 않는 실행을 의미한다. 이는 하나의 task에서 입력과 처리 출력은 항상 한 묶음으로 여겨진다는 것을 말한다. 은행원이 한명만 근무중인 은행을 생각해 보면 한 고객의 업무를 처리 하기 전까지 다른 고객들은 자기 번호가 되기를 기다릴 수 밖에 없다.
현대의 웹사이트가 동기적으로 구동된다고 생각해보면 아마 아무도 인터넷으로 무언갈 하려고 하지 않을 것이다. 페이지가 로딩되고, 검색창이나 배너등이 하나하나 순서대로 뜨고... 아마 로딩 시간이 분단위로 필요할 것이다.
이와 같이 다양한 기능을 빠르고 효율적으로 구현하기 위해선 동기적 처리에서 벗어나야 한다. 그렇기에 비동기적 처리가 등장하게 된 것이다. 사실 일상 생활에서도 비동기적 처리 상황이 더 흔하다. 스타벅스에 가서 커피를 주문할때 나의 주문을 받은 직원분은 내 커피가 완성되 내가 받을때 까지 다른 주문을 받지 않는가? 만약 그렇다면 오히려 내가 되려 괜찮으시냐며 뒤에 주문이 밀렸다고 말씀드리지 않을까?
비동기적 처리는 한가지 일이 수행되기를 기다리는 시간동안 다른 일을 수행하고 결과만 원하는 순서대로 받을 수 있는 것을 의미한다.
비동기적 처리를 구현하는데 가장 먼저 드는 생각은 바로 setTimeout 일것이다.
window.setTimeout(func(), 1000);//func를 실행하되 1초뒤에 실행 결과를 출력할 것
하나의 함수(기능)에 대해서 비동기 처리를 할때는 간단하지만 여러개의 함수를 또는 여러 기능을 비동기 처리 해야 한다면? 각 함수의 실행 순서를 기억하고 각각의 타이머 시간을 지정해 줘야 하는등 구현이 매우 어려워 진다.
우리의 목표는 함수의 결과를 기다리지 않고 다른 함수를 실행시키고 여러개의 함수 결과를 의도하는 순서대로 받기만 하면 되는 것이다. 그럼 이번엔 콜백을 사용해 보면 어떨까?
콜백 함수를 이용하면 작업을 수행할 때 해당 작업이 완료 될때 까지 스택이 해소 되지 않아 다른 작업을 블락하는 상황이 발생하지 않는다. 즉 비동기적인 처리가 가능하게 된다.
문제는 비동기 처리시 처리후 결과를 반환받는 순서를 주의하여야 한다는 점이다. 빨리 처리되는 순서로 결과를 처리하게 된다면 로직이 엉망진창으로 영켜버리고 말것이다.
그리고 콜백 함수를 이용하는 동시에 순서를 주의해서 코딩을 하면 '콜백 지옥'에 빠져 버리게 된다.
step1(function(value1) {
step2(function(value2) {
step3(function(value3) {
step4(function(value4) {
step5(function(value5) {
step6(function(value6) {
//Do something with value4, 5, 6....
console.log('wake up! you are in callback hell!!')
})
})
})
})
})
})
코드가 위에서 아래로 흐르는것이 아니라 들여쓰기가 너무 깊어져서 가독성이 심각하게 낮아진다.
콜백 지옥에서 탈출해 콜백을 더 유연하고 가독성이 좋게 사용하기 위해 promise 객체를 사용할 수 있다. promise 객체는 비동기 함수를 받아 처리할 동안 콜 스택을 막지 않으며 비동기 작업이 맞이할 미래의 완료 또는 실패의 값을 나타낸다.
프로미스에 연결한 함수는 그 프로미스의 then 메서드에 의해 대기열에 오른다.
p.then(fulfilledCallback, rejectedCallback);
p.then(function(value) {
//이행
}, function(reason) {
//거부
});
p.catch(null, rejectedCallback);
체이닝을 보기 위해 fetch API를 참고 해보자. fetch API는 프로미스를 리턴한다.
window
.fetch(serverURI)
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.log(err));
fetch에 then 메소드가 계속하여 연결되어 있는 모습을 볼 수 있다.
이상적인 프로그래밍 세계에서는 모든 비동기 함수는 promise을 반환해야 하지만. 불행히도 일부 API는 여전히 success 및 / 또는 failure 콜백을 전달하는 방식일거라 생각합니다.
setTimeout도 promise를 반환하지 않는 오래된 API에 해당된다. 그러나 오류가 발생하는 경우도 확인하기 위해 node.js 의 filesystem 모듈을 예시로 보도록 하자.
const fs = require("fs");
const getDataFromFilePromise = filePath => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, data) => {
if(err) return reject(err);
return resolve(data);
})
})
};
promise를 생성자 호출로 리턴 받는 함수안에 API를 넣어두면 함수를 호출하는 것으로 해당 API를 프로미스 객체 처럼 사용할 수 있다.
여러개의 프로미스의 결과를 한번에 모아 보고 싶다면 .all 메서드를 사용하면 된다.
.all 과 같이 순회 가능한 객체를 매개변수로 받지만 프로미스들을 race(경주) 시켜 가장 빠른 값만 반환하는 메소드이다.
.race와 비슷하게 순회 가능한 객체를 매개변수로 받아 가장 빠르게 완료된 값을 반환하지만, .any는 이행된 값만 반환한다.
프로미스의 이행/거부 여부와 관계 없이 프로미스가 처리된 후 무조건 매개변수의 콜백을 한 번 실행한다. Promise의 then(), catch() 핸들러의 중복을 피할 수 있게 해주는 유용한 기능
promise를 통해 콜백 지옥에서 빠져 나올 수 있었지만 여전히 동기 함수를 사용하는 것처럼 직관적이지 못하다. 만일 동기 함수의 장점을 살리면서 비동기 함수를 사용할 수 있다면 코드가 직관적이고 잘 읽히면서도 비동기 처리를 보장할 수 있는 멋진 코드를 작성할 수 있을것 같다.
그리고 ECMAscript 2017에서 등장한 것이 'async and await'이다.
async 함수의 가독성을 보기 위해 예를 들어보자.
async function getNewsAndWeatherAsync() {
let news = await fetch(newsURL).then((new1) => {
return new1.json();
})
let weather = await fetch(weatherURL).then((weather1) => {
return weather1.json();
})
return {news: news.data, weather: weather};
}
async 함수는 try...catch... 구문을 통해 에러 핸들링을 한다. MDN 예제를 살펴 보자
async function getProcessedData(url) {
let v;
try {
v = await downloadData(url);
} catch (e) {
v = await downloadFallbackData(url);
} finally {
v = await downloadEssentialData(url);
}
return processDataInWorker(v);
}