자바스크립트는 싱글 스레드 기반의 언어입니다. 싱글 스레드란 한 번에 하나의 작업만 처리할 수 있는 구조를 의미하며, 이로 인해 먼저 실행되는 작업이 오래 걸릴 경우 애플리케이션 전체가 멈추는 문제가 발생할 수 있습니다. 예를 들어, 복잡한 연산이 포함된 코드나 대용량 파일을 처리하는 작업이 실행 중이라면, 자바스크립트의 싱글 스레드는 해당 작업이 완료될 때까지 다른 작업을 처리할 수 없습니다. 사용자 인터페이스는 응답하지 않게 되고, 결과적으로 사용자 경험이 크게 저하될 수 있습니다.
실제 웹 개발에서는 네트워크 요청, 파일 읽기/쓰기, 데이터베이스 조회 등 시간이 오래 걸리는 작업이 자주 발생합니다. 자바스크립트는 이러한 한계를 극복하기 위해 이벤트 루프와 비동기 처리 기술을 제공합니다. 이를 통해 시간이 오래 걸리는 작업을 수행하는 동안에도 애플리케이션이 계속해서 다른 작업을 진행할 수 있도록 합니다.
자바스크립트 비동기 처리를 이해하면, 복잡한 작업을 보다 효율적으로 관리하고 애플리케이션 성능을 최적화할 수 있습니다. 이번 포스팅에서는 비동기 처리의 개념과 동작 방식에 대해 알아보도록 하겠습니다.
동기와 비동기
먼저, 자바스크립트의 실행 흐름을 이해하기 위해 동기와 비동기란 무엇인지 알아보겠습니다.
동기(Synchronous)란 작업이 순차적으로 실행되는 것을 의미합니다. 먼저 실행된 작업이 완료된 후에 다음 작업이 시작됩니다. 작업 순서가 명확하고 직관적이지만 작업 실행 중에는 다른 작업을 할 수 없으며 계속해서 대기해야 합니다. 동기 방식은 간단한 작업을 수행할 때 효과적입니다.
비동기(Asynchronous)란 각 작업이 독립적으로 실행되는 것을 의미합니다. 동기 방식과 달리 먼저 실행된 작업의 완료를 기다리지 않으며, 한 작업이 진행되는 동안 다른 작업을 병행하여 수행할 수 있습니다. 따라서 네트워크 요청 등과 같이 시간이 오래 걸리는 작업을 처리할 때 효과적입니다.
앞서 말씀드린 바와 같이 현대 웹 개발에서는 비동기 처리가 필수적인 요소입니다. 그런데 비동기 처리 작업이 많아지면 코드가 복잡해지고 관리가 어려워지는 문제가 발생할 수 있습니다. 이러한 문제를 해결하고, 자바스크립트의 비동기 처리를 효율적으로 관리하는 기술이 바로 이벤트 루프입니다.
이벤트 루프
이벤트 루프는 자바스크립트의 비동기 처리를 관리하는 핵심 메커니즘입니다. 자바스크립트 코드가 실행되면, 다음과 같은 요소들이 협력하여 작업을 처리합니다.
- 호출 스택(Call Stack) : 자바스크립트 코드가 실행될 때 함수 호출이 쌓이는 곳입니다. 동기적으로 실행되는 코드는 이 스택에 쌓였다가 차례로 처리됩니다.
- 웹 API (Web APIs) : setTimeout, fetch 등 비동기 작업을 처리하는 브라우저 제공 기능입니다. 웹 API는 비동기 작업을 백그라운드에서 처리하고 완료되면 콜백 함수를 태스크 큐에 추가합니다.
- 태스크 큐(Task Queue) : 웹 API에서 전달받은 콜백 함수들이 대기하는 곳입니다. 마이크로태스크 큐 등과 같은 다양한 큐가 존재하며, 각 큐의 우선 순위에 맞게 콜백 함수가 호출되어 처리됩니다.
- 이벤트 루프(Event Loop) : 호출 스택이 비어있을 때마다 큐를 확인하고, 콜백 함수를 호출 스택으로 옮겨 실행합니다. 이 과정을 반복하며 비동기 작업을 순차적으로 처리하는 것처럼 보이게 합니다.
console.log("시작");
setTimeout(() => {
console.log("비동기 작업 완료");
}, 1000);
console.log("끝");
위 코드는 다음과 같은 순서로 실행됩니다
console.log(“시작”) 이 호출 스택에 쌓이고 즉시 실행됩니다.
2️⃣ setTimeout이 호출되어 비동기 작업을 예약합니다. 이 작업의 콜백은 태스크 큐에 추가됩니다.
3️⃣ console.log(“끝”)이 호출 스택에 쌓이고 즉시 실행됩니다.
4️⃣ 1초 후 태스크 큐에 있던 setTimeout의 콜백 함수가 호출 스택으로 이동하여 실행됩니다.
[실행 결과] 시작
끝
비동기 작업 완료
비동기 처리 방법
이러한 비동기 작업을 구현하는 방식에는 어떤 것들이 있을까요? 대표적인 비동기 처리 방법에 대해 알아보겠습니다.
callback
function fetchData(callback) {
setTimeout(() => {
const data = 'Hello, world!';
callback(data);
}, 1000);
}
fetchData((data) => {
console.log(data);
});
callback 함수는 비동기 작업의 결과를 처리하는 가장 기본적인 방법으로, 다른 함수의 매개변수로 전달되어 특정 작업이 완료된 후 호출되는 함수를 말합니다. 예를 들어, 서버에서 데이터를 가져오는 함수에 callback 함수를 전달하면, 데이터를 가져온 후에 callback 함수가 실행되어 받아온 데이터를 처리할 수 있습니다.
하지만 다음과 같이 callback 함수를 여러 번 중첩해서 사용하면 코드가 복잡해지고 가독성이 떨어지는 문제가 발생합니다. 이를 ‘콜백 지옥’이라고 부릅니다.
fetchData1(() => {
fetchData2(() => {
fetchData3(() => {
console.log("콜백 지옥...");
});
});
});
✅ Promise
function fetchData() {
return new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
resolve("성공");
} else {
reject("실패");
}
}, 2000);
});
}
fetchData()
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
console.log("완료");
});
이러한 callback 함수의 단점을 보완하기 위해 등장한 것이 Promise입니다. Promise는 비동기 작업의 성공 또는 실패를 처리하는 객체로, 다음과 같은 상태를 갖습니다.
- 대기(pending) : Promise가 생성된 초기 상태로, 비동기 작업이 아직 완료되지 않은 상태입니다.
- 이행(fulfilled) : 비동기 작업이 성공적으로 완료된 상태로, 작업 결과값이 반환됩니다.
- 거부(rejected) : 비동기 작업이 실패했거나 에러가 발생한 상태로, 실패 이유가 반환됩니다.
Promise의 결과값은 .then(), .catch(), 그리고 .finally() 메서드를 통해 처리합니다.
- .then : Promise가 이행되었을 때 호출할 함수를 정의합니다.
- .catch : Promise가 거부되었을 때 호출할 함수를 정의합니다.
- .finally : Promise의 성공 여부와 관계없이 작업이 완료된 후 실행할 함수를 정의합니다.
✅ await / async
async function fetchData() {
const data = await new Promise((resolve) => {
setTimeout(() => {
resolve('Hello, world!');
}, 1000);
});
console.log(data);
}
fetchData();
async/await는 Promise를 기반으로 비동기 코드를 더 간단하고 직관적으로 작성할 수 있도록 도와주는 문법입니다.
- async : async 키워드는 함수를 비동기로 선언하는 데 사용됩니다. 비동기 함수는 Promise 객체를 반환하며, 함수 내부에서 반환한 값이 자동으로 Promise.resolve()로 감싸집니다.
- await : await 키워드는 Promise가 처리될 때까지 함수의 실행을 일시 중단하고, 처리된 결과 값을 반환합니다. 반드시 async 함수 내부에서만 사용할 수 있으며, Promise가 성공적으로 처리되면 await는 그 결과 값을 반환하고 실패 시에는 Promise가 던진 에러를 전달합니다.
async/await를 사용하면 기존의 .then() 체인에 비해 코드가 간결하고 가독성이 높아집니다. 비동기 코드를 동기 코드처럼 논리적인 흐름으로 작성할 수 있어 유지보수성이 좋아지고, try…catch 블록을 통해 에러를 쉽게 처리할 수 있습니다.
이렇게 비동기 처리 방법에 대해 간단히 알아보았습니다. 관련 기술을 올바르게 이해하고 활용한다면 다양한 작업에서 더욱 안정적이고 효율적인 코드를 작성할 수 있을 것 입니다. 이번 포스팅이 비동기 처리의 기본 원리와 주요 개념을 이해하는 데 도움이 되었기를 바라며, 여기에서 마무리하도록 하겠습니다😊