[프라미스와 async await]

1. 프라미스

팬 -> 가수 “언제 신곡 나와요?” 매번 이렇게 하는 것에 답하면 너무 비효율이니까 팬(정보-Email등…)을 받아서 가수 -> 팬 “신곡 나왔어!” (구독리스트) 이렇게 해주는 게 좋다는 것이다.

‘제작 코드(producing code)’ : 원격에서 스크립트를 불러오는 것 (시간이 걸리는 일을 진행). 위 비유에선 ‘가수’ ‘소비 코드(consuming code)’ : ‘제작 코드’의 결과를 기다렸다가 이를 소비. 이때 소비 주체(함수)는 여럿이 될 수 있음. 위 비유에서 ‘팬’

프라미스(promise) : ‘제작 코드’와 ‘소비 코드’를 연결해 주는 특별한 자바스크립트 객체임. 위 비유에서 ‘구독 리스트’.

‘프라미스’는 시간이 얼마나 걸리든 상관없이 약속한 결과를 만들어 내는 ‘제작 코드’가 준비되었을 때, 모든 소비 코드가 결과를 사용할 수 있도록 해줌.

  • 프라미스를 만드는 방법
      let promise = new Promise(function(resolve, reject) {
    // executor (제작 코드, '가수')
      });
    
  • executor new Promise에 전달되는 함수를 executor(실행자, 실행 함수) 라고 한다. executor는 new Promise가 만들어질 때 자동으로 실행됨 결과를 최종적으로 만들어내는 제작 코드를 포함. 위 비유에서 ‘가수’

  • executor의 인수 resolve와 reject 자바스크립트가 자체적으로 제공하는 콜백임. 개발자는 resolve와 reject를 신경 쓰지 않고 executor 안 코드만 작성하면 됨. ※ 대신 executor에선 결과 얻으면 상황에 따라 인수로 넘겨준 콜백 중 하나를 반드시 호출해야 함!
      //resolve(value) — 일이 성공적으로 끝난 경우, 그 결과를 나타내는 value와 함께 호출
      //reject(error) — 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
    
  • promise 객체의 내부 프로퍼티 //state — 변화는 다음과 같음 1) 처음엔 “pending”(보류) 2) resolve가 호출되면 “fulfilled” 3) reject가 호출되면 “rejected” //result — 변화는 다음과 같음 1) 처음엔 undefined 2) resolve(value)가 호출되면 value로 3) reject(error)가 호출되면 error로 변함.

  • promise 예시
      let promise = new Promise(function(resolve, reject) {
    // 프라미스가 만들어지면 executor 함수는 자동으로 실행됩니다.
      
    // 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result가 'done'이 됩니다.
    setTimeout(() => resolve("done"), 1000);
      });
    
  • executor는 new Promise에 의해 자동 & 즉각 호출.

  • executor의 인자 resolve와 reject 함수는 자바스크립트 엔진에서 정의 되어 있으며, 하나는 반드시 호출해야 함

  • 위의 예시의 경우 promise 객체의 상태 변화
      new Promise(executor)
      state : "pending"           => resolve("done") =>       state : "fulfilled"
      result : "undefined"                                    result : "done"
    
  • 에러가 난다면 state : “rejected” , result : “error” 로 바뀌도록 코딩하는 것도 가능하다.

  • resolve와 reject를 같이 쓸 경우, 위에 있는 것이 실행되면 나중에 있는 것은 무시됨.
  • reject를 호출할 시에는 Error 객체를 사용할 것

  • 소비자 then, catch, finally
  • then 기본 문법
      promise.then(
    function(result) { /* 결과(result)를 다룹니다 */ },
    function(error) { /* 에러(error)를 다룹니다 */ }
      );  
    

    첫 번째 인수는 프라미스가 이행되었을 때 실행, 실행 결과를 받음 두 번째 인수는 프라미스가 거부되었을 때 실행, 에러를 받음.

  • 함수가 실행된 경우
      let promise = new Promise(function(resolve, reject) {
    setTimeout(() => resolve("done!"), 1000);
      });
      // resolve 함수는 .then의 첫 번째 함수(인수)를 실행합니다.
      promise.then(
    result => alert(result), // 1초 후 "done!"을 출력
    error => alert(error) // 실행되지 않음
      );
    

    위와 같이 코딩한 경우 result 함수가 실행된다.

  • 함수가 실행되지 않은 경우
      let promise = new Promise(function(resolve, reject) {
    setTimeout(() => reject(new Error("에러 발생!")), 1000);
      });
      // reject 함수는 .then의 두 번째 함수를 실행합니다.
      promise.then(
    result => alert(result), // 실행되지 않음
    error => alert(error) // 1초 후 "Error: 에러 발생!"를 출력
      );
    
  • 작업이 성공적으로 진행된 경우만 다루고 싶을 땐 아래와 같이 첫번째 인수만 넘겨주면 된다.
      promise.then(alert); // 1초 뒤 "done!" 출력
    
  • 만약 에러가 난 경우만 다루고 싶다면 promise.then(null, errorHandlingFunction) 으로 넘겨주면 된다. 이때 동일한 기능을 하는 함수가 .catch이다. promise.catch(errorHandlingFunction) 이 경우는 앞에 null을 쓸필요가 없음

  • finally finally 핸들러는 then, catch와는 성격이 다르다.

    ** 특징 1) finally 핸들러엔 인수가 없음. finally에선 프라미스가 이행되었는지, 거부되었는지 알 수 없음. finally에선 절차를 마무리하는 ‘보편적’ 동작을 수행하기 때문에 성공·실패 여부를 몰라도 됨. “여기서 보편적이란?”

    2) finally 핸들러는 자동으로 다음 핸들러에 결과와 에러를 전달.

    ** 코드 예시

    new Promise((resolve, reject) => {
      setTimeout(() => resolve("결과"), 2000)
    })
      .finally(() => alert("프라미스가 준비되었습니다."))
      .then(result => alert(result)); // <-- .then에서 result를 다룰 수 있음
    

    finally는 결과를 처리하기 위해서 사용되는 핸들러가 아님. finally를 통과해서 전달되는 것임

  • loadScript 를 프라미스 기법을 사용하여 전달하기 예시)
    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('또다른 핸들러...'));
    

    document 객체를 사용하여 script를 만드는 것까지는 이전 챕터 (콜백) 에서 배운 것과 동일하다. 이제 핸들러를 넘겨주는 방법은 성공했을 때 resolve를 넘겨주고 실패했을 때 reject로 넘겨주면 된다.

    함수 바깥에서 loadScript를 썼을 때. then을 사용하여 해당 결과를 출력할 수 있다.

    • 프라미스와 콜백의 차이점
    • 프라미스 흐름이 자연스러움. 미리 콜백처럼 함수가 다 준비되어 있을 필요 없이 그 다음에 읽어서 사용하면 되게끔 분리되어 있음. loadScript(script)로 스크립트를 읽고, 결과에 따라 그다음(.then)에 무엇을 할지에 대한 코드를 작성하면 됨.

    • 콜백 loadScript(script, callback)를 호출할 때, 함께 호출할 callback 함수가 준비되어 있어야 한다. loadScript를 호출하기 이전에 호출 결과로 무엇을 할지 미리 알고 있어야 함.
  1. 프라미스 체이닝 비동기 처리를 위함. 예시
  new Promise(function(resolve, reject) {

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

result가 .then 핸들러의 체인(사슬)을 통해 전달되기 때문에 위와 같이 사용할 수 있는 것이다

위 예시의 실행 순서

1) 1초 후 최초 프라미스가 실행 2) 첫번째 .then 핸들러가 호출됩니다. 3) 2에서 반환한 값이 다음 .then 핸들러에 전달 4) 위 과정 반복

  • 프라미스 체이닝이 가능한 이유는 프라미스의 결과로 프라미스가 반환되기 때문임.

  • 프라미스 반환하기

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

    promise 만든 후 .then은 1을 출력하고 그 안에서 또 promise를 반환함. 1초 후 실행된 프라미스는 그 다음 .then으로 전달 됨.

이렇게 프라미스를 반환하여 비동기 작업을 처리할 수도 있음.

  • fetch와 체이닝 함께 응용하기
      let promise = fetch(url);
    

    : url에 네트워크 요청을 보내고 프라미스를 반환

fetch가 프라미스를 반환하기 때문에 결과로 .then을 불러올 수 있다.

<코드예시> ```javascript fetch('/article/promise-chaining/user.json') // 원격 서버가 응답하면 .then 아래 코드가 실행됩니다. .then(function(response) { // response.text()는 응답 텍스트 전체가 다운로드되면 // 응답 텍스트를 새로운 이행 프라미스를 만들고, 이를 반환합니다. return response.text(); }) .then(function(text) { // 원격에서 받아온 파일의 내용 alert(text); // {"name": "iliakan", "isAdmin": true} }); ``` 1) server =(response)> client 시점에. promise 는 객체와 함께 이행됨. (response 전체가 다운로드 되기 전에 이행됨) 2) 응답을 읽기 위해서 메서드 response.text()를 호출. 3) response.text()는 원격 서버에서 전송한 텍스트 전체가 다운로드되면, 이 텍스트를 result 값으로 갖는 이행된 프라미스를 반환. * response.json 을 사용하여 text 형태가 아닌 json 형태로 출력할 수 있다. 3. 프라미스와 에러핸들링 프라미스는 제대로 동작을 안할 경우 reject가 호출됨에 따라 에러핸들링을 유연하게 할 수 있다. .catch를 사용하여 처리. 마지막 체인 끝에 .catch(error => alert(error.message));를 추가해주는 것 만으로도 에러를 검지하여 메세지를 띄워줄 수 있다. - 암시적 try... catch ```javascript new Promise((resolve, reject) => { throw new Error("에러 발생!"); }).catch(alert); // Error: 에러 발생! ``` executor가 있기 때문에 그 안에서를 try라고 보면 되고 그 다음 .catch 부분으로 catch를 구현하여 암시적 try.. catch 구문을 형성하는 것이다. 혹은 .then 다음 .catch 를 사용함으로써도 구현이 가능하다. - 다시 던지기 .catch 안에서 throw를 사용하면 다시 던져서 다음 .catch 로 이동하게 된다. 만약 에러가 잘 처리되었다면 그 다음 .then 으로 넘어가게 된다. - 처리되지 못한 거부 <예시코드> ```javascript new Promise(function() { noSuchFunction(); // 에러 (존재하지 않는 함수) }) .then(() => { // 성공상태의 프라미스를 처리하는 핸들러. 한 개 혹은 여러 개가 있을 수 있음 }); // 끝에 .catch가 없음! ``` 위와 같이 .catch가 없다면 에러를 처리할 수 있는 부분이 없어 전역 에러가 뜨게 되고 에러페이지가 출력될 것임 이를 방지하기 위해서 window.addEventListener('unhandledrejection', function(event)) 를 사용하여 에러를 잡을 수 있다. ```javascript window.addEventListener('unhandledrejection', function(event) { // 이벤트엔 두 개의 특별 프로퍼티가 있습니다. alert(event.promise); // [object Promise] - 에러를 생성하는 프라미스 alert(event.reason); // Error: 에러 발생! - 처리하지 못한 에러 객체 }); ``` 1. 프라미스 API Promise 클래스의 5가지 정적 메서드 - Promise.all 여러 개의 프라미스를 동시에 실행 모든 프라미스가 준비될 때까지 기다린다면. 예시) 복수의 URL에 동시에 요청을 보내고, 다운로드가 모두 완료된 후에 콘텐츠를 처리할 때 ```javascript let promise = Promise.all([...promises...]); ``` Promise.all은 배열값으로 반환이 된다. ```javascript Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]).then(alert); // 프라미스 전체가 처리되면 1, 2, 3이 반환됩니다. 각 프라미스는 배열을 구성하는 요소가 됩니다. ``` 위의 경우 프라미스 전체가 실행되면 1,2,3의 배열을 반환해준다. 주목해야 할 것은 첫번째 프라미스는 3초뒤 실행되므로 가장 마지막에 실행되지만 첫번째 배열 값에 반환된다. 배열 요소 순서는 전달된 promise의 순서와 같다. Promise.all에 전달되는 프라미스 중 하나라도 거부되면, Promise.all이 반환하는 프라미스는 에러와 함께 바로 거부 됨. 에러 발생 시 다른 프라미스들은 무시됨. - Promise.allSettled Promise.all 의 경우는 모든 프라미스 중에 하나라도 거절 되면 전체를 거절한다. 그러나 Promise.allSettled 의 경우는 모든 프라미스가 처리될 때까지 기다린다. 1) 응답이 성공할 경우 – {status:"fulfilled", value:result} 2) 에러가 발생한 경우 – {status:"rejected", reason:error} ```javascript let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://no-such-url' ]; Promise.allSettled(urls.map(url => fetch(url))) .then(results => { // (*) results.forEach((result, num) => { if (result.status == "fulfilled") { alert(`${urls[num]}: ${result.value.status}`); } if (result.status == "rejected") { alert(`${urls[num]}: ${result.reason}`); } }); }); ``` 위의 코드에서 results 라는 애 안에는 { status : 'fulfilled', value : ...응답.. status : 'fulfilled', value : ...응답.. status : 'rejected', reason : ...에러... } 으로 구성되어 있다. - Promise.race Promise.all 과 비슷하지만 가장 먼저 처리되는 프라미스 만 출력한다. ```javascript Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1 ``` 위 예시의 결과는 1이다. - Promise.resolve 호환성을 위해 함수가 프라미스를 반환하도록 해야 할 때 사용할 수 있음 캐시를 처리할 때 ```javascript let cache = new Map(); function loadCached(url) { if (cache.has(url)) { // cache에 url에 해당하는 키값이 있으면 return Promise.resolve(cache.get(url)); // (*) 캐시 url을 달라. } return fetch(url) .then(response => response.text()) .then(text => { cache.set(url,text); return text; }); } ``` 5. 프라미스 화 콜백을 받는 함수를 프라미스를 받는 함수로 바꾸는 것을 프라미스화라고한다. 프라미스화를 도와주는 헬퍼함수를 만든다. 'promisfy' <코드 예시=""> ```javascript function promisify(f) { return function (...args) { // 래퍼 함수를 반환함 return new Promise((resolve, reject) => { function callback(err, result) { // f에 사용할 커스텀 콜백 if (err) { reject(err); } else { resolve(result); } } args.push(callback); // 위에서 만든 커스텀 콜백을 함수 f의 인수 끝에 추가합니다. f.call(this, ...args); // 기존 함수를 호출합니다. }); }; }; // 사용법: let loadScriptPromise = promisify(loadScript); loadScriptPromise(...).then(...); ``` ### 6. 마이크로태스크 ```javascript let promise = Promise.resolve(); promise.then(() => alert("프라미스 성공!")); alert("코드 종료"); // 이 얼럿 창이 가장 먼저 나타납니다. ``` "코드 종료"가 먼저 출력되는 이유가 뭘까? 비동기 처리가 먼저 되어야하는데? - 마이크로태스크 큐 비동기 잡을 관리하기 위한 PromiseJobs라는 내부 큐(internal queue). V8 엔진에선 이를 '마이크로태스크 큐(microtask queue)'라고 부름 (우리도 이렇게 부름) 마이크로태스크 큐는 먼저 들어온 작업을 먼저 실행(FIFO, first-in-first-out). * 실행할 것이 아무것도 남아있지 않을 때만 마이크로태스크 큐에 있는 작업이 실행 그러므로 가장먼저 실행되는 것은 프라미스 작업을 안한 함수들 순차적으로 처리하고 그다음에 마이크로태스크 큐에 들어있는 프라미스들 실행 ### 7. async와 await async와 await라는 특별한 문법을 사용하여 프라미스를 편하게 사용할 수 있음. async/await는 놀라울 정도로 이해하기 쉽고, 사용법도 어렵지 않음. - async 함수 ```javascript async function f() { return 1; } f().then(alert); // 1 ``` function 앞에 async를 붙이면 해당 함수는 항상 프라미스를 반환 프라미스가 아닌 값을 반환하더라도 이행 상태의 프라미스(resolved promise)로 값을 감싸 이행된 프라미스가 반환되도록 해줌. - await ```javascript // await는 async 함수 안에서만 동작합니다. let value = await promise; ``` 자바스크립트는 await 키워드를 만나면 프라미스가 처리(settled)될 때까지 기다림. ```javascript async function f() { let promise = new Promise((resolve, reject) => { setTimeout(() => resolve("완료!"), 1000) }); let result = await promise; // 프라미스가 이행될 때까지 기다림 (*) alert(result); // "완료!" } f(); ``` 함수 호출, 함수 본문이 실행되는 도중에 실행이 잠시 '중단'되었다가 프라미스가 처리되면 실행이 재개됨 이때 프라미스 객체의 result 값이 변수 result에 할당. 1초 뒤에 '완료!'가 출력. promise.then() 보다 더 가독성도 좋고, 세련됨 - 에러핸들링 await는 정상적으로 실행되면 result에 결과를 반환하지만 에러가 발생했을 시 throw 문을 작성한 것처럼 에러가 던져진다. ```javascript async function f() { await Promise.reject(new Error("에러 발생!")); // throw new Error(...) 와 동일하다. } ``` 혹은 try.. catch를 사용하여 에러를 잡을 수도 있다. ```javascript async function f() { try { let response = await fetch('http://유효하지-않은-주소'); } catch(err) { alert(err); // TypeError: failed to fetch } } f(); ``` 아니면 .catch()를 사용할 수도 있다. ```javascript async function f() let response = await fetch('http://유효하지-않은-url'); } // f()는 거부 상태의 프라미스가 됩니다. f().catch(alert); // TypeError: failed to fetch // (*) ``` 지금까지 보면 알겠지만 promise.then을 사용하는 것보다 async/await를 사용하는 것이 대개는 더 편리함. ```javascript // 프라미스 처리 결과가 담긴 배열을 기다립니다. let results = await Promise.all([ fetch(url1), fetch(url2), ... ]); ``` await를 Promise.all 과 함께 쓸 수도 있다.