이 포스트는 Contra 로부터 후원을 받아 작성된 포스트입니다.
일반적으로 React 기반의 CRUD SPA앱을 개발하다 보면 가장 신경쓰게 되는 부분이 데이터 페칭 방식이다. CRUD SPA 앱은 보통 서버에서 보내주는 내용을 UI에서 표시하고, 사용자와의 인터랙션을 통해 원격으로 존재하는 데이터를 조작하는 사용 흐름을 가지고 있다. 하지만 앱이 점점 크고 복잡해질수록 사용자 경험은 나빠지고, 버그가 발생하기 쉬워진다.
이런 현상에 대한 대안으로서 Facebook에서 개발한 프레임웍이 Relay인데, 'Render as you fetch'라는 렌더링과 데이터 로딩 방식을 달성하기 위함이다.
React 문서에서는 데이터 로딩을 하는 세가지 접근방식을 설명하고 있다.
Fetch-on-render: render가 이뤄진 후 쿼리 훅이나, useEffect 훅 안에서 데이터 페칭을 하는 가장 일반적인 방식을 생각하면 될 것 같다. 이 방식이 가진 문제는 '네트워크 워터폴' 현상을 일으킨다는 것인데, 상부에서 하부 컴포넌트로 내려가면서 렌더링하고, 요청을 하는 일이 반복되고 많은 네트워크 로딩 상태(그리고 기다리는 시간)을 순차적으로 유저에게 보여주게 되기 때문이다.
Fetch-then-render: Render 전에 하부 컴포넌트들에게 필요한 모든 데이터를 불러오는 방식이다. 구현이 가장 간단하고 위에서 서술한 네트워크 워터폴 문제가 발생하지 않기에 로딩한 다음에 빠른 인터랙션이 보장되지만, 부분적으로 데이터를 가져오고 변경하는 등의 구현을 하는 것이 어려워 중복된 데이터 요청을 많이 하게 될 뿐만 아니라, 코드 변경에 취약하다.
Render-as-you-fetch: Fetch-then-render 전략처럼 모든 필요한 데이터를 불러오지만, 다른 점은 데이터가 모두 불러와질 때 까지 렌더링을 멈추고 기다리지 않는다는 점이다.
특정 route를 render할 때, 컴포넌트가 '어떤 데이터를 불러오기 시작해야 할 지' 알고있어야 한다.
👉 Route 별 preloading function, loader function 같은 형태로 보통 API를 제공한다.
assistPreload
라든지)// routes.js
// routes 모듈에서 flat한 형태로 route들을 정의하고,
// `preload` 필드를 줘서 프리로딩할 데이터까지 정의할 수 있다.
export const routes = [
{
component: async () => {
const module = await import('./pages/About');
return module.AboutPage;
},
// Relay는 쿼리 리퍼런스를 사용부에서 `usePreloadedQuery` 같은 훅에서
// consume하는 형태의 API를 가지고 있다.
// 이렇게 사용부들에서 정의한 개별적 쿼리들이,
// Relay 컴파일러를 통해 더 적은 수의 네트워크 요청으로 통합된다.
preload: () => ({
query: loadQuery(RelayEnvironment, AboutPageQuery, {})
}),
path: '/about',
},
{
component: async () => {
const module = await import('./pages/Home');
return module.HomePage;
},
path: '/',
},
{
component: async () => {
const module = await import('./pages/NotFound');
return module.NotFoundPage;
},
path: '*', // 매치되지 않은 쿼리들은 NotFound 페이지를 표시할 것이다.
},
];
// App.js
// Route를 실질적으로 렌더링하는 부분이다.
import { Suspense } from 'react';
import { RouteRenderer } from 'yarr';
export default function App() {
return (
<Suspense fallback={<p>loading...</p>}>
// Suspense로 감싸줘야 함
<RouteRenderer
pendingIndicator={<p>pending...</p>}
routeWrapper={({ Route }) => (
<>
<nav>Navigation</nav>
<div className="route">{Route}</div>
</>
)}
/>
</Suspense>
);
}
// index.js
import ReactDOM from 'react-dom';
import { RouterProvider, createBrowserRouter } from 'yarr';
import { routes } from './routes';
import App from './App';
ReactDOM.render(
<RouterProvider router={createBrowserRouter({ routes })}>
<App />
</RouterProvider>,
document.getElementById('root')
);
Relay를 포함한 실제 작동 예제는 공식 codesandbox 예제와 guide를 살펴보도록 하자.
또한 아래에서 API를 간단하게 살펴보고, 공식 예제를 변환해 hands-on 해보도록 하겠다.
<RouterProvider />
createBrowserRouter
등의 router object를 만드는 함수를 이용해 router 오브젝트를 초기화시 넘겨줄 수 있다.awaitComponent
awaitPreload
등의 옵션을 줄 수 있는데, 이는 preload 시점과 컴포넌트 로딩 시점을 정의하는 플래그이다. 참고useRouter
훅을 통해 RouterContext
에 접근할 수 있다.<RouteRenderer />
<Link />
<Redirect />
useRouteProps
params
search
들을 반환하며, react-router-dom의 useLocation
과 useParams
가 섞인 형태이다.useNavigation
useHistory
에 해당하는 훅으로, history 조작을 할 수 있다. (back, push, replace 등)useBlockTransition
beforeunload
이벤트를 이용한) swapi-graphql을 이용해 (별로 실용적이지는 않지만) 각 필드들의 관계들을 wiki처럼 볼 수 있는 예제를 만들었다.
Relay의 usePreloadedQuery
훅 API를 이용한 render-as-you-fetch 패턴의 구현으로, 로딩 상태의 표현이나 데이터 로딩 방식에 대한 특별한 고민 없이 굉장히 쉽고 빠르게 페이지를 구성할 수 있는 점이 인상적이었다.
위에서 API와 핸즈온까지 포함해, 최신 라우팅 라이브러리인 Yarr를 간단히 살펴보았다. 그 외 현재 비슷한 기능을 가지고 있는 라이브러리와 프레임워크들 또한 비교해보자면:
코드를 읽어봤을 때, React 18을 고려해서 기존 라이브러리(특히 react-router)들의 구현을 발전시킨 형태라고 느껴져 fresh-baked라는 느낌보다는 구현이 안정적이라고 느껴졌다.
현재로서는 Next나 Remix와 같은 SSR 프레임웍이 아닌, 클라이언트 렌더링만 하는 경우를 고려해 Render-as-you-fetch 를 고려한 라우팅 라이브러리를 찾아보기 힘든 상황이다. Relay 지원을 염두에 두고 만들어졌기 때문에,
와 같은 환경이라면, 비록 신생(?) 라이브러리지만 사용을 충분히 고려해볼만한 하다는 결론이다.