-
[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> <button type="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 Error fetchRandomDog(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> <button type="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 error throw error; } return ( <div className="App"> {status === "pending" ? ( <div>is Loading...</div> ) : ( <> <button type="button" onClick={fetchDog}> Change photo </button> <button type="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> <ErrorBoundary FallbackComponent={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 COMMENT