비동기
엘리스 SW트랙 4기에서 배운 내용들을 정리한 것입니다.
이야기 할 전체적인 내용
- 싱글스레드와 멀티스레드 차이, 장단점
- 자바스크립트는 싱글스레드지만 비동기 실행 방식으로 부족한 성능을 극복
- 블로킹과 논블로킹은 무엇? 자바스크립트는 논블로킹 방식을 지원함으로 일정 수준의 동시성을 보여줌
- 자바스크립트 실행 환경(Runtime)의 구조와 각 구성요소의 역할
- 자바스크립트의 비동기 작업 수행 방식: callback, promise, async/await
- 오늘날의 자바스크립트는 멀티스레드를 지원함(web worker Or worker thread)
01. 싱글스레드 vs 멀티스레드
싱글스레드(single-threaded)
- 스레드(Thread) = 일하는 사람, 싱글 스레드 = 일하는 사람 1명
- 한 사람이 주어진 작업을 하나씩 처리하는 방식

멀티스레드(Multi-threaded)
- 멀티스레드 = 일하는 사람 여러명
- 여러 사람이 주어진 작업을 같이 처리하는 방식

싱글스레드 vs 멀티스레드
| 싱글스레드 | 멀티스레드 |
---|
장점 | - 경쟁 상태, 교착 상태 X(하지만 비동기 작업을 동반하는 JS의 경우는 위험이 있다.) - 멀티스레드 대비 코드 복잡도가 낮음 | - 평균적으로 싱글스레드보다 작업 처리 효율이 높음 |
| | |
단점 | - 여러 명이 일하는 병렬처리보다는 작업 처리 효율이 평균적으로 떨어짐 | - 사용시 경쟁 상태, 교착 상태를 고려해야함(코드 복잡도가 높음) |
경쟁 상태
한 계좌를 A, B가 공유를 하고 있다고 가정

- 정상적인 계좌 입/출금 흐름
- 한 사람이 입/출금을 한 후에 다음 사람이 입/출금을 수행
- 중첩되는 현상이 없음

- 비정상적인 계좌 입/출금 흐름
- 한 사람이 입/출금을 하는 도중에 다른 사람이 입/출금
- 경쟁 상태 문제
- 한 사람이 작업을 할 때는 다른 사람이 작업을 할 수 없도록 해야함: 계좌에 lock걸기
교착 상태
- 2명 이상이 각자 리소스를 점유하고 있는데, 내놓지 않는 현상
- 두 개 이상의 작업이 서로 상대방의 작업이 끝나기만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태

- 멀티스레드에서는 이런 상황을 다 고려해야한다.
정리
- 자바스크립트는 대표적인 싱글스레드로 작동하는 언어
- 하지만 싱글스레드인 상황에서 성능(작업 처리 효율)을 극대화하기 위해 비동기(non-blocking, asynchronous) 처리 방식을 채택
- 자바스크립트: 싱글스레드로 비동기 환경에서 실행된다?!?!?!?
02. 블로킹 vs 논블로킹
- 자바스크립트 세계에서는 블로킹&동기, 논블로킹&비동기 라는 용어와 혼용
블로킹(blocking), 동기적 제어 흐름
- 현재 실행 중인 코드가 종료되기 전까지 다음 줄의 코드를 실행하지 않는 것
- 분기문, 반복문, 함수 호출 등이 동기적으로 실행된다.
- 코드의 흐름과 실제 제어 흐름이 동일하다.
- (단점) 싱글 스레드 환경에서 메인 스레드를 긴 시간 점유하면, 프로그램을 멈추게 한다.
- 무한 루프에 빠진다 했을 때 전체 프로그램을 중단시킴
- 브라우저가 렌더링을 멈춘다거나, 다른 작업들을 하지 못하게 된다.
function waitFiveSeconds() {
const startTime = Date.now();
while (Date.now() - startTime < 5000) {}
console.log("5초 후!");
}
function sayHello() {
console.log("Hello, World!");
}
waitFiveSeconds();
sayHello();
→ 선언문, console.log와 같은 동기적 실행, 반복문, 함수 호출문 모두 동기적 제어흐름이라고 볼 수 있다.

논 블로킹(non-blocking), 비동기적 제어 흐름
- 현재 실행 중인 코드가 종료되기 전에 다음 라인의 코드를 실행하는 것을 의미
- “작업을 실행시켜 놓기만 하는 행위” (작업해~ 어어 끝나면 알려줘~?)
- 작업이 길어져도 다음 작업이 지연이 되는 문제가 없다.
- 보통 결과값을 return문으로 리턴을 바로 받지 못하며 콜백함수를 통해 받는 경우가 많다.
- 단, Promise의 등장으로 콜백함수가 아닌 다른 방식으로 받게 된다.
- 코드 흐름과 실제 제어 흐름이 다르다.
- 기술적으로 논블로킹 방식은 싱글스레드만으로는 불가 → 누군가의 도움이 필요! ⇒ 도움 주는 친구가 바로 💡이벤트 루프
function waitFiveSeconds() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("5초 후!");
resolve();
}, 5000);
});
}
function sayHello() {
console.log("Hello, World!");
}
waitFiveSeconds();
sayHello();
코드는 바로 실행되지 X, 순서가 뒤죽박죽 실행됨

비동기, 왜 필요한가?
웹 페이지 로딩되거나, 어떤 동작 하나가 30초 이상 걸린다고 생각, 웹 페이지는 이 동작이 끝날 때까지 화면에 나타나지 않거나 다음 동작을 수행하는 데 지장을 주게 된다. 또 요즘 사용자들은 느리고 응답이 없는 웹 사이트를 원하지 않는다. 그렇기에 자바스크립트가 웹사이트에서 동작할 때 비동기적으로 동작할 수 있어야 함.
자바스크립트는 싱글스레드라 했는데 왜 동시 작업 가능?
- 자바스크립트 엔진은 비동기 처리를 제공하지 않는다.
- 대신, 비동기 코드는 정해진 함수를 제공하여 활용할 수 있다. 이 함수들을 API라 한다.
- 비동기 API의 예시로, setTimeout, XMLHttpRequest, fetch등의 Web API가 있다.
setTimeout(() => console.log('타이머 끝'), 1000);
setTimeout(() => console.log('인터벌 타이머'), 1000);
fetch('https://naver.com')
.then(() => console.log('네트워크 요청 성공.'))
.catch(() => console.log('네트워크 요청 실패.'));
- 마치 스레드가 하나이지 않은 것처럼 작동한다 → 그렇다 자바스크립트는 싱글스레드이면서 멀티스레드이다. → 이벤트루프 덕분에!
- 개발자가 작성한 자바스크립트 코드는 싱글스레드로 순차적으로 실행시키되 특정 비동기 작업에 대해서는 멀티스레드를 사용한다.
03. 자바스크립트 실행 환경: Runtime
자바스크립트 실행 환경 조감도
- 자바스크립트 코드를 실행하기 위한 환경의 구성요소들
- 세가지가 유기적으로 돌아가면서 자바스크립트 실행환경을 이룬다.

자바스크립트 엔진
- 자바스크립트 코드를 읽어서 해석하고 작업을 수행하는 역할
- 엔진 자체는 작업을 수행만 할 뿐 비동기/동기와는 관계가 없음
- 그래서 자바스크립트 엔진만 있으면 비동기 작업이 불가능함, 왜? 그냥 싱글스레드니까~
- 예: V8(chromium), SpiderMonkey(Mozilla, firefox), JavaScriptCore(safari)
큐(Queue)
- 비동기 작업을 마친 후 실행될 콜백함수가 쌓이는 곳
- 예: Micro Task Queue - Promise에 then으로 붙은 것들, Task Queue보다 높은 우선순위를 가짐
Task Queue(Macro Task Queue) - 그 외의 것들
이벤트 루프
- 자바스크립트 코드가 비동기(non-blocking OR asynchronous)로 실행될 수 있도록 하는 역할
- 이벤트 루프가 없으면 자바스크립트 코드는 모두 동기 방식으로 실행될 수 밖에… 실세임
- 이벤트 루프는 자바스크립트 엔진 외부에 존재
- 큐에 쌓인 콜백함수들을 꺼내서 자바스크립트 엔진에게 전달해주는 역할
- 엔진은 해당 함수들을 전달받은 순서대로 실행
플랫폼 API
- 비동기/동기 작업들의 묶음
- 비동기 작업 실행은 대부분 플랫폼 API를 통해 일어난다.
- 비동기 API의 경우 작업이 완료되면 큐에 콜백함수를 등록한다.
- 자바스크립트는 플랫폼 API를 사용해서 비동기 작업을 위임하며 작업이 마쳤을 때 실행되는 콜백함수를 대부분 해당 API 매개변수로 전달한다. (비동기 API의 경우, Promise, aync/await의 경우 제외)
- 예) setTimeout, setInterval, fetch, console, fs, path
자바스크립트 실행 환경 실생활 예시 : 음식 배달
Z라는 음식점에서 세 가지 배달 플랫폼을 사용: A, B, C
- 각 플랫폼의 주문 요청 리스트: 큐
- 음식점 사장: 이벤트 루프
- 음식점 주방장: JS엔진
- 배달 기사: 플랫폼 API

- 음식점 사장은 오픈 시간에 각 배달 플랫폼에서 주문을 받을 수 있도록 설정
- 각 소비자는 각자가 원하는 플랫폼을 사용하여 Z 음식점 음식을 주문
- 음식점 사장은 한 플랫폼의 주문 요청 리스트를 순서대로 확인, 선입선출로 주문을 수락 및 주방으로 순서대로 요리 요청
- 음식점 주방장은 주문에 맞게 요리 + 완료되면 배달 기사에게 전달 + 배달 완료되면 배달 완료 알림
- 음식점 사장은 현재 확인 중인 요청 리스트에 주문 요청이 하나도 없으면 다음 플랫폼 리스트 확인
- 주문 요청이 하나도 없으면 대기
- 2~5과정 반복
전체 흐름 예제
- 비동기 처리가 끝나면, 태스크 큐에 콜백 함수를 넣는다. fetch, XMLHttpRequest 등의 API 또한 마찬가지이다.
- 이벤트 루프는 콜 스택이 비워지는 것을 기다렸다 콜 스택이 비워졌을 때, 테스크 큐에서 콜백 함수가 들어온 순서대로 꺼내서 콜스택으로 함수를 내보낸다. 단, 큐는 여러개가 존재하며 그 중 우선순위가 높은 큐에서 나중에 들어온 콜백함수가 실행될 수 있다.
function logA() { console.log('A') }
function logB() { console.log('B') }
function logC() { console.log('C') }
function logD() { console.log('D') }
logA();
setTimeout(logB, 0);
Promise.resolve().then(logC);
logD();
JS Visualizer 9000

04. 자바스크립트에서 비동기 작업을 수행하는 방식
전통: 콜백 패턴(callback pattern)
“나 끝났으니, 알려줘~”, 작업을 한 주체가 반대로 끝났다는 것을 알려줌
- 비동기 작업들은 return이 아닌 별도의 결과를 반환하는 방식이 필요했고 콜백 패턴이 그 대안
- 만약 콜백 패턴이 없었다면 JS는 비동기 작업의 완료 여부를 수동으로 확인했어야 했을 것
⇒ “끝났ㄴㅣ? 끝났니? 끝났니?”(polling), 이는 프로그램 측면에서 매우 낭비적인 행위
- JS는 함수를 일급 객체로 취급했기에 가능
setTimeout(() => {
console.log("1분 후 실행");
}, 60000)
콜백 패턴의 문제: 콜백 지옥
- 중첩 함수로 인해 가로로 길어지는 코드
- 매우 안좋은 개발자 경험(DX; Developer Experience)을 제공

혁명: 프로미스 패턴(Promise pattern)
- 콜백 지옥에서 벗어나기 위한 움직임
- 로직 상 순차적으로 호출되어야하는 비동기 함수들을 쉽게 연결해주는 then
- 비동기 함수에서 에러가 발생했을 때 쉽게 에러 처리를 할 수 있도록 해주는 catch
- 비동기 함수를 일관성 있는 형식으로 관리(then, catch, finally)
- DX향상: 코드 가독성, chaining 기법
fetch('/data/sample.json')
.then(response => response.json())
.then(samples => samples.map(sample => sample.id))
.catch(error => console.log(error.message));
Promise 패턴의 문제
- 초기 호환성 문제: Promise가 ECMAScript 표준이 되기 이전에 만들어진 비동기 함수들
- 아직은 아쉬운 DX: then 체이닝 → then() 지옥
function setTimeoutToPromise(miliseconds) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, miliseconds);
});
}
setTimeoutToPromise(1000)
.then(asyncFn1)
.then(asyncFn2)
.then(asyncFn3)
.then(()=>{
})
Promise를 넘어서
then~then보다는 다른 언어들처럼 순차적으로 받아와서 작업을 할 순 없을까?

평정: async/await 패턴(문법)
- 비동기 코드를 동기 코드와 같이 보자.
- 가독성 향상, 익숙치 않은 사람들도 읽기 쉬움, 코드 흐름 보기 쉬움, 익숙한 문법
- 높은 호환성: Promise로 되어있는 비동기 함수들을 async/await만 붙여서 손쉽게 코드 치환
- await은 promise 반환 값을 받아옴
- 즉, 겉으로 보기엔 새로운 문법이지만 결국에는 promise 문법임 (syntax sugar)
- await은 async 함수 안에서만 동작, await들이 의존 관계일 시에 각각 await을 붙여줘야함
async function main() {
try{
const response = await fetch('/data/sample.json');
const samples = await response.json();
for(let i = 0; i < samples.length; i++) {
}
} catch(error) {
console.log(error.message);
}
}