오늘은 Front-End의 중요한 영역인 Data Fetching 에서 사용되는 Promise 문법에 대해서 알아보겠습니다.
Promise
프로미스는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. - MDN
promise는 ES6에서 나온 새로운 기능으로, 어떤 "문제점"을 해결하기 위해서, 나온 기능입니다. 즉, 그 문제점이 뭐냐를 이해하면 Promise의 정체를 밝힐 수 있을 것 같습니다.
Promise의 등장배경
작은 규모의 웹에서는 비동기 요청이 많지 않아서, 각 비동기 요청간의 의존성이 크지 않았다고 합니다. 웹이 발전하고, 다양한 플랫폼으로 진출하면서 , 단순히 callback만으로는 모든 상태를 통제하기 어려웠다고 합니다. 예를들어, 로그인 과정에서 서버와 통신과정이 필요합니다. 세션 로그인방식이라고 생각해보겠습니다.
로그인 과정
[Client]
- 로그인 페이지에서, 아이디, 비밀번호를 서버로 POST 합니다.
[Server]
- 서버는 기존에 회원가입을 한 사람이면, 서버에서 세션아이디를 만들어줍니다.
- 세션 아이디를 담을 변수와 DB공간을 마련해서, 거기에 저장합니다.
- 쿠키 안에 Portion해서, Request한 브라우저에 쿠키를 Push해줍니다.
[Client]
- mypage(로그인 해야만 들어갈 수 있는, 회원페이지)를 요청합니다.
[Server]
- 서버는 해당 요청을 받으면 일단 로그인 했는지 여부를 확인해야 함
- 쿠키에 세션 아이디가 포함되어 있는지 검사
- 만약, 있으면 통과시켜주고 그 과정에서 회원의 이름, 나이, 성별 등의 DB정보가 필요하면 꺼내옴
이때, Client의 동작은 연속적으로 일어나야하는걸 알 수 있습니다. 즉, client ->아이디, 비밀번호를 Post 한 뒤 쿠기가 Push되면 mypage를 요청합니다. 이때, 브라우저가 멈추면 안되기 때문에, 서버와의 통신은 비동기적으로 일어나게 됩니다. 즉, 비동기가 연속적으로 일어나야 합니다.
연속적인 비동기의 문제점(Callback Chain)
비동기가 연속적으로 일어난다는 이야기는 , callback을 연속적으로 던져줘야했다는 이야기입니다. 그리고 callback함수로 구현되어지는 비동기 함수는 JS의 실행흐름에 의해서, 따로 분리가 될 수 없습니다. 왜냐하면, 결과값을 받는 순간을 예측할 수 없기 때문입니다. 즉 , 아래와 같이 코드를 짜야한다는 이야기입니다.
UserLogin(()=>{
//로그인을 한 뒤에, 연속적으로 mypage를 요청해야한다.
UserLogin(()=>{
})
},1000)
위와 같이 중첩에 중첩에 중첩을 계속하게되면, 유명한 "콜백지옥"이 완성됩니다.
가독성도 물론 엄청 떨어지지만, 근본적인 원인은 비동기 안에 연속적으로 일어나는 ,또 다른 비동기 로직들을 분리할 수 없던게 문제였습니다. 그래서, 비동기를 요청한 뒤 , 결괏값을 기억하고 있는 방식으로 구현하기로 했습니다.
즉 비동기 함수 안에 연속적으로 일어나는 로직들을 분리할 수 있는 방식이 바로 Promise입니다. 그래서, promise는 비동기 요청을 해놓고, 나중에 then()을 통해서, 결과값을 받을 수 있게 됩니다. 이 말을 다르게 해볼까요 . "미래의 어떤 시점에 결과를 제공하는 약속을 하겠다" ==> 그래서, Promise 입니다.
유명한 callbackHell을 Promise로 바꾸어보기
- 로그인하는 예제입니다. 서버가 없기 때문에,setTimeout을 이용
- 아래와 같이, UserStorage 객체가 준비되어있습니다.
class UserStorage{
loginUser(id,password,onSuccess,onError){
setTimeout(()=>{
if(id==="admin" && password ==="root"){
onSuccess(id)
}else{
onError(new Error("not Found!"))
}
},2000);
}
getRoles(user,onSuccess,onError){
setTimeout(()=>{
if (user ==="admin"){
onSuccess({name:"admin",role:"admin"});
}else{
onError(new Error('no Access'));
}
},1000)
}
}
로그인 과정 분석
- login했을 때, 성공시에 해당 유저의 권한을 부여해야합니다. 따라서, onSuccess라는 callback을 던져주는 과정이 필요합니다.
- 가독성을 높이기 위해서 , 주석을 달아놨습니다. 하지만, 콜백안에 또 콜백이 있고, 또 콜백이 있고 .. 가독성이 떨어지며 , 프로그래머도 헷갈리기 시작합니다.
- 프로그래머가 "헷갈리기 시작"한다 => 기본적으로 에러날 확률이 높다. 뿐만 아니라, "유지보수"하기가 매우 힘들고, 확장성을 갖기가 힘들다. => 현재와 같은 고도화된 서비스를 넣기에 충분하지 않다. => 돈이 되지 않는다. => "확장성"을 확보하지 않은 코드는 필요가 없다.=>
이런 코드를 짜는 사람은 필요가 없다(극단적이지만) - 즉, 안티 패턴인거죠!
const userStorage = new UserStorage()
const id = prompt("아이디 입력")
const password = prompt("비밀번호 입력")
userStorage.loginUser(
id,
password,
//성공 callback
(user)=>{
userStorage.getRoles(
user,
//로그인 성공 한뒤, 유저 권한 획득 callback
(Userdata)=>{
alert(`${Userdata.name}님 안녕하세요. ${Userdata.role}권한으로 입장했습니다.`)
},
//로그인 한뒤, 유저 권한 획득 실패 callback
(error)=>{
console.log("로그인 한뒤, 유저 권한 획득 실패 callback ")
},
)
},
//error callback
(error)=>{
console.log("로그인 실패")
},
)
위와 같은 문제점을 해결하기 위해서 Promise가 등장했습니다. Promise는 비동기처리를 미리 약속해놓고, 결과값은 "나중에 then을 통해서 너가 원할 때 써라" 입니다.
Promise의 WorkFlow
- Promise는 객체입니다. 따라서, new 연산자를 통해서 호출이 됩니다. 내부에 executor이라는 callback함수를 갖고있습니다. executor또한, resolve,reject라는 callback을 갖고있습니다. "콜백 지옥을 해결한다면서 또 콜백을 중첩시키면어쩌자는거야!"라고 할 수 있지만, 진정하고 들어주세요
const p1 = new Promise((resolve,reject)=>{})
- Promise의 Executor은 선언되자마자 실행됩니다. 왜냐하면, Promise또한 객체니까요! new Promise()는 생성자입니다.
위의 로그인 코드를 Promise로 바꾸는 연습을 해보겠습니다.
loginUser
loginUser(id, password) {
//callback이 아닌, promise를 리턴해준다.
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === "admin" && password === "root") {
resolve(id);
} else {
reject(new Error("error!"));
}
}, 2000);
});
}
getRoles
getRoles(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user === "admin") {
resolve({ name: "admin", role: "admin" });
} else {
reject(new Error("no Access"));
}
}, 1000);
});
}
실행
const userStorage = new UserStorage();
const id = prompt("Eneter your id ");
const password = prompt("Enter your pw ! ");
//then으로 chainning 가능!!
userStorage
.loginUser(id, password)
.then(userStorage.getRoles)
.then((data) =>
alert(`${data.name}님 환영합니다.${data.role}권한으로 접속했습니다.`)
)
.catch(console.log);
하지만, Promise 또한, 작성을 하다보면, then chainning도 엄청 길게 생길 수 있고, promise가 promise를 반환하는 또 ,Promise 지옥이 생길 수 있습니다. Then에서도 Promise를 넘겨주면, 다음 then에서 resolve된 값을 받을 수 있거든요!
Async Function의 등장
- Promise를 반환해주는 Producer 역할을 하는 함수를 흔하게 작성합니다. 이와 같은 함수들은 async function으로 작성이 가능합니다. 즉, async 함수 => Promise를 Return 해주는 함수입니다.
- await은 async 함수 내에서 사용할 수 있는 키워드로, Promise의 then역할을 하게 됩니다. 코드의 실행 순서가 오른쪽에서 왼쪽으로 바뀐다고 이야기도 하는데, "delay(3000)이 실행될 때 까지 기다려라!"라는 이야기 입니다.
// 실행을 ms 만큼 지연시키는 delay
function delay(ms){
return new Promise((resolve,reject)=>{
setTimeout(resolve,ms)
})
}
async getApple(){
await delay(3000)
return "Apple"
}
/* Promise
function getApple(){
return delay(3000).then(()=>"Apple");
}
*/
async getBanana(){
await delay(3000)
return "banana"
}
/* Promise
function getbanana(){
return delay(3000).then(()=>"Banana");
}
*/
pickFruits().then(console.log);
Async Function의 편리함
위와같은 Promise(Async 함수)들이 있을 때, 모든 Promise를 논리적으로 한 동작으로 묶어야할 필요가 있을 수 있습니다.
예를들어, "사과를 딴 뒤에, 바나나를 따고, 그 둘을 출력한다" 와 같이, 이전 Promise의 scope가 필요할 때, 유용하게 쓸 수 있습니다.
async function AsyncpickFruits(){
const apple = await getApple()
const banana = await getBanana()
return `Fruits: ${apple} , ${banana}`
}
/* Promise
function pickFruits (){
return getApple()
.then((apple)=>{
return getBanana().then(banana=>`Fruits: ${apple} , ${banana}`)
})
}
*/
병렬적으로 처리하기
흠..근데, 굳이 사과를 따고나서, 바나나를 따야할까요? 아니죠! 사과와 바나나는 동시에 딸 수 있습니다. promise.all 혹은 ,promise.allSettled와 같은 메소드를 이용해서, promise들을 한꺼번에 처리할 수 있습니다!
function pickAllFruits(){
return new Promise.allsettled([getApple(),getBanana()])
.then((fruits)=>fruits.join(','))
}
pickAllFruits().then(console.log)
3줄 정리
- Promise는 비동기 상태를 "값"으로 다룰 수 있게 한다는 점이 제일 큰 장점이다.
- "값"으로 다룬 다는 이야기는 변수에 담고, 매개변수에도 넣을 수 있는 1급 객체로 다룬다는 이야기고, 이건 내가 원할 때, 비동기 상태의 값들을 쓸 수 있다이야기 입니다.
- 여러개의 Promise를 묶어서, 병렬적으로 처리할 수도 있고, Async/await를 써서, Promise의 then chain지옥을 빠져나갈 수도 있다.
다음 글은, Promise의 장점을 활용하여 비동기 상태를 여러 함수를 합성하는 걸 보겠습니다. 그리고, 그걸 가능하게 하는 Monad의 개념에 대해서 간단하게 알아보겠습니다!
refer
'Web Front-end > JavaScript' 카테고리의 다른 글
[JS/객체/프로토타입] JS의 프로토타입 (0) | 2022.10.23 |
---|---|
[JS/호이스팅/실행 컨텍스트/이벤트 루프] JS 코드의 흐름 (0) | 2022.10.19 |
[JS/this/호출스택] JS의 This 정리 (호출스택,This) (0) | 2022.10.17 |
[JS/Array/Basic] Array를 잘 다루어보자- 2 (0) | 2022.10.13 |
[JS/배열/Method/Array] Array를 잘 다루어보자! (1) | 2022.10.13 |