자바스크립트는 싱글스레드이기 때문에 한번에 하나의 작업만 수행할 수 있고, 이를 해결하기 위해 비동기가 생겨났다.
비동기란 특정 작업의 완료를 기다리지 않고 다른 작업을 동시에 수행하도록 하는 방식을 뜻한다.
자바스크립트에서 비동기를 처리하기 위해 콜백 함수, Promise, async/await를 쓰는데 각각의 방식이 어떻게 어떤 방식으로 쓰이는지 , 그리고 어떤 문제점이 존재하는지 다뤄보고자 한다 !
1️⃣ 콜백함수
콜백함수란 매개변수로 함수 객체를 전달해서 호출 함수 내에서 매개변수 함수를 실행하는 것을 의미한다.
콜백함수로 비동기 프로그래밍을 짤 수 있지만, 모든 콜백 함수가 비동기이지는 않다.
console.log('작업 시작');
setTimeout(() => {
console.log('2초 후에 실행되는 작업');
}, 2000);
console.log('작업 끝');
// 출력 순서: 작업 시작 → 작업 끝 → 2초 후에 실행되는 작업
이 코드의 실행 결과는 작업 시작 -> 작업 끝 -> 2초 후에 실행되는 작업 이 순서대로 찍히게 된다.
setTimeout은 비동기 함수라, 2초 후에 실행할 콜백 함수를 등록하고, 즉시 다음 코드로 넘어간다.
비동기 처리가 되기 때문에 시간이 걸리는 작업을 백그라운드에서 처리하고 메인 스레드에서는 다른 작업을 계속 진행하게 된다.
콜백지옥
function getData(callback) {
setTimeout(() => {
callback('데이터1');
}, 500);
}
function getMoreData(data, callback) {
setTimeout(() => {
callback(data + ' → 데이터2');
}, 500);
}
function getFinalData(data, callback) {
setTimeout(() => {
callback(data + ' → 최종데이터');
}, 500);
}
// 콜백 지옥
getData(function (x) {
getMoreData(x, function (y) {
getFinalData(y, function (z) {
console.log('결과:', z);
});
});
});
콜백 함수를 사용해서 비동기를 처리하다 보면 자연스럽게 여러 비동기 작업을 연속으로 처리해야 하는 상황이 생긴다.
위의 코드와 같이 데이터를 단계적으로 가져와야 할 경우에, 코드가 복잡해짐에 따라 가독성이 떨어지고 유지보수가 어려워지는 상황이 발생하게 된다. 이러한 상황을 Callback Hell, 콜백 지옥이라고 부른다.
또한 각 단계마다 에러 처리를 해야하는데, 중복되어서 복잡해질 뿐더러 어느 단계에서 에러가 발생했는지 추적하기가 어렵다.
그래서 이러한 콜백 지옥의 문제점들을 해결하기 위해 Promise가 등장하였다.
2️⃣ Promise
Promise 객체는 이러한 콜백 지옥의 한계점을 극복하기 위해 비동기 처리를 위한 전용 객체로서 탄생하였다.
Promise는 비동기 작업의 성공 또는 실패와 그 결과값을 나타내는 객체이다. 따라서 Promise를 사용하면 비동기 작업을 더 직관적이고 관리하기 쉽게 만들어줄 수 있다.
Promise의 상태

promise는 new Promise()로 생성할 수 있으며, 종료될 때 다음 세가지 상태 중 하나를 가진다.
- Pending(대기) : 비동기 처리가 아직 완료되지 않은 상태
- Fulfilled(이행) : 비동기 처리가 완료되어 결과값을 반환한 상태
- Rejected(거부) : 비동기 처리가 실패하거나 오류가 발생한 상태
// Promise 생성 예시
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
resolve('성공!'); // Fulfilled 상태로 변경
} else {
reject('실패!'); // Rejected 상태로 변경
}
}, 1000);
});
Promise는 위와 같이 resolve와 reject 2개의 인자를 받으며, 비동기 처리가 성공하면 Promise 상태가 Pending에서 Fulfilled 상태로 변경이 되고, 만약 실패하면 Pending에서 Rejected로 상태값이 변경된다.
Promise의 에러처리
위에서 봤던 콜백 함수의 경우 에러처리가 곤란하다는 문제점이 있었다. 프로미스는 에러를 문제없이 처리할 수 있다.
posts
.then((res) => console.log(res))
.catch((err) => console.error(err))
.finally(() => console.log('끝'));
비동기 처리 결과에 대한 후속 처리는 프로미스가 제공하는 then, catch, finally를 사용하여 수행한다.
비동기 처리에서 발생한 에러는 catch를 사용해 처리할 수 있다.
Promise.all()
Promise.all 메소드는 여러 개의 Promise를 병렬로 실행하고, 모든 Promise가 완료될 때까지 기다린 후에 모든 결과를 배열로 반환하는 메소드이다. 모든 프로미스 비동기 처리가 이행될때까지 기다려서, 모든 프로미스가 완료되면 그때 then 핸들러가 실행되는 형태로 볼 수 있다.
const promise1 = new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 3000);
});
const promise2 = new Promise((resolve) => {
setTimeout(() => {
resolve(2);
}, 2000);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('에러'));
}, 2500);
});
Promise.all([promise1, promise2, promise3])
.then((result) => console.log(result))
.catch((e) => console.error(e));
만약 배열에 있는 프로미스 중에서 하나라도 reject가 호출된다면, 성공한 프로미스 응답은 무시된채로 바로 catch에 빠지게 된다.
그러면 다시 또 요청을 보내야 하는데 여러개의 요청을 한꺼번에 다시 보내야하니 비효율적일 수 있다.
Promise.allSettled()
Promise.all()의 경우에 하나의 프로미스라도 실패하면 에러로 처리되었다.
반면에 Promise.allSettled는 여러 프로미스를 병렬적으로 처리하되, 하나의 프로미스가 실패해도 무조건 이행한다.

Promise.allSettled 메소드가 반환한 배열에는 fulfilled 또는 rejected 상태와 상관없이 모든 프로미스들의 처리 결과가 담겨있다.
프로미스가 fulfilled 상태인 경우 status 프로퍼티와 결과를 나타내는 value 프로퍼티를 가지고,
프로미스가 rejected 상태인 경우 status 프로퍼티와 에러를 나타내는 reason 프로퍼티를 가진다.
Promise 체이닝
Promise Chaining이란 .then()을 연속으로 연결해서 여러개의 비동기 작업을 순차적으로 실행하는 방법이다.
getData(function(x) {
getMoreData(x, function(y) {
getFinalData(y, function(z) {
console.log(z);
});
});
});
// Promise 체이닝
getData()
.then(x => getMoreData(x))
.then(y => getFinalData(y))
.then(z => console.log(z));
Promise는 이렇게 .then()을 통해 순차적인 비동기 처리 결과를 깔끔하게 표현할 수 있다.
다만 콜백함수 못지않게 프로미스의 then() 메소드도 지나치게 체인되어 반복되면 코드가 장황해지고 가독성이 떨어질 수 있다.
then()을 중첩해서 사용하게 되면 코드가 지나치게 길어지고, 각 then 메소드가 어떤 값을 반환하는지 파악하기 어렵게 된다.
이러한 문제점을 해결하기 위해 나온 문법이 바로 async/await 키워드이다.
3️⃣ async와 await
콜백 지옥이 있듯이 지나친 then 핸들러 함수의 남용으로 프로미스체이닝이 존재하기 때문에 async/await라는 문법이 추가되었다.
async/await는 프로미스를 기반으로 동작하며, 비동기 코드를 마치 동기 코드처럼 사용할 수 있다.
async
async function func1() {
return 1;
}
const data = func1();
console.log(data); // 프로미스 객체가 반환된다

async는 function 앞에 붙이는 키워드이며, 해당 함수는 항상 promise를 반환한다.
만약 promise가 아닌 값을 반환하더라도 항상 이행 상태의 프로미스 객체 형태로 반환되도록 한다.
그리고 await는 무조건 async 함수 안에서만 동작한다.
await
await 키워드는 promise.then()보다 더 세련되게 promise의 result 값을 얻을 수 있도록 해주는 문법이다.
자바스크립트는 await 키워드를 만나면 promise가 처리될 때까지 기다리고, 결과는 그 이후에 반환된다.
async function func() {
const res = await fetch(url); // 요청을 기다림
const data = await res.json(); // 응답을 JSON으로 파싱
// data 처리
console.log(data);
}
func();
await 키워드를 사용하면 then 핸들러를 복잡하게 처리할 필요 없이, await만 명시해주고 결과값을 변수에 받도록 정의하면 된다.
then과 콜백 함수를 남발하여 코드가 들여쓰기로 깊어지는 것을 방지하고, 한 줄 레벨에서 코드를 나열하여 가독성을 높일 수 있다.
async/await 사용법
async function main() {
let data = await getDB(); // await 키워드로 Promise가 완료될 때까지 기다린다
data *= 2;
console.log('data의 값 : ', data);
}
main(); // 메인 스레드 실행
async 키워드는 함수 앞에 붙여서 해당 함수가 항상 Promise 객체를 반환하도록 만들어준다. 즉 main() 함수 내에서 await 키워드를 사용할 수 있게 해준다. await 키워드는 Promise가 완료될때까지 다음 줄의 코드 실행을 멈추고 기다린다.
async/await는 복잡한 .then() 체인 없이도 흐름을 자연스럽게 처리할 수 있어서 가독성을 높일 수 있고 유지보수를 용이하게 해준다.
async/await의 에러 처리
async function func() {
try {
const res = await fetch(url); // 요청을 기다림
const data = await res.json(); // 응답을 JSON으로 파싱
// data 처리
console.log(data);
} catch (err) {
// 에러 처리
console.error(err);
}
}
func();
async/await에서 에러처리를 할 때는 일반 에러처리를 할 때처럼 try/catch 문을 사용하면 된다.
Promise.all()과 병행하기
// ❌ 비효율적인 방식 (순차 처리)
const user = await fetchUser();
const posts = await fetchPosts(user.id);
// ✅ 병렬 처리 (Promise.all)
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(userId)
]);
await는 그 줄이 끝날 때까지 다음 줄이 실행되지 않는다. 따라서 await를 연속으로 사용하면 순차적으로 실행되므로 느려질 수 있다.
병렬 처리가 가능한 경우는 위에서 언급했던 Promise.all()과 병행하면 더욱 효율적이다.
참고문헌
'JavaScript' 카테고리의 다른 글
| [JavaScript] var, let, const를 비교해보자 (재선언, 재할당, 스코프, 호이스팅) (3) | 2025.08.19 |
|---|---|
| [JavaScript] 자바스크립트는 싱글 스레드 언어인데 어떻게 비동기 처리가 가능할까? (0) | 2025.08.18 |
| [JavaScript] TDZ (Temporal Dead Zone) 이란? (0) | 2025.08.16 |
| [JavaScript] 자바스크립트의 실행 컨텍스트란 무엇일까? (4) | 2025.08.14 |