자바스크립트는 싱글 쓰레드로 동작하는 언어이지만 비동기적 처리가 가능하다.
"멀티테스킹"
A 작업은 1시간이 걸리는 작업이고 B 작업은 1초가 걸리는 작업이라고 할 때,
동기적인 처리 방법에서는 A 작업이 끝나야지만 B 작업이 시행된다.
1초가 걸릴 작업이 1시간 1초가 걸리는 셈이다.
[웹사이트에서 이런 동기적 처리만 가능하다면 큰 문제가 발생할 것이다.]
따라서 A의 완료 여부와 상관없이 B 작업을 먼저 실행해주는 것을 비동기 처리라고 한다.
자바스크립트는 싱글 쓰레드 엔진을 갖고있지만 이러한 비동기적 처리가 가능하다.
프로세스는 메모리 상에서 실행중인 작업을 뜻하며, 이러한 프로세스 내의 실행 단위를 쓰레드라고 한다.
프로세스: 각각의 은행 지점
스레드: 은행 지점 하나에 속한 고객 창구 여러 개
쓰레드가 하나인 경우 쓰레드에서 호출되는 함수들은 콜스택에 쌓이고 이 함수들은 LIFO 방식으로 한번에 하나의 함수만 동기적으로 실행이 가능하게된다. 즉, 원론적으로는 싱글 쓰레드 환경에서는 비동기적인 함수 실행을 할 수 없다.
그렇다면 싱글 쓰레드이지만 비동기 작업이 가능한 JS의 경우는?
자바스크립트의 V8 엔진은 런타임시 WebAPI(dom, ajax, setTimeout...), Callback Queue, Event Loop 등과 함께 동작한다.
Memory Heap : 메모리 할당을 담당하는 곳으로 선언한 변수와 함수들이 들어간다.
Call Stack: 코드가 호출되면 LIFO 방식으로 실행되는 곳이다.
(스택이 할당된 공간보다 많은 공간을 차지하게 되면 스택오버플로우 에러가 발생한다.)
Web api :브라우저에서 자체 지원하는 api로 Dom 이벤트, Ajax (XmlHttpRequest), setTimeout 등의 비동기 작업들을 수행할 수 있도록 api를 지원한다.
Event Loop : 이벤트 발생 시 호출되는 콜백 함수들을 관리하여 태스크 큐에 전달하고, 콜스택이 비어있을때 태스크 큐에 담겨있는 콜백 함수들을 콜스택에 넘겨준다.
(콜스택의 함수들이 먼저 실행된다)
Callback Queue : Web API에서 비동기 작업들이 실행된 후 호출되는 콜백함수들이 기다리는 공간이다. 이벤트 루프가 정해준 순서대로 줄을 서있으며, FIFO방식을 따른다.
자바스크립트 런타임시 같이 작동하는 WebAPI, Callback Queue, Event Loop 를 통해 비동기 작업이 가능하게된다.
Call Stack
에서 비동기 함수가 실행되면, 자바스크립트 엔진은 Web API
에게 비동기 작업을 위임한다.Web API
는 해당 비동기 작업을 수행한 후, 콜백함수를 Event Loop
를 통해 Callback Queue
에게 넘겨준다.Event Loop
는 Call Stack
이 비어있을 때, Callback Queue
의 콜백함수를 Call Stack
으로 넘겨준다.Call Stack
에서 제거된다.콜백은 자바스크립트가 비동기 처리를 하기 위한 패턴 중 하나이다.
A 함수에서 자동으로 B 함수가 실행되도록 B 함수를 콜백하는 것이다.
콜백에는 제한이 없기 때문에 B함수에서 C 함수를 콜백할 수 있고, C 함수에서 D 함수를 콜백 하는 등 꼬리에 꼬리를 무는 콜백이 가능하다.
function async1('a', function (err, async2){
if(err){
errHandler(err);
}else{
...
async2('b', function (err, async3){
...
}) {
...
}
}
});
꼬리에 꼬리를 무는 비동기 처리 콜백이 늘어나면 코드는 깊어지고, 관리는 어려워지는 콜백 헬
이 발생한다. 또한 비동기 처리 시에는 실행 완료를 기다리지 않고 바로 다음 작업을 실행하기 때문에 작업 실행의 순서가 엉키거나 메모리 사용상에도 문제가 발생할 수 있다.
이러한 콜백 헬
을 해결하기위해 ES6 부터 Promise 라는 개념을 도입하였다.
ES6부터 도입된 비동기 처리 패턴으로 비동기 연산이 종료된 이후 결과를 알기 위해 사용하는 객체이다.
Promise를 사용하면 비동기 메서드를 마치 동기 메서드처럼 값을 반환할 수 있다.
즉, 비동기 처리 시점을 더 명확하게 표현할 수 있다.
["작업 B를 다른 작업들과는 비동기적으로 수행하지만 단서 조항으로 작업 A가 끝나야지만 실행해"]
resolve,reject를 인자로 갖는 실행 함수
를 인자에 넣는 Promise 생성자
new Promise((resolve, reject) => { ... });
resolve: 작업이 성공한 경우 호출할 콜백
reject: 작업이 실패한 경우 호출할 콜백
// new Promise((resolve, reject) => { ... });
// resolve: 작업이 성공한 경우 호출할 콜백
// reject: 작업이 실패한 경우 호출할 콜백
const promise = new Promise((resolve, reject) => {
if(...){
...
resolve("성공!");
} else{
...
reject("실패!");
}
});
프라미스로 구현된 비동기 함수는 프라미스 객체를 반환한다.
프라미스로 구현된 비동기 함수를 호출하는 측에서는 이 프라미스 객체의 후속 처리 메서드를 통해 비동기 처리 결과(성공 결과나 에러메시지)를 받아서 처리해야 한다.
.then(성공 시, 실패 시)
then의 첫 인자는 성공 시 실행, 두번째 인자는 실패 시 실행된다. (첫 번째 인자만 넘겨도 실행됨)
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000);
});
// resolve
promise.then(result => {
console.log(result); // 완료!가 콘솔에 찍힌다.
}, error => {
console.log(error); // 실행되지 않는다.
});
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("오류!")), 1000);
});
// reject
promise.then(result => {
console.log(result); // 실행되지 않는다.
}, error => {
console.log(error); // Error: 오류!가 찍힌다.
});
.catch(실패 시)
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("오류!"), 1000);
});
promise.catch((error) => {console.log(error};);
Promise 는 후속 처리 메서드(then)를 여러 개의 Promise로 연결할 수 있다.
new Promise((resolve, reject) => {
setTimeout(() => resolve("promise 1"), 1000);
}).then((result) => { // 후속 처리 메서드 하나를 쓰고,
console.log(result); // promise 1
return "promise 2";
}).then((result) => { // 이렇게 연달아 then을 써서 이어준다.
console.log(result);
return "promise 3";
}).then(...);
콜백을 해결했지만 Promise의 사용은 직관적이지 않고 간결하지 않기 때문에 비동기처리의 간편화를 위해 ES7 부터 async/await
기능을 추가하였다.
함수 앞에 async를 붙여서 사용한다.
항상 Promise를 반환하여 Promise가 아닌 값도 Promise를 감싸서 반환해준다.
// async는 function 앞
async function myFunc() {
return "프라미스 반환";
}
myFunc().then(result => {console.log(result)});
async 함수 안에서만 동작한다. 즉, async 없이는 사용할 수 없다.
await는 프라미스가 처리될 때까지 기다렸다가 그 이후에 결과를 반환한다.
async function myFunc(){
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000);
});
console.log(promise);
let result = await promise; // promise가 끝나도록 기다림
console.log(promise);
console.log(result); // then(후처리 함수)를 쓰지 않았는데도, 1초 후에 완료!가 콘솔에 찍힌다.
}
await를 만나면, 실행이 잠시 중단되었다가 프라미스 처리 후에 실행을 재개한다.즉, await를 쓰면 함수 실행을 기다리게 하는 것이다.