Skip to content
This page has been auto-translated and may contain errors.View in English

데이터 가져오기

서버의 데이터를 보여주는 컴포넌트는 처음 렌더링되는 순간에 그 데이터를 갖고 있지 않습니다. 데이터를 요청해야 하고, 기다렸다가 응답이 오면 보여주거나 요청이 실패하면 그에 따라 처리해야 합니다. 이 장에서는 React 앱이 이를 처리하는 패턴을 다룹니다: 이펙트 내에서 fetch를 호출하고, 결과, 로딩 상태, 에러를 useState로 관리하는 방식입니다.

다음은 URL로 사용자를 가져오는 Profile 컴포넌트입니다. 렌더링이 먼저 일어나고 그 직후에 서버에 요청을 보내는 것이 바로 useEffect가 존재하는 이유입니다:

jsx
function Profile({ url }) {
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)

  useEffect(() => {
    let active = true
    setData(null)
    setError(null)
    fetch(url)
      .then(r => {
        if (!r.ok) throw new Error(`Request failed: ${r.status}`)
        return r.json()
      })
      .then(d => { if (active) setData(d) })
      .catch(e => { if (active) setError(e) })
    return () => { active = false }
  }, [url])

  if (error) return <p>Failed to load</p>
  if (!data) return <p>Loading...</p>
  return <h1>{data.name}</h1>
}

fetch, Promise, .then이 아직 낯설다면 JavaScript 핸드북의 비동기 JavaScript에서 기초부터 다루고 있으니 참고하세요. 이 장의 내용은 그것을 바탕으로 합니다.

useEffect는 React가 컴포넌트를 렌더링한 후에 코드를 실행하는 훅으로, 렌더링 중에는 실행되지 않습니다. 실행할 함수와 의존성 배열(값이 변할 때 이펙트를 다시 실행해야 하는 값들의 목록)을 전달합니다. 여기서는 url만 의존성으로 설정했습니다. Profile은 두 가지 상태로 시작합니다: 결과를 저장할 data와 오류를 저장할 error입니다. 이펙트 맨 위에서 setData(null)setError(null)은 이전 실행에서 남겨진 값들을 초기화합니다. 이것이 중요한 이유는 두 가지입니다: 이 초기화 없이 url이 변경되면 data에 여전히 이전의 성공한 결과가 남아있어서 "Loading..." 대신 오래된 프로필을 계속 보여주게 되고, 이전에 실패한 요청 이후에 성공한 요청이 와도 errornull을 설정하는 코드가 없어서 오래된 에러가 계속 남아있게 됩니다. 그다음 fetch는 응답에 대한 Promise를 반환합니다. fetch는 네트워크 실패 시에만 Promise를 거부하므로, 404나 500도 정상적으로 응답 객체로 처리됩니다. 그래서 첫 번째 .then에서 r.ok를 확인하고 응답이 .json()에 도달하기 전에 에러를 던집니다. 이 확인을 건너뛰면 실패한 요청도 마치 성공한 것처럼 data로 흘러들어갑니다. 확인을 통과한 후 .json()이 본문을 읽고 결과가 setData를 통해 data에 들어갑니다. 이 과정에서 거부되는 것이 있으면 .catch가 문제를 error에 저장합니다. JSX 아래에서 errordata는 다른 상태처럼 확인됩니다: 에러가 있으면 메시지를 렌더링하고, 아직 데이터가 없으면 로딩 메시지를 렌더링하고, 데이터가 있으면 실제 UI를 렌더링합니다.

여기서 별도의 로딩 변수는 없습니다. dataerror가 모두 null인 한, 컴포넌트는 요청을 보낸 후 응답을 받기 전 상태이므로 if (!data) return <p>Loading...</p>만으로도 충분합니다. 일부 컴포넌트는 더 세밀한 제어를 위해 전용 isLoading boolean을 추가하기도 하지만, 이미 있는 상태에서 로딩 상태를 유도하는 것이 더 간단한 경우가 많고, 동기화할 값이 하나 줄어듭니다.

이펙트 내의 active 변수는 정리 플래그입니다. useEffect는 함수를 반환할 수 있으며, React는 이펙트가 다시 실행되기 직전과 컴포넌트가 언마운트될 때 이 함수를 호출합니다. 여기서 반환된 함수는 activefalse로 설정하고, .then.catch 모두 setDatasetError를 호출하기 전에 이 값을 확인합니다. 이 확인 없이 url이 변경되거나 컴포넌트가 화면에서 사라질 때 진행 중인 요청이 결국 해결되고 더 이상 관련이 없는 상태를 업데이트하려고 시도하게 됩니다. 플래그는 늦게 도착한 응답을 버그 대신 아무 효과도 없는 동작으로 만듭니다.

이렇게 수동으로 fetch 하는 것을 이해할 가치가 있습니다. 거의 모든 것이 이 패턴을 기반으로 하기 때문입니다. 실제 앱에서는 모든 요청마다 useEffect와 몇 가지 상태를 작성하는 대신, React Query 같은 데이터 가져오기 라이브러리나 Next.js 같은 프레임워크의 내장 데이터 로딩(이들은 라우팅 레이어에서 처리)을 보통 사용합니다. 이런 도구들은 캐싱, 재시도, 아래의 타이밍 문제들을 대신 처리해줍니다. 이 장의 패턴이 그들의 기반입니다.

겹치는 요청들 사이의 경쟁 조건은 직접 언급할 가치가 있는 까다로운 부분입니다. url이 빠르게 변경되면, 누군가 검색창에 입력하거나 프로필 사이를 클릭할 때처럼, 컴포넌트는 이전 요청이 해결되기 전에 새 요청을 발생시키고, 네트워크 응답이 요청받은 순서대로 도착하지 않을 수 있습니다. 정리 플래그 없이 첫 번째 요청에 대한 느린 응답이 두 번째 요청에 대한 빠른 응답 이후에 도착하면 data가 더 이상 보고 있지 않은 url에 대한 오래된 콘텐츠로 덮어씌워집니다. 이것이 정확히 active가 방지하는 것입니다: 이펙트의 각 실행이 자신의 active 변수를 닫으므로, 이전 실행의 요청은 자신의 정리가 실행된 후 현재 렌더링의 상태에 절대 쓸 수 없습니다.

AbortController가 이 문제의 다른 도구이며, 더 나아갑니다: 나중에 도착한 오래된 응답을 무시하는 것이 아니라 진행 중인 요청 자체를 취소합니다.

jsx
useEffect(() => {
  const controller = new AbortController()
  setData(null)
  setError(null)
  fetch(url, { signal: controller.signal })
    .then(r => {
      if (!r.ok) throw new Error(`Request failed: ${r.status}`)
      return r.json()
    })
    .then(setData)
    .catch(e => { if (e.name !== 'AbortError') setError(e) })
  return () => controller.abort()
}, [url])

이 버전도 위의 이펙트와 같은 두 가지 수정이 필요합니다: 실행 맨 위에서 dataerror를 초기화하여 재시도가 이전 실패나 오래된 프로필을 실제로 지울 수 있도록 하기, 그리고 본문을 파싱하기 전에 r.ok를 확인하기 (404나 500을 거부 대신 완료된 요청으로 취급하는 fetch의 동작 때문). AbortController는 정리 플래그가 수행하던 작업(더 이상 필요 없는 요청 취소)만 대체하므로, 실행 간에 상태가 어떻게 넘어가는지나 fetch가 실패한 HTTP 응답을 어떻게 취급하는지에 대해서는 아무것도 하지 않습니다. 두 방법 모두 순서 문제를 해결하지만, 취소는 또한 네트워크가 더 이상 원하지 않는 요청을 끝내는 수고를 덜어줍니다.

React 19 이후의 새로운 방향은 <Suspense>로 감싼 컴포넌트 내에서 use 훅으로 비동기 데이터를 읽는 것입니다. 이는 모든 컴포넌트에서 수동으로 하는 대신 프레임워크 레벨에서 React가 대기 중인 상태와 로딩 상태를 처리하도록 합니다. use는 보통 혼자 사용되지 않습니다. 일반적으로 캐싱과 요청 라이프사이클을 관리하는 프레임워크나 데이터 라이브러리와 쌍을 이룹니다. 기초를 넘어서에서 이를 다른 광범위한 데이터 가져오기 생태계와 함께 다룹니다.

Juno이펙트에서 fetch 호출, 상태들 지켜보기 기억해야 할 패턴: useEffect 내에 fetch를 넣고, dataerror를 상태로 유지한 후, JSX에서 이들을 확인해 무엇을 보여줄지 결정합니다. 로딩 상태는 둘 다 아직 비어있을 때입니다. active 플래그는 늦게 도착한 응답이 더 이상 그 데이터를 보고 있지 않은 컴포넌트의 상태를 설정하지 못하도록 막습니다.
Juno이펙트에서 fetch 호출, 상태들 지켜보기useEffect 내에서 fetch를 호출하고, 결과와 모든 에러를 상태에 저장한 후, 따로 로딩 boolean이 필요하지 않으면 이들을 직접 기반으로 렌더링합니다. active 플래그를 뒤집는 정리 함수를 반환하여, 이전 url의 오래된 응답이나 언마운트된 컴포넌트가 현재 상태를 덮어쓸 수 없도록 합니다. 일회성 요청을 넘어서면, 모든 곳에서 이를 직접 작성하는 대신 React Query나 프레임워크의 데이터 로딩을 사용합니다.
Juno이펙트에서 fetch 호출, 상태들 지켜보기 각 이펙트 실행은 자신의 active 클로저를 가지고 있으며, 이것이 url이 빠르게 변할 때 순서가 맞지 않는 응답으로부터 안전하게 하는 것입니다. AbortController는 같은 작업을 수행하면서 낭비되는 요청도 취소합니다. 원본 useEffect fetch를 한 번 이해할 가치가 있는 메커니즘으로 취급합니다. 실제 앱은 실제 요청을 위해 데이터 라이브러리나 Suspense와 함께 use 훅에 의존하며, 둘 다 정확히 이 패턴 위에 구축되어 있습니다.

다음: Refs와 DOM, DOM 노드 자체가 필요한 작업을 위한 탈출구입니다.