728x90
러닝 리엑트을 요약한 내용입니다.
- Suspense는 대규모 앱에서 겪은 문제를 해결하기 위해 설계됐다.
- 모두가 페이스북과 같은 문제를 겪지는 않는다.
- 우리가 직면한 문제에 대한 해법으로 선택하기 전에 여러번 더 생각해보기 바란다.
- 이러한 도구를 채택하면 불필요한 복잡도가 추가될 수 있다.
- 동시성 모드는 실험적인 기능이고, 리액트 팀은 프로덕션 환경에서 동시성 모드를 사용하지 말라고 엄중히 경고한다.
- 매일 매일 커스텀훅을 개발하는 것이 아니라면 아마도 이런 기능을 결코 알 필요가 없을 것이다.
- Suspense와 관련한 대부분의 기계장치는 훅을 통해 추상화 가능하다.
- 여러 단점을 고려하더라도 이번장에서 다루는 주제는 흥미진진한 주제이다.
- 기능을 제대로 사용한다면 더 나은 사용자 경험을 만들어낼 수 있다.
- 훅이나 컴포넌트 등으로 구성된 리액트 라이버르리를 수유하거나 관리하고 있다면 이번장에서 다루는 개념이 귀하게 느껴질 것이다.
- 8장에서 만든 앱을 다시 구축해서 사용할 것이다.
- SiteLayout 컴포넌트
- export default function SiteLayout({ children, menu = c => null }) { return ( <div className="site-container"> <div>{menu}</div> <div>{children}</div> </div> ) }
- SiteLayout은 App 컴포넌트 안에서 렌더링되며 UI 합성을 돕는다.
- export default function App() { return ( <SiteLayout menu={<p>Menu</P>}> <> <Callout>Callout</Callout> <h1>Contents</h1> <p>This is the main part of the example layout</p> </> </SiteLayout> ) }
9.1 오류 경계
- 지금까지는 오류를 처리하기 위해 최선을 다하지 않았다.
- 컴포넌트 트리 안에서 발생한 오류는 전체 어플리케이션을 타고 내려갔다.
- 컴포넌트 트리가 커지면 프로젝트가 더 복잒해지고 디버깅도 더 복잡해진다.
- 오류가 발생한 장소를 정확하기 알아내기 어려울 때도 있다.
- 오류 경계는 오류가 전체 앱을 망가뜨리지 못하게 막는 컴포넌트를 뜻한다.
- 오류 경계가 있으면 프로턱션 환경에서 발생한 원인과 위치를 알아보기 쉬운 오류 메세지를 표시할 때도 도움이 된다.
- 오류를 한 컴포넌트가 처리하기 때문에 이 컴포넌트가 애플리케이션에서 발생할 가능성이 있는 오류를 추적하고 이슈 관리 시스템에 등록할 수도 있다.
- 현재 오류 경계 컴포넌트를 만드는 유일한 방법은 클래스 컴포넌트를 사용하는 것 뿐이다.
- 이번 장에서 설명하는 대부분의 주제와 마찬가지로 이 사실도 언젠가는 변할 수 있다.
- 클래스를 만들지 않아도 되는 훅 등의 다른 해법을 통해 오류 경계를 만들게 될 수 있다.
- ErrorBoundary 컴포넌트
- 클래스 컴포넌트 이다.
- 클래스 컴포넌트는 상태를 다른 방식으로 저장하며, 훅도 사용하지 않는다.
- 클래스 컴포넌트는 컴포넌트 생명주기에 따라 호출되는 구체적인 메서드를 사용할 수 있다.
- getDerivedStateFromError 는 생명주기 메서드 이다.
- 렌더링 과정에서 Children 내부에서 오류가 발생했을 때 호출 된다.
- 오류가 발생하면 state.error가 설정된다.
- 오류가 있으면 fallback 컴포넌트가 랜더링되며 컴포넌트의 프로퍼티로 오류가 전달된다.
import React, { Component } from "react" export default class ErrorBoundary extends Component { state = { error: null } static getDerivedStateFromError(error) { return { error } } render() { const { error } = this.state const { children, fallback } = this.props if (error) { return <fallback error={error} /> } return children } }
- ErrorScreen은 사용자에게 오류가 발생했음을 알려주는 친절한 메시지를 표시한다.
- 이 컴포넌트는 오류의 상세 정보를 렌더링한다.
- 앱 내부에서 오류가 발생했을 때 추적할 수 있는 장소를 제공한다.
function ErrorScreen({ error }) { // // 여기서 메시지를 랜더링하기 전에 오류를 추적하거나 처리할 수 있다. // return ( <div className="error"> <h3>We are sorry... something went wrong</h3> <p>We cannot process yout request at this moment.</p> <p>ERROR: {error.message}</p> </div> ) }
- 약간의 CSS를 사용해 더 보기 좋게 만들 수 있다.
- .error { background-color: #efacac; border: double 4px darkred; color: darkred; padding: 1em; }
- 컴포넌트를 테스트하려면 의도적인 오류를 발생시키는 컴포넌트를 만들어야 한다.
- BreakThings는 항상 오류를 던진다.
const BreakThings = () => { throw new Error("We intentionally broke something") }
- 오류 경계를 합성할 수도 있다.
- 각 ErrorBoundary는 자신의 자식안에서 오류가 발생하면 fallback을 렌더링한다.
- 여기서는 menu와 Callout안에서 오류를 발생시킨다.
- ErrorBoundary가 각가의 위치에 렌더링된 모습을 볼수 있다.
- 2가지 오류가 서로다른 영역에 포함되어 있음에 유의하자.
- 나머지 부분은 정상적으로 나오게 된다.
return ( <SiteLayout menu={ <ErrorBoundary fallback={ErrorScreen}> <p>Site Layout Menu</p> <BreakThings /> </ErrorBoundary> } > <ErrorBoundary fallback={ErrorScreen}> <Callout>CalloutBreakThings</Callout> </ErrorBoundary> <ErrorBoundary fallback={ErrorScreen}> <h1>Contensts</h1> <p>this is the main part of the example layout</p> </ErrorBoundary> </SiteLayout> )
- 애플리케이션 전방적으로 오류를 일관성 있게 처리하고 싶을 때 이런 방법을 사용할 수 있다.
- render() { const { error } = this.state const { children } = this.props if (error && !fallback) { return <ErrorScreen error={error} /> } if (error) { return <fallback error={error} /> } }
- 컴포넌트 트리에서 원하는 부분을 ErrorBoundary로 감싸기만하면 나머지 오류처리는 ErrorBoundary 컴포넌트가 알아서 해준다.
- <ErrorBoundary> <h1>Contents</h1> <p>this is the main part of the example layout</p> <BreakThings /> </ErrorBoundary>
- 오류 경계를 적용하는 것은 오류 처리에 좋은 생각일 뿐 아니라 프로덕션 앱에서 사용자를 유지하기 위해서는 필수적이기까지 하다.
- 상대적으로 중요하지 않은 컴포넌트에서 생긴 작은 버그가 애플리케이션 전체를 망치는 일을 방시할 수 있다.
9.2 코드 분리하기
- 만들어진 애플리케이션이 지금은 작다고 해도 나중에도 작다는 보장이 없다
- 수백줄 심지어는 수천줄짜리 컴포넌트가 들어 있는 거대한 코드기반이 될 수도 있다.
- 휴대폰의 느린 네트워크를 사용하는 곳에서는 랜더링이 늦을 수 있다.
- 코드를 모두 다운로드할 때까지 랜더링이 되지 않기 때문에
- 코드 분리는 코드기반을 다루기 쉬운 덩어리로 나누고, 필요에 따라 불러오는 방식을 뜻한다.
- 코드 분리의 강력함을 보여주기 위해 애플리케이션에 사용자 동의 화면을 추가하자
- export default function Agreement() { return ( <div> <p>Terms...</p> <p>These are the terms and stuff. Do you agree?</p> <button onClick={onAgree}>I agree</button> </div> ) }
- App이라는 컴포넌트로부터 Main이라는 컴포넌트로 옴기자.
- Main은 현재 사이트의 레이아웃이 렌더링되는 위치이다.
import React from "react" import ErrorBoundary from "./ErrorBoundary" const SiteLayout = ({ children, menu = c => null }) => { return ( <div className="site-container"> <div>{menu}</div> <div>{children}<div> </div> ) } const Menu = () => { <ErrorBoundary> <p style={{ color: "white" }}>TODO: Build Menu</p> </ErrorBoundary> } const Callout = ({ children }) => ( <ErrorBoundary> <div className="callout">{children}</div> </ErrorBoundary> ) export default function Main() { return ( <SiteLayout menu={<Menu />}> <Callout>Welcome to the site</Callout> <ErrorBoundary> <h1>TODO: Home Page</h1> <p>Complete the main contents for this home page</p> </ErrorBoundary> </SiteLayout> ) }
- App 컴포넌트를 변경해서 사용자가 동의할 때까지 Agreement를 표시하게 하자.
- 사용자가 동의하면 Agreement 마운트를 해제하고 Main 웹사이트 컴포넌트를 렌더링한다.
- 처음 렌더링되는 컴포는트는 Agreement 컴포넌트뿐이다.
- 사용자가 동의하면 agree 값이 true가 되고, Main 컴포넌트가 랜더링된다.
- 문제는 Main 컴포넌트와 자식들은 모든 코드가 한 자바스크립트 파일, 즉 번들로 패키징된다는 점이다.
- 이로 인해 모든 코드기반이 다룬로드 되어야만 Agreement가 처음 렌더링될 수 있다.
import React, {useState} from "react" import Agreement from "./Agreement" import Main from "./Main" import "./SiteLayout.css" export default function App() { const [agree, setAgree] = useState(false) if (!agree) { return <Agreement onAgree={() => setAgree(true)} } return <Main /> }
- 처음부터 컴포넌트를 임포트하지 않고, React.lazy를 사용하면 렌더링이 이뤄지는 시점까지 Main 컴포넌트 적재를 늦출 수 있다.
- 리액트가 컴포넌트가 처음 렌더링되기 전까지는 코드 기반을 적재하지 않도록 지시한다.
- 컴포넌트라 렌더링 되는 시점에 import 함수를 통해 컴포넌트가 import된다.
const Main = React.lazy(() => import("./Main"))
- 실행 시점에 코드를 import하는 것은 인터넷에서 필요한 것을 가져오는 것과 비슷하다.
- 우선 자바스크립트 코드에 대한 요청이 대기상태가 된다.
- 코드 요청이 성공해 자바스크립트 파일이 변환되거나, 코드 요청이 실패해 오류가 발생한다.
- 사용자에게 데이터를 불러오는 중이라고 알려야하는 것처럼, 사용자에게 코드를 불러오는 중이라고 알려야 할 필요가 있다.
9.2.1 소개: Suspense 컴포넌트
- Suspense 컴포넌트는 ErrorBoundary와 비슷하게 작동한다.
- Suspense로 트리상의 특정 컴포넌트를 감싼다.
- 오류가 발생하면 fallback 메시지를 표시하는 대신, Suspense 컴포넌트는 지연 적재가 발생할 때 적재 중 메시지를 렌더링해준다.
- Main 컴포넌트를 지연 적재하도록 변경할 수 있다.
- 앱 초기에 React와 Agreement, ClimbingBoxLoader의 코드 기반만 적재한다.
- React는 Main 컴포넌트를 적재를 보류해 뒀다가 사용자가 Agreement에 동의하면 컴포넌트를 적재한다.
- Main 컴포넌트를 Suspense 컴포넌트로 감쌌다.
- 사용자가 동의하자마자 Main 컴포넌트의 코드기반을 적재하기 시작한다.
- 코드기반에 대한 요청이 진행 중인 상태이기 때문에 Suspense 컴포넌트는 코드 기반이 성공적으로 적재될 때까지 ClimbingBoxLoader를 렌더링해준다.
- 코드기반 적재가 성공하면 Suspense 컴포넌트는 ClimbingBoxLoader 마운트를 해제하고 Main 컴포넌트를 렌더링해준다.
import React, { useState, Suspense, lazy } from "react" import Agreement from "./Agreement" import ClimbingBoxLoader from "react-spinners/ClimbingBoxLoader" const Main = lazy(() => import("./Main")) export default function App() { const [agree, setAgree] = useState(false) if (!agree) { return <Agreement onAgree{() => setAgree(true)} /> } return ( <Suspnese fallback={<ClimbingBoxLoader />}> <Main /> </Suspense> ) }
리액트 스피너는 앱이 동작중이거나 무언가를 적재 중임을 표시하는 스피너 애니메이션으로 이뤄진 라이브러리다. 이 라이브러리에 있는 여러 컴포넌트를 사용할 것이다. 반드시 npm i react-spinners로 라이브러리를 설치하자
- Main 컴포넌트를 적재하려 시도하면서 인터넷 연결이 끊어지면 어떤일이 벌어질까?
- 오류가 발생한 경우 Suspense 컴포넌트를 ErrorBoundary로 감싸면 이런 경우를 처리할 수 있다.
<ErrorBoundary> <Suspnese fallback={<ClimbingBoxLoader />}> <Main /> </Suspense> </ErrorBoundary>
- 이렇게 세 컴포넌트를 합성을 사용하면 대부분의 비동기 요청을 처리할 수 있다.
9.2.2 Suspense를 데이와 함께 사용하기
- 8장에서 github에 요청을 보낼때 생기는 3가지 상태 진행 중, 성공, 실패를 다루기 위한 useFetch 훅과 Fetch 컴포넌트를 만들었다.
- 3가지 상태를 관리하는 것에 관해 우리의 해법이 Fetch와 useFetch 이다.
- ErrorBoundary와 Suspense 컴포넌트를 합성해서 이 3가지 상태를 처리 할수 있다.
- 상태 메시지를 표시할 수 있는 Status라는 컴포넌트가 있다고 가정하자.
- loadStatus 함수를 호출해서 현재 상태 메시지를 얻는다.
import React from "react" const loadStatus = () => "success -ready" function Status() { const status = loadStatus() return <h1>status: {status}</h1> }
- App 컴포넌트 안에서 Status 컴포넌트를 렌더링할 수 있다.
- 상태를 적재하는 동안 무언가 잘못되면 ErrorBoundary가 디폴트 오류 화면을 렌더링한다.
export default function App() { return ( <ErrorBoundary> <Status /> </ErrorBoundary> ) }
- 오류화면을 보기 위해서는 loadStatus 함수 안에서 오류를 발생시켜야 한다.
- const loadStatus = () => { throw new Error("something went wrong") }
3가지 상태중 성공과 실패에 대한 처리를 했다.- 진행 중인 상태는 어떻게 처리해야 할까?
- 프라미스를 throw로 넘기면 이런 상태를 만들 수 있다.
const loadStatus = () => { throw new Promise(resolves => null) }
- throw 할 때마다 이에 따른 대안을 렌더링해주는 Suspense 컴포넌트가 필요하다.
- loadStatus 함수는 여전히 프라미스를 throw하지만 컴포넌트 트리의 상위 노드중에 Suspense 컴포넌트가 설정되어 있기 때문에 이 경우를 처리할 수 있다.
- 프라미스를 throw하면 리액트에게 진행 중인 프라미스를 기다리고 있다고 알려주는 것이다.
- 리액트는 대안인 GridLoader 컴포넌트를 렌더링 하는 방식으로 이에 대응한다.
- loadStatus가 성공적으로 결과를 반환하면 계획대로 Status 컴포넌트를 렌더링한다.
- 무언가 잘못되면 ErrorBoundary가 이를 처리한다.
- loadStatus가 프라미스를 발생시키면 대기 중 상태가 촉발되면서 Suspense 컴포넌트가 이를 처리한다.
export default function App() { return ( <Suspense fallback={<GridLoader />}> <ErrorBoundary> <Status /> </ErrorBoundary> </Suspense> ) }
9.2.3 프라미스 던지기
- 자바스크립트에서 throw 키워드는 기술적으로 오류를 처리하기 위해 사용한다.
- 오류를 처리하지 않으면 전체 앱이 종료된다.
- 브라우저에서 표디쇠는 오류 화면을 create-react-app이 개발자 모드에서 제공하는 기능이다.
- 개발자 모드에 있을 때는 처리되지 않은 예외를 잡아서 화면에 직접 표시해준다.
- 프로덕션 사용자가 볼수 있는 화면은 빈화면, 흰 화면, 아무것도 표시되지 않은 화면이다.
- 자바스크립트에서는 아무 타입의 값이나 던질 수 있다.
- 프라미스도 던질 수 있다.
throw new Promise(resolves => null)
728x90