자바스크립트는 싱글 쓰레드이다. 메인 쓰레드인 이벤트 루프가 싱글 쓰레드이기 때문에 자바스크립트를 싱글 쓰레드 언어라고 부른다. 하지만, 웹 브라우저나 Node.js같은 멀티 쓰레드 환경에서 실행되기 때문에 자바스크립트 자체는 싱글 쓰레드가 맞지만 자바스크립트 런타임은 싱글 쓰레드가 아니다.
그렇다면 동기(Synchronous)와 비동기(Asynchornous)의 차이는 무엇일까?
기존 동기식 요청은, 코드를 한줄 한줄 차례대로 실행한다.
하나의 요청에 서버가 응답하기까지 다음 작업을 처리하지 못하고 기다려야 한다. 앞의 작업시간이 길어질수록 시간이 오래 걸린다는 단점이 있다.
이렇게 된다면 c 손님은 무려 20분이 지나서야 주문이 가능하다.
하지만 비동기 호출은 하나의 요청이 완료될 때 까지 기다리지 않고 동시에 다른 작업을 실행한다. 햄버거가 만들어지는 동안 다른 손님들 주문도 받고, 순서대로 조리를 시작한다.
위 그림의 Total time
에 주목해보자.
동기 호출을 했을 때 task 1이 끝나고 2, 3, 4 실행하다 보니 작업이 모두 끝나는 데 까지 500 밀리세컨드가 걸렸다.
비동기는 동시에 작업을 실행하다 보니 토탈시간이 200 밀리세컨드밖에 걸리지 않았다. 즉, 가장 오래 걸리는 작업이 끝나는 시점이 토탈시간이 되는 것이다. 아주 효율적이다!
그런데 이런 비동기 프로그래밍을 할 시 작업시간이 각각 모두 다르므로, 순서가 일정하지 않다는 특징이 있다.
그래서 비동기 함수의 순서를 제어하고 싶을 때 콜백(Callback) 을 이용한다.
// 일반적인 비동기 호출시
const printString = (string) => {
setTimeout(function () {
console.log(string);
}, Math.floor(Math.random() * 100) + 1);
};
const printAll = () => {
printString('A');
printString('B');
printString('C');
};
printAll(); // B, C, A || A, B, C 랜덤으로 호출되어 순서 제어가 불가능
콜백함수를 통해 비동기 코드의 순서를 제어할 수 있다. 즉, 동기화 할 수 있다.
const printString = (string, callback) => {
setTimeout(function () {
console.log(string);
callback();
}, Math.floor(Math.random() * 100) + 1);
};
const printAll = () => {
printString('A', () => {
printString('B', () => {
printString('C', () => {});
});
});
};
printAll(); //A, B, C
비동기 함수와 달리 콜백함수를 이용해 순서를 제어해 줬더니 의도한 대로 A, B, C가 순서대로 호출되는 것을 볼 수 있다. 하지만 이런 콜백함수에도 치명적인 단점이 있었으니..
콜백함수를 배우면서 꼭 한번은 보게 되는.. 콜백지옥(Call back Hell)에 갇히게 된다는 단점이다.
콜백함수로 순서를 제어하는게 가능하지만, 무한 들여쓰기로 인해 가독성이 너무 좋지 않고 이렇게 코드가 길어진다면 당연히 비효율적인 프로그래밍을 하게 될 것이다.
Promise는 자바스크립트 비동기 처리에 사용되는 객체로, 3가지 상태가 있다.
pending(대기) : 비동기 로직이 아직 완료되지 않은 상태
new Promise((resolve, reject) => {...})
fulfilled(이행) : 비동기 처리가 완료되어 promise가 결과 값을 반환해준 상태
new Promise((resolve, reject) => {resolve()})
콜백 함수의 인자 resolve
를 실행하면 이행(fulfilled) 상태가 되고, 이행 상태에서는 then
을 이용해 처리 결과 값을 받을 수 있다.
function getData() {
return new Promise(function(resolve, reject) {
let data = 100;
resolve(data);
});
}
// resolve()의 결과 값 data를 resolvedData로 받음
getData().then(function(resolvedData) {
console.log(resolvedData); // 100
});
function getData() {
return new Promise(function(resolve, reject) {
reject(new Error("Error!!!"));
});
}
// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function(err) {
console.log(err); // Error: Error!!!
});
async와 await는 가장 최근에 나온 비동기 처리 패턴이다.
기존 비동기 처리 방식이었던 콜백 함수와 promise의 단점을 보완하고, 보다 가독성 높은 코드를 작성할 수 있게 도와준다.
또한, 비동기 처리 코드 방식으로 사고하지 않아도 await 키워드만 붙여주면 일반 함수를 사용하듯 사용할 수 있다는게 가장 큰 장점이다.
async & await의 기본 문법은 다음과 같다.
async function 함수명 () {
await 비동기_처리_메서드명();
}
미리 만들어준 fetchUser
와 fetchTodo
함수를 이용한 비동기 함수를 만들시에, 이렇게 함수 앞에 async
, 실행할 함수 앞에 await
을 붙이면 훨씬 가독성 좋은 비동기 처리 함수 작성이 가능해진다.
async function logTodoTitle() {
var user = await fetchUser();
if (user.id === 1) {
var todo = await fetchTodo();
console.log(todo.title); // delectus aut autem
}
}
async과 await에서 예외처리는 바로 try catch
이다.
위 예제코드에서 오류를 잡으려면
async function logTodoTitle() {
try {
var user = await fetchUser();
if (user.id === 1) {
var todo = await fetchTodo();
console.log(todo.title); // delectus aut autem
}
} catch(error) {
console.log(error);
}
}
try{}
로 감싸준 뒤,catch
로 error를 잡아주면 된다. 네트워크 통신 오류 뿐 아니라 간단한 타입 오류등의 일반적 오류까지 잡아낼 수 있다. 발견된 에러는 error 객체에 담기게 된다.