[JS] 프라미스, 프라미스 체이닝

Yuno·2021년 5월 21일
0

모던JS

목록 보기
8/16
post-thumbnail

수정 사항
8/19 - 콜백의 문제점 추가, 가독성 수정
8/19 - Promise.all 추가

콜백의 문제점

콜백으로 비동기를 처리 할 때 꼬리에 꼬리를 무는 처리가 많아지면, 중첩이 깊어집니다.
이를, Promise로 해결할 수 있습니다.

Promise란

비동기 동작 처리를 위해 ES6에서 도입된 클래스입니다.
new로 인스턴스화 한 Promise 객체로 비동기 동작을 처리합니다.

Promise 이해를 위해 다음 단어를 알아야 합니다.

  • 제작 코드(Producing Code)는 시간이 걸리는 일을 하는 코드를 말합니다.

  • 소비 코드(Consuming Code)는 제작 코드의 결과를 기다렸다가 이를 사용(소비)하는 코드를 말합니다. 여러 함수에서 소비할 수 있습니다.

  • 프라미스(Promise)는 '제작 코드와 소비 코드를 연결하는 특별한 자바스크립트 객체'입니다.

시간이 얼마나 걸리든, 제작 코드가 준비 될 때, 모든 소비 코드가 결과를 사용할 수 있게 합니다.

Promise 객체

Promise 객체는 아래와 같은 방법으로 만듭니다.

let promise = new Promise(function(resolve, reject){
	//executor
})

Promise 생성자에 전달되는 함수를 executor(실행자) 라고 부릅니다.
Promise 객체가 생성될 때, 자동으로 실행합니다.

excutor콜백 중 하나를 반드시 호출해야 합니다.

  • resolve : 일이 성공적으로 끝난 경우 결과를 인자로 호출
  • reject : 에러 발생시 에러 객체를 인자로 호출

Promise의 프로퍼티

Promise 객체는 stateresult라는 내부 프로퍼티를 가집니다.
특히, state에 대한 이해가 필요합니다.

  • state
    보류(Pending) : 제작 코드가 완료되지 않음
    이행(Fulfilled) : 제작 코드가 완료되어, 프로미스가 값을 반환 함
    실패(Rejected) : 제작 코드가 실패하거나 오류가 발생

  • result
    제작 코드 결과에따라, 결과 값이나 오류 객체를 가집니다.

프라미스는 성공 또는 실패만 합니다.
또한, 변경된 상태는 더이상 변하지 않습니다.

stateresult는 내부 프로퍼티로 개발자가 직접 접근할 수 없습니다.

핸들러 메서드

then

프라미스에서 가장 중요하고 기본이 되는 메서드입니다.

promise.then(
    function(result){/*결과를 다룸*/},
    function(error){/*에러를 다룸*/}
)

then첫 번째 인자는 프라미스가 이행 되었을 때 실행되는 함수입니다.
두번째 인자는 프라미스가 거부되었을 때 실행되는 함수입니다.

catch

에러가 발생한 경우만 다루고 싶다면, then의 첫 번째 인수를 null로 전달해도 되고,
.catch를 써도 됩니다.

catchthen의 첫 인수를 null로 전달하는 것과 동일하게 작동합니다.

promise.catch(
    function(error){/*에러를 다룸*/}
)

finally

try...catch절에 finally가 있는 것 처럼 프라미스에도 있습니다.
프라미스가 이행이나 거부로 처리되면, 항상 실행됩니다.

즉, then(f,f)와도 유사합니다.

결과가 어떻든 마무리가 필요할 때 유용합니다.

다만, then(f,f)와 완전히 같진 않습니다.
1. finally 핸들러엔 인수가 없습니다.
보편적 작업을 수행하므로 이행인지 거부인지 여부를 알 수 없습니다.
2. finally 핸들러는 자동으로 다음 핸들러에 결과와 에러를 전달합니다.

 promise
   .finally(() => alert("프라미스가 준비되었습니다.")) // 다음 핸들러에 전달
   .catch(err => alert(err)); // <-- .catch에서 에러 객체를 다룰 수 있음

때문에, finally는 결과를 처리하기 보다, 통과되어 전달하는 역할을 합니다.

프라미스가 이미 처리 된 상태라면, 이 후 등록 된 .then/catch/finally핸들러는 등록하면 즉각 실행됩니다.

예시

function loadScript(src){
    //작업이 끝날 때 콜백을 호출하는 대신, Promise 객체를 반환
    return new Promise(function(resolve,reject){
      	// executor : 시간이 걸리는 작업
        let script = document.createElement('script');
        script = src
        
        //resolve, reject 중 반드시 호출
        script.onload = ()=> resolve(script);
        script.onerror = ()=> reject(new Error(`${src}를 불러오는 중 에러가 발생함`));
      
        document.head.append(script)
    })
}

let promise = loadScript('./src/script.js');

//프라미스가 처리되면, 등록된 핸들러를 결과와 함께 수행.
promise.then(
    script=> console.log(`${script.src}를 불러왔습니다.`),
    err=> console.log(`Error: ${err.massage}`)
)

promise.then(
    // 또 다른 핸들러..
)

콜백 기반 비동기 프로그래밍 에서는, 인자로 callback을 받고,
오류 우선 콜백이라면, 첫 인수에 에러 객체, 두 번째 인수에 결과를 넣어 호출했습니다.

이번에는, executor 에서 resolvereject를 호출하는 프라미스를 반환하고,
then을 이용해 정상 상태와 에러 상태의 핸들러를 더합니다.

Promise.all

여러 프라미스를 다 하고 난 뒤,처리를 하고 싶을 때

Promise.all(promiseArray).then(handler);

다음 같은 상황에 사용합니다.

fetchUser = id => {
 return fetch(`${url}/${id}`).then(response => response.json());
}

const requests = selectIds.map(id => fetchUser(id));
      
Promise
  .all(requests)
  .then(users => {
    //...
  })

프라미스 체이닝

순차적으로 처리해야하는 작업이 중첩되어 여러개 있다고 했을 때,
콜백 기반 비동기 프로그래밍은 중첩이 많아져 피라미드를 만들어 냈습니다.

프라미스를 사용하면 여러 해결책을 만들 수 있습니다.

resolve가 호출되면 then에서 결과를 처리할 수 있습니다.
결과가 then 체인을 통해 전달됩니다.

프라미스가 처리되고 then 핸들러가 호출됩니다.
호출된 then 핸들러에서 반환한 값은 다음 then 핸들러에 result로 전달됩니다.

즉, 순차적으로 처리해야하는 작업이 있을 때,
앞선 작업을 수행하고, 프라미스 체이닝으로 그 결과를 처리할 수 있습니다.

new Promise( function(resolve,reject) {
    setTimeout(()=> resolve(1),1000)
})
.then( result=>{
    alert(result) // 1
    return result*2
})
.then( result=>{
    alert(result) // 2
    return result*2
})
.then( result=>{
    alert(result) // 4
    return result*2
})

이렇게 체이닝이 가능한 이유는 then을 호출하면 프라미스가 반환되기 때문입니다.
반환된 프라미스는 당연히 then 메서드를 가집니다.

프라미스 반환하기

then에 사용된 핸들러가 프라미스를 생성하거나, 반환하는 경우도 있습니다.
이 때는, 다음 핸들러가 프라미스의 처리를 기다리고 실행됩니다.

new Promise( function(reslove) {
    setTimeout(()=> resolve(1),1000);
}
.then( result=>{
  return new Promise(resolve=>{
      setTimeout(()=> resolve(result*2), 1000);
  })
})
.then(result => {
    //...
})
     
            
            
loadScript(src) // 프라미스 반환하는 함수
.then(script => loadScript(src2))
.then(script => loadScript(src3))

스크립트 로드가 끝나면 다음 스크립트를 로드해야하는 등, 순차적으로 실행해야 할 때
콜백을 사용할 때처럼 중첩이 깊어지지 않고, 코드를 작성할 수 있습니다.

fetch와 함께 사용하기

프론트 단에서, 네트워크 요청시 프라미스를 자주 사용합니다.

let promise = fetch(url);

url에 네트워크 요청을 보내고, promise가 반환됩니다.

이제, 서버가 응답을 보내면 프라미스는 response를 결과로 이행됩니다.
그런데, response가 다운되지 않아도 응답이 오면 이행되기 때문에,
바로 respose의 데이터를 쓸 수 없습니다.

response.text()를 호출하면,
응답의 텍스트가 다운될 때, 텍스트를 결과로 하여 이행되는 프로미스가 반환됩니다.

메서드 response.json()도 같은 역할을 하며, 데이터를 JSON으로 파싱합니다.

이처럼 사용합니다.

fetch(url)
.then(response => response.json())
.then(data => {
  //...
})

예시

fetch(url)
.then(response => response.json())
.then(user => fetch(`xxxx.com/users/${user.id}`) )
.then(response => response.json())
.then(xxxUser => {
    let img = document.createElement('img')
    img.src = xxxUser.img_url;
    img.className = "avartar";
    doucument.body.append(img);
    
    setTimeout(()=>img.remove(), 3000)
})

fetch API를 통해 네트워크 요청을 하고, 프라미스 체인을 통해 응답이 다운되면,
결과를 가지고 이미지를 만들고 3초 후에, 지워지게 됩니다.

잘 동작하지만, 이 이후에 무언가 하고 싶다면 어떻게 해야 할까요?

체인을 확장할 수 있도록 이행 프로미스를 반환해야합니다.

fetch(url)
.then(response => response.json())
.then(user => fetch(`server/users/${user.id}`) )
.then(response => response.json())
.then(user => new Promise((resolve,reject)=> {
    let img = document.createElement('img')
    img.src = user.img_url;
    img.className = "avartar";
    doucument.body.append(img);
    
    setTimeout(() => {
        img.remove();
        resolve(user);
    }, 3000);
  
}))
//3초 후에 remove 되고 동작함
.then(user=> ....)

지금은 체인을 확장하지 않더라도, 비동기 동작은 항상 프라미스를 반환하는 것이 좋습니다.

profile
web frontend developer

0개의 댓글