본문 바로가기

FrontEnd

[Epic React][실전]Make HTTP Requests

반응형

bookshelf/INSTRUCTIONS.md at exercises/03-data-fetching · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

브라우저에서 백엔드에 데이터를 요청하는 방법은 window.fetch와 함께 HTTP를 사용하는 것이다.
window
  .fetch('http://example.com/movies.json')
  .then(response => {
    return response.json()
  })
  .then(data => {
    console.log(data)
  })
모든 HTTP 메서드가 지원된다. 다음은 데이터를 POST하는 방법이다.
window
  .fetch('http://example.com/movies', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      // if auth is required. Each API may be different, but
      // the Authorization header with a token is common.
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify(data), // body data type must match "content-type" header
  })
  .then(response => {
    return response.json()
  })
  .then(data => {
    console.log(data)
  })

상태 코드(>= 400)와 함께 요청이 실패하면 응답 개체의 ok 속성이 false가 된다. 이 경우 보통 Promise를 reject한다.

window.fetch(url).then(async response => {
  const data = await response.json()
  if (response.ok) {
    return data
  } else {
    return Promise.reject(data)
  }
})

인증, host과 같은 기본값을 공통 처리하기 위해 보통 wrapper 함수를 사용한다. (client 등으로 명명)

그리고 해당 기능을 훅으로 만들면 useState나 useEffect와 같이 사용한다.

이벤트 핸들러에서 네트워크 요청 처리를 하는 것보단, useEffect내에서 하는 것이 좋다.

이벤트 핸들러 내에서 경합 처리나, 취소 메커니즘을 구현할 수 없기 때문이다.

📜 https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

 

Using Fetch - Web APIs | MDN

The Fetch API provides a JavaScript interface for accessing and manipulating parts of the HTTP pipeline, such as requests and responses. It also provides a global fetch() method that provides an easy, logical way to fetch resources asynchronously across th

developer.mozilla.org

실습

1. 클라이언트 객체를 만든다.

function client(endpoint, customConfig = {}) {
  const config = {
    method: 'GET',
    ...customConfig,
  }

  return window
    .fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
    .then(async response => {
      const data = await response.json()
      if (response.ok) {
        return data
      } else {
        return Promise.reject(data)
      }
    })
}

export {client}

window.fetch의 흥미로운 점은 네트워크 요청 자체가 실패하지 않는 한 promise를 reject하지 않는다는 것이다.

즉, 상태 코드가 500이든 400은 무조건 resolve된다.

따라서 response.ok(statusCode<400)인 경우에만 동작하도록  설정한다.

2. useAsync 훅을 만든다.

import * as React from 'react'

// 라이프 사이클 상 LayoutEffect가 useEffect보다 먼저 발생한다.
// 마운트 여부를 useEffect보다 먼저 설정하기 위함.
// 마운트 된 상태에서만 비동기 함수를 실행할 수 있다.
// 이거 없으면 unMount된 컴포넌트에서 비동기 호출 발생
function useSafeDispatch(dispatch) {
  const mounted = React.useRef(false)
  React.useLayoutEffect(() => {
    mounted.current = true
    return () => (mounted.current = false)
  }, [])
  return React.useCallback(
    (...args) => (mounted.current ? dispatch(...args) : void 0),
    [dispatch],
  )
}

// Example usage:
// const {data, error, status, run} = useAsync()
// React.useEffect(() => {
//   run(fetchPokemon(pokemonName))
// }, [pokemonName, run])


const defaultInitialState = {status: 'idle', data: null, error: null}


function useAsync(initialState) {
  const initialStateRef = React.useRef({
    ...defaultInitialState,
    ...initialState,
  })
  // 리듀서는 이전 스테이트와 액션을 이용해 새로운 상태를 반환하면 된다.
  // 이 경우 액션이 변경될 state만을 포함한 객체이며
  // 단순히 둘을 합쳐서 새로운 상태를 반환한다.
  const [{status, data, error}, setState] = React.useReducer(
    (s, a) => ({...s, ...a}),
    initialStateRef.current,
  )

  const safeSetState = useSafeDispatch(setState)

  const setData = React.useCallback(
    data => safeSetState({data, status: 'resolved'}),
    [safeSetState],
  )
  const setError = React.useCallback(
    error => safeSetState({error, status: 'rejected'}),
    [safeSetState],
  )
  // 단순 초기화
  const reset = React.useCallback(
    () => safeSetState(initialStateRef.current),
    [safeSetState],
  )
  // 쿼리를 실행함.
  const run = React.useCallback(
    promise => {
      if (!promise || !promise.then) {
        throw new Error(
          `The argument passed to useAsync().run must be a promise. Maybe a function that's passed isn't returning anything?`,
        )
      }
      safeSetState({status: 'pending'})
      return promise.then(
        data => {
          setData(data)
          return data
        },
        error => {
          setError(error)
          return Promise.reject(error)
        },
      )
    },
    [safeSetState, setData, setError],
  )

  return {
    // react-query 내부 구현과 동일한 방식.
    isIdle: status === 'idle',
    isLoading: status === 'pending',
    isError: status === 'rejected',
    isSuccess: status === 'resolved',

    setData,
    setError,
    error,
    status,
    data,
    run,
    reset,
  }
}

export {useAsync}

3. 사용하기

// 함수 컴포넌트 내부
const {data, error, run, isLoading, isError, isSuccess} = useAsync()
// input 입력 검색어
const [query, setQuery] = React.useState()
const [queried, setQueried] = React.useState(false)

// 마운트 시 호출 막기 위해 queried 플래그 사용 (클릭 시에만 사용)
React.useEffect(() => {
    if (!queried) {
        return
    }
	run(client(`books?query=${encodeURIComponent(query)}`))
}, [query, queried, run])


function handleSearchSubmit(event) {
    event.preventDefault()
    setQueried(true)
    setQuery(event.target.elements.search.value)
}

 

 

 

참고

react-query나 swr에는 캐시 관리 기능이 추가되어 있으니 그걸 쓰도록 하자.

해당 버전에는 또한, deduplicating이나 디바운싱, 쓰로틀링이 없다.

어떻게 구현하는지는 다음에 살펴보자

 

encodeURIComponent() - JavaScript | MDN (mozilla.org)

 

encodeURIComponent() - JavaScript | MDN

The encodeURIComponent() function encodes a URI by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character (will only be four escape sequences for characters composed of tw

developer.mozilla.org

 

반응형