isLoading , isError분기처리)은 모든 하위컴포넌트에서 데이터 상태에 따른 UI 로직과 데이터 로직이 뒤섞여 복잡도를 높이며 유지보수를 어렵게 했다.Suspense 와 ErrorBoundary 를 도입하여 데이터가 준비될 때까지 렌더링을 유예하고 에러를 한곳에서 처리하는 선언적 방식으로 전환하여 DX를 개선하고 사용자에게는 일관된 로딩 및 에러 UI를 제공하는 예측 가능한 아키텍처를 구축하고자 했다.Suspense 가 동작하려면 화면 렌더링 중 promise가 발생해야지만 fallback UI를 보여주게 된다.
그러므로 일반적인 쿼리가 아닌 useSuspenseInfiniteQuery 를 사용하여 데이터 패칭을 진행했다.
| 비교 항목 | useQuery (명령적) | useSuspenseQuery (선언적) |
|---|---|---|
| 데이터 타입 | `T | undefined` (체크 필요) |
| 로딩 처리 | if (isLoading) 분기 처리 직접 작성 |
상위 <Suspense> 컴포넌트가 위임받음 |
| 에러 처리 | if (isError) 분기 처리 직접 작성 |
상위 <ErrorBoundary>가 위임받음 |
| 핵심 메커니즘 | 상태(State) 기반 렌더링 | Promise Throwing 기반 렌더링 |
| 비교 항목 | useInfiniteQuery (기존) | useSuspenseInfiniteQuery (도입) |
|---|---|---|
| 렌더링 방식 | 데이터 유무와 상관없이 즉시 렌더링 | 데이터가 올 때까지 렌더링을 일시 중단 |
| 데이터 타입 | data가 undefined일 수 있음 (Optional) |
data가 항상 존재함을 보장 (Required) |
| 로딩 상태 관리 | if (isLoading) 등 조건부 렌더링 필요 |
상위 <Suspense> 가 전담 (로직 분리) |
| 에러 상태 관리 | if (isError) 등 조건부 렌더링 필요 |
상위 <ErrorBoundary> 가 전담 |
| 컴포넌트 책임 | 데이터 상태에 따른 UI 분기 책임까지 가짐 | 오직 데이터 출력에만 집중 (관심사 분리) |
이때 컴포넌트는 use client + client fetch 로 작성을 했는데 서버환경에서 실행되어 에러가 발생했다.
TypeError: Failed to parse URL
패치 경로가 상대경로가 아닌 절대경로여야 하는 에러가 발생했다
그러면 Suspense 쿼리가 어떻게 동작하기에 서버에서 실행이 되는가?
Promise Throwing과 렌더링 블로킹
Suspense 쿼리는 쿼리에 데이터가 없으면 Promise를 Throw 한다
React 는 이 Promise를 캐치하여 컴포넌트 렌더링을 일시중단 상태로 만들고 가장 가까운 Suspense boundary의 fallback 을 보여준다.
이때 서버에서도 해당 로직이 작동하여 데이터를 다 채운뒤 완성된 HTML을 전달하려고 시도한다.
Promise가 resolve되면 React는 중단됐던 지점부터 다시 렌더링을 시작한다.
즉 Hydration을 보장한다.
이런 매커니즘으로 인해 클라이언트 패치로 작성 했지만 서버 환경에서 실행 될 수 있었던 것이다.
useSuspenseInfiniteQuery는 기본적으로 이 데이터 없이는 렌더링 안한다고 선언참고 : 브라우저는 `window.location`를 통해 상대 경로의 기준점을 찾을 수 있어
현재 도메인을 자동으로 붙여주지만 서버환경에서는 기준 도메인을 찾을 수 없기 때문에
`fetch` 호출 시 `TypeError: Failed to parse URL`이 발생한다.
그럼 여기서 궁금한게 발생한다.