작업이 완료될 때까지 기다리지 않고 다음 작업을 계속 수행하는 방식.
자바스크립트는 단일 스레드로 동작한다.
작업은 순차적으로 진행되지만, 작업의 결과를 기다렸다가 다음 작업을 실행하는게 아니라..
뒤에 있는 작업을 바로 실행시켜버린다!
그렇기에 HTTP 요청 등의 이유로 요청의 결과가 뒤늦게 반환되는 경우에는..
결과값이 반환되어 올 때에는 이미 다음 작업이 실행되어버린다.
이럴 때 필요한 것이 비동기 처리.
function fetchData(callback) {
setTimeout(() => {
callback("데이터를 가져왔습니다!");
}, 1000);
}
fetchData((data) => {
console.log(data);
});
특정 작업이 완료된 후 실행할 코드를 함수로 전달하는 방식.
완벽해보이지만.. 작업이 복잡해지면 치명적인 단점이 하나 부각된다.
function fetchUser(userId, callback) {
setTimeout(() => {
console.log("사용자 정보 가져오기 완료");
callback(null, { id: userId, name: "홍길동" });
}, 1000);
}
function fetchOrders(user, callback) {
setTimeout(() => {
console.log(`${user.name}의 주문 목록 가져오기 완료`);
callback(null, ["주문1", "주문2", "주문3"]);
}, 1000);
}
function fetchOrderDetails(order, callback) {
setTimeout(() => {
console.log(`${order}의 상세 정보 가져오기 완료`);
callback(null, { id: order, details: "상세 내용" });
}, 1000);
}
// 콜백 지옥의 구현
fetchUser(1, (error, user) => {
if (error) {
console.error("사용자 정보를 가져오는 중 에러 발생:", error);
return;
}
fetchOrders(user, (error, orders) => {
if (error) {
console.error("주문 목록을 가져오는 중 에러 발생:", error);
return;
}
fetchOrderDetails(orders[0], (error, orderDetails) => {
if (error) {
console.error("주문 상세 정보를 가져오는 중 에러 발생:", error);
return;
}
console.log("최종 결과:", orderDetails);
});
});
});
단계적으로 이루어지는 작업이 여러 개 있다고 가정해보자.
이들을 콜백 함수 형태로 이어지게 하면..
함수 뒤에 콜백 함수, 콜백 함수 뒤에 또 다른 콜백 함수, 또 다른 콜백 함수 뒤에 또또 다른 콜백 함수...
가독성은 저하되고, 유지보수가 어려워지고, 에러 핸들링 난이도도 높아진다.
콜백 함수의 콜백 지옥 문제를 해결해주기 위해 개발된 개념.
비동기 작업의 완료 또는 실패를 나타내는 객체.
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("데이터를 가져왔습니다!");
}, 1000);
});
fetchData
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error(error);
});
Promise는 자바스크립트에서 비동기 작업의 결과를 처리하기 위해 사용하는 객체.
비동기 작업이 성공하거나 실패했을 때 각각의 상태와 결과를 표현할 수 있다. 3가지 상태로 표현된다.
- Pending (대기): 초기 상태, 비동기 작업이 아직 완료되지 않음.
- Fulfilled (이행): 비동기 작업이 성공적으로 완료됨. resolve()를 통해 전달된 값이 결과로 제공됨.
- Rejected (거부): 비동기 작업이 실패함. reject()를 통해 전달된 값이 에러로 제공됨.
// Promise의 기본 사용법
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("작업이 성공했습니다!");
} else {
reject("작업이 실패했습니다.");
}
});
myPromise
.then((result) => {
console.log(result); // 작업이 성공했습니다!
})
.catch((error) => {
console.error(error); // 작업이 실패했습니다.
});
여러 개의 Promise를 병렬로 처리할 때 사용되는 메서드.
전달된 모든 Promise가 이행(Fulfilled)되면, 결과값 배열을 반환.
만약 하나의 Promise라도 거부(Rejected)되면, 전체가 거부.
각 Promise의 결과를 순서대로 배열에 담아 반환된다.
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then((results) => {
console.log(results); // [1, 2, 3]
})
.catch((error) => {
console.error("에러 발생:", error); // "에러 발생: 오류 발생"
});
ES8에서 도입된 최신 문법, 비동기 코드를 동기 코드처럼 작성할 수 있도록 개발됨.
현대 프론트엔드에서는 특별한 이유가 없는 이상 async/await을 사용한다.
async function main() {
try {
const user = await fetchUserData(1);
console.log(user); // { id: 1, name: "홍길동" }
} catch (error) {
console.error(error); // 유효하지 않은 사용자 ID입니다.
}
}
async/await의 동작 원리는..
사실 내부적으로 Promise를 사용한다.
Promise를 더 직관적이고 간결하게 사용할 수 있도록 만든 문법이기 때문.
그래서 async 키워드로 선언된 함수는 항상 Promise 객체를 반환해야한다.
그리고 await 키워드는 뒤에 오는 값이 반드시 Promise이어야 한다.
function fetchUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("사용자 정보 가져오기 완료");
resolve({ id: userId, name: "홍길동" });
}, 1000);
});
}
function fetchOrders(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`${user.name}의 주문 목록 가져오기 완료`);
resolve(["주문1", "주문2", "주문3"]);
}, 1000);
});
}
function fetchOrderDetails(order) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`${order}의 상세 정보 가져오기 완료`);
resolve({ id: order, details: "상세 내용" });
}, 1000);
});
}
fetchUser(1)
.then((user) => fetchOrders(user))
.then((orders) => fetchOrderDetails(orders[0]))
.then((orderDetails) => console.log("최종 결과:", orderDetails))
.catch((error) => console.error("에러 발생:", error));
async function main() {
try {
const user = await fetchUser(1);
const orders = await fetchOrders(user);
const orderDetails = await fetchOrderDetails(orders[0]);
console.log("최종 결과:", orderDetails);
} catch (error) {
console.error("에러 발생:", error);
}
}
main();
비동기 처리에서 에러를 처리하는 두 가지 방식.
둘 다 Promise를 기반으로 동작하지만, 코드 작성 방식과 처리 방식에 차이가 있다.
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => {
if (!response.ok) throw new Error("HTTP 에러 발생");
return response.json();
})
.then((data) => {
console.log("데이터:", data);
})
.catch((error) => {
console.error("에러:", error.message);
});
Promise의 메서드 체이닝을 사용하여 비동기 작업과 에러를 처리하는 방식.
then 메서드에서 작업 결과를 처리하고, catch 메서드에서 에러를 처리한다.
코드가 간단하고 체이닝을 통해 여러 작업을 순차적으로 처리하기에 적합하지만, 복잡한 로직에서는 체이닝이 길어지면 가독성이 떨어진다.
async function fetchData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
if (!response.ok) throw new Error("HTTP 에러 발생");
const data = await response.json();
console.log("데이터:", data);
} catch (error) {
console.error("에러:", error.message);
}
}
fetchData();
async/await와 함께 사용하여 비동기 작업의 에러를 처리하는 방식.
try 블록 안에서 비동기 작업을 수행하고, catch 블록에서 에러를 처리한다.
동기 코드와 비슷한 방식으로 작성되어 가독성이 좋고, 복잡한 로직을 처리하기 쉽다. 비동기 작업 중간에 조건문 혹은 반복문 등의 로직을 삽입할 수 있는 것도 강점.
다만 async/await 환경 외에는 사용이 불가능하고, 간단한 작업에서는 then-catch보다 코드가 길어질 수 있다.