비동기? 그게뭔데!

목차
Frontend 코딩을 하다보면 'Async'와 'Promise'라는 단어를 많이 보게된다. 비동기 기능인건 알고 보통 API 통신에 사용되는 건 알겠는데... 그래서 그게 뭔데? 이 글에선 비동기에 관해 설명해보려 한다
비동기? 동기? 뭐이리 복잡해!
작업은 2가지로 나뉜다.
- 동기 프로세스 (Synchronous)
- 비동기 프로세스 (Asynchronous)
비(Be 가 아니다) 가 붙었으니까, 아무튼 비동기는 동기의 반대일 것이다. 그럼 동기 프로세스란 무엇일까?
동기 프로세스란 순차적으로 진행되는 작업을 의미한다.
예시를 들어보자.
우리가 빨래를 한다고 가정해보자.
세탁이 끝난 뒤에야 건조기를 돌릴 수 있다.
이처럼 앞 작업이 끝나야만 다음 작업을 시작할 수 있는 경우, 이를 동기 프로세스라고 한다.
하지만 비동기 프로세스는 조금 다르다.
세탁기를 돌려놓고, 세탁이 끝나길 기다리는 동안 다른 일을 할 수 있다면?
예를 들어 그 시간에 게임을 한다면, 두 작업은 서로 독립적으로 병렬 수행되는 셈이다.
이처럼 하나의 작업이 끝날 때까지 기다리지 않고 다른 작업을 병행할 수 있는 흐름이 바로 비동기 프로세스다.
비동기와 실행시간 그리고 UX
동기 프로세스와 비동기 프로세스의 예시 플로우차트
위에서 설명한 동기와 비동기의 차이를 플로우차트로 시각화하면 위와 같다. 그렇다면 우리는 왜? 비동기 프로그래밍을 하는걸까?
가장 큰 이유는 비동기 방식이 동기 방식에 비해 전체 처리 시간이 짧기 때문이다.
만약 작업1이 'T2' 작업2가 'T1' 만큼 작업3이 'T1' 의 시간이 걸린다고 하자.
동기 작업으로 처리할 경우 총 소비시간은 'T4' 만큼 걸리게 된다.
여기서 작업1을 비동기로 처리한다면, 작업1이 진행되는 동안 작업2와 작업3도 병렬로 실행될 수 있다.
결과적으로 전체 소요 시간은 가장 오래 걸리는 작업인 T2만큼만 소요되므로, 총 처리 시간이 대폭 단축된다.
다른 이유는 더 나은 유저경험 에 있다.
동기 프로세스로 작업을 처리할 경우 앞선 작업이 끝날때까지 추가적인 조작을 할 수 없게된다. 앞선 예시의 작업1이 데이터를 가져오는데 T2만큼 시간이 걸린다면 우리는 T2동안 가만히 기다려야한다. 이러한 기다림과 로딩은 UX에 있어 치명적으로 작용하게 된다.
감이 잘 오지 않는다면 예시를 들어보겠다.
인스타그램에 스토리를 업로드한다고 가정해보자.
동기 프로세스 방식이라면, 스토리 업로드가 완료될 때까지 피드 탐색이나 DM 확인도 하지 못하게 된다.
반면, 비동기 방식이라면 스토리는 백그라운드에서 업로드되고, 동시에 피드를 보거나 DM을 확인할 수 있다.
비동기가 필요한 건 알겠어. 어떻게 구현해야 할까?
비동기의 개념은 이제 알겠다.
그렇다면 이걸 실제 코드에서는 어떻게 구현할 수 있을까?
가장 기본적인 비동기 프로세싱 구현방법으로는 Asynchronous Function이 있다.
console.log("1");
setTimeout(() => console.log("2"), 0);
async function asyncFunc() {
console.log("3");
await Promise.resolve();
console.log("4");
}
asyncFunc();
console.log("5");
위 코드를 실행 하면 어떻게 될까? 이걸 이해하기 위해서는 javascript의 비동기 흐름을 이해해야 한다.
일단 우리는 1개의 Stack과 2가지 Queue를 알아야 한다
- Call Stack
- Macrotask Queue
- Microtask Queue
Call Stack - 현재 실행되는 코드에 위치 Macrotask Queue - WebAPI 기반 비동기 Task 에 대한 Queue Microtask Queue - Promise 기반 비동기 Task 에 대한 Queue
WebAPI? 그건 무슨 인터페이스야?
WebAPI는 웹브라우저에서 코드의 비동기 작업을 위해 제공해주는 API 의 일종이다. Javascript 는 단일스레드 언어이기 때문에 한번에 한개의작업만 Callstack 으로 부를 수 있다. 이때, Javascript가 시간을 측정하여 지연 실행 해야 하는 코드를 마주하게 될 때, 브라우저에서 "시간 계산해서 좀 이따 실행 해야하는건 내가 시간을 측정해줄께" 하면서 스톱워치를 재주는 역할을 해준다고 이해하면 편하다.
Callback, 좀 이따 나한테 다시 전화해줄 수 있어?
WebAPI로 시간 지연을 두고 실행되는 코드들은 'Callback 함수' 라는걸 포함하게된다. 표현을 Callback 함수라고 특별한 것 처럼 할 뿐 WebAPI에서 Callback함수는 그저 '조금 이따 실행될 함수'이다.
일반적인 상황으로 예시를 들어보겠다.
A는 친구B에게 전화를 해서 '나 10분뒤에 나가야 되는데 그때 다시 전화해서(Callback) 알려줄 수 있어?' 라고 부탁하면 B는 10분을 확인하고 있다가 10분뒤에 A에게 '너 지금 나가야돼' 라고 전화를 주면 A는 '아 맞다, 고마워!' 하고 외출을 할것이다.
이걸 Javascript 와 WebAPI로 대입해보면. Javascript 는 10분 뒤 실행해야 하는 코드가 있어서 WebAPI에게 Call하여 10분뒤에 알려달라 요청**한다, WebAPI는 10분 후 Javascript 에게 '너 그 코드 실행해야해' 하고 Callback을 하면 Javascript 가 CallBack함수를 실행하는 구조라 볼 수 있다.
전화를 너무 많이한 것 같은데? Callback Hell
만약 우리가 2분뒤에 할일, 그 할일을 하고 2분뒤에 할일, 그 일을 하고 또 2분뒤에 할 일을 전부 Callback 을 요청하면 어떻게 될까?
callback1(function (value1) {
callback2(function (value2) {
callback3(function (value3) {
callback4(function (value4) {
callback5(function (value5) {
callback6(function (value6) {
value6 = n;
});
});
});
});
});
});
위와 같이 코드 깊이가 과도하게 깊어지고 유지 보수가 어려워 진다. 이러한 상황을 Callback Hell 이라고 표현한다.
보기도 어렵고 사용하기도 어려워진 Callback System을 해결하기위해 구원투수 Promise 개념이 도입되게 된다
Promise? 뭘 약속하는데?
Promise는 영어로 약속이라는 뜻인걸 모두 알고있을거다. 그럼, Promise 객체는 대체 뭘 약속하는걸까?
Promise 는 "아직은 값이 없지만, 미래에 어떤 작업이 끝나면 결과를 줄게" 라는 약속을 하는 비동기 작업 객체이다
Promise의 상태는 총 3가지가 있다
- pending (대기)
- fulfilled (이행)
- rejected (실패)
async function Sample() {
return 1;
}
위 코드는 무엇을 반환할까?
async로 감싸진 function은 항상 promise를 반환하게 된다. 즉, 위 코드는 1이라는 result 를 포함한 fulfille(이행) Promise 를 반환하게 된다!
그럼 결과를 바로 사용하고 싶다면 어떻게 사용해야할까?
약속해, 그리고 이렇게 해줘 'then'
Promise는 then이라는 메서드로 Chain하여 즉시 사용할 수 있다.
async function Sample() {
return 1;
}
Sample().then(alert); //alert 1
이 코드를 평문으로 해석하면 이런 문장이 될것이다. "Sample이라는 함수의 결과값을 **약속(Promise)**해줘. 그리고(then) 약속된 값이 오면 **알림(alert)**으로 알려줘"
그렇다면, Promise의 결과값은 함수를 호출할때 then으로만 사용할 수 있을까?
알았어, 약속 지킬테니까 좀 기다려줘 'await'
async 함수 내부에서 바로 Promise값을 await라는 키워드를 이용해서 사용할 수 있다.
async function Sample(){
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료"),1000)
});
const result = await promise;
alert(result);
}
Sample();
await 키워드는 '지금 바로 약속 이행할테니까 조금 기다려줄래?'라는 의미에, async 함수 내부에서 promise resolve를 기다리게 설정하는 키워드이다.
await을 사용하여 async 함수를 구성하게 되면 비동기 함수를 마치 동기함수 처럼 가독성을 향상시킬 수 있다.
async function getData() {
const res = await fetch("https://api.example.com/data");
const json = await res.json();
console.log(json);
}
위 함수는 백엔드 서버에서 데이터를 fetch 해오는 함수의 예시이다. 확인해보면 비동기 작업이 2개 들어가 있는것을 알 수 있는데.
읽어보면 비동기 작업이 2개나 들어가 있음에도 불구하고, await 으로 비동기를 줄 단위로 처리하여 "res를 받고 그걸 기반으로 json을 받고 json 을 출력해줘." 라는 동기 함수처럼 받아들일 수 있게 가독성을 향상시켜준다.
놓칠수 있는 Promise에 숨은 함정과 예외들.
Promise와 await 을 알게되면 "와, 가독성도 좋고. Await 으로 Promise를 쭉 처리하면 되겠는걸?" 이라는 생각이 들수도 있다. 하지만, await만 평생 쓸 수 없는법. Promise의 함정과 예외처리, 그리고 await의 함정을 소개해보려한다.
중첩 Promise Chaining
우리가 promise값을 받아서 여러 작업을 해야한다고 가정해보자. 아래와 같이 코드를 구성할 수 있을것이다
promise()
.then((res) => parseJSON(res))
.then((data) => doSomething(data))
.catch((err) => handleError(err));
뭔가 어디서 본 것 같은 가독성 문제 아닌가? 맞다, Promise도 작업이 중첩되어 Chaining 될 경우 Callback Hell 처럼 가독성이 급격히 떨어질 수 있다.
"그래서 await를 쓰는거 아니에요? 동기함수 처럼 보이니까 가독성이 좋아지잖아요." 사실 await도 만능 맥가이버 칼은 아니다
await 병렬처리 문제
우리는 비동기 함수의 장점은 병렬 처리라고 이미 언급하였다. 이 사실을 마음에 세기고 아래 코드를 확인해보자
async function promise() {
const res1 = await promise1();
const res2 = await promise2();
}
await는 코드가 위에서 아래로 순차적으로 실행되기 때문에, 여러 비동기 작업을 병렬로 처리하고 싶다면 Promise.all() 같은 병렬 도구를 활용해야 한다.
Promise.all
Promise는 자신의 장점을 최대한 살리기 위해 Promise.all 이라는 메서드를 제공한다. 기능은 모든 iretable 동시에 병렬처리 하여 Promise 값을 받는것이다.
const [res1, res2] = await Promise.all([promise1(), promise2()]);
이런식으로 await 뒤에 Promise.all 을 사용하여 동시에 처리하면 병렬처리를 통한 성능향상도 유도할 수 있다.
다시 예시 코드로 돌아가보자
console.log("1");
setTimeout(() => console.log("2"), 0);
async function asyncFunc() {
console.log("3");
await Promise.resolve();
console.log("4");
}
asyncFunc();
console.log("5");
우리는 1부터 5까지 숫자를 출력하려고 한다.
개념을 이해하고 다시 확인해보면.
2는 Macrotask Queue, 4는 Microtask Queue 에 들어갈 것이다.
여기서 우리가 알아야 하는것은 Microtask Queue는 Macrotask Queue 보다 우선순위가 높다는 것이다.
이걸 모두 이해하고 코드를 머리속에서 컴파일하면 아래처럼 플로우가 형성 될 것이다.
console.log("1") 출력
SetTimeout Macrotask Queue 등록 (setTimeout의 최소 콜백 대시시간은 4ms 이다)
asyncFunc() 호출 -> console.log("3") 출력
await Promise.resolve() 에서 Microtask Queue에 callback인 console.log("4")를 등록
console.log("5") 출력
Microtask Queue 에 있는 console.log("4") 출력
Macrotask Queue에 있는 console.log("2") 출력
출력 : 1,3,5,4,2
끝내며
비동기는 User 에게 UX적 쾌적함을 제공하는 강력한 기능이자 동시에 개발자에게 생각할것을 한가지 추가해주는 기능이다.
WebAPI, Promise 모두 장단점이 존재하고 상황에 따라 필요한것이 달라질 수 있다. 그렇기에 적절한 상황에 올바르게 사용하는것이 중요하다. 자칫하면 프로세스 플로우가 엄청나게 꼬여버리는 일 또한 일어날 수 있기때문에 이에대해선 항상 조심해야한다.
읽어주셔서 감사합니다.



