프라미스 (Promise) 란?
ES2015에서 생겼다.- 비동기 결과를 나타내는 객체이다.
- 작업을 비동기화하진 않으며, 비동기식의 결과를 관찰하는 방법이다.
- 여러 언어에서
promise,future,deferred라는 다양한 이름으로 불리는 패턴이다. - Promises/A+ 사양에 크게 의존한다.
ES2018에서 나온async와 상호작용하는 방식으로 많이 쓰인다.
Promise라는 이름은 엔진이 어떠한 비동기 작업을 하겠다고 약속한다는 의미에서 지어졌다고 한다.
프라미스를 사용하는 이유
프라미스는 자바스크립트에서 지원하는 대수타입의 모나드이며, 비동기 작업에서 서로간의 제어를 분리할 수 있도록 도와주는 역할을 한다.
비동기 작업의 제어권은 자바스크립트 엔진의 비동기 큐에게 맡기고, 비동기 작업 이후의 제어는 다시 사용자가 작성한 코드로 넘어온다.
- 콜백 지옥의 해결을 돕는다.
async키워드와 함께 썼을 때 더 효과적이다.- 코드의 가독성을 매우 좋게 만들어준다.
- 순차적 작업이나 병렬 작업을 명확히 나타내줄 수 있다.
async await을 통해 손쉽게 순차 작업을 진행한다.- 병렬 작업은
Promise.all()을 이용하여 진행한다.
- 더 나은 에러 핸들링을 제공한다.
- 일반 콜백 함수에서는 성공/실패를 나타내는 표준 방법이 없어 복잡성을 관리할 수 없었다.
- 프라미스는 비동기의 작업 결과를 '관리 가능하게 (managable)' 만들어준다.
- 작업에 여러 콜백을 추가할 수도 있다.
- 이미 완료된 프로세스에 콜백을 추가하는 것도 이전까지 표준이 없었다.
- 비동기 요청의 경우엔 외부 라이브러리인 제이쿼리의
ajax를 이용해왔다.
- 비동기 요청의 경우엔 외부 라이브러리인 제이쿼리의
프라미스와 thenable
- 자바스크립트 프라미스는 Promises/A+ 사양을 완전히 준수한다.
- 이 사양의 특징은
Promise와 구별되는thenable개념이다. - 이 사양에서
Promise를then()메서드를 사용하는 객체 혹은 함수(thenable) 라고 본다. Promise는thenable이지만, 모든thenable이Promise는 아니다.- 객체의
then()메서드가 있다면,Promise와 관련이 없더라도thenable이다.
- 이 사양의 특징은
Promise에서 메서드 체인으로 연결되는then()은 JS에서 마이크로 태스크로 취급되어 처리된다.
프라미스 기본 예제 코드
const example = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
const succeed = Math.random() < 0.5;
if (succeed) {
console.log("resolve 42");
resolve(42);
return;
}
console.log("new Error('failed')");
throw new Error("failed");
} catch (e) {
reject(e);
}
}, 100);
});
};
example()
.then((value) => {
console.log("resolved value: ", value);
})
.catch((error) => {
console.error("rejected error: ", error);
})
.finally(() => {
console.log("finally");
});
Promise생성자에 전달되는 함수는실행자 함수 (executor function)라는 이름을 가지고 있다.- 실행자 함수는 콜백의 인자로 받은
resolve()와reject()를 상황에 맞게 동기적으로 호출한다.resolve()의 인자로 다른Promise를 주면, 결과가 다른Promise로 확정된다.reject()를 호출하면,reject()로 전달된 사유로Promise가 실패한다.resolve()와 달리 인자로Promise를 줘도reject()의 사유로만 사용된다.
Promise는 비동기 작업을 생성하는 것이 아니라 작업을 관찰한다.- 위의 예에서는
setTimeout()이라는 비동기 작업을 관찰하고 있다.
- 위의 예에서는
프라미스의 3가지 상태
대기 (pending): 비동기 작업의 결과가 나오기 전 초기 상태이다.- 프라미스가 아직 확정되지 않은 상태.
이행 (fulfilled): 비동기 작업이 성공적으로 마무리 되었음을 나타낸다.- 프라미스가 값으로 정해진 상태.
- 프라미스가 "resolved" 된 상태라고도 한다.
거부 (rejected): 비동기 작업이 실패했음을 의미한다.- 프라미스가 "rejected" 된 상태라고도 한다.
프라미스 상태의 특징
- 초기엔
대기 (pending)상태이다. - 단 한번만,
이행 (fulfilled)혹은거부 (rejected)로 변화할 수 있다.- 일단 한번 변화하면 다시 돌아가거나 다른 상태로 변하는 것은 불가능하다.
프라미스의 3가지 핸들러
Promise는then(),catch(),finally()3가지 핸들러가 있다.
Promise.prototype.then
- 성공(
resolved) 시 호출되는 핸들러이다. catch를 작성하지 않고, 여기에 인자로 성공과 예외 시 동작할 두 개의 콜백 함수를 받을 수도 있다.- 첫번째 인자의 이름은
onFulfilled이고, 두번째 인자의 이름은onRejected이다.
- 첫번째 인자의 이름은
const example = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success");
}, 100);
});
};
example().then((result) =>
console.log(`then handler works. result: ${result}`)
);
- 결과는
then handler works. result: success이 된다.
Promise.prototype.catch
- 실패 (
rejected) 시 호출되는 핸들러이다.
const example = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("failed");
}, 100);
});
};
example()
.then((result) => console.log(`then handler works. result: ${result}`))
.catch((result) => console.log(`catch handler works. result: ${result}`));
- 결과는
catch handler works. result: failed이 된다.
Promise.prototype.finally
- 성공과 실패 상관없이 프라미스가 확정되면 호출되는 핸들러이다.
ES2018에 추가되었다.then과catch모두에 들어갈 내용을 여기에 넣으면 공통으로 들어간다.
const example = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("failed");
}, 100);
});
};
example()
.then((result) => console.log(`then handler works. result: ${result}`))
.catch((result) => console.log(`catch handler works. result: ${result}`))
.finally(() => console.log(`finally handler works.`));
- 결과는
catch handler works. result: failed후에finally handler works.가 된다.
doAsyncJob()
.then((result) => alert(`the result: ${result}`))
.catch((result) => alert(`it failed reason is: ${result}`))
.finally(() => hideLoading());
- 위와 같이 비동기 작업을 할 때 마지막에 로딩을 숨기는 등의 공통 작업을
finally로 옮길 수 있다.
핸들러와 메서드 체인
- 보통 핸들러는 위의 예제코드에서도 보았듯 메서드 체인 형식으로 이용한다.
- 이는 모든 메서드가 즉시 Promise 객체를 즉시 반환하기 때문이다.
메서드 체인 살펴보기
- 메서드 체인은 보통
then(),catch(),finally()로 구성되지만,then()이 여러번 와도 상관 없다.
const example = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(100);
}, 100);
});
};
example()
.then((v) => v + 100)
.then((v) => v + 100)
.then((v) => v + 100)
.then((v) => console.log(v)); // 400
then()만 3번 사용해본 예제이다.- 만일 기능별 함수 분리가 잘 되어있다면, 체인으로 이어지는 가독성 좋은 코드 작성이 가능하다.
const example = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(100);
}, 100);
});
};
const plus100 = (v) => v + 100;
const multiply100 = (v) => v * 100;
const log = (v) => console.log(v);
example().then(plus100).then(multiply100).then(log); // 20000
- 보기엔 별 것 아닌것처럼 보여도 잘 추상화된 콜백 함수를 녹여내면 가독성 좋은 코드가 된다.
핸들러 사용 시 주의점
- 이름과 사용 방법이 꽤 직관적인 핸들러이지만, 가끔 혼동을 일으키기도 한다.
throw 와 catch
try {
throw new Error("에러!!!");
} catch (error) {
console.log("에러가 발생했습니다. 에러: " + error);
}
- 기존의
try/catch구문을 이용하여 에러 처리를 작성해보았다. - 결과는
에러가 발생했습니다. 에러: Error: 에러!!!라는 로그를 콘솔에 남긴다.
const example = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
throw new Error("ERROR!!!!!");
}, 100);
});
};
example()
.then((result) => console.log(`then handler works. result: ${result}`))
.catch((result) => console.log(`catch handler works. result: ${result}`));
- 위의 코드 결과는 어떻게 될까?
- 정답은
Uncaught Error: ERROR!!!!!라는 에러 로그만 띄운다.
- 정답은
- 헷갈리면 안되는 게
try/catch처럼 에러를 잡는 것이 아니라reject()메서드로 넘어온 값을catch로 처리할 뿐이다.
의도치 않게 에러 가리기
const example = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const person = {
name: "jake",
};
resolve(person);
}, 100);
});
};
example()
.catch((result) => console.log(`catch handler works. result: ${result}`))
.then((result) => console.log(`the name is ${result.name}`));
/*
the name is jake
*/
- 위와 같이 코드를 작성하였다.
- 정상적인 경우에는 별 탈 없이 잘 동작하는 코드이다.
const example = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const person = {
name: "jake",
};
reject("ERROR");
}, 100);
});
};
example()
.catch((result) => console.log(`catch handler works. result: ${result}`))
.then((result) => console.log(`the name is ${result.name}`));
/*
catch handler works. result: ERROR
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'name')
at <anonymous>:15:55
*/
- 하지만 이렇게
reject()메서드가 동작하는 경우에는then()메서드의 역할이 달라진다. catch()메서드 가 반환하는 값을 받아 그것을 처리하는then()메서드가 되어버린다.- 위의 경우엔 코드가 간단하고 일부러 이 상황을 의도했기에 알아보기 쉽지만, 실무에서 이러한 상황이라면 생각보다 알아보기 어려울 수 있다.
try/catch/finally 와 비교하기
try {
// ...
} catch (error) {
// ...
} finally {
// ...
}
then/catch/finally는try/catch/finally구문과 흡사하다.- 사용하다보면 엄청난 차이가 있다는 것을 깨닫는다.
try/catch/finally가 중첩될 수 밖에 없는 구문을 생각해보자.
try {
try {
try {
// ...
} catch (error) {
console.error(error);
} finally {
// ...
}
} catch (error) {
console.error(error);
} finally {
// ...
}
} catch (error) {
console.error(error);
} finally {
// ...
}
- 위 코드를
Promise로 변경하면 어떻게 될까?
someAsyncJob()
.then(...)
.then(...)
.then(...)
.catch(...)
.finally(...);
- 훨씬 알아보기 쉬운 코드를 작성할 수 있다..
finally() 에서 값을 반환하기
const example = () =>
new Promise((resolve, reject) => {
setTimeout(resolve, 500, 100);
});
example()
.then((result) => console.log(result))
.finally(() => {
return 50000;
})
.then((result) => console.log(result)); // undefined
finally()콜백에서 반환된50000은 아무런 의미를 갖지 못한다.finally()에서 값을 반환하는 것은 early return 이외의 의미는 없다.finally()의 주 사용 사례는 성공/실패 여부에 상관 없이 리소스를 정리하는 것이다.
then() 속 두 개의 콜백 파라미터
- 첫번째 자리엔
onResolved에 대한 콜백 함수 작성이 가능하다. - 두번째 자리엔
onRejectd에 대한 콜백 함수 작성이 가능하다. - 여기서 오해하기 쉬운 부분이 "그럼
catch()메서드를 작성하지 않고onRejected자리에 콜백을 넣으면 같은 동작을 하겠지?" 생각하는 것이다.- 그러나 실제로 둘의 동작은 다르다.
const example = () =>
new Promise((resolve, reject) => {
setTimeout(resolve, 500, 100);
});
example().then(
(onResolvedResult) => {
throw new Error("ERROR!!!");
console.log(`onResolvedResult: ${onResolvedResult}`);
},
(onRejectedResult) => {
console.log(`onRejectedResult: ${onRejectedResult}`);
}
);
/*
Uncaught (in promise) Error: ERROR!!!
*/
then()의 두번째 인자로reject시에 동작할 핸들러를 작성하면,then()의 첫번째 인자 핸들러에서 에러가 던져진 경우를 처리하는 것이 불가능하다.- 오직 최초의
Promise객체 내부 블록에서reject()가 호출된 경우에만 처리가 가능하다.
const example = () =>
new Promise((resolve, reject) => {
setTimeout(resolve, 500, 100);
});
example()
.then((onResolvedResult) => {
console.log(`onResolvedResult: ${onResolvedResult}`);
})
.then(() => {
throw new Error("ERROR!!!");
})
.catch((onCatchResult) => {
console.log(`onCatchResult: ${onCatchResult}`);
});
/*
onResolvedResult: 100
onCatchResult: Error: ERROR!!!
*/
then()의 두번째 인자 대신에catch()를 명시적으로 작성하는 경우는 위의 어떤 체인에서 에러가 던져지더라도 에러를 받아낼 수 있다.- 상황에 따라 다르게 사용된다.
Promise 의 좋은 예제 Fetch API
- 자바스크립트에서 HTTP 요청을 보낼 때 원래는 XMLHttpRequest API 를 사용했다.
- ES6 부터는 Fetch API 라는 것이 나와 더 손쉽게 HTTP 비동기 요청을 처리할 수 있게 되었다.
fetch()메서드는Promise객체를 반환한다.
fetch("https://api.publicapis.org/entries").then((res) => {
console.log(res);
});
/*
Response {type: 'basic', url: 'https://api.publicapis.org/entries', redirected: false, status: 200, ok: true, …}
body: (...)
bodyUsed: false
headers: Headers {}
ok: true
redirected: false
status: 200
statusText: ""
type: "basic"
url: "https://api.publicapis.org/entries"
[[Prototype]]: Response
*/
- 위의 예제는
https://api.publicapis.org/entries라는 무료 공개 api 를 제공해주는 웹사이트를 이용했다. fetch()의 반환 값은Promise객체이다.resolve()에 인자로 들어가는 값은 Response 객체이다.Response객체는 실질적으로 반환된 데이터보다는 반환된 HTTP 응답에 대한 모든 정보를 포괄한다.- 그래서 보통 여기서
status정보 등을 한번 더 체크하고 데이터를 이용한다.
- 그래서 보통 여기서
fetch("https://api.publicapis.org/entries")
.then((res) => {
if (res.ok) {
return res.json();
}
})
.then((json) => {
console.log(json); // {count: 1425, entries: Array(1425)}
});
Response에서 올바른 HTTP 응답을 주었는지 확인 후에then()을 한번 더 수행한다.
fetch() 에서 then() 과 catch() 를 이용해 에러의 종류 구분해보기
- 테스트용으로 2가지 API 를 만들었다.
/correct-json에서는 파싱 가능한 JSON ("{\"data\": 100}") 을 응답한다./wrong-json에서는 파싱 불가능한 JSON ("{\"data\": 100";) 을 응답한다.
Promise에서는 에러 핸들링이 중요하다. 비동기 작업이 어떤 단계에서 실패했는지 잘 나오는지 확인해보자.- 에러 핸들링이 되지 않는 경우 계속 똑같은 자리에서 맴돌 위험이 있다.
class FetchError extends Error {
constructor(response, message = "HTTP error " + response.status) {
super(message);
this.response = response;
}
}
const myFetch = (...args) => {
return fetch(...args).then((response) => {
if (!response.ok) {
throw new FetchError(response);
}
return response;
});
};
myFetch("http://localhost:8080/correct-json")
.then(
(response) => response.json(),
(rejected) => console.log(`rejected in myFetch method: ${rejected}`)
)
.then((json) => {
console.log(json);
})
.catch((error) => console.log(`rejected after fetch method: ${error}`));
/*
{data: 100}
*/
- 정상적으로 응답했다.
myFetch("http://localhost:8080/wrong-json")
.then(
(response) => response.json(),
(rejected) => console.log(`rejected in myFetch method: ${rejected}`)
)
.then((json) => {
console.log(json);
})
.catch((error) => console.log(`rejected after fetch method: ${error}`));
/*
rejected after fetch method: SyntaxError: Expected ',' or '}' after property value in JSON at position 12
*/
fetch()메서드 다음에서reject되었다고 알려주고 있다.
myFetch("http://localhost:8080/wrong-jsona")
.then(
(response) => response.json(),
(rejected) => console.log(`rejected in myFetch method: ${rejected}`)
)
.then((json) => {
console.log(json);
})
.catch((error) => console.log(`rejected after fetch method: ${error}`));
/*
GET http://localhost:8080/wrong-jsona 404
rejected in myFetch method: Error: HTTP error 404
*/
myFetch()메서드에서reject되었다고 알려주고 있다.fetch()중 에러가 난 것들 (HTTP Status 가 OK 가 아닌 것들) 과JSON Parse중 에러가 난 것들을 잘 구분해주고 있다.
Promise 의 정적 팩터리 메서드
- 정적 팩터리처럼 사용할 수 있는 static method 가 존재한다.
Promise.resolve()
Promise.resolve()는x instanceof Promise ? x : new Promise(resolve => resolve(x))와 같다.- x 가
Promise의 인스턴스라면x를 반환하고, 아니라면 새Promise오브젝트를 생성하고resolve(x)를 한 결과를 가져온다.
- x 가
Promise.resolve(100).then((value) => {
console.log(value); // 100
});
Promise.reject()
Promise.reject()는x instanceof Promise ? x : new Promise(reject => reject(x))와 같다.- x 가
Promise의 인스턴스라면x를 반환하고, 아니라면 새Promise오브젝트를 생성하고reject(x)를 한 결과를 가져온다.
- x 가
- 몇몇 프로그래머들은
throw new Error()보다Promise.reject(new Error())를 선호한다.then((value) => value == null ? Promise.reject(new Error()) : value);- 위 스타일을 사용하면 딱히 코드 블록을 만들 필요 없이 에러를 식으로 처리 가능하다.
Promise.reject(100)
.then((v) => console.log(v + 100)) // then 에는 걸리지 않는다.
.catch((v) => console.log(v * 100)); // 10000'자바스크립트 > 모던 자바스크립트' 카테고리의 다른 글
| 모던 자바스크립트, 이터러블 (iterable) 과 이터레이터 (iterator) (0) | 2023.02.28 |
|---|---|
| 모던 자바스크립트, 프라미스 2 - 유틸 메서드와 작업 패턴 그리고 안티 패턴 (0) | 2023.02.15 |
| 모던 자바스크립트, 디스트럭처링 (Desctructuring) (0) | 2023.02.08 |
| 자바스크립트 클래스 3 - 상속 (0) | 2023.02.01 |
| 자바스크립트 클래스 2 - 클래스 바디 (0) | 2023.02.01 |