yceffort

자바스크립트에서 http 요청하기 - fetch에 대한 고찰

Published on January 21, 2020

1. 서론

자바스크립트에서 http 요청을 하는 것은 이제 비일비재한 일이 되었다. 서버에서 모든 데이터를 가져와서 static 한 html을 만들어서 보여주고 있는 웹페이지는 아마 찾기 어려울 것이다. 맨 처음 웹을 배울 때, jquery의 ajax 요청을 배우 던 것이 한 5년 전 쯤 되었다. 비동기 http 요청이 비일비재한 요즘, 지금은 그 기술이 어디까지 왔을까? 그리고 어떻게 써야 더 깔끔하게 쓸수 있을까?

2. XMLHttpRequest

가장 원초적으로 요청을 날리는 방법이다. 지금이 API를 이용하여 호출하고 있는 사람은 아마 없을 것이다.

var xmlHttp = new XMLHttpRequest()

xmlHttp.onreadystatechange = function() {
  if (this.status == 200 && this.readyState == this.DONE) {
    console.log(xmlHttp.responseText)
  }
}

xmlHttp.open("GET", "/yceffort/request.txt", true)

xmlHttp.send()

어차피 쓸 일도 거의 없고, 스펙은 위 링크에서 자세히 나와있을 테니 생략한다.

3. JQuery Ajax

아직도 많은 곳에서 쓰고 있을 우리 친구 JQuery와 그의 친구 JQuery.Ajax다.

$.ajax({
  url: "/yceffort/request.txt",
  success: function(data) {
    console.log(data)
  },
})

마찬가지로 자세한 스펙 설명은 마찬가지로 생략한다. 물론 여기까지만 안다 하더라도, 왠만한 수준의 request는 처리할 수 있다. 그러나 복잡한 유즈케이스에서는 조금 더 이야기하기 피곤해진다.

만약 1번 request의 정보를 받아서 2번 request를 날리고, 3번 request를 날려야 하면 어떻게 될까?

$.ajax({
  url: "/yceffort/request1.json",
  success: function(data) {
    const result = JSON.parse(data);
    $.ajax({
        url: `/yceffort/request2.json?data=${result.data}`
        success: function(data2){
            const result2 = JSON.parse(data2);
            $.ajax({
                url: `/yceffort/request2.json?data=${result2.data}`
                success: function(data3){
                    ......
                }
            })
        }
    })
  },
})

Promise의 callback hell의 지옥도가 여기서도 보이게 된다. 물론 이래저래 callback을 풀어내는 방법도 있지만, 여전히 then(success)의 체이닝 콤보를 벗어날 수가 없다.

3. async await & fetch

fetch는 물론 promise로도 쓸 수 있다.

es7 에서 추가된 async await과 fetch api를 활용한다면, 위의 코드를 조금더 깔끔 하게 쓸 수 있다.

const response1 = await fetch("/yceffort/data1.json")
const result1 = await response1.json()

const response2 = await fetch(`/yceffort/data2.json?${result1.data}`)
const result2 = await response2.json()

const response3 = await fetch(`/yceffort/data3.json?${result2.data}`)
const result3 = await response3.json()

fetch api는 XMLHttpRequest와 비슷하지만, 조금더 강력하고 유연한 조작이 가능하다. 또한 CORS, http origin header에 관한 개념도 정리되어 있다.

fetch("/yceffort/data1.json", {
  method: "POST",
  mode: 'cors',
  cache: 'no-cache',
  headers:  {"Content-Type", "application/json"},
  credentials: "same-origin",
  body: JSON.stringify(bodyData)
});

이 외에도 다양한 옵션들이 있으니, 스펙을 참고해보자. 그러나 이 fetch api에는 단점이 존재한다. 바로 우리가 사랑하는 익스플로러를 지원하지 않는다는 것이다.

Caniuse: Fetch

아쉽게도, fetch를 바로 쓸 수는 없다. (이미 async, await을 쓴 시점 부터 글렀지만)

4. fetch polyfill

여러가지 fetch Polyfill이 존재하지만, 그 중에서 가장 많이 사용되는 것은 isomorphic-fetchaxios가 있는 것 같다. 둘 중에 뭘 써야 되는 글이 여기 저기 많이 존재한다. 대충 요약하면, isomorphic-fetch은 polyfill이 필요한 대신 원래 fetch와 가장 비슷하고(이름부터가 isomorphic다!) 가볍다. 반면에 axios는 사용법은 조금 다르지만 무겁고 더 여러가지 기능을 제공하는 것 같다. 취향 껏 쓰자. 여기서는 isomorphic-fetch를 기준으로 쓴다.

5. deep dive to fetch

데이터를 제공하는 api 서버가 존재하고, 여기에서 모든 응답을 json으로 내려 준다고 가정하자. 어떠한 경우에도 사용자에게 에러를 보여주지 않고 (100% 커버할 순 없지만) 최대한 자연스럽게 fetch를 해야 한다면 어떻게 해야할까?

5-1. 에러 처리

const response = await `/yceffort/data1`

// 200이 아닐 경우의 처리
if (!response.ok) {
  captureException(`failed to fetch /yceffort/data1. [${response.code}]`)
}

try {
  const result = await response.json()
} catch (e) {
  // json 으로 파싱을 못할때의 처리
  captureException(`failed to parse /yceffort/data1, ${e}`)
}

5-2. Abortable Fetch

참고

몇몇 fetch 요청은 그 시간이 오래 걸리거나, 사용자의 요청으로 취소를 할 수도 있어야 하는 경우가 발생한다. 그 경우 사용하는 것이 AbortController다.

const controller = new AbortController()
const signal = controller.signal

setTimeout(() => controller.abort(), 5000)

fetch(url, { signal })
  .then(response => {
    return response.text()
  })
  .then(text => {
    console.log(text)
  })

5초 뒤에 자동으로 abort 되는 코드이다. fetch를 abort하게되면, request와 response 모두 취소된다. 따라서, response.text()도 취소된다.

DOMException: The user aborted a request.

fetch시에 발생한 exception이 abort인지를 구별하기 위해서는 아래와 같이 처리하면 된다.

fetch(url, { signal })
  .then(response => {
    return response.text()
  })
  .then(text => {
    console.log(text)
  })
  .catch(err => {
    if (err.name === "AbortError") {
      console.log("Fetch aborted by user")
    } else {
      console.error("other error", err)
    }
  })

5-3. fetch in react

이렇게 복잡한 fetch를 리액트스럽게 처리하는 라이브러리가 여기저기 있다.

대충 여기서 얘기 한 것 들을 기준으로, useFetch를 만들어 보자.

const useFetch = (url, options) => {
  const [response, setResponse] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  const controller = new AbortController()
  const signal = controller.signal

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      try {
        const response = await fetch(url, {...options, {signal});
        const result = await res.json();
        setResponse(result);
        setIsLoading(false)
      } catch (error) {
        setError(error);
      }
    };
    fetchData();
  }, []);
  return { response, error, isLoading, signal };
};

6. 결론

잘 만들어진 걸 가져다 쓰자.