
프론트엔드 개발에서는 사용자와 빠르게 상호작용하고 서버와도 끊임없이 데이터를 주고받아야 하는데, 이 과정에서 비동기 처리를 어떻게 다루느냐에 따라 사용자 경험(UX)의 품질과 코드의 유지보수성이 달라지게 됩니다.
자바스크립트는 콜백(callback) → 프로미스(Promise) → async/await 순으로 점차 발전해왔어요. 이번 글에서는 이 흐름을 예제와 함께 살펴보며 각 방식의 특징과 차이를 정리해보려 합니다.
비동기 처리란, 하나의 작업이 완료될 때까지 다른 작업이 블로킹되지 않도록 실행 흐름을 유지하는 방식을 말해요. 예를 들어 서버에 데이터를 요청했을 때, 응답이 도착할 때까지 기다리지 않고 그동안 다음 코드를 계속 실행할 수 있습니다.
자바스크립트는 이벤트 루프(Event Loop)를 기반으로 비동기 작업을 처리합니다.
이벤트 루프는 실행 중인 코드가 모두 끝난 뒤, 대기 중인 작업들을 순차적으로 처리합니다. 이때 사용되는 큐에는 두 가지가 있어요.
setTimeout, setInterval, DOM 이벤트 같은 작업들이 들어오는 큐Promise.then, async/await에서 사용하는 .then() 이후 작업이 들어오는 큐이벤트 루프는 항상 마이크로태스크 큐를 먼저 확인하고 그 다음에 콜백 큐를 처리합니다. 이 구조로 브라우저는 UI 렌더링을 멈추지 않고 부드럽게 처리할 수 있으며, 사용자 경험(UX)을 향상시킬 수 있어요.
자바스크립트는 단일 스레드(single-threaded) 기반의 언어로, 한 번에 하나의 작업만 실행할 수 있습니다. 모든 작업이 순차적으로 처리되기 때문에 네트워크 요청이나 파일 읽기처럼 시간이 오래 걸리는 작업을 동기적으로 수행하면 UI 렌더링이 멈추거나 사용자 입력에 반응하지 않는 등의 문제가 발생할 수 있어요.
이를 보완하기 위해 자바스크립트는 비동기 처리 방식을 지원하며, 실제 비동기 작업은 자바스크립트 엔진이 아닌 브라우저 또는 Node.js와 같은 런타임 환경의 백그라운드에서 병렬로 처리됩니다. 완료된 작업은 이벤트 루프를 통해 콜백 큐에 등록되고, 콜 스택이 비게 되면 순차적으로 실행되어 UI 반응성과 사용자 경험을 유지할 수 있게 돼요.
가장 기본적인 비동기 처리 방식은 콜백 함수(callback) 를 사용하는 것입니다. 콜백은 어떤 작업이 끝난 후 실행될 함수로 다른 함수의 인자로 전달되는 함수를 의미해요.
setTimeout()은 자바스크립트에서 일정 시간 뒤에 특정 코드를 실행하도록 예약할 수 있게 해주는 비동기 함수입니다.
setTimeout은 일정 시간 뒤 실행될 코드를 예약할 수 있어요. 실무에서는 토스트 메시지 자동 닫기, 애니메이션 지연 실행 등에서 자주 사용됩니다.
콜백 함수를 중첩해 사용하는 방식은 일정 수준까지는 유용하지만 콜백이 여러 단계로 깊어질수록 코드의 들여쓰기 구조가 복잡해지고 가독성도 급격히 떨어져요.

이러한 구조적 한계를 개선하고 비동기 코드를 보다 선언적이고 구조화된 방식으로 작성하기 위해 도입된 개념이 바로 Promise입니다.
Promise는 비동기 작업의 성공 또는 실패 결과를 나타내는 객체로 비동기 처리를 구조적이고 선언적인 방식으로 표현할 수 있도록 도와줍니다.
Promise는 비동기 작업의 상태를 표현하는 객체로 다음과 같은 세 가지 상태를 가질 수 있어요.
pending 대기 상태, 아직 작업이 완료되지 않음fulfilled 작업 성공, 비동기 작업이 정상적으로 완료됨rejected 작업 실패, 에러가 발생하거나 작업이 실패함fetch() 함수는 Promise를 반환하는데, .then(), .catch(), .finally() 같은 메서드를 사용해서 비동기 작업의 결과를 다룰 수 있어요.

.then() 작업이 성공(fulfilled)했을 때 실행할 코드를 등록해요..catch() 작업이 실패(rejected)했을 때 실행할 코드를 등록해요..finally() 성공하든 실패하든 항상 마지막에 실행되는 코드를 등록해요.위 메서드들을 통해 서버 응답을 다룰 때도 복잡한 콜백 없이 단계별로 깔끔하게 코드 흐름을 작성할 수 있어요.


Promise.all, Promise.race를 활용하면 여러 비동기 작업을 동시에 실행하고, 모든 작업이 완료되거나 특정 조건을 만족할 때까지 기다리는 로직도 구현할 수 있어요.
async/await는 Promise를 더 간결하고 읽기 쉽게 만들어주는 문법적 설탕(Syntactic Sugar)입니다. 내부적으로는 여전히 Promise를 기반으로 동작하지만, 동기 코드처럼 자연스러운 흐름으로 작성할 수 있어 가독성과 유지보수성이 훨씬 좋아집니다.
🍬 문법적 설탕이란?
기존 기능을 더 편리하게 사용할 수 있도록 만든 문법을 말해요 .then().catch()로도 가능하지만, async/await를 사용하면 흐름이 위에서 아래로 읽히는 구조가 되어 디버깅과 유지보수에 훨씬 유리합니다.
async 키워드를 붙인 함수는 항상 Promise를 반환합니다. 반환값이 자동으로 Promise.resolve()로 감싸지기 때문에 별도로 Promise를 생성할 필요가 없어요.await 키워드는 Promise가 처리될 때까지 기다렸다가 결과 값을 반환합니다.(await는 async 함수 내부에서만 사용할 수 있습니다.)try/catch를 함께 사용하면 비동기 코드의 예외 처리를 구조적으로 다룰 수 있어 에러 핸들링이 깔끔해집니다.
try/catch 블록을 통해 성공과 실패 처리 로직을 명확하게 분리할 수 있어 가독성과 유지보수 측면에서도 좋아요.
사용자의 액션에 따라 API를 호출하고, 그 과정에서 로딩 상태를 함께 관리하는 패턴입니다.
isLoading 상태를 통해 사용자에게 피드백을 제공하고 finally 블록을 활용해 성공 여부와 관계없이 항상 로딩 상태를 해제함으로써 UX 품질을 안정적으로 유지할 수 있어요.
폼 제출, 결제 처리, 주문 요청 등 사용자 액션 기반의 네트워크 요청 흐름에서 자주 사용됩니다.
비동기 처리는 실무에서 자주 마주치는 핵심 개념으로 콜백부터 Promise, async/await까지 각각의 흐름과 동작 방식을 이해하고, 상황에 맞게 적절한 방식을 선택할 수 있다면 가독성이 높고 유지보수하기 쉬운 코드를 만들 수 있어요. 💪🏻
이 글은 공식 문서를 기반으로 내용을 정리한 포스팅입니다.
혹시 내용 중 틀린 부분이나 보완할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다. 🙏🏻