데이터 가져오기
서버의 데이터를 보여주는 컴포넌트는 처음 렌더링되는 순간에 그 데이터를 갖고 있지 않습니다. 데이터를 요청해야 하고, 기다렸다가 응답이 오면 보여주거나 요청이 실패하면 그에 따라 처리해야 합니다. 이 장에서는 React 앱이 이를 처리하는 패턴을 다룹니다: 이펙트 내에서 fetch를 호출하고, 결과, 로딩 상태, 에러를 useState로 관리하는 방식입니다.
다음은 URL로 사용자를 가져오는 Profile 컴포넌트입니다. 렌더링이 먼저 일어나고 그 직후에 서버에 요청을 보내는 것이 바로 useEffect가 존재하는 이유입니다:
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..." 대신 오래된 프로필을 계속 보여주게 되고, 이전에 실패한 요청 이후에 성공한 요청이 와도 error에 null을 설정하는 코드가 없어서 오래된 에러가 계속 남아있게 됩니다. 그다음 fetch는 응답에 대한 Promise를 반환합니다. fetch는 네트워크 실패 시에만 Promise를 거부하므로, 404나 500도 정상적으로 응답 객체로 처리됩니다. 그래서 첫 번째 .then에서 r.ok를 확인하고 응답이 .json()에 도달하기 전에 에러를 던집니다. 이 확인을 건너뛰면 실패한 요청도 마치 성공한 것처럼 data로 흘러들어갑니다. 확인을 통과한 후 .json()이 본문을 읽고 결과가 setData를 통해 data에 들어갑니다. 이 과정에서 거부되는 것이 있으면 .catch가 문제를 error에 저장합니다. JSX 아래에서 error와 data는 다른 상태처럼 확인됩니다: 에러가 있으면 메시지를 렌더링하고, 아직 데이터가 없으면 로딩 메시지를 렌더링하고, 데이터가 있으면 실제 UI를 렌더링합니다.
여기서 별도의 로딩 변수는 없습니다. data와 error가 모두 null인 한, 컴포넌트는 요청을 보낸 후 응답을 받기 전 상태이므로 if (!data) return <p>Loading...</p>만으로도 충분합니다. 일부 컴포넌트는 더 세밀한 제어를 위해 전용 isLoading boolean을 추가하기도 하지만, 이미 있는 상태에서 로딩 상태를 유도하는 것이 더 간단한 경우가 많고, 동기화할 값이 하나 줄어듭니다.
이펙트 내의 active 변수는 정리 플래그입니다. useEffect는 함수를 반환할 수 있으며, React는 이펙트가 다시 실행되기 직전과 컴포넌트가 언마운트될 때 이 함수를 호출합니다. 여기서 반환된 함수는 active를 false로 설정하고, .then과 .catch 모두 setData나 setError를 호출하기 전에 이 값을 확인합니다. 이 확인 없이 url이 변경되거나 컴포넌트가 화면에서 사라질 때 진행 중인 요청이 결국 해결되고 더 이상 관련이 없는 상태를 업데이트하려고 시도하게 됩니다. 플래그는 늦게 도착한 응답을 버그 대신 아무 효과도 없는 동작으로 만듭니다.
이렇게 수동으로 fetch 하는 것을 이해할 가치가 있습니다. 거의 모든 것이 이 패턴을 기반으로 하기 때문입니다. 실제 앱에서는 모든 요청마다 useEffect와 몇 가지 상태를 작성하는 대신, React Query 같은 데이터 가져오기 라이브러리나 Next.js 같은 프레임워크의 내장 데이터 로딩(이들은 라우팅 레이어에서 처리)을 보통 사용합니다. 이런 도구들은 캐싱, 재시도, 아래의 타이밍 문제들을 대신 처리해줍니다. 이 장의 패턴이 그들의 기반입니다.
useEffect 내에 fetch를 넣고, data와 error를 상태로 유지한 후, JSX에서 이들을 확인해 무엇을 보여줄지 결정합니다. 로딩 상태는 둘 다 아직 비어있을 때입니다. active 플래그는 늦게 도착한 응답이 더 이상 그 데이터를 보고 있지 않은 컴포넌트의 상태를 설정하지 못하도록 막습니다. 다음: Refs와 DOM, DOM 노드 자체가 필요한 작업을 위한 탈출구입니다.

