프라미스 (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 |