비동기 처리라고 많이 얘기를 하는데, 나는 비동기 대기 처리라는 표현이 더 적절하다고 생각한다. 비동기처리 라는 단어는 마치 코드를 비동기로 만들겠다는 어감이 강한 것 같다. 하지만 실제론 비동기적으로 작동하는 코드를 어떻게 동기적으로 작동시켜 데이터를 받아올 때 까지 대기시킬 수 있는지를 말한다. 왜냐하면 결과값이 필요한 코드를 비동기적으로 데이터를 받아오기 전에 실행시켜버리면 에러가 발생할 수 밖에 없기 때문이다.
비동기의 의미는 asynchronous, 싱크가 안맞는다는 뜻이다.
흔히들 자막과 소리가 안 맞을 때 싱크가 안 맞네라고 말한다.
즉, 자막과 소리가 동시에 발생해야지 synchronous 하다고 한다.
이것을 프로그래밍 개념에 접목시켜서 이해하려고 하면 쓰레드의 관점에서 생각해야 한다.
완벽하게 쓰레드에 빙의해 보면, 난 메인쓰레드다.
사용자는 나에게 api 요청 혹은 네트워크 요청(데이터를 받아오는 데 시간이 걸리는) 을 지시했다.
동기 방식으로 처리하면 난 api 요청을 보내고 아무 transaction을 처리하지 않고 대기한다. 대기하는 동안 쓰레드가 죽어있다고 생각하고 대기가 끝나고 쓰레드가 다시 살아난다면, api 요청과 동시에 결과를 받아오는 느낌일 것이다.
반면 비동기 방식으로 처리하면 난 api요청을 보내고 다른 transaction 들을 계속 처리한다(예를 들어 ui 렌더링 작업)
그러면 요청과 결과는 동시에 이루어지지 않는다. 쓰레드는 계속 살아있었으니까.
위 예가 정확한 예시는 아니지만 이해를 돕기 위해 들어봤다.
비동기로 코드를 실행하면 먼저 실행되어야 하는 코드가 끝나기도 전에 다른 코드가 실행되는 경우가 발생할 수 있기 때문에, 반드시 선행 데이터를 받아오거나 하는 경우 이 비동기를 처리해주는 방식이 필요하다.
웹에서 데이터를 받아오는 동안 다른 화면으로 전환을 가능하게 하거나 로딩화면을 띄워주는 비동기적인 처리는 es6 이전 문법에서는 콜백함수를 통해 구현했다.
콜백함수는 함수(1)에 함수(2)를 인자로 받아 함수(1) 의 처리가 끝나고 함수(2)가 호출될 경우 함수(1) 내부 코드가 끝나야지만 함수(2)가 호출되는 원리를 이용한다.
function requestData1 (callback) {
var data = fetch(url)
callback(data)
}
requestData1(function callback(data) {
console.log(data);
});
위가 콜백함수를 사용해서 data를 받아온 이후에 callback 이 실행되는 예이다.
콜백함수는 코드 흐름이 뒤죽박죽이라 코드가 읽기 힘들고 여러번의 콜백함수가 중첩될 경우 콜백 지옥이라 불리는 현상이 일어난다.
es6 이후 자바스크립트 문법은 이 콜백 문제를 promise를 통해 해결했다.
프로미스는 비동기 상태를 값으로 다룰 수 있는 객체이다. 즉 비동기 처리에 사용되는 객체이다.
프로미스를 사용하면 비동기 코드를 순차적으로 작성할 수 있는 장점이 있다.
function getData(callbackFunc) {
$.get('url 주소/products/1', function(response) {
callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
});
}
getData(function(tableData) {
console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});
위 코드는 간단하게 콜백함수를 통해 비동기 처리를 한 내용이다.
위 내용에 프로미스를 적용하면
function getData(callback) {
// new Promise() 추가
return new Promise(function(resolve, reject) {
$.get('url 주소/products/1', function(response) {
// 데이터를 받으면 resolve() 호출
resolve(response);
});
});
}
// getData()의 실행이 끝나면 호출되는 then()
getData().then(function(tableData) {
// resolve()의 결과 값이 여기로 전달됨
console.log(tableData); // $.get()의 reponse 값이 tableData에 전달됨
});
가 된다. 함수에서 프로미스 객체를 리턴함으로 resolve 함수를 통해 성공했을 때 실행할 함수, reject 함수를 통해 비동기 처리에 실패했을 때 실행할 함수를 구분지을 수 있다.
프로미스가 콜백보다 확연히 가독성에서 차이가 나는 점은 여러 비동기 함수가 중첩되었을 경우이다.
콜백 지옥이라고 불리는 이 함수 체이닝에서 프로미스는 확연하게 가독성에서 좋다.
$.get('url', function(response) {
parseValue(response, function(id) {
auth(id, function(result) {
display(result, function(text) {
console.log(text);
});
});
});
});
위는 콜백 지옥을 나타낸 예시이다.
getData(userInfo)
.then(parseValue)
.then(auth)
.then(diaplay);
function parseValue() {
return new Promise({
// ...
});
}
function auth() {
return new Promise({
// ...
});
}
function display() {
return new Promise({
// ...
});
}
프로미스를 통해 위처럼 코드를 개선할 수 있다.
한 눈에 봐도 가독성이 높아진 코드를 볼 수 있다.
프로미스의 then 체이닝 형식의 가독성을 조금 더 보완하기 위해 async await 키워드가 도입되었다. 하지만 이 개념은 프로미스를 완벽하게 커버하는 업그레이드 버전이 아니다. 프로미스의 부족한 부분을 보완하는 기능을 한다고 생각하면 될 것 같다.
async/await 는 함수에 적용되는 개념으로 일반 함수에 적용이 되면 프로미스 객체를 반환하게 된다.
async function getData(){
return 123;
}
getData().then(data => console.log(data));
console.log(getData());
async를 적용하는 함수를 만들면 프로미스 객체를 리턴하는 함수로 바뀐다. getData 함수는 프로미스를 리턴해 then() 을 사용해 콜백함수를 실행하고 있다.
아래 콘솔로그는 "Promise { 123 }" 라는 결과가 나온다.
function requestData(value){
return new Promise(resolve => setTimeout(()=>{
console.log("requestData" , value);
resolve(value);
},100);
);
};
async function getData(){
const data1 = await requestData(10);
const data2 = await requestData(20);
console.log(data1, data2);
return [data1, data2];
}
console.log(getData());
위 처럼 실행하면
Promise {
<pending>
}
requestData 10
requestData 20
10 20
가 나온다.
pending 은 대기중이라는 뜻. 결과값을 기다리고 있는 중이라는 뜻이다.
async 함수 자체는 promise 객체를 반환하고, 그 결과값을 then()을 통해 처리할 수 있다.
나머지는 setTimeout함수가 실행됨에도 불구하고, data1, data2에 undefined가 아닌, 정상적인 값이 들어가 출력된다. 비동기처리를 async await가 한 것이다.
async await 은 의존성이 있는 체인형태의 비동기 코드를 다룰 때 효과적이다. 즉 연쇄적으로 이전 코드의 데이터를 필요로 해 대기해야 하는 경우 async await은 코드의 가독성을 효과적으로 높여준다.
하지만 이런 특성으로 인해, 병렬 처리를 할 수 있는 즉, 의존성이 없는 비동기 코드를 실행할 때는 효과가 떨어진다.
동시에 실행해도 되는 코드를 굳이 대기해야 하니 효율성이 떨어지는 것이다.
이럴 경우에는 두 가지 방법을 사용해서 병렬 처리를 할 수 있다.
async function getData(){
const p1 = asyncFunc1();(프로미스 객체 리턴 함수)
const p2 = asyncFunc2();
const data1 = await p1;
const data2 = await p2;
const data3 = await asyncFunc3(data1, data2);
}
위 코드 처럼 프로미스를 먼저 생성함으로써 비동기 코드를 실행하는 방법을 통해 병렬처리가 가능하다.
async function getData(){
const [data1, data2] = await Promise.all([asyncFunc1(), asyncFunc2()])
}
처럼 병렬처리를 할 수도 있다.
비동기 처리는 네트워크를 다루는 개발자라면 반드시 숙지해야 할 내용이고 면접에서도 단골로 나오는 내용이니 꼭 숙지해야 한다.