아란정 2025. 3. 18. 09:48
728x90
반응형

비동기 처리란 시간이 오래 걸리는 함수와 빨리 끝나는 함수가 섞여있을 때 함수를 원하는 처리 순서에 맞게 프로그래밍하는 것을 말한다.

  • 동기 프로그래밍 : 커피 주문을 받고, 커피가 완성될 때까지 다른 사람 주문을 안 받는 방식
  • 비동기 프로그래밍 : 주문을 몰아서 받고 커피도 몰아서 처리하도록(동시에 진행) 하는 방식. 완료되지 않아도 다른 작업을 실행할 수 있다. 

 

Call back : 

함수를 다른 함수에 인자로 넘겨주는 함수
- 아메리카노 주문이 3초 걸린다고 할 때, 완료 메세지를 setTimeout으로 3초 후에 나타나도록 한다. 

const order = (coffee, callback) => {
	console.log(`${coffee} 주문 완료`);
    setTimeout(()=> {
    	callback(coffee);
       }, 3000);
   }
  
const display = (result) => {       
    	console.log(`{result} 완료`);
       }
 
 order('아메리카노', display);

 

Promise :

성공이나 실패에 따라 반환할 값만 있을 뿐 성공/실패에 따라 실행할 명령을 가지진 않는다. 판단만 하고 뭘 해주진 않는다.

const fs = require("fs").promises; // 모듈 불러오기

let lovekimchi = true;
const kimchi = new Promise((resolve, reject) => {
	if (lovekimchi)
    	resolve('신라면 끓여');
    else
    	reject('do you know kimchi?');
 });
 
 lovekimchi
 	.then(result => console.log(result))
    .catch(err => console.log(err))
    .finally(console.log('hello'));

 

async / await :

promise 가 callback hell을 막고자 만들어졌지만, 프로미스 체이닝도 결국 복잡해지긴 하기에 생긴 개념이다. 

await 예약어는 async 함수에서만 사용할 수 있다. ‼️ await을 붙일 수 있는 함수는 promise를 반환하는 메서드여야 한다. 그리고 문제가 발생했을 때 실행을 멈추지 않고 다음 동작으로 이어가도록 try, catch문으로 예외처리를 해준다. 

const fs = require("fs").promises;

async function init() {
try {
        const response = await fetch("https://~");
        const users = await response.json();
        console.log(users);
    } catch(error) {
    	// 예외처리 코드
        // error 객체는 오류 이름과 오류 설명이 들어있다.
    }
}

init();

 


JS의 비동기 처리

근데 자바스크립트에서 비동기 처리가 가능하게 된 이유가 뭐지? 스레드 하나의 인터프리터 언어가 어떻게 비동기 처리하지??

“이벤트 루프(Event Loop)“와 “Web APIs” 덕분

JS는 스레드 하나만 쓰지만, 브라우저나 Node.js의 이벤트 루프 + 백그라운드 API 덕분에 비동기 작업을 “딜레이 실행” 방식으로 처리할 수 있는 거예요.

console.log("Start");              // (1)
setTimeout(() => {
  console.log("Timeout Callback"); // (3)
}, 2000);
console.log("End");                // (2)

1. (1) “Start” 출력 – 코드가 실행되면 우선 콜 스택에서 console.log("Start")가 실행되어 “Start”가 출력됩니다.

2. 타이머 설정 – 다음으로 setTimeout(...) 함수가 호출되면, 그 내부의 콜백 함수는 즉시 실행되지 않고 브라우저의 Web API 타이머에 맡겨집니다 . 자바스크립트 엔진은 타이머가 끝날 때까지 기다리지 않고 곧바로 콜 스택에서 setTimeout 호출을 제거합니다.

3. (2) “End” 출력 – 메인 스레드는 곧이어 다음 코드인 console.log("End")를 실행하여 “End”를 출력합니다. 이 시점까지 메인 스레드는 타이머 완료를 기다리지 않고 할 일을 모두 끝낸 상태가 됩니다.

4. 콜백 대기 및 실행 – 약 2초의 타이머 시간이 지난 후, 브라우저 Web API는 타이머가 만료되었음을 감지하고 미리 등록된 콜백 () => { console.log("Timeout Callback") }Task Queue에 넣습니다 . 이벤트 루프콜 스택이 비어 있음을 확인하고 그 큐에 있던 콜백을 꺼내어 콜 스택에서 실행합니다 . 그 결과 (3) “Timeout Callback” 출력이 나옵니다.


브라우저는 자바스크립트 엔진 바깥쪽에서 동작하는 다양한 Web API(예: DOM 이벤트, 타이머, HTTP 요청 등)를 제공하여, 이러한 오래 걸리는 작업들을 비동기적이고 논블로킹 방식으로 처리합니다 . 다시 말해, 자바스크립트 메인 스레드가 직접 동시에 여러 작업을 하는 것이 아니라, 브라우저가 별도의 백그라운드 스레드로 작업을 대신 수행하고 결과를 나중에 자바스크립트의 메인 스레드에 전달하는 식으로 동시성을 구현하는 것입니다 . (Node.js 환경에서도 libuv라는 내장 라이브러리가 동일한 역할을 수행합니다 .)

브라우저의 Web API와 비동기 처리

브라우저가 제공하는 Web API들은 타이머(예: setTimeout)DOM 이벤트 처리AJAX/페치 요청 등의 기능을 별도 스레드에서 수행합니다. 자바스크립트 코드에서 이러한 비동기 함수나 이벤트를 호출하면, 해당 작업은 자바스크립트 엔진의 콜 스택을 떠나 브라우저의 Web API 영역으로 보내져 백그라운드에서 진행됩니다 . 예를 들어 setTimeout을 호출하면 지정한 시간이 흐르는 동안 타이머 기능은 브라우저(Web API)가 담당하고, 메인 자바스크립트 스레드는 그 시간을 기다리지 않고 다음 코드들을 실행하게 됩니다 . 이처럼 자바스크립트 자체는 한 번에 하나의 일만 하지만, 브라우저가 별도로 제공하는 Web API들이 동시에 여러 작업을 처리해주기 때문에 겉보기에는 여러 작업이 병렬로 실행되는 효과를 얻는 것입니다 . 중요한 점은 이러한 비동기 처리를 가능하게 하는 핵심 요소는 자바스크립트 언어 자체가 아니라 브라우저 환경이라는 사실입니다 .

이벤트 루프(Event Loop)의 역할

그렇다면 이벤트 루프(Event Loop)는 어떤 역할을 할까요? 이벤트 루프는 메인 스레드(콜 스택)와 백그라운드 작업(Web API) 사이에서 연결 다리 역할을 합니다. 구체적으로, 이벤트 루프는 끊임없이 자바스크립트의 콜 스택이 비어있는지 확인하고, 동시에 비동기 작업의 결과로 대기 중인 콜백 함수가 있는지를 확인합니다 . 만약 콜 스택이 비어 있고 처리해야 할 대기 중 콜백(Task)이 있다면, 이벤트 루프는 그 콜백을 콜 스택으로 가져와 실행시킵니다 . 이러한 작업 흐름은 반복(loop) 되는데, 이처럼 주기적으로 스택과 큐를 확인하며 동작한다고 해서 이벤트 루프라는 이름이 붙었습니다 . 이벤트 루프가 있음으로써 자바스크립트는 메인 스레드를 블로킹하지 않고도 다수의 비동기 작업을 관리할 수 있게 됩니다.

자바스크립트 비동기 실행 모델: 자바스크립트 엔진, Memory Heap(힙 메모리 영역)과 Call Stack(호출 스택), Web APIs, Task Queue(태스크 큐, 또는 콜백 큐)로 구성

Web APIs 부분은 브라우저가 제공하는 비동기 API 실행 영역으로, DOM 조작이나 AJAX 요청, 타이머 설정 같은 작업들이 이곳에서 처리됩니다.

호출 스택은 현재 실행 중인 함수나 코드를 쌓아 두는 자료구조로, 하나뿐이므로 한 순간에 하나의 함수만 실행됩니다 .

Task Queue는 Call Stack 들어가기 전 콜백 함수들이 기다리는 대기실

Event Loop는 대기실과 Web APIs, 콜 스택을 왔다갔다하면서 다음 참가자 교통 관리해주는 역할

예를 들어 사용자의 클릭 이벤트, 서버 응답 처리, 타이머 만료 등의 작업이 Web API 영역에서 완료되면, 그에 연결된 콜백 함수가 Task Queue(태스크 큐, 또는 콜백 큐)에 들어가 대기하게 됩니다 . Event Loop 는 이벤트 루프가 계속해서 Call Stack과 Task Queue를 체크하면서, 콜 스택이 비는 순간 태스크 큐의 콜백을 가져와 실행한다. 이러한 메커니즘 덕분에 자바스크립트는 비록 한 개의 스레드로 동작하지만 마치 여러 일을 동시에 처리하는 것처럼 비동기적 동시성을 얻을 수 있습니다 .

 

자바의 실행환경, 브라우저는 멀티 스레드로 동작하는 Web API들을 통해 시간이 오래 걸리는 작업을 대신 수행하고, 연결된 콜백함수를 Task Queue에 넣는다. 자바스크립트 엔진은 이벤트 루프를 통해 그 결과를 순차적으로 받아 처리합니다 . 이벤트 루프는 콜 스택이 비는 순간을 놓치지 않고 태스크 큐의 콜백을 실행시킴으로써 메인 스레드의 논블로킹(non-blocking) 동작을 실현합니다 . 

 


“논 블로킹(non-blocking)“과 “비동기(asynchronous)“의 차이

비동기 (Asynchronous) 작업을 요청한 뒤 즉시 다음 코드로 넘어가고, 결과는 나중에 콜백/프로미스 등으로 전달됨
논 블로킹 (Non-blocking) 어떤 작업을 기다리지 않고, 바로 다음 명령을 실행함 (즉, 스레드가 멈추지 않음)

뭐가 다른건지 감 잡기가 쉽지 않다. 둘은 관점의 차이다. 내가 할 일 vs 남이 할 일의 순서를 관리한다고 비유할 수 있다. 

동기와 비동기의 차이는 확실히 알겠다. 들어온 대로 순차적으로 작업해서 결과도 그 순서대로 나게 하는 것과 작업 받는 건 순차적으로 받지만 결과는 먼저 끝난 것부터 보여주는 것의 차이다. 결국 작업을 수행하는 주체가 인풋과 리턴의 시점을 동기화 할 것이냐 아니냐로 생각할 수 있다. 

논블로킹과 블로킹은, 내가 어떻게 할 수 없는 일(system call)의 실행 순서를 말한다. 배구의 blocking을 생각하면, 제어권이 block 되냐 아니냐이다. CPU인 내가 OS에게 시스템 call 했는데 complete 될 때까지 기다리라 해서 막히거나, 아님 OS가 실행 다 하면 줄테니 넌 니 할 일하라고 제어권을 non block 당하냐이다. 

꼭 참고 👇

 

이해를 돕기 위해 Asynchronous blocking, Synchronous non-blocking을 살펴보자.

좌: Asynchronous blocking, 우: Synchronous non-blocking

 

Asynchronous blocking: 제어권을 안 돌려줬는데 다음 함수를 실행할 수 있나 ? 우선권이 어디에 있을까

read()라는 I/O 를 system call로 커널에 요청하면 kernel의 response가 오기 전까지 Application 스레드는 I/O descriptor(I/O 자원번호)에 어떤 활동이 있는지 select() 라는 system call로 확인한다. I/O 응답이 오기 전까지 App은 blocked 상태로 대기한다. select system call은 여러 I/O descriptor에 대해 데이터를 쓸 수 있는 상태인지를 관리할 수 있다는 특징이 있다. 알림을 대기하다 response가 오면 read()가 재실행된다.  

Synchronous non-blocking: I/O 작업을 즉시 수행하기 보단, read() 함수가 명령어가 실행될 수 없다는 에러(EAGAIN or EWOULDBLOCK)을 반환할 확률이 높다는 것을 말한다. 마우스 클릭 등의 I/O를 커널에서 처리해줘야 앱에서 다음 함수를 실행해도 처리할 값이 있는데, 커널 처리 끝내기도 전에 그 값에서 이어지는 다른 함수를 실행하라고 하니 에러가 난다. 


 

JS의 비동기 요약

1. Web APIs (또는 Node APIs)

• setTimeout, fetch, addEventListener 같은 비동기 함수는 실제로 JS 엔진이 직접 처리하는 게 아님! 이 함수들은 브라우저의 Web API 영역에서 처리

2. Callback Queue (Task Queue)

• Web API에서 작업이 끝나면, 콜백을 여기에 넣어둔다.

3. Event Loop

• 메인 스레드가 할 일이 없을 때, 큐를 확인해서 콜 스택으로 콜백을 가져와 실행한다.

 

 

728x90