💡Promise란?
Promise(프로미스)는 자바스크립트에서 비동기 작업을 처리하는 객체이다. 주로 서버에서 데이터를 요청하거나 파일을 읽는 등의 비동기 작업을 수행할 때 사용된다. Promise는 성공 또는 실패와 같은 비동기 작업의 결과를 나타내는 데 사용되며, 자바스크립트에서 비동기 처리가 가장 많이 일어나는 XMLHTTPRequest 처리에서 유용하게 사용된다.
XMLHTTPRequest
XMLHttpRequest는 웹 브라우저에서 제공하는 자바스크립트 객체로, 서버와 비동기적으로 데이터를 교환할 수 있는 기능을 제공한다. 이 객체를 사용하면 웹 페이지를 새로 고치지 않고도 서버로부터 데이터를 가져와서 웹 페이지의 일부분을 업데이트할 수 있다.
XMLHttpRequest 객체를 사용하여 서버로 데이터를 요청하는 것을 AJAX(Asynchronous JavaScript and XML)라고 한다. AJAX를 사용하면 사용자와 상호작용하는 동안 웹 페이지의 다른 부분은 변화 없이 그대로 유지하면서도 백그라운드에서 서버와 데이터를 주고받을 수 있다.
이러한 XMLHttpRequest 객체는 주로 JSON, XML, HTML 또는 일반 텍스트와 같은 형식의 데이터를 서버로부터 가져오거나 서버로 데이터를 보내는 데 사용된다. 예를 들어, 사용자가 입력한 데이터를 서버로 보내고, 서버는 해당 데이터를 처리한 후 결과를 다시 클라이언트에게 보내주는 데 사용할 수 있다.
Promise의 역할
프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있다. 그러나 이 반환된 값은 최종 결과가 아니라, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'을 나타내는 Promise 객체이다. 이 약속은 비동기 작업이 완료되면 이행되거나 실패할 때 거부된다.
Promise의 3가지 상태
Promise는 다음 세 가지 중 하나의 상태(State)를 가진다.
- 대기(Pending): 비동기 작업이 아직 수행되지 않은 상태 (이행도 거부도 하지 않는 초기 상태)
- 이행(Fulfilled): 비동기 작업이 성공적으로 완료된 상태 (연산 성공)
- 거부(Rejected): 비동기 작업이 실패한 상태 (연산 실패)
💡Promise의 탄생
Promise는 콜백 지옥(Callback hell)을 피하기 위해 등장했다. 이전에는 비동기 처리를 위해 콜백 함수를 사용했는데, 이로 인해 코드가 복잡해지고 가독성이 떨어지며, 유지보수가 어려워지는 문제가 발생했다.
콜백 함수
setTimeout(function() { // 콜백1
console.log('안녕하세요');
setTimeout(function() { // 콜백2
console.log('잘 지내세요');
setTimeout(function() { // 콜백3
console.log('고마워요');
setTimeout(function() { // 콜백4
console.log('그럼 당신은요?');
}, 3000);
}, 3000);
}, 3000);
}, 3000);
예를 들어, 위와 같이 콜백 함수를 사용하여 타이머 함수를 구현했다고 하자.
이런 코드 구조는 콜백 함수가 계속 중첩되어 가독성이 떨어지고, 로직 변경이 어렵다. 이 코드를 Promise를 사용하여 개선할 수 있다.
Promise는 체이닝(Chaining)이 가능하므로 순차적으로 처리해야 하는 함수들을 연결하여 호출할 수 있다. (체이닝에 대해서는 아래에서 다룬다.) 이를 통해 코드의 가독성을 향상하고, 복잡한 비동기 로직을 보다 간결하게 표현할 수 있다.
// Promise를 사용한 개선
function sayHello() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('안녕하세요');
}, 3000);
});
}
function howAreYou() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('잘 지내세요');
}, 3000);
});
}
function thankYou() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('고마워요');
}, 3000);
});
}
function andYou() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('그럼 당신은요?');
}, 3000);
});
}
sayHello()
.then((message) => {
console.log(message);
return howAreYou();
})
.then((message) => {
console.log(message);
return thankYou();
})
.then((message) => {
console.log(message);
return andYou();
})
.then((message) => {
console.log(message);
})
.catch((error) => {
console.error(error);
});
Promise를 사용하여 위의 예제를 개선하였다. Promise를 사용하면 콜백 지옥을 피하고, 비동기 코드를 보다 명확하고 관리하기 쉽게 구성할 수 있다.
💡Promise의 처리
Promise의 생성
Promise의 생성은 Promise 생성자를 사용하여 이루어진다. 이 생성자는 executor라는 하나의 인자로 콜백 함수를 받으며, 이 콜백 함수는 resolve와 reject라는 두 개의 함수를 인자를 받는다.
const promise = new Promise((resolve, reject) => {
// 비동기 작업 수행
if (/* 작업이 성공했을 경우 */) {
resolve("Success"); // 작업이 성공했을 때 호출
} else {
reject(new Error("Failed")); // 작업이 실패했을 때 호출
}
});
위의 코드에서 비동기 작업은 resolve 함수 또는 reject 함수 중 하나를 호출하여 완료된다.
resolve 함수는 비동기 작업을 성공적으로 완료하여 결과를 반환할 때 호출되고, reject 함수는 작업이 실패하거나 오류가 발생했을 때 호출된다. 이렇게 생성된 Promise 객체는 비동기 작업의 상태와 결과를 나타내며, 이후 then() 또는 catch() 메서드를 통해 처리될 수 있다. 만약 실행 함수에서 예외를 던지면 프로미스가 거부되며, 실행 함수의 반환값은 무시된다.
executor 매개변수
new Promise(executor);
executor는 resolve 및 reject 함수를 전달할 실행 함수이다. 실행 함수는 즉시 호출되며, 프로미스 생성자가 생성한 객체를 반환하기 전에 실행된다.
executor 함수는 프로미스가 생성될 때 즉시 호출된다. 그리고 프로미스 생성자가 생성한 객체를 반환하기 전에 실행된다. 이 함수는 두 개의 콜백 함수, 즉 resolve와 reject를 인자로 받는다.
Promise의 속성
Promise의 속성인 Symbol.species는 프로미스 체이닝 메서드(then(), catch(), finally())가 새로운 프로미스를 생성할 때 사용하는 생성자 함수를 결정한다.
기본적으로 Symbol.species는 Promise 생성자 함수를 반환한다. 하지만 하위 클래스에서 Symbol.species를 재정의하여 다른 생성자 함수를 반환할 수 있다. 이를 통해 프로미스 체이닝 메서드가 새로운 프로미스를 생성할 때 사용할 생성자 함수를 지정할 수 있다.
Promise[Symbol.species];
Symbol.species의 재정의를 통해 하위 클래스에서 부모 클래스의 형식을 반환하도록 변경할 수 있다.
class MyPromise extends Promise {
static get [Symbol.species]() {
return Promise;
}
}
이렇게 하면 프로미스 체이닝 메서드가 항상 기본 Promise 형식을 반환한다.
Promise의 사용
프로미스는 then 메서드를 사용하여 성공 및 실패 상황에 대한 처리를 지정할 수 있다.
promise.then(
(result) => {
console.log(result); // 성공했을 때 실행될 코드
},
(error) => {
console.error(error); // 실패했을 때 실행될 코드
}
);
첫 번째 콜백 함수는 프로미스가 이행됐을 때 실행되며, 이행된 결과를 매개변수로 받는다.
두 번째 콜백 함수는 프로미스가 거부됐을 때 실행되며, 거부된 이유를 나타내는 에러를 매개변수로 받는다.
then 메서드는 연결된 프로미스가 이행되거나 거부될 때 각각의 콜백 함수를 호출하여 처리한다.
Promise 체이닝
Promise는 여러 개의 비동기 작업을 연결하여 실행할 수 있다. 이를 Promise 체이닝이라고 한다.
const promise = new Promise((resolve, reject) => {
// 비동기 작업 수행
resolve("First step");
});
promise
.then((result) => {
console.log(result); // "First step"
return "Second step"; // 두 번째 비동기 작업의 결과를 반환
})
.then((result) => {
console.log(result); // "Second step"
})
.catch((error) => {
console.error(error); // 오류 처리
});
First step
Second step
첫 번째 then 메서드는 프로미스가 이행되면 실행되며, "First step"을 출력하고 "Second step"을 반환한다. 반환된 값은 다음 then 메서드로 전달되어 "Second step"을 출력한다.
만약 중간에 에러가 발생한다면 catch 메서드가 호출되어 오류를 처리한다. 이처럼 then 메서드를 연속적으로 호출하여 여러 개의 비동기 작업을 연결하여 실행할 수 있다.
Promise의 에러 처리
프로미스 체인에서 발생하는 에러는 catch 메서드를 사용하여 처리할 수 있다.
promise.catch((error) => {
console.error(error); // 오류 처리
});
promise 객체가 reject 상태로 변할 때 발생하는 에러를 catch하여 처리한다. 이렇게 함으로써 프로미스 체인 전체에서 발생한 에러를 쉽게 추적하고 관리할 수 있다.
💡Promise의 활용
Promise의 동작 예시
// delay 함수: 주어진 시간(ms)만큼 지연된 Promise를 반환
const delay = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
// delay 함수를 사용한 Promise 체이닝
delay(1000)
.then(() => {
console.log("After 1 second");
return delay(2000); // 1초 후에 실행되는 작업이 완료되면 2초간 대기
})
.then(() => {
console.log("After 2 seconds");
throw new Error("Error occurred"); // 오류 발생
})
.catch((error) => { // 오류 처리
console.error(error); // 발생한 오류 출력
return "Error handled"; // 오류 처리 후 반환
})
.then((result) => { // 결과 출력
console.log(result); // "Error handled" 출력
});
After 1 second
After 2 seconds
Error: Error occurred
at ...
Error handled
위 예시에서는 delay 함수를 사용하여 비동기적으로 지연된 작업을 수행하고, 그 결과를 출력하거나 오류를 처리한다.
delay 함수를 사용하여 1초 뒤에 "After 1 second"을 출력하고, 그 후에 2초간 대기한다. 그 후에 "After 2 seconds"를 출력하고, 오류를 발생시킨다. 오류가 발생하면 catch 블록에서 오류를 처리하고 "Error handled"를 반환하며, 마지막으로 반환된 값을 출력한다.
XMLHttpRequest 비동기 함수 구현 1
function myAsyncFunction(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText); // 성공적으로 데이터를 받아온 경우 resolve 호출
} else {
reject(new Error("Failed to fetch data")); // 오류 발생 시 reject 호출
}
};
xhr.onerror = () => {
reject(new Error("Network Error")); // 네트워크 오류 등으로 요청이 실패한 경우 reject 호출
};
xhr.send(); // 요청 전송
});
}
Promise를 사용하여 XMLHttpRequest를 비동기적으로 수행하는 함수인 myAsyncFunction을 만들었다.
이 함수는 URL을 매개변수로 받아서 해당 URL에서 데이터를 가져오고, 그 결과에 따라 resolve 또는 reject를 호출하여 프로미스를 처리한다. 이 함수를 사용하면 XMLHttpRequest를 감싸 비동기적으로 데이터를 가져오는 작업을 Promise로 처리할 수 있다.
XMLHttpRequest 비동기 함수 구현 2
function fetchData(url) {
return new Promise((resolve, reject) => {
// XMLHttpRequest 객체 생성
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
// 비동기 작업 완료 시 처리
xhr.onload = () => {
if (xhr.status === 200) {
// 성공적으로 데이터를 받아온 경우 resolve 호출
resolve(xhr.responseText);
} else {
// 오류 발생 시 reject 호출
reject(new Error("Failed to fetch data"));
}
};
// 네트워크 오류 등으로 요청이 실패한 경우 처리
xhr.onerror = () => {
// reject를 호출하여 오류 처리
reject(new Error("Network Error"));
};
// 요청 전송
xhr.send();
});
}
// Promise를 활용한 비동기 데이터 요청과 처리
fetchData("https://api.example.com/data")
.then((data) => {
// 데이터 요청이 성공한 경우 처리
console.log("Data received:", data);
})
.catch((error) => {
// 데이터 요청 중 오류 발생 시 처리
console.error("Error fetching data:", error);
});
Promise를 활용하여 XMLHttpRequest를 사용하여 비동기적으로 데이터를 요청하고 처리한다.
fetchData 함수는 URL을 매개변수로 받아 해당 URL에서 데이터를 가져오는 Promise를 반환한다. 이후 then 메서드를 사용하여 데이터 요청이 성공한 경우와 catch 메서드를 사용하여 오류가 발생한 경우를 처리한다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Using_promises
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise
참고 자료
Promise, MDN contributors, 2023.08.09.
Using promises, MDN contributors, 2023.10.25.
[JavaScript] Promise란?, Ko Seoyoung, 2021.05.25.
자바스크립트 비동기 처리와 콜백 함수, Captain Pangyo, 2018.02.06.