티스토리 뷰

 

Fetch API

const promise = fetch(url[, options]);

자바스크립트에서 서버에 네트워크 요청을 보내고 데이터를 받아올 수 있다. 그 방법 중 하나는 브라우저에서 제공하는 fetch API이다. options를 주지 않으면 기본적으로 GET 메서드가 진행된다. 

(먼저 동기와 비동기, 자바스크립트의 비동기 처리에 대해서 다음 포스팅을 참고하자)

 

JS 동기와 비동기, Callback, Promise, async/await

동기와 비동기 커피숍에서 커피를 시킬 때, 한명이 주문하고 그 주문이 완료되면, 다음 사람이 주문하고 그 주문을 완료하고, 그리고 또 다음 사람이 주문하고 완료시킨다면 대기 줄이 굉장히

hahagarden.tistory.com

 


 

프로미스 체이닝

Response 객체를 래핑한 프로미스 객체를 반환한다

fetch()를 호출하면 브라우저는 네트워크 요청을 보내고 프로미스가 반환된다. 비동기적이다. 이 프로미스가 가지고 있는 fulfilled result는 Response 객체라는 것인데, 이것이 우리가 관심있는 데이터이다. 이 Response 객체는 완전한 것이 아니라서 완성되는 데 시간이 걸린다. 후속 처리 메서드를 통해 동기화를 해주어야 한다.

 

// 프로미스체이닝으로 fetch

function getNewsAndWeather() {
  return fetch(newsURL)
    .then((response) => response.json()) // 1️⃣
    .then((json1) =>
      fetch(weatherURL)
        .then((response) => response.json()) // 3️⃣
        .then((json2) => ({ news: json1.data, weather: json2 })) // 4️⃣ newsURL응답은 data프로퍼티에 값이 들어있다
    ); // 2️⃣
}

fetch()를 호출한 후에 Response 객체가 완성이 되면 프로미스는 fulfilled 상태가 되고 후속 처리 메서드의 콜백 함수가 호출이 된다. 위의 경우 후속처리 메서드인 1️⃣ then 메서드의 콜백함수의 매개변수 response는 이 Response객체가 된다.
이 Response 객체는 자바스크립트가 읽을 수 없다. Response.prototype.json 메서드를 사용하여 우리가 필요한 응답 내용을 JSON 포맷의 문자열에서 자바스크립트 객체로 변환해야 한다.

response.json() 역시 비동기적이다. 프로미스를 반환하기 때문에 후속처리를 해주어야 한다. response.json()은 프로미스를 반환하므로 후속처리 메서드인 2️⃣ then 메서드의 콜백함수 매개변수 json1은 response.json()이 반환하는 프로미스의 fulfilled result(Response객체가 response.json()를 거친 결과 자바스크립트 객체)이다.

같은 맥락으로 weatherURL도 fetch GET을 해주면 프로미스 체이닝을 통해  4️⃣ 마지막에는 news, weather의 데이터가 담긴 객체를 fulfilled result로 갖는 프로미스가 반환된다. getNewsAndWeather() 함수는 { news: ~~, weahter: ~~ }를 fulfilled result로 갖는 프로미스를 반환하는 것이다.

 

프로미스 체이닝을 통해 newsURL과 weatherURL 두 url에 fetch요청을 하고 각각 응답으로 받은 데이터를 객체에 저장해 보았다. 그런데 newsURL과 weatherURL은 서로 상관이 없으므로 위 코드처럼 동기처리를 해주지 않아도 된다. 100개의 url에서 데이터를 받아온다면 더더욱 동기처리를 하면 안될 것이다. 이들을 Promise.all()을 통해 병렬적으로 빠르게 처리할 수 있다.

 


 

Promise.all()

//Promise.all()으로 fetch

function getNewsAndWeatherAll() {
  return Promise.all([fetch(newsURL), fetch(weatherURL)])
    .then((responses) => Promise.all(responses.map((response) => response.json()))) // 🅰
    .then((jsons) => ({ news: jsons[0].data, weather: jsons[1] })); // 🅱
}

Promise.all()으로 병렬적으로 비동기처리를 하니 코드가 더 간결해졌고 처리 시간도 훨씬 빨라진다.
그런데 이 과제를 하면서 Promise.all이 왜 두 번 사용되는지 의문이 들었다. 그 이유를 알아보았다.

먼저 프로미스에 대해서 기억할 특징들이다.

1. 프로미스는 프로미스를 반환한다. 

2. then 메서드는 프로미스를 반환한다. 이 프로미스는 다음과 같이 정해진다.
  - then 메서드의 콜백함수가 프로미스를 반환하면 이 프로미스를 그대로 반환한다.
  - then 메서드의 콜백함수가 프로미스가 아닌 값을 반환하면 그 값을 암묵적으로 resolve 또는 reject하여 프로미스를 생성해 반환한다.

3. then의 콜백함수의 전달인자는 자신이 후속 처리중인 프로미스의 처리 결과(fulfilled result 또는 rejected result)이다.

4. Promise.all은 프로미스를 반환한다. [promise, promise, promise]를 받으면 [fulfilled result, fulfilled result, fulfilled result] 배열을 fulfilled result로 갖는 프로미스를 반환한다. 

5. 프로미스는 후속 처리 메서드를 통해서만 fulfilled result또는 rejected result 에 접근할 수 있다.(async/await 제외)

 

다음은 내가 처음에 생각했던 틀린 답이다.

// 이것은 처음 작성했던 틀린 코드이다

function getNewsAndWeatherAll() {
  return Promise.all([fetch(newsURL), fetch(weatherURL)])
    .then((responses) => responses.map((response) => response.json())) // 1️⃣
    .then((jsons) => ({ news: jsons[0].data, weather: jsons[1] })); // 2️⃣
} // 테스트 통과 실패

이 코드에서 1️⃣의 매개변수 responses는 배열의 각 프로미스가 처리된 후 fulfilled result가 들어있는 [Response, Response] 배열이다. 여기까진 쉽다. 이 Response 객체는 자바스크립트가 읽지 못하기 때문에 프로미스 체이닝에서 해주었던 것처럼 Response.prototype.json 메서드를 적용해준다. map을 이용하여 요소에 각각 적용하고, 각각 적용한 결과를 가진 새로운 배열을 반환한다. 2️⃣의 매개변수 jsons을 통해 이 반환된 배열의 인덱스0과 인덱스1의 요소를 각각 객체에 저장한다. 그런데 테스트 통과 실패. 왜그럴까??

주목할 것은 1️⃣첫번째 then 메서드의 콜백 함수이다. 이 then 메서드가 반환하는 프로미스의 fulfilled result 가 2️⃣두번째 then 메서드의 콜백함수의 매개변수인 jsons가 된다. 그런데 1️⃣ then메서드의 콜백함수가 반환하는 것은 프로미스가 아니라 map의 반환값인 새로운 배열이다. 그 새로운 배열을 반환하면 위에서 언급한 2번 특징에 따라 '콜백 함수가 프로미스가 아닌 값을 반환하면 그 값을 암묵적으로 resolve 하여 프로미스를 생성해 반환한다'가 적용된다. 1️⃣ then 메서드는 새로운 배열을 fulfilled result로 갖는 프로미스를 생성해 반환한다.

그런데 map()의 콜백 함수를 보자. map()의 콜백 함수는 response.json()을 반환하고, 이 response.json()은 프로미스를 반환한다. map()이 반환하는 새로운 배열은 Response.prototype.json 메서드를 통해 Response 객체가 자바스크립트 객체로 변환된 결과를 fulfilled result로 갖는 프로미스로 이루어져 있는 것이다. 그러므로 map()이 반환하는 새로운 배열은 프로미스들로 이루어져 있다. [promise', promise''] 말이다.

종합적으로 1️⃣ then 메서드는 배열 [promise', promise'']를 fulfilled result로 갖는 프로미스를 반환한다. 매개변수 jsons는 배열 [promise', promise'']이고, 프로미스는 후속 처리 메서드가 아니면 값에 접근할 수 없으므로 jsons[0]과 jsons[1]은 프로미스 객체이고 jsons[0].data는 undefined이다.

그래서 이 [promise', promise'']를 한 번 더 Promise.all()을 해주는 것이다. 
Promise.all([promise', promise''])를 하면 🅰 then 메서드는 response.json()이 반환하는 프로미스가 가진 fulfilled result(자바스크립트 객체)로 이루어진 배열(프로미스체이닝 예시로 치면 [json1, json2])을 fulfilled result로 가지는 프로미스를 반환한다. (말장난같지만 그렇다..)

 

회고

저번 주부터 불타는 자바스크립트였다. 고차함수, 클래스, 객체 지향 프로그래밍, 비동기까지 너무 어려웠다. 직접 코드를 짜보니 아직 이해를 다 못했고, 또 어느 정도까지는 이해의 영역이 아닌 것 같았다. 그냥 수용하고 사용하다보니 감이 오는 것 같기도 하다. 

오늘 과제였던 Promise.all 메서드로 fetch를 비동기적으로 하는 문제는 오늘 나의 하루를 온전히 차지했다. 프로미스는 플라나리아같다. 프로미스는 프로미스를 반환한다.

우리가 사용하는 값은 프로미스 객체 안에 있는 result 프로퍼티의 값(에러를 처리할 때에는 rejected result, 그 외에는 모두 fulfilled result)이다. 이 result 값을 사용하려면 후속 처리 메서드 then, catch, finally의 콜백 함수의 매개변수를 가지고 사용할 수 있다. result 값이 전달되기 때문이다. 그리고 후속 처리 메서드의 반환값은 또다시 프로미스이다. 콜백함수가 반환하는 값이 프로미스이면 그 프로미스를 그대로 반환하고, 프로미스가 아니면 플라나리아처럼 반환값에 프로미스 옷을 입혀 프로미스로 만들어서 결국에는 프로미스를 반환하도록 한다. 그리고 다음 후속 처리 메서드에서 그 프로미스의 result값을 전달인자로 사용하고 체이닝이 된다.

프로미스가 복잡한만큼 글도 복잡해졌다 😂 말장난같지만 그만큼 복잡한 프로미스의 세계..
조금은 이해가 된 것 같다.....

 

반응형

'개발 > JS, TS, React' 카테고리의 다른 글

SPA와 MPA / SSR과 CSR / AJAX  (0) 2023.03.23
React JSX / CRA / React Router  (0) 2023.03.22
JS 동기와 비동기, Callback, Promise, async/await  (0) 2023.03.17
JS 프로토타입  (0) 2023.03.16
JS 고차함수  (0) 2023.03.15
댓글