동기/비동기
자바스크립트는 싱글 스레드 언어로, 동기적으로 실행되는 언어이다. 즉, 한 번에 하나의 작업만 처리할 수 있는 특성을 가지고 있다.
동기식과 비동기식 📝
유형 | 실행방식 | 특징 |
---|---|---|
동기식 | 순서대로 실행 | 한 작업이 완료될 때까지 다음 작업이 대기 |
비동기식 | 작업의 완료를 기다리지 않음 | 작업의 완료를 기다리지 않고 다음 코드를 실행, 완료 시 콜백 함수나 프로미스로 처리 |
Javascript는 싱글 스레드 언어지만, 비동기 작업을 이벤트루프를 통해 처리한다. 브라우저나 Node.js 환경에서는 내부적으로 Web API나 스레드 풀을 사용하여 비동기 작업을 처리하지만, JS 코드 자체는 메인 스레드에서 실행된다.
프로세스와 스레드 관계 🔄
프로세스란, 프로그램을 실행해달라고 요청이 들어오면, 프로그램을 메모리에 로드 시켜서 cpu가 처리할 수 있게 하는 것을 말한다.
.exe
의 확장자를 가지는 파일은 프로그램이라 한다.- 해당 프로그램이 메모리에 로드 되는것이 프로세스
- 프로그램을 프로세스에 올라가도록 도와주는게 바로 운영체제, OS(Operating System)이다.
작업관리자에서 작업을 종료시킬때도, 프로그램을 없애는것이 아니라, 프로세스를 종료시켜서 메모리에서 내려주는 것이다.
Context 스위칭 🔄
컨텍스트 스위칭이란, OS가 시분할을 통해 동시 실행을 지원하면서, 여러개의 프로세스를 왔다갔다 하면서 처리하는 것을 말한다.
OS에서 시분할 기능이 나와서 이 작업 저 작업을 실행하다보면, 다시 했던 작업으로 돌아오면 내가 어디까지 작업을 했는지 에 대한 정보를 저장해놔야한다. 이를 프로세스 컨텍스트, 문맥 객체라고 부른다.
프로세스, 컨텍스트 스위칭은 보통 OS에서 담당했었다.
초기 컴퓨팅 시스템에서는 새로운 작업마다 프로세스를 생성하는 방식을 사용했었고, 이로 인해 프로세스 간의 컨텍스트 스위칭 비용이 증가하는 문제가 발생했다. 프로세스가 늘어남에 따라 자기한테 할당된 처리 시간에서, 자꾸 자식 프로세스가 파생되어 처리 시간이 점점 짧아지는 문제점이 발생하기 시작했다.
스레드 🧵
웹브라우저에서는 검색, 다운로드 등을 전부 동시에 진행하고 싶어 했지만, 프로세스는 이런게 되지 않았었다. 이에따라 스레드를 사용하여 이런 문제를 해결하였다.
스레드는 프로세스보다 더 적은 단위이며, 자기 프로세스에 할당 된 시간을 그대로 보존하면서, 해당 시간을 쪼개서 자식 프로세스를 생성하지 않고 스레드로 생성하는 방식으로 진행하게 됐다.
이 때문에 프로세스의 Context스위칭에서 Thread의 Context스위칭으로 바뀌게 되었다. 스레드는 프로세스보다 더 적은 단위이며, 자기 프로세스에 할당 된 시간을 그대로 보존하면서, 해당 시간을 쪼개서 자식 프로세스를 생성하지 않고 스레드로 생성하는 방식으로 진행하게 됐다.
동기식과 비동기식 실행 흐름 비교 ⚖️
동기식 실행 흐름:
- 일반적으로 스레드 스위칭 기능을 사용하지 않는 일반적인 프로그램은 동기형 흐름을 따른다.
- 예: 브라우저에서 3개 파일 다운로드 시, 첫번째 파일 다운로드 완료 후 두번째 파일 다운로드 시작 → 블로킹 발생
비동기식 실행 흐름:
- 작업의 완료를 기다리지 않고 다음 코드를 실행하는 방식
- 작업이 완료되면 콜백함수나 프로미스를 통해 결과를 처리
- 시분할 스위칭을 이용해 여러 작업을 효율적으로 처리
스레드 구성:
- 멀티스레드: 메인스레드 1개와 서브 스레드 여러개로 구성 (웹브라우저: 메인스레드, 다운로드 작업: 서브 스레드)
- 싱글 스레드: 메인스레드 1개만 존재 (동기식 실행)
이벤트 루프
이벤트 루프는 콜스택이 비었을 때 태스크 큐에서 작업을 가져와 콜스택에 추가하는 역할을 한다. 이를 통해 비동기 작업이 적절한 시점에 실행될 수 있게 된다.
이벤트 루프 단계 🔄
이벤트루프는 여러 페이즈로 나뉘어 각 단계마다 실행해야할 콜백들이 존재한다.
단계 | 설명 | 예시 |
---|---|---|
1. Timers | setTimeout, setInterval 콜백 실행 | 타이머 콜백 |
2. Pending Callbacks | 시스템 I/O 작업의 콜백 실행 | TCP 에러 처리 콜백 |
3. Idle/Prepare | Node.js 내부 준비 작업 | 내부 초기화, 리소스 관리 |
4. Poll | I/O 이벤트 확인 및 처리 | 파일 읽기 완료 콜백 |
5. Check | setImmediate 콜백 실행 | 즉시 실행 콜백 |
6. Close Callbacks | 닫힘 관련 이벤트 처리 | 소켓 종료 콜백 |
7. Microtasks | Promise, nextTick 등 처리 | Promise.then() 콜백 |
/*
예를 들어, TCP 소켓에서 에러가 발생하여 아래와 같이 등록한 에러 핸들러가 내부적으로 Pending Callbacks 단계에서 실행될 수 있다.
*/
socket.on("error", (err) => {
console.error("소켓 에러 발생:", err);
});
이때, 해당 콜백은 우리가 직접 호출하는 코드가 아니라, Node.js 이벤트 루프
에 의해 자동으로 실행됨. 보통 이벤트 루프 단계들에 대해 개발자는 오류나 연결 종료 이벤트 등에 대해 콜백을 등록할 수는 있지만, 어떤단계에 의해 호출될지는 JS 시스템인 Node.js 런타임
이 결정하는 것이다.
실행 순서 요약 📋
- 동기 코드가 실행된 후 바로 마이크로태스크가 처리됨
- 그 뒤 각 이벤트 루프 단계 (Timers, Pending Callbacks, …, Check 등)에서 상황에 맞게 콜백이 실행됨
⚠️ 이벤트 루프 주의사항
만약 메인 모듈에서 setTimeout(..., 0)
과 setImmediate()
를 모두 호출하면, I/O 사이클이 없을 경우 대부분 setImmediate()
의 콜백이 먼저 실행되지만, I/O 작업이 있는 경우 순서가 바뀔 수 있다.
따라서 단순히 “동기 → 마이크로태스크 → Check → Timer”로만 이해하기는 어렵고, 상황과 코드 작성 위치에 따라 실제 실행 순서는 달라질 수 있다.
이벤트 루프 예제 코드 🧩
// event-loop-order.js
console.log("시작");
setTimeout(() => {
console.log("타이머 콜백 (Timers 단계)");
}, 0);
setImmediate(() => {
console.log("setImmediate 콜백 (Check 단계)");
});
process.nextTick(() => {
console.log("process.nextTick (마이크로태스크)");
});
console.log("끝");
예상 출력 순서 👇
- 시작 → 동기 코드 실행
- 끝 → 동기 코드 실행 종료
process.nextTick
→ 현재 작업(동기 코드) 종료 후 즉시 실행- 이후, 어느 단계가 먼저 도달하는지에 따라 다르지만 일반적으로 Check 단계의 setImmediate 콜백이 실행되고, 그 다음에 Timers 단계의 setTimeout 콜백이 실행될 수 있습니다. (실행 순서는 Node.js의 내부 I/O 상황, 시스템 부하 등에 따라 미세하게 달라질 수 있습니다.)
타이머 함수 ⏱️
-
setTimeout
- 일정 시간 후 실행setTimeout(() => { console.log("타이머 콜백 (Timers 단계)"); }, 0);
-
setInterval
- 일정 시간 간격으로 실행setInterval(() => { console.log("인터벌 콜백 (Timers 단계)"); }, 1000);
-
setImmediate
- 즉시 실행setImmediate(() => { console.log("setImmediate 콜백 (Check 단계)"); });
※ clearTimeout
, clearInterval
, clearImmediate
등은 타이머 함수를 취소하는 함수들이다.
JavaScript 비동기 처리 구성요소 🧩
Call Stack 📚
자바스크립트 엔진은 함수를 호출하면 함수의 실행 컨텍스트를 생성하고 스택에 쌓는다. 이 스택을 콜 스택이라고 한다.
콜스택은 말그대로 스택의 형태를 가지고 있어, 마지막에 들어온 함수가 가장 먼저 실행되고, 가장 먼저 실행된 함수가 가장 마지막에 실행된다. 이 때문에 Interval과 같은 비동기 처리 함수를 실행도중에 사용자가 수동으로 요소 이벤트를 트리거 시키면 콜스택이 꼬이게 되어 반드시 이러한 현상을 막기 위해 스택 플로우를 리셋시켜주는 행위가 필요하다. (clearInterval, clearTimeout 등)
콜백 함수 🔄
콜백 함수는 다른 함수의 인자로 전달되어 특정 시점에 실행되는 함수이다.
콜백함수에는 두 가지 종류가 있습니다:
- 비동기 콜백: 작업이 완료된 후에 실행됨
- 동기 콜백: 즉시 실행됨 (예: Array.forEach())
// 동기 콜백 예시
[1, 2, 3].forEach((item) => {
console.log(item);
});
// 비동기 콜백 예시
setTimeout(() => {
console.log("3초 후 실행됩니다");
}, 3000);
위 코드에서 forEach
함수는 콜백 함수를 인자로 받아 배열의 각 요소에 대해 콜백 함수를 즉시 실행한다. forEach의 인자값으로 준 콜백 함수는 배열의 각 요소를 매개변수로 받아 실행된다.
반면 setTimeout의 콜백은 지정된 시간이 지난 후에야 실행된다.
🔧 왜 콜백함수가 필요한가?
콜백함수는 일반적으로 비동기를 처리할때 사용한다.
비동기 작업이 필요한 상황
- 두 엔드포인트간의 통신이 필요한 경우
- 전송과 응답이 동시에 일어나야함. 이러기 위해서는 전송 데이터를 받을 공간 즉, 완충제가 필요함.
- 이가 바로 버퍼의 역할(택배를 받을때 우리가 준비되어 있지 않아도 택배를 놓을 장소가 있어서 가능한 것처럼 버퍼도 마찬가지)
- 하지만 버퍼를 놓긴했는데, 사용할 시점은? 나는 현재 다른 작업을 하느라 너무 바빠, 너가 오면 나를 호출해줘! -> callback(어떤 상황을 처리할때 실행할 함수 )
Promise
Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체이다.
Promise는 다음 세 가지 상태 중 하나를 가집니다:
- 대기(pending): 초기 상태
- 이행(fulfilled): 작업이 성공적으로 완료됨
- 거부(rejected): 작업이 실패함
// Promise 예시
const myPromise = new Promise((resolve, reject) => {
// 비동기 작업 수행
const success = true;
if (success) {
resolve("작업 성공!"); // 이행(fulfilled)
} else {
reject("작업 실패..."); // 거부(rejected)
}
});
myPromise
.then((result) => console.log(result))
.catch((error) => console.error(error));
Async/Await ⌛
Async/Await는 Promise를 더 쉽게 사용할 수 있게 해주는 문법적 설탕(Syntactic sugar)이다.
// Async/Await 예시
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("데이터 가져오기 실패:", error);
}
}
fetchData();
비동기 처리 방식 비교 📊
방식 | 장점 | 단점 |
---|---|---|
콜백 함수 | 간단한 구현 | 중첩 시 콜백 지옥 발생 |
Promise | 체이닝 가능, 에러 처리 용이 | 다소 복잡한 문법 |
Async/Await | 동기 코드와 유사한 가독성 | ES2017 이상 필요, 항상 Promise 반환 |
async & await
async
와 await
는 비동기 코드를 동기적으로 보이게 작성할 수 있는 JavaScript 문법입니다.
주요 특징:
- 실제 멀티스레딩이 아닌 이벤트 루프 기반 비동기 처리 방식으로 처리
- Node.js는 이벤트 큐를 활용하여 비동기 작업을 효율적으로 처리
- 단일 스레드로 동작하지만 비동기 I/O 모델을 통해 높은 성능 제공
- 이벤트 큐에 작업을 순차적으로 집어넣고 처리해도 속도에 지장이 없다는 것이 Node.js의 중요한 발견
스레드를 직접 만들어서 구현하고 싶다면, web worker
을 사용해야한다.