Search
Duplicate

리액트 서스펜스

수업
React 완벽 가이드
주제
5 more properties

Suspense

Suspense를 사용하면 자식이 로딩을 완료할 때까지 fallback을 표시할 수 있습니다.
<Suspense fallback={<Loading />}> <SomeComponent /> </Suspense>
TypeScript
복사
children: 렌더링하려는 실제 UI입니다. 렌더링 하는 동안 일시 중지되면, children Suspense 경계가 fallback으로 전환됩니다.
fallback: 로드가 완료되지 않은 경우 실제 UI 대신 렌더링될 대체 UI입니다. Suspense는 fallback 일시 중단되면 자동으로 children 전환됩니다. 렌더링 중에 fallback이 일시 중단되면 가장 가까운 suspense 경계가 활성화 됩니다.

주의사항

React는 처음 마운트하기 전에 일시 중단된 렌더링의 state를 보존하지 않습니다. 컴포넌트가 로드되면 React는 일시 중단된 트리의 렌더링을 처음부터 다시 시도합니다.
Suspense가 트리에 대한 콘텐츠를 표시하고 있다가 다시 일시 중단된 경우, 그 원인이 된 업데이트 startTransition이나 useDeferredValue로 인한 것이 아니라면 fallback이 다시 표시됩니다.
React가 다시 일시 중단되어 이미 표시된 콘텐츠를 숨겨야하는 경우, 콘텐츠 트리에서 layout Effect를 클린업 합니다. 콘텐츠가 다시 표시될 준비가 되면 React는 layout Effect를 다시 실행합니다.
React에는 Suspense와 통합된 스트리밍 서버 렌더링 및 선택적 Hydration과 같은 내부 최적화가 포함되어 있습니다.

사용법

<Suspense fallback={<Loading />}> <Albums /> </Suspense>
TypeScript
복사
콘텐츠 로딩동안 폴백
export default function ArtistPage({ artist }) { return ( <> <h1>{artist.name}</h1> <Suspense fallback={<Loading />}> <Albums artistId={artist.id} /> </Suspense> </> ); } function Loading() { return <h2>🌀 Loading...</h2>; }
TypeScript
복사
로딩 → Album 출력
오직 Suspense를 도입한 데이터 소스에서만 Suspense 컴포넌트를 활성화할 수 있습니다.
Relay, Next.js와 같은 Suspense 도입 프레임워크
lazy를 사용한 지연 로딩 컴포넌트
Effect나 이벤트 핸들러 내부에서 페칭하는 경우는 감지하지 않습니다.

콘텐츠 한번에 드러내기

기본적으로 Suspense 내부의 전체 트리는 단일 단위로 취급됩니다. 예를 들어 아래의 코드는 컴포넌트 중 하나만 데이터 대기를 위해 일시 중단하더라도 모든 컴포넌트가 함께 로딩 표시기로 대체됩니다.
<Suspense fallback={<Loading />}> <Biography /> <Panel> <Albums /> </Panel> </Suspense>
TypeScript
복사
export default function ArtistPage({ artist }) { return ( <> <h1>{artist.name}</h1> <Suspense fallback={<Loading />}> <Biography artistId={artist.id} /> <Panel> <Albums artistId={artist.id} /> </Panel> </Suspense> </> ); } function Loading() { return <h2>🌀 Loading...</h2>; }
TypeScript
복사
모든 항목이 표시될 준비가 되면, 한꺼번에 표시됩니다.
Biography와 Albums 모두 데이터를 가져오지만, 단일 Suspense 경계 아래에 그룹화되어 있기 때문에 항상 동시에 등장합니다.

중첩된 콘텐츠가 로드될 때 표시하기

컴포넌트가 일시 중단되면 가장 가까운 상위 Suspense 컴포넌트가 fallback을 표시합니다.
<Suspense fallback={<BigSpinner />}> <Biography /> <Suspense fallback={<AlbumsGlimmer />}> <Panel> <Albums /> </Panel> </Suspense> </Suspense>
TypeScript
복사
export default function ArtistPage({ artist }) { return ( <> <h1>{artist.name}</h1> <Suspense fallback={<BigSpinner />}> <Biography artistId={artist.id} /> <Suspense fallback={<AlbumsGlimmer />}> <Panel> <Albums artistId={artist.id} /> </Panel> </Suspense> </Suspense> </> ); } function BigSpinner() { return <h2>🌀 Loading...</h2>; } function AlbumsGlimmer() { return ( <div className="glimmer-panel"> <div className="glimmer-line" /> <div className="glimmer-line" /> <div className="glimmer-line" /> </div> ); }
TypeScript
복사
여러 Suspnese컴포넌트를 중첩하여 로딩 시퀀스를 만들 수 있으며, 각 Suspense 경계의 폴백은 다음 레벨의 콘텐츠를 사용할 수 있게 되면 채워집니다.
Suspense 경계를 사용하면 UI의 어떤 부분이 항상 동시에 등장 해야 하는지, 어떤 부분이 로딩 상태의 시퀀스에 점진적으로 더 많은 컨텐츠를 표시해야 하는지 조정할 수 있습니다.
모든 컴포넌트에 suspense 경계를 설정하지 말고, 정확히 어떤 위치에 넣어야 하는지 잘 판단해야합니다.

새 콘텐츠가 로드되는 동안 오래된 콘텐츠 표시하기

아래 예제에서는 검색 결과를 가져오는 동안 SearchResults 컴포넌트가 일시 중단 됩니다.
export default function App() { const [query, setQuery] = useState(''); return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <SearchResults query={query} /> </Suspense> </> ); }
TypeScript
복사
일반적인 대체 UI패턴은 목록 업데이트를 연기하고, 새 겨로가가 준비될 때 까지 이전 결과를 계속 표시하는 것입니다. useDeferredValue 훅을 사용하면 쿼리의 지연된 버전을 전달할 수 있습니다.
export default function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <div style={{ opacity: isStale ? 0.5 : 1 }}> <SearchResults query={deferredQuery} /> </div> </Suspense> </> ); }
TypeScript
복사

ReactQuery 전역설정

import { MutationCache, MutationKey, QueryCache, QueryClient, QueryKey, } from 'react-query'; // 전역 쿼리 캐시 설정 const queryCache = new QueryCache({ onError: (error: Error, query: { queryKey: QueryKey }) => { // 모든 쿼리에 대한 기본 에러 핸들링 로직 console.error(`Query failed: ${query.queryKey}`, error); }, onSuccess: (data: unknown, query: { queryKey: QueryKey }) => { // 모든 쿼리의 성공 로직 console.log(`Query succeeded: ${query.queryKey}`, data); } }); // 전역 뮤테이션 캐시 설정 const mutationCache = new MutationCache({ onError: ( error: Error, variables: unknown, context: unknown, mutation: { mutationKey: MutationKey } ) => { // 모든 뮤테이션에 대한 기본 에러 핸들링 로직 console.error(`Mutation failed: ${mutation.mutationKey}`, error); }, onSuccess: ( data: unknown, variables: unknown, context: unknown, mutation: { mutationKey: MutationKey } ) => { // 모든 뮤테이션의 성공 로직 console.log(`Mutation succeeded: ${mutation.mutationKey}`, data); } }); // QueryClient 인스턴스 생성, 캐시 설정 적용 const queryClient = new QueryClient({ queryCache, mutationCache, defaultOptions: { queries: { refetchOnWindowFocus: false, // 예시 옵션 // 기타 쿼리 관련 전역 설정... }, mutations: { // 기타 뮤테이션 관련 전역 설정... }, }, });
TypeScript
복사

ReactSuspenseQuery

Error Boundary

Error Boundary는 컴포넌트 트리의 어느 지점에서 발생한 JS 에러를 포착하여, 에러 메시지를 로깅하고 대체 UI를 표시하는 메커니즘 입니다. 이를 통해 에러로 인해 전체 앱이 망가지는것을 방지할 수 있습니다.
Error Boundary는 하위 컴포넌트 트리에서 발생한 에러를 포착하며, 이는 렌더링, 라이프사이클 메서드 및 컴포넌트의 생성자 내에서 발생한 에러를 포함합니다.
에러가 발생했을 때 대체 UI를 생성해서, 나머지 부분이 정상적으로 동작하도록 합니다.
초기 렌더링, 이벤트 핸들러 내부 오류, 비동기 코드에서 오류를 감지하지 않으며 오류 경계는 트리에서 아래 구성 요소의 오류만 감지하고 오류 경계 자체의 오류는 감지하지 않습니다.

useQueryErrorResetBoundary

reactQuery에서 ErrorBoundary와 useQueryErrorRestBoundary를 결합해 선언적으로 에러가 발생했을 때 Fallback UI를 보여줄 수 있습니다.
useQueryErrorResetBoundary는 ErrorBoundary와 함께 사용되는데 이는, 기본적으로 리액트 공식문서에서 기본 코드 베이스가 제공되긴 하지만 좀 더 쉽게 활용할 수 있게 해주는 react-error-boundary가 존재하며, react-query 공식문서에서도 해당 라이브러리로 예제를 제공합니다.
yarn add react-error-boundary
TypeScript
복사
이후 App에다 QueryErrorBoundary 컴포넌트를 추가하면 된다
interface Props { children: React.ReactNode; } export const QueryErrorBoundary = ({ children }: Props) => { const { reset } = useQueryErrorResetBoundary(); // (*) return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> Error!! <button onClick={() => resetErrorBoundary()}>Try again</button> </div> )} > {children} </ErrorBoundary> ); };
TypeScript
복사
const queryClient = new QueryClient({ defaultOptions: { queries: { throwOnError: true, // (*) 여기서는 글로벌로 셋팅했지만 개별 쿼리로 셋팅가능 }, }, }); function App() { return ( <QueryClientProvider client={queryClient}> <QueryErrorBoundary>{/* 하위 컴포넌트들 */}</QueryErrorBoundary> </QueryClientProvider> ); }
TypeScript
복사