
JavaScript에서는 비동기 처리를 다룰 수 있는 방법에는 여러가지가 있다.
주로 callback, Promise, async/await 를 활용한다.
하지만 Callback 함수는 비동기적으로 처리해야 하는 일이 많아질수록,
즉 코드의 깊이가 계속 깊어지는 현상이 있기 때문에 효율적이지 못하다.
따라서, 비동기 처리를 할때 Promise, async/await을 주로 사용한다.
다른 함수에 인자로 전달되어, 그 함수의 내부에서 실행되는 함수
JS에서 자주쓰는 map(), filter(), reduce(), forEach()도 콜백함수이다.
배열의 각 요소에 대해 콜백 함수를 실행하고, 그 결과를 새로운 배열로 반환하기 때문이다.
하지만 콜백 함수의 결과값이 그 다음 콜백 함수 실행에 필요한 경우가 계속 이어지게 되면,
아래와 같이 콜백 지옥 현상이 일어난다.
fetchData((data) => {
parseData(data, (parsed) => {
filterData(parsed, (filtered) => {
sortData(filtered, (sorted) => {
console.log(sorted); // 최종 결과 처리
});
});
});
});
// 실행순서: fetchData → parseData → filterData → sortData → console.log
Callback의 중첩으로 인한 비동기 처리작업을 좀 더 체계적이고 가독성 좋게 처리할 수 있는 객체
Promise에는 3가지 상태가 존재한다.
Promise가 생성되어 비동기 처리가 아직 완료되지 않은 상태
Promise는 작업이 완료되기 전까지 이 상태에 머무르며, 작업이 진행 중임을 의미
비동기 처리가 성공적으로 완료되어, Promise가 결과 값을 반환한 상태
resolve 함수가 호출되면 프로미스는 '성공' 상태가 되며, 이 함수를 통해 전달된 값이 Promise의 결과가 됌
비동기 처리 중 오류가 발생하거나 실패하여 Promise가 에러 원인을 반환한 상태
reject 함수가 호출되면 프로미스는 '실패' 상태가 되며, reject에 전달된 값(보통 에러 메시지나 오류 객체)이 실패의 원인으로 처리
Promise 생성const promise = new Promise((resolve, reject) => {
// 비동기 작업 작성
resolve("🎉 성공!");
reject("💥 실패!");
})
new 생성자를 통해 생성
생성자는 실행 함수 resolve, reject를 파라미터로 받음
Promise 사용promise
.then((result) => {
// 성공시 동작할 코드
})
.catch((error) => {
// 실패시 동작할 코드
})
.finally(() => {
// 성공 유무에 상관 없이 마지막에 실행할 코드
})
promise 가 성공했을 때 실행될 콜백 (resolve() 값을 받는다.)promise 가 실패했을 때 실행될 콜백 (reject() 값을 받는다.) 여러 개의
then()을 연결하여 비동기 처리를 순차적으로 실행하는 방식
Promise의 then()이 또 다른 Promise를 반환하여, 다음 then()이 실행될 수 있도록
체인처럼 연결하는 구조
즉, 이전 작업의 결과가 다음 작업의 입력으로 필요할 때 사용
function getUser() {
return new Promise(resolve => {
resolve({ userId: 1, name: "Alice" });
});
}
function getPosts(userId) {
return new Promise(resolve => {
resolve([{ postId: 101, title: "첫 번째 게시물" }]);
});
}
function getComments(postId) {
return new Promise(resolve => {
resolve("좋은 글이네요!");
});
}
/* Promise 체인 미사용시
getUser().then(user => {
getPosts(user.userId).then(posts => {
getComments(posts[0].postId).then(comments => {
}).catch(error => {
console.error(error);
});
}).catch(error => {
console.error(error);
});
}).catch(error => {
console.error(error);
});
*/
// Promise 체인 사용시
getUser()
.then(user => {
return getPosts(user.userId);
})
.then(posts => {
return getComments(posts[0].postId);
})
.then(comments => {
console.log("📌 최종 결과:", { comments });
})
.catch(error => {
console.error(error);
});
위와 같이, Promise Chain을 사용하면 코드가 훨씬 깔끔하고 유지보수가 쉬워진다.
또한 에러 핸들링이 간소해진다. (하나의 catch로 가능)
모든 작업의 결과가 동시에 필요한 경우에 사용하는 방식
| 특징 | Promise.all() | Promise.allSettled() |
|---|---|---|
| 성공 조건 | 모든 Promise가 성공해야 전체 성공 | 개별적으로 성공 여부 확인 가능 |
| 실패 조건 | 하나라도 실패하면 즉시 catch()로 이동 | 실패해도 무시하고 전체 결과 반환 - catch()문 없음 |
| 반환 값 | 모든 Promise의 결과 배열 | { status, value } 또는 { status, reason } 객체 배열 |
| 에러 처리 방식 | 하나의 실패가 전체 실패로 간주됨 | 개별적으로 성공/실패 여부 확인 가능 |
| 사용 예시 | 모든 요청이 성공해야 의미가 있을 때 | 하나 실패하더라도 나머지 요청을 진행해야 할 때 |
const p1 = Promise.resolve(10);
const p2 = Promise.reject("에러 발생!"); // 하나의 Promise가 실패
const p3 = Promise.resolve(30);
// Promise all()
Promise.all([p1, p2, p3])
.then(results => console.log("✅ 성공:", results))
.catch(error => console.error("❌ 실패:", error));
/*
❌ 실패: 에러 발생!
*/
// Promise allSettled()
Promise.allSettled([p1, p2, p3])
.then(results => console.log("✅ 성공:", results))
/*
[
{ status: 'fulfilled', value: 10 },
{ status: 'rejected', reason: '에러 발생!' },
{ status: 'fulfilled', value: 30 }
]
*/
Promise를 기반으로 동작하고,Promise의then/catch/finally등의 후속처리 메서드 없이
마치 동기처럼 Promise 문법을 좀더 간략하게 사용할 수 있게 도와주는 도구이다.
따라서 async 함수는 무조건 Promise를 반환하며, await은 무조건 async 안에서만 사용 할 수 있다!
다만, async/await는 reject를 처리하는 부분이 없기 때문에 오류를 대비하기 위해 try~catch 구문으로 감싸주는 과정이 필요하다!
만약 try~catch구문으로 에러처리를 안하면 에러를 reject하는 Promise를 반환한다.
// async는 언제나 반환값을 resolve하는 promise를 반환한다!
// await는 언제나 promise앞에서 사용해야한다!
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);
});
// 에러처리 X
// promise의 then then then 보다 훨씬 간결하다!
const asyncexample = async () => {
const a = await promise1(); // 3초
const b = await promise2(); // 2초
console.log(a,b); // 1,2
}
asyncexample();
// 에러처리
const asyncexample2 = async () => {
try {
const a = await promise3();
} catch (e){
console.log(e);
}
}
asyncexample2();
하지만 예시의 promise함수들은 개별적으로 수행되는 비동기 처리이다.
따라서 이런 경우에는 모든 promise함수에 await을 사용하는 것보다 Promise.all을 사용해 아래와 같이 사용하는것이 바람직하다.
// async는 언제나 반환값을 resolve하는 promise를 반환한다!
// await는 언제나 promise앞에서 사용해야한다!
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);
});
const asyncallexample = async () => {
const a = await Promise.all([promise1(), promise2(), promise3()]);
console.log(a);
}
asyncallexample();
// 결과값
// [
// { status: 'fulfilled', value: 1 },
// { status: 'fulfilled', value: 2 },
// {
// status: 'rejected',
// reason: Error: 에러!!!
// }
// ]
리액트만 쓰다가 앵귤러 사용하면서 알게된 사실...
보통 axios를 사용해 서버와의 통신에서 사용하는 경우가 많은데, axios는 자동적으로 HTTP 요청에 대한 Promise를 반환해주기 때문에 코드가 간결해져서 개꿀이다..
(일반적인 비동기처리는 위와 같이 await뒤에 Promise 함수를 써줘야됌)
위에서 살펴본 Promise와 async/await 모두 비동기를 동기처럼 보이게 하는 기술이다.
따라서, Promise와 async/await를 사용한다고 해서 블로킹 방식이 되는 것이 아니라,
모두 비동기 & 논블로킹 방식으로 실행된다.
async function fetchData() {
console.log("✅ 데이터 요청 시작");
await new Promise(resolve => setTimeout(() => {
console.log("✅ 2초 후 실행");
resolve();
}, 2000));
console.log("비동기 작업 완료");
}
console.log("함수 호출 전");
fetchData();
console.log("함수 호출 후");
/*
함수 호출 전
✅ 데이터 요청 시작
함수 호출 후
✅ 2초 후 실행
비동기 작업 완료
*/
위의 결과처럼 await은 현재 실행 중인 async 함수 내에서만 실행을 중지하고, 프로그램의 나머지 부분은 차단하지 않는다.
await을 만나도 메인 스레드는 블로킹되지 않고 "함수 호출 후"를 먼저 실행한다.
즉, JavaScript는 해당
Promise와async/await의 해결을 기다리는 동안,
다른 작업(예: 이벤트 처리, 다른 스크립트 실행)을 계속 처리할 수 있기 때문에
비동기 & 논블로킹 방식이라고 볼 수 있다.