자바스크립트로 비동기 코드를 작성할 때 가장 자주 마주치는 개념이 바로 Promise이다.
하지만 “비동기 처리 도구”라는 말만 들어서는 실제로 어떻게 동작하고, 왜 필요한지 감이 잘 오지 않는다.
이 글에서는 Promise의 핵심 개념부터 프론트엔드에서 실무적으로 활용하는 패턴까지 하나씩 정리한다.
1. Promise란 무엇인가
자바스크립트의 Promise는 “미래에 완료될 작업”을 나타내는 객체이다.
즉, 지금은 값이 없지만 언젠가 결과를 반환할 비동기 연산의 약속을 표현한다.
Promise는 세 가지 상태를 가진다.
상태 | 의미 |
pending | 아직 결과가 결정되지 않은 초기 상태 |
fulfilled | 작업이 성공적으로 끝나 값을 반환한 상태 |
rejected | 작업이 실패하여 에러를 반환한 상태 |
이 상태 변화는 한 번만 일어난다.
즉, 한 번 fulfilled나 rejected 상태로 바뀌면 다시 바뀌지 않는다.
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("작업 성공");
} else {
reject("작업 실패");
}
});
promise
.then((result) => console.log(result)) // 성공 시
.catch((error) => console.error(error)) // 실패 시
.finally(() => console.log("완료"));
JavaScript
복사
then은 성공 결과를, catch는 실패 이유를, finally는 성공/실패와 상관없이 실행할 코드를 등록한다.
2. 프론트엔드에서 Promise를 활용하는 방법
(1) 네트워크 요청 처리
프론트엔드에서 Promise의 가장 흔한 활용 예는 API 호출이다.
fetch 함수는 Promise를 반환하며, 서버 응답이 도착하면 fulfilled 상태가 된다.
fetch("/api/users")
.then((res) => res.json())
.then((data) => console.log("유저 목록:", data))
.catch((err) => console.error("요청 실패:", err));
JavaScript
복사
이렇게 하면 비동기 통신 중에도 UI가 멈추지 않고, 응답이 오면 그때 결과를 처리할 수 있다.
(2) async / await 문법으로 가독성 높이기
Promise 체인을 여러 개 연결하면 코드가 복잡해진다.
이를 단순하게 만든 것이 async / await 문법이다.
async function loadUsers() {
try {
const res = await fetch("/api/users");
const data = await res.json();
console.log("유저 목록:", data);
} catch (err) {
console.error("요청 실패:", err);
}
}
JavaScript
복사
이 코드는 동기 코드처럼 읽히지만 내부적으로는 Promise를 사용한다.
즉, await 키워드는 Promise가 fulfilled될 때까지 기다린다.
(3) UI 상태 관리와 함께 사용하기
비동기 작업은 종종 로딩 상태, 성공 상태, 실패 상태로 UI를 나눠야 한다.
React 예시를 보자.
import { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
try {
const res = await fetch("/api/users");
const data = await res.json();
setUsers(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
fetchUsers();
}, []);
if (loading) return <p>로딩 중...</p>;
if (error) return <p>에러 발생: {error.message}</p>;
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
JavaScript
복사
여기서 useEffect 안의 fetchUsers()는 Promise 기반 비동기 함수이며,
상태(loading, error, users)를 업데이트하면서 UI를 자연스럽게 전환한다.
(4) 여러 Promise를 동시에 처리하기
서버에서 여러 데이터를 한꺼번에 받아야 할 때는 Promise.all이 유용하다.
const [users, posts] = await Promise.all([
fetch("/api/users").then((res) => res.json()),
fetch("/api/posts").then((res) => res.json()),
]);
JavaScript
복사
여러 요청이 병렬로 처리되기 때문에 속도가 빨라진다.
단, 하나라도 실패하면 전체가 reject 상태가 되므로 예외 처리를 주의해야 한다.
(5) 순차적 비동기 처리
반대로, 순서가 중요한 경우는 for-await-of 문법을 활용한다.
const urls = ["/a", "/b", "/c"];
for await (const url of urls.map((u) => fetch(u))) {
const data = await url.json();
console.log(data);
}
JavaScript
복사
이 코드는 각 요청을 순서대로 기다리며, Promise 체인을 간결하게 표현한다.
3. 마무리
Promise는 단순히 “비동기 처리를 도와주는 문법”이 아니라,
비동기 흐름을 구조적으로 표현할 수 있는 약속 객체이다.
프론트엔드에서는 이를 통해
•
로딩 상태를 세밀하게 제어하고,
•
여러 API를 병렬 혹은 순차로 호출하며,
•
코드의 가독성을 유지한 채로 에러를 명확하게 처리한다.