자바스크립트는 싱글스레드(single-thread) 언어이다. 즉 스레드가 한개라서 한 번에 하나의 작업밖에 하지 못한다. 그러면 왼쪽처럼 작동돼야 하는게 아닌가 생각할 수 있다. 하지만 왼쪽처럼 작동한다면 많은 프로세스들을 하나의 스레드로 처리해야하고 그렇게 한다면 너무나 많은 시간이 걸릴 수 있기 때문에 실제로는 오른쪽 처럼 작동 된다.
왼쪽: 동기, 오른쪽: 비동기
동기(Synchronous): main 작업에서 sub 작업을 실행할 때 sub 작업이 끝날 때까지 main 작업이 기다리는 것
비동기(Asynchronous) : main 작업에서 sub 작업을 실행해놓고 기다리지 않고 다음 작업이 계속해서 실행되는 것
위 그림만 본다면 비동기가 동기에 비해 처리속도가 빠르므로 당연히 좋다고 생각이 들 수 있다. 그게 바로 비동기의 장점이기도 하다. 그렇다면 비동기의 문제점은 무엇일까??
다음 코드를 한 번 살펴 보자.
setTimeout(function, time)
: time(ms)만큼 시간이 지난 후에 function을 실행하세요.
const func = () => {
setTimeout(()=>{
console.log('executed setTimeout!');
}, 1000); // 1초 후에 실행하세요
}
console.log('start');
func();
console.log('end');
예상:
start
executed setTimeout!
end
실제:
start
end
executed setTimeout!
이상한 일이 벌어졌다.
코드가 순서대로 실행되지 않고 end가 setTimeout 전에 먼저 출력이 돼버렸다.
나는 나의 예상대로 코드가 실행되게 하고 싶다!
=> 비동기를 처리해보자.
callback 이란?
const func = (callback) => {
setTimeout(()=>{ // 1초 뒤에 console.log 실행
console.log('executed setTimeout!');
callback();
}, 1000);
}
console.log('start');
func(() => {
console.log('end');
});
결과
start
executed setTimeout!
end
우리가 원하는 순서대로 잘 출력되었다.
이번엔 또 다른 예제를 한 번 보자.
const randomGen = () => {
console.log("난수 생성 중");
setTimeout(() => {
const randomNum = Math.random();
console.log("난수 생성 완료!");
return randomNum;
}, 1000);
};
const printNum = (randomNum) => {
console.log('나의 난수는', randomNum);
}
printNum(randomGen());
결과
함수가 난수를 만들기 전에 return이 먼저 실행되어 undefined가 return 되었다.
이것도 callback을 이용하여 해결해보자.
const randomGen = (callback) => {
console.log("난수 생성 중");
setTimeout(() => {
const randomNum = Math.random();
console.log("난수 생성 완료!");
callback(randomNum);
}, 1000);
};
const printNum = (randomNum) => {
console.log("나의 난수는", randomNum);
};
randomGen(printNum);
결과
정상적으로 잘 출력되는 것을 볼 수 있다!
이처럼 callback을 이용하여 비동기를 처리할 수 있다.
실제로도 이 방법은 많이 쓰이고 있지만 눈에 띄는 단점이 있다.
서로 연관을 갖고 있는 여러개의 callback함수들이 중첩되어 적어지면
가독성이 너무나도 떨어지고 추후에 유지보수도 굉장히 힘들어진다.
정말 멋있는 callback hell
이처럼 callback의 문제점이 나타나고 그 외의 여러가지 commonJS의 문제점이 드러나면서 js의 새로운버전인 ES6(ECMAScript 2015, ES2015) 버전이 새로 나왔다.
또한, ES6가 되면서 비동기 처리 표준이 callback에서 promise로 바뀌었다.
Promise의 어원 : "내가 할수 있는 한 빨리 너의 요청의 응답을 가지고 돌아간다고 약속(promise)할게" 라는 의미
Promise의 동작 구조
Promise를 사용함으로 인해 callback hell 도 예방할 수 있게 됐고, 비동기 처리를 원하는대로 하기에 더욱 편리해졌다.
Promise 사용 방법
Promise.then() // Promise가 실행(수락)됐을 때 할 일
Promise.catch() // Promise가 거절됐을 때 할 일, 마지막에 한 번만 작성
Promise.finally() // Promise의 수락 여부에 관계없이 항상 실행 될 일, 마지막에 한 번만 작성
코드 예제
const myFirstPromise = new Promise((resolve, reject) => {
// 우리가 수행한 비동기 작업이 성공한 경우 resolve(...)를 호출하고, 실패한 경우 reject(...)를 호출한다.
// resolve 결과를 변수에 할당할 경우 resolve는 일반 함수의 return과 같은 역할을 한다
// 이 예제에서는 setTimeout()을 사용해 비동기 처리를 발생시킨다.
setTimeout(() => {
resolve("성공!"); // "성공!" 을 다음에 호출될 함수 또는 Promise의 인자로 넘겨줌.
}, 500);
});
myFirstPromise
.then((successMessage) => {
// successMessage는 위에서 resolve() 안에 넣어준 값. => "성공!"
console.log("와! " + successMessage); // 와! 성공!
})
.catch((rejectedMessage) => {
console.log("와.... " + rejectedMessage); // 거절 됐을 때 실행될 구문
})
.finally(() => {
console.log("이것은 finally"); // 항상 실행될 구문
});
짧게 요약하자면
Promise1
.then(Promise2)
.then(Promise3)
.then(Promise4)
.catch(promiseRejected)
.finally(promiseFinally); // catch와 finally는 생략 가능
// 이런 방식으로 처리하는 것을 promise chaining 이라고 함.
위의 난수 예제를 callback이 아닌 promise를 이용하여 해결해보자.
const randomGen = new Promise((resolve) => {
console.log("Promise로 난수 생성 중");
setTimeout(() => {
const randomNum = Math.random();
console.log("Promise로 난수 생성 완료!");
resolve(randomNum);
}, 1000);
});
const printNum = (randomNum) => {
console.log("나의 난수는", randomNum);
};
randomGen.then(printNum);
결과
예제의 callback의 깊이가 깊지 않아 promise로 인한 장점이 뚜렷하게 드러나진 않는다. 하지만 callback들이 많아질수록 promise의 장점이 더욱 뚜렷하게 보일 것이다.
그러나(두둥) 이렇게 아름다운 Promise 에게도 단점이 존재했다..
Promise의 단점을 보완하기 위해 ES8(ECMAScript 8) 버전에서 async/await 키워드가 등장했다.
async는 함수 선언 앞에 붙어 해당 함수가 async 함수라는 것을 명시한다.
또한 async 함수는 항상 promise를 반환한다.
promise가 아닌 값을 반환하더라도 resolved promise로 감싸서 반환하게 된다. (then으로 받을 수 있음)
async function f1() {} // function 키워드를 이용하는 경우
const f2 = async function() {} // function을 변수 할당식으로 이용하는 경우
const f3 = async () => {} // 화살표함수를 이용하는 경우
이게 다다. 매우 간단하고 아주아주 쉽다. 그럼 async의 짝꿍인 await를 알아보러 가자.
await의 가장 중요한 특징: await 키워드는 async 함수 안에서만 사용할 수 있다. 그렇지 않으면 에러 발생.
const f = async () => {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // promise가 이행될 때까지 기다림
console.log(result); // 완료!
}
// 아래는 잘못된 예제
const f = async () => {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error, await을 썼지만 async함수가 아님
}
callback hell이나 then hell을 예방하면서도 가독성이 매우 좋아진 것을 볼 수 있다.