-
[Kent C. dodds - Epic React] React hooks(2) - HTTP Request & Error HandlingFrontend 2020. 11. 1. 17:58
Kent C. dodds의 Epic React 강의에서 React hooks - useEffect: HTTP requests 챕터를 정리해보려고 한다.
일단 간단하게 API를 호출해서 화면에 API 응답으로 온 랜덤 강아지 이미지를 보여주는 예시를 만들어보려고 한다.
const fetchRandomDog = (error) =>fetch("https://dog.ceo/api/breeds/image/random").then((data) => {if (error) {throw error;}return data.json();});그리고 에러가 나는 상황을 연출하기 위해 인자로 error가 넘어오면 error를 throw하고,
그 외의 경우에는 fetch받은 data를 JSON format으로 변환해서 return 시켜주기로 했다.
import React from "react";import "./styles.css";const fetchRandomDog = (error) =>fetch("https://dog.ceo/api/breeds/image/random").then((data) => {if (error) {throw error;}return data.json();});export default function App() {const [loading, setLoading] = React.useState(false);const [image, setImage] = React.useState(null);const [error, setError] = React.useState(null);const fetchDog = (_, error) => {setLoading(true);fetchRandomDog(error).then(({ message }) => {setImage(message);setLoading(false);},(error) => {setError(error);setLoading(false);});};React.useEffect(() => {fetchDog();}, []);if (error) {return (<div className="App"><div>{error.message}</div><button type="button" onClick={fetchDog}>Retry</button></div>);}return (<div className="App">{loading ? (<div>is Loading...</div>) : (<><button type="button" onClick={fetchDog}>Change photo</button><buttontype="button"onClick={(e) =>fetchDog(e, new Error("oops! something went wrong"))}>Throw error</button><img src={image} alt="random dog" /></>)}</div>);}이제 앱은 대충 이렇게 생겼다.
Change photo 버튼을 클릭하면 매 번 새로운 사진을 HTTP request를 통해 가져오고 화면에 보여준다.
위 상황에서 문제는 에러가 발생하고 난 이후인데,
Throw error 버튼을 클릭하면 아래와 같은 에러뷰가 뜬다.
이 상황에서 내가 Retry 버튼을 누르면 다시 fetch가 일어나서 정상적으로 사진을 가져와야하는데
fetch가 일어날때마다 가져온 데이터를 콘솔창에 출력하도록 한 후에 어떻게 되는지 살펴보자!
const fetchDog = (_, error) => {setLoading(true);fetchRandomDog(error).then(({ message }) => {console.log(message);setImage(message);setLoading(false);},(error) => {setError(error);setLoading(false);});};분명 fetch가 일어남에도 불구하고 화면은 계속 에러 뷰만 보여주고 있다.
그 이유는 fetch가 일어나서 image state가 업데이트되더라도
기존에 업데이트된 error state가 리셋되지 않았기 때문이다.
이걸 해결하려면 매번 fetch가 시작되기 전에 setError(null)을 통해 error state를 reset해 주어야 한다!
const fetchDog = (_, error) => {setLoading(true);setError(null); // Reset ErrorfetchRandomDog(error).then(({ message }) => {setImage(message);setLoading(false);},(error) => {setError(error);setLoading(false);});};이 얼마나 귀찮은 일이란 말인가!
그래서 이걸 해결하는 간단한 방법이 있는데,
HTTP request의 상태를 나타내는 string 값을 value로 가지는 state를 하나 만들어서 처리해주는 것이다.
- idle: no request made yet
- pending: request started
- resolved: request successful
- rejected: request failed
export default function App() {const [status, setStatus] = React.useState("idl");const [image, setImage] = React.useState(null);const [error, setError] = React.useState(null);const fetchDog = (_, error) => {setStatus("pending");fetchRandomDog(error).then(({ message }) => {setImage(message);setStatus("resolved");},(error) => {setError(error);setStatus("rejected");});};React.useEffect(() => {fetchDog();}, []);if (status === "rejected") {return (<div className="App"><div>{error.message}</div><button type="button" onClick={fetchDog}>Retry</button></div>);}return (<div className="App">{status === "pending" ? (<div>is Loading...</div>) : (<><button type="button" onClick={fetchDog}>Change photo</button><buttontype="button"onClick={(e) =>fetchDog(e, new Error("oops! something went wrong"))}>Throw error</button><img src={image} alt="random dog" /></>)}</div>);}이렇게 처리해주면 error를 매 번 reset 해주지 않아도 원하는 데로 작동한다.
이제 위 코드를 조금 더 개선한다면 state값을 하나의 객체로 저장하는 것이다.
const [state, setState] = React.useState({status: "idl",image: null,error: null});const fetchDog = (_, error) => {setState({ status: "pending" });fetchRandomDog(error).then(({ message }) => {setState({ status: "resolved", image: message });},(error) => {setState({ status: "rejected", error });});};기존에는 fetch가 일어난 이후 setImage와 setStatus를 각각 호출하고,
그 결과 2번의 re-render가 일어나게 된다.
그런데 하나의 object에 state값을 저장하고 위와 같이 state를 update해준다면 딱 한 번만 re-render가 된다.
ErrorBoundary
만약에 Network 에러는 전혀 없었지만 다른 곳에서 예상치 못한 에러가 발생한 경우에는 어떻게 해야할까?
다음과 같은 경우를 생각해보자.
const [state, setState] = React.useState({status: "idl",image: { message: null },error: null});// ...return (<img src={image.message} alt="random dog" />);초기 image state 값이 위와 같은 객체이고, image.message를 사용해서 이미지를 표현해주고 있는데
정상적으로 fetch가 이루어졌으나 데이터가 { message: '' } 와 같은 형태의 객체 대신에 null이 왔다고 가정해보자.
const fetchDog = (_, error) => {setState({ status: "pending" });fetchRandomDog(error).then((data) => {setState({ status: "resolved", image: null });},(error) => {setState({ status: "rejected", error });});};물론 내가 그렇게 나오도록 API를 조작할 수는 없으니,
null이 왔다고 가정한 후에 대충 위와 같이 image state 값으로 null을 업데이트해주었다.
현재 Network 에러가 발생했을 때만 에러뷰가 보여지도록 설계되어 있기 때문에
위 케이스에서는 유저에게 그 저 하얀색 공백 페이지만 보여질 것이다.
바로 이런 경우에 필요한 것이 ErrorBoundary이다.
안타깝게도 현재까지 ErrorBoundary는 class component로만 선언이 가능하다.
class ErrorBoundary extends React.Component {state = { error: null };static getDerivedStateFromError(error) {return { error };}render() {const { error } = this.state;if (error) {return <this.props.FallbackComponent error={error} />;}return this.props.children;}}대충 위와 같은 구조로 만들 수 있다.
초기 state에 error를 저장해주고,
getDerivedStateFromError method를 통해 error가 발생했을때 인자로 error를 받을 수 있다.
이 때 getDerivedStateFromError에서 return해주는 값으로 state가 update된다.
그러면 error가 존재하면 인자로 넘어 온 에러뷰를 보여주고,
error가 없을 때는 children을 return하도록 하면 된다!
import React from "react";import ReactDOM from "react-dom";import App from "./App";class ErrorBoundary extends React.Component {state = { error: null };static getDerivedStateFromError(error) {return { error };}render() {const { error } = this.state;if (error) {return <this.props.FallbackComponent error={error} retry={() => window.reload()} />;}return this.props.children;}}const FallbackComponent = ({ error, retry }) => (<div className="App"><div>{error.message}</div><button type="button" onClick={retry}>Retry</button></div>);const rootElement = document.getElementById("root");ReactDOM.render(<React.StrictMode><ErrorBoundary FallbackComponent={FallbackComponent}><App /></ErrorBoundary></React.StrictMode>,rootElement);이렇게 컴포넌트 상위에 ErrorBoundary를 감싸주면,
ErrorBoundary 하위 컴포넌트에서 발생한 에러를 모두 감지하여 핸들링할 수 있다.
export default function App() {const [state, setState] = React.useState({status: "idl",image: { message: null },error: null});const fetchDog = (_, error) => {setState({ status: "pending" });fetchRandomDog(error).then((data) => {setState({ status: "resolved", image: data });},(error) => {setState({ status: "rejected", error });});};React.useEffect(() => {fetchDog();}, []);const { status, image, error } = state;if (status === "rejected") {// Throw error to let ErrorBoundary handle this errorthrow error;}return (<div className="App">{status === "pending" ? (<div>is Loading...</div>) : (<><button type="button" onClick={fetchDog}>Change photo</button><buttontype="button"onClick={(e) =>fetchDog(e, new Error("oops! something went wrong"))}>Throw error</button><img src={image.message} alt="random dog" /></>)}</div>);}이제 네트워크 에러가 발생했을 때,
에러뷰를 App 컴포넌트 내에 만들 필요가 없이 그냥 throw하기만 하면,
ErrorBoundary가 핸들링해준다!
react-error-boundary
ErrorBoundary를 만들어쓰기 귀찮다면 react-error-boundary라는 유용한 패키지가 있다.
(강의에서는 아래 라이브러리 사용을 매우 권장했다!)
function ErrorFallback({error, resetErrorBoundary}) {return (<div role="alert"><p>Something went wrong:</p><pre>{error.message}</pre><button onClick={resetErrorBoundary}>Try again</button></div>)}function Bomb() {throw new Error('💥 CABOOM 💥')}function App() {const [explode, setExplode] = React.useState(false)return (<div><button onClick={() => setExplode(e => !e)}>toggle explode</button><ErrorBoundaryFallbackComponent={ErrorFallback}onReset={() => setExplode(false)}resetKeys={[explode]}>{explode ? <Bomb /> : null}</ErrorBoundary></div>)}onReset이라는 prop으로 state를 초기화하는 함수를 넣어주고,
FallbackComponent에서 resetErrorBoundary라는 함수를 재시도 버튼과 연결해주면,
재시도 버튼을 클릭했을때 state가 초기화된다.
여튼 다양한 기능들을 제공해주니 나중에 한 번 프로젝트에 적용해봐야겠다.
반응형'Frontend' 카테고리의 다른 글
JavaScript, 숫자 타입이 아닌 값을 숫자로 바꾸는 다양한 방법 - feat. Number()/parseInt/Unary plus(+)/Unary negation(-) (127) 2020.11.08 [Kent C. dodds - Epic React] Advanced React Hooks(1) - useReducer 이해하기 (252) 2020.11.08 [Kent C. dodds - Epic React] React hooks(1) - Lazy initial state / useRef (252) 2020.10.25 알아두면 유용한 TypeScript의 Utility type 정리 (252) 2020.06.21 [Svelte] Svelte 기초 - Svelte로 Form 다루기 / Custom Event Dispatch하기 (127) 2020.06.21