JS 비동기 처리
자바스크립트는 여러 작업을 동시에 처리하는 것이 아니라 한 번에 하나의 작업을 처리할 수 있는 싱글 스레드방식입니다. '근데 어떻게 동시에 작업들을 처리할 수 있나요?' 라고 의아해할 수 있지만 자바스크립트를 실행 하는 콜 스택은 싱글 스레드이지만, 브라우저에서 리소스를 요청하거나 파일 입출력 혹은 타이머 대기 작업을 실행하는 Web APIs들은 멀티 스레드이기 때문입니다.
비동기 병렬 처리 원리
브라우저는 Web APIs, Event Table, Callback Queue, Event Loop 등 각 API마다 스레드들이 할당되어 있는데, 이들이 모여서 멀티 스레드로 이루어져 있습니다. 즉 브라우저가 멀티 스레드이기 때문에 메인 자바스크립트 스레드를 차단하지 않고 다른 스레드를 사용하여 동시 처리(병렬 처리)가 가능하게 해주는 것입니다.
*모든 Web ApI들이 비동기로 동작되는 것은 아니다. DOM API나 Console API는 동적으로 처리되고, XMLHttpRequest나 Timer API는 비동기적으로 처리가 된다.
위의 코드를 보면서 설명하면
1. CallStack에 setTimeout 비동기 컨텍스트가 쌓입니다.
2. WebAPI에게 setTimeout 컨텍스트가 전달됩니다. (비동기 작업 진행으로 1초대기)
3. 함수 b 컨텍스트가 CallStack에 쌓인 후 진행되어 2이 출력됩니다.
4. 함수 a 컨텍스트가 CallStack에 쌓인 후 진행되어 1이 출력됩니다.
5. WebAPI가 1초가 지난 후 setTimeout 컨텍스트를 TaskQueue로 전달합니다.
6. EventLoop가 CallStack과 TaskQueue를 지속적으로 감시하며 CallStack이 비어 있을 때 TaskQueue에 있는 SetTimeout 컨텍스트를 Callstack으로 전달합니다.
7. setTimeout 컨텍스트는 그럼 CallStack으로 쌓인 후 진행 하여 3이 출력됩니다.
비동기적 코드 결과 처리 방법
- CallBack
- Promise
- Async / Await
콜백(Callback) 함수
전통적 비동기 결과 처리 방법으로, 비동기 방식은 요청과 응답의 순서를 보장하지 않기 때문에 응답에 대한 결과 처리를 받고 싶은 경우에는 콜백 함수를 이용하여 작업순서를 간접적으로 끼워 맞출 수 있습니다.
함수의 파라미터로 넘겨 파라미터를 받은 함수에게 실행권을 넘기는 것을 말합니다. 콜백함수는 동기적으로 작동할 수도 있고 비동기적으로 작동할 수도 있는데 코드를 보면서 설명하겠습니다.
// 동기적 콜백
function taskSyncFunction(callback){
console.log("첫 번째 작업");
console.log("두 번째 작업");
callback();
}
taskSyncFunction(() => {
console.log("콜백 함수 실행");
});
console.log("실행완료!")
taskSyncFunction이 선언된 뒤 taskSyncFunction 함수가 실행이되면 "첫 번째 작업" "두 번째 작업" 로그가 뜨고 함수내 작업이 다 끝났으므로 "콜백 함수 실행"이 출력된 뒤 "실행완료가" 출력됩니다.
// 비동기적 콜백
function taskAsyncFunction(callback){
console.log("start")
setTimeout(() => {
console.log("첫 번째 작업");
console.log("두 번째 작업");
callback();
} , 2000)
console.log("end")
}
taskAsyncFunction(() => {
console.log("콜백 함수 실행");
});
console.log("실행완료!")
taskAsyncFunction선언이 되고 taskAsyncFunction이 실행이됩니다. 실행이 되면 콜백함수가 넘어가서 "start"가 출력됩니다. setTimeout()은 비동기함수이기때문에 바로실행이안되고 넘어가서 "end"가 출력됩니다. 그 이후에 함수가 끝났으니 함수블럭을 나와서 "실행완료!"가 출력되고 그 후 2초가 지나면 setTimeout에 넘겨진 콜백함수를 실행합니다. "첫 번째 작업" , "두 번째 작업"이 출력되고 "콜백 함수 실행"이 출력되면서 마무리됩니다.
콜백 지옥(CallBack Hell) / 운명의 피라미드( Pyramid of Doom)
콜백함수의 결과값이 그 다음 콜백함수 실행에 필요한 경우 콜백 함수가 반복되어 코드가 수없이 길어져 들여쓰기 수준이 감당하기 힘들정도로 깊어지는 현상을 콜백 지옥이라고 합니다. 이 현상은 코드의 가독성을 떨어트리고 유지보수가 힘들기 때문에 개발자들 사이에선 '아도겐 코드'라고도 불립니다.
.
프로미스(Promise)
위에 콜백 지옥을 해결하기 위해 새로운 비동기 처리 방법으로 성공 또는 실패 상태를 알려줍니다. Promise의 상태는 대기(pending), 성공(Fulfilled), 실패(Rejected) 이렇게 3가지의 상태를 갖고 있습니다.
- 대기(pending) : 비동기 처리 로직이 처리가 되지 않은 상황
- 성공(Fulfilled) : 비동기 처리에 의해 올바른 결과를 얻어와 그 결과를 정상적으로 처리하고자 resolve가 호출된 상태
- 거부(Rejected) : 무언가 잘못되어 예외로 처리하고자 reject가 호출된 상태
const promise = new Promise((resolve, reject) => { //대기 상태
getData(
response => resolve(response.data), //성공 상태
error => reject(error.message) //실패 상태
)
})
프로미스(Promise) 생성
const promise = new Promise((resolve, reject) => {
// 비 동기 작업 작성
})
new 생성자를 통해 생성하고, 생성자는 실행함수 resolve와 reject를 파라미터로 받습니다.
- resolve
- 비동기 작업이 성공적으로 완료되면 호출
- 상태 값을 fulfilled 상태로 변경
- reject
- 비동기 작업이 실패했을 때 호출
- 상태 값을 rejected 상태로 변경
프로미스(Promise) 처리 방법
promise
.then((result) => {
// 성공시 동작할 코드
})
.catch((error) => {
// 실패시 동작할 코드
})
.finally(() => {
// 성공 유무에 상관 없이 마지막에 실행할 코드
})
- then()
- 성공(resolve) 시에 then 메서드에 실행할 콜백 함수를 인자로 넘김
- catch()
- 실패(reject) 시에 catch 메서드에 실행할 콜백 함수를 인자로 넘김
- promise chain 형태 에서도 하나의 catch에서 일괄 처리 진행 가능
- finally()
- 성공/실패 여부와 상관없이 모두 실행 시에는 finally 메서드에 실행할 콜백 함수를 인자로 넘김
*promise chain : 여러 비동기 프로미스 작업을 순차적으로 실행하기 위한 패턴 중 하나
Promise Hell / Nested Promise
Promise Hell은 콜백 지옥과 마찬가지로 then() 메서드가 지나치게 중첩되고 복잡해지면서 가독성이 떨어지는 상황을 말합니다. 이 형태 역시 유지 보수가 어렵고 버그가 발생시 찾기 어려울 수 있습니다.
fetchData()
.then(data => {
parseData(data)
.then(parsed => {
filterData(parsed)
.then(filtered => {
sortData(filtered)
.then(sorted => {
console.log(sorted); // 최종 결과 처리
})
.catch(error => {
console.error(error); // sortData 에러 처리
});
})
.catch(error => {
console.error(error); // filterData 에러 처리
});
})
.catch(error => {
console.error(error); // parseData 에러 처리
});
})
.catch(error => {
console.error(error); // fetchData 에러 처리
});
Async / Await
다시한번 Promise Hell을 해결하기 위해 ES2017(ES8)에서 도입된 비동기 작업을 처리하기 위한 패턴입니다.
Callback과 Promise를 보다 간결하게 사용할수가 있습니다. Promise를 대체하기 위한 기능이 아닌 Promise 로직을 더 쉽고 간결하게 사용할 수 있게 해주는 것입니다.
Async / Await의 기본 사용법은 function 키워드 앞에 async을 붙여주면 되고 Promise를 반환하는 함수 앞에 await을 붙여 해당 Promise의 상태가 바뀔때까지 코드를 기다립니다.
async function getData() {
try {
const response = await fetch('/api');
const data = await response.json();
return data;
} catch (error) {
console.error(error);
}
}
async 함수 getData를 정의하고 await을 사용하여 fetch와 json() 메서드를 기다리고 데이터를 반환하는데 await은 Promise 객체가 완료될 때까지 코드를 중지시키므로 fetch에서 에러가 발생할경우 코드가 넘어가지 않고 에러처리를 할 수 있습니다.
비동기 처리 Callback, Promise 기반
CallBack 기반 비동기 처리 방식
AJAX(Asynchrounuous Javascript And XML)
- 자체 빌트인 함수인 XMLHttpRequest 를 사용
- 사용하기에는 너무 복잡한 원시적인 코드
JQuery AJAX
- 크로스 브라우저 환경에서 일관된 자바스크립트 문법 제공을 위한 라이브러리
- DOM, CSS 조작이 매우 간편해지고 기능이 매우 다양
- AJAX 만을 사용하기 위해서 쓰기에는 너무 거대 (사용시에는 분명한 목적이 있어야함)
Promise 기반 비동기 처리 방식
Fetch API
- ES6 지원 비동기 통신을 위한 JS 내장 API
- 라이브러리가 아니기 때문에 매우 가볍게 동작 가능
Axios
- 서드파티 라이브러리
- 자동으로 JSON 변환 과정이 포함되어 있어 편리
Reference
- https://velog.io/@kim_unknown_/JavaScript-Asynchronous
- https://trustmitt.tistory.com/85
- https://velog.io/@tosspayments/%EC%98%88%EC%A0%9C%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-awaitasync-%EB%AC%B8%EB%B2%95
- https://velog.io/@tosspayments/Promise-%EC%8B%A4%EC%A0%84%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0
- https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%B2%98%EB%A6%AC-async-await