자바스크립트 비동기 작업의 최종적인 결과(또는 에러)를 담고 있는 객체. 작업 결과에 따라 성공 또는 실패를 리턴하며 결과 값을 전달받을 수 있습니다.
* 비동기 처리 - 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 자바스크립트의 특성
promise의 3가지 상태 및 처리 흐름
- pending(대기): 처리가 완료되지 않은 상태
- fullfilled(이행): 성공적으로 처리가 완료된 상태
- rejected(거부): 처리가 실패로 끝난 상태
이행(fullfillment) 값이나 거부(rejection)와 관련된 에러(원인)를 받기 위해 프라미스 인스턴스의 then() 함수를 사용할 수 있습니다.
promise.then(onFulfilled, onRejected)
promise 객체가 비동기 함수의 처리 상태를 보고 완료되었는지 판단하여 성공 여부에 따라 다음 처리를 다르게 수행할 수 있도록 해줍니다.
프라미스가 코드를 어떻게 변화시킬 수 있는지 보기 위해 다음과 같은 콜백 기반 코드를 생각해보면
// 콜백 기반 코드
asyncOperation(arg, (err, result) => {
if(err){
// 에러 처리
}
// 결과 처리
}
// 프라미스 기반 코드
asyncOperationPromise(arg)
.then(result => {
// 결과 처리
}, err => {
// 에러 처리
})
아래 코드에서 asyncOperationPromise()는 프라미스를 반환합니다.
then() 함수의 특성은 또 다른 프라미스를 동기적으로 반환한다는 것입니다.
게다가 onFulfilled나 onRejected 함수가 x라는 값을 반환한다면 then() 메소드에 의해 반환된 프라미스는 다음과 같이 동작합니다.
- x가 값이면 x를 가지고 이행(fulfill)합니다.
- x가 프라미스라면 프라미스의 x의 이행값으로 가지고 이행(fulfill)합니다.
- x가 프라미스라면 프라미스 x의 거부 사유를 최종적인 거부 사유로 하여 거부(reject)합니다.
이러한 동작으로 여러 가지 환경에서 비동기 작업들을 손쉽게 통합하고 배치할 수 있게 해주는 프라미스 체인을 구성할 수 있습니다.
또한 onFulfilled 또는 onRejected 핸들러를 명시하지 않는다면, 이행값 또는 거부 사유는 자동으로 체인 내의 다음 프라미스로 전달됩니다.
asyncOperationPromise(arg)
.then(result1 => {
// 다른 프라미스를 반환
return asyncOperationPromise(arg2_
})
.then(result2 => {
// 값을 반환
return 'done'
})
잘못된 비동기 방식 예제
function goToSchool() {
console.log("학교에 갑니다.");
}
function arriveAtSchool_asis() {
setTimeout(function() {
console.log("학교에 도착했습니다.");
}, 1000);
}
function study() {
console.log("열심히 공부를 합니다.");
}
goToSchool();
arriveAtSchool_asis();
study();
올바른 비동기 방식 예제
function arriveAtSchool_tobe() {
return new Promise(function(resolve, reject){
setTimeout(function() {
console.log("학교에 도착했습니다.");
resolve();
}, 1000);
});
}
구조를 보면 콜백함수를 사용한 방법과 유사한 형태에서 promise 객체를 적용하여 리턴
new Promise에 전달되는 함수는 executor(실행자, 실행 함수)라고 부른다.
- resolve(value): 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
- reject(error): 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
executor는 자동으로 실행되는데 여기서 원하는 일이 처리되고 처리가 끝나면 executor는 처리 성공 여부에 따라 resolve나 rejected 로 호출한다.
한편, new Promise 생성자가 반환하는 promise 객체는 다음과 같은 내부 프로퍼티를 갖는다
- state: 처음엔 "pending"(보류)이다가 resolve가 호출되면 "fulfilled", reject가 호출되면 "rejected"로 변한다.
- result: 처음엔 undefined이다가 resolve(value)가 호출되면 value로, reject(error)가 호출되면 error로 변한다.
promise는 성공 또는 실패만 합니다.
goToSchool();
arriveAtSchool_tobe().then(function(){
study();
});
이 함수를 위와 같이 호출하여 사용할 수 있다.
arriveAtSchool_tobe 함수는 promise 객체를 리턴하는데 promise는 then이라는 메서드를 가지고 있고 그 메서드 파라미터에 콜백 함수를 대입하면 앞서 resolve()라고 정의했던 구문이 실행되는 구조
1. promise 연결하기(체이닝)
goToSchool();
arriveAtSchool_tobe().then(function(){
study();
});
.then은 프라미스에서 가장 중요하고 기본이 되는 메서드.
체이닝 기법을 활용함으로써 콜백 함수 사용 시 발생할 수 있는 콜백 지옥에서 탈출이 가능.
프로미스 하나에 .then을 여러 개 추가한 후, 이를 체이닝이라고 착각할 수 있는데 이는 체이닝이 아니다.
fetch와 체이닝 함께 응용하기
let promise = fetch(url);
fetch('/article/promise-chaining/user.json')
// 원격 서버가 응답하면 .then 아래 코드가 실행된다.
.then(function(response) {
// response.text()는 응답 텍스트 전체가 다운로드되면
// 응답 텍스트를 새로운 이행 프라미스를 만들고, 이를 반환한다.
return response.text();
})
.then(function(text) {
// 원격에서 받아온 파일의 내용
alert(text); // {"name": "iliakan", "isAdmin": true}
})
이 코드에 화살표 함수를 쓰고 Github에 요청을 보내 사용자 프로필을 불러오고 아바타를 출력하는 것을 하면
//user.json에 요청을 보낸다.
fetch('/article/promise-chaining/user.json')
// 응답받은 내용을 json으로 불러온다.
.then(response => response.json())
// Github에 요청을 보낸다.
.then(user => fetch(`https://api.github.com/users/${user.name}`))
// 응답받은 내용을 json 형태로 불러온다.
.then(response => response.json())
// 3초간 아바타 이미지(githubUser.avatar_url)를 보여준다.
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => img.remove(), 3000); // (*)
});
코드는 주석에 적은대로 잘 동작하지만 (*)로 표시한 줄을 보면 만약, 아바타가 잠깐 보여다가 사라진 이후에 사용자 정보를 수정할 수 있게 해주는 폼을 보여주는 것 같은 작업을 추가하는 경우같은 일을 하고 싶으면 지금 코드로는 방법이 없다.
체인을 확장할 수 있도록 만들려면 아바타가 사라질 때 이행 프라미스를 반환해 줘야 한다.
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
// 3초 후 동작함
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
(*)로 표시한 곳의 .then 핸들러는 이제 setTimeout 안의 resolve(githubUser)를 호출했을 때 (**)만 처리 상태가 되는 new Promise를 반환한다. 체인의 다음 .then은 이를 기다린다.
코드를 재사용 가능한 함수 단위로 분리해 마무리하면
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return fetch(`https://api.github.com/users/${name}`)
.then(response => response.json());
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// 함수를 이용하여 다시 동일 작업 수행
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
2. promise 예외 처리
goToSchool()
.then(function(){
return arriveAtSchool();
})
.then(function(){
return studyHard();
})
.then(function(){
return eatLunch();
})
.catch(function(){
leaveEarly();
});
학교에 가는 도중 다치거나 공부를 하다가 열나거나 점심을 먹다가 체하는 일이 생겨도 마지막 catch 구문에서 조퇴하는 것으로 한 번에 처리가 가능하다. 이 경우 최초 발생하는 rejected 상태의 작업만 처리학 구문을 빠져 나오게 된다.
최근에 promise를 다루는 async awiat 구문이 있는데 비동기 함수를 동기 함수처럼 다룰 수 있어 매우 깔끔하고 유용하게 사용할 수 있는 방법
예시) loadScript
먼저 콜백 기반으로 작성한 함수를 보면
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생함`));
document.head.append(script);
}
그리고 프라미스를 이용해서 함수를 작성하면
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`${src}를 불러오는 도중에 에러가 발생함`));
document.head.append(script);
});
}
사용법은 다음과 같다.
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src}을 불러왔습니다!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('또다른 핸들러...'));
프로미스를 사용한 코드가 콜백 기반 코드보다 더 나은 점을 정리하면
프로미스
- 프로미스를 이용하면 흐름이 자연스럽다. loadScript로 스크립트를 읽고, 결과에 따라 그 다음(.then)에 무엇을 할지에 대한 코드를 작성하면 된다.
- 프로미스에 원하는 만큼 .then을 호출할 수 있다.
콜백
- loadScript(script, callback)를 호출할 때, 함께 호출한 callback 함수가 준비되어 있어야 한다.
- loadScript를 호출하기 이전에 호출 결과로 무엇을 할지 미리 알고 있어야 한다.
- 콜백은 하나만 가능하다.
3. promise와 에러 핸들링
존재하지 않는 주소를 fetch로 넘겨주는 예시를 보면 .catch에서 에러를 처리합니다.
fetch('https://no-such-server.blabla') // 거부
.then(res => res.json())
.catch(err => alert(err)) // TypeError: failed to fetch
이번엔 사이트에는 아무런 문제가 없지만 응답으로 받는 JSON의 형식이 잘못된 경우를 살펴보면
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
정상적인 경우라면 .catch는 절대 트리거 되지 않습니다. 그런데 네트워크 문제, 잘못된 형식의 JSON 등으로 인해 위쪽 promise 중 하나라도 거부되면 .catch 에서 에러를 잡게 됩니다.
암시적 try...catch
promise executor와 promise 핸들러 코드 주위엔 '보이지 않는(암시적) try..catch'가 있습니다. 예외가 발생하면 암시적 try..catch에서 예외를 잡고 이를 reject처럼 다룹니다.
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(alert); // Error: 에러 발생!
위 예시는 아래 예시와 똑같이 동작합니다.
new Promise((resolve, reject) => {
reject(new Error("에러 발생!"));
}).catch(alert); // Error: 에러 발생!
executor 주위의 '암시적 try..catch'는 스스로 에러를 잡고, 에러를 거부 상태의 promise로 변경시킵니다.
이런 일은 executor 함수뿐만 아니라 핸들러에서도 발생합니다. .then 핸들러 안에서 throw를 사용해 에러를 던지면, 이 자체가 거부된 promise를 의미하게 됩니다. 따라서 제어 흐름이 가장 가까운 에러 핸들러로 넘어갑니다.
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
throw new Error("에러 발생!"); // 프라미스가 거부됨
}).catch(alert); // Error: 에러 발생!
throw문이 만든 에러뿐만 아니라 모든 종류의 에러가 암시적 try..catch에서 처리됩니다. 암시적 try..catch가 프로그래밍 에러를 어떻게 처리하는지 살펴보면
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
blabla(); // 존재하지 않는 함수
}).catch(alert); // ReferenceError: blabla is not defined
마지막 .catch는 이렇게 명시적인 거부뿐만 아니라 핸들러 위쪽에서 발생한 비정상 에러 또한 잡습니다.
다시 던지기
체인 마지막의 .catch는 try..catch와 유사한 역할을 합니다. .then 핸들러를 원하는 만큼 사용하다 마지막에 .catch 하나만 붙이면 .then 핸들러에서 발생한 모든 에러를 처리할 수 있습니다.
일반 try..catch에선 에러를 분석하고, 처리할 수 없는 에러라 판단되면 에러를 다시 던질 때가 있습니다. promise에도 유사한 일을 할 수 있습니다.
.catch 안에서 throw를 사용하면 제어 흐름이 가장 가까운 곳에 있는 에러 핸들러로 넘어갑니다. 여기서 에러가 성공적으로 처리되면 가장 가까운 곳에 있는 .then 핸들러로 제어 흐름이 넘어가 실행이 이어집니다.
아래 예시의 .catch 는 에러를 성공적으로 처리합니다.
// 실행 순서: catch -> then
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(function(error) {
alert("에러가 잘 처리되었습니다. 정상적으로 실행이 이어집니다.");
}).then(() => alert("다음 핸들러가 실행됩니다."));
.catch 블록이 정상적으로 종료되었기 때문에 다음 성공 핸들러 .then이 호출된 것을 확인할 수 있습니다.
.catch를 활용한 또 다른 사례를 살펴보면 (*)로 표시한 핸들러에서 에러를 잡는데, 여기서는 에러를 처리하지 못하기 때문에(URIError 처리 방법만 알고 있음) 에러를 다시 던집니다.
// 실행 순서: catch -> catch
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// 에러 처리
} else {
alert("처리할 수 없는 에러");
throw error; // 에러 다시 던지기
}
}).then(function() {
/* 여기는 실행되지 않습니다. */
}).catch(error => { // (**)
alert(`알 수 없는 에러가 발생함: ${error}`);
// 반환값이 없음 => 실행이 계속됨
});
실행 흐름이 첫 번째 .catch (*)로 넘어갔다가 다음 .catch (**) 로 이어지는 것을 확인할 수 있습니다.
처리되지 못한 거부
new Promise(function() {
noSuchFunction(); // 에러 (존재하지 않는 함수)
})
.then(() => {
// 성공 상태의 promise를 처리하는 핸들러. 한 개 혹은 여러 개가 있을 수 있음
}); // 끝에 .catch가 없음!
에러를 처리하지 못하면 위 예시처럼 체인 끝에 .catch 를 추가하지 못하는 경우가 생깁니다.
에러가 발생하면 promise는 거부 상태가 되고, 실행 흐름은 가장 가까운 rejection 핸들러로 넘어갑니다. 그런데 위 예시엔 예외를 처리해 줄 핸들러가 없어서 에러가 '갇혀버립니다.' 에러를 처리할 코드가 없기 때문입니다.
이런 식으로 코드에 처리하지 못한 에러가 남게 되면 실무에선 끔찍한 일이 벌어집니다.
일반적인 에러가 발생하고 이를 try..catch 에서 처리하지 못하는 경우를 생각해보면 스크립트가 죽고 콘솔 창에 메시지가 출력됩니다. 거부된 프로미스를 처리하지 못했을 때도 유사한 일이 발생합니다.
자바스크립트 엔진은 프라미스 거부를 추적하다가 위와 같은 상황이 발생하면 전역 에러를 생성하는데 콘솔창을 열고 위 예시를 실행하면 전역 에러를 확인할 수 있습니다.
브라우저 환경에선 이런 에러를 unhandledrejection 이벤트로 잡을 수 있습니다.
window.addEventListener('unhandledrejection', function(event) {
// 이벤트엔 두 개의 특별 프로퍼티가 있습니다.
alert(event.promise); // [object Promise] - 에러를 생성하는 프라미스
alert(event.reason); // Error: 에러 발생! - 처리하지 못한 에러 객체
});
new Promise(function() {
throw new Error("에러 발생!");
}); // 에러 처리 핸들러, catch가 없음
unhandledrejection 이벤트는 HTML 명세서에 정의된 표준 이벤트입니다.
브라우저 환경에선 에러가 발생했는데 .catch가 없으면 unhandledrejection 핸들러가 트리거 됩니다.
unhandledrejection 핸들러는 에러 정보가 담긴 event 객체를 받기 때문에 이 핸들러 안에서 원하는 작업을 할 수 있습니다.
대개 이런 에러는 회복할 수 없기 때문에 개발자로서 할 수 있는 최선의 방법은 사용자에게 문제 상황을 알리고 가능하다면 서버에 에러 정보를 보내는 것입니다.
'Javascript > Node.js' 카테고리의 다른 글
ESLint (0) | 2021.09.13 |
---|---|
async/await (0) | 2021.09.13 |
[Node.js] 3. 코드 스타일 (0) | 2021.09.13 |
[Node.js] 2. 에러 처리 방법 (0) | 2021.09.10 |
1. 프로젝트 구조 설계 (0) | 2021.09.10 |