자바스크립트/러닝 리엑트

Suspense

막이86 2023. 11. 13. 13:02
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

'자바스크립트 > 러닝 리엑트' 카테고리의 다른 글

리액트 라우터  (0) 2023.11.13
리액트 테스트  (1) 2023.11.13
데이터 포함시키기  (0) 2023.11.13
훅스 컴포넌트 개선하기  (1) 2023.11.13
리액트 상태 관리  (0) 2023.11.13