Yarr와 Relay를 이용해 Render-as-you-fetch 라우팅을 구현하기

Jaeho Lee·2022년 2월 15일
4
post-thumbnail

이 포스트는 Contra 로부터 후원을 받아 작성된 포스트입니다.

일반적으로 React 기반의 CRUD SPA앱을 개발하다 보면 가장 신경쓰게 되는 부분이 데이터 페칭 방식이다. CRUD SPA 앱은 보통 서버에서 보내주는 내용을 UI에서 표시하고, 사용자와의 인터랙션을 통해 원격으로 존재하는 데이터를 조작하는 사용 흐름을 가지고 있다. 하지만 앱이 점점 크고 복잡해질수록 사용자 경험은 나빠지고, 버그가 발생하기 쉬워진다.

이런 현상에 대한 대안으로서 Facebook에서 개발한 프레임웍이 Relay인데, 'Render as you fetch'라는 렌더링과 데이터 로딩 방식을 달성하기 위함이다.

Render-as-you-fetch란 무엇인가

React 문서에서는 데이터 로딩을 하는 세가지 접근방식을 설명하고 있다.

  • Fetch-on-render: render가 이뤄진 후 쿼리 훅이나, useEffect 훅 안에서 데이터 페칭을 하는 가장 일반적인 방식을 생각하면 될 것 같다. 이 방식이 가진 문제는 '네트워크 워터폴' 현상을 일으킨다는 것인데, 상부에서 하부 컴포넌트로 내려가면서 렌더링하고, 요청을 하는 일이 반복되고 많은 네트워크 로딩 상태(그리고 기다리는 시간)을 순차적으로 유저에게 보여주게 되기 때문이다.

  • Fetch-then-render: Render 전에 하부 컴포넌트들에게 필요한 모든 데이터를 불러오는 방식이다. 구현이 가장 간단하고 위에서 서술한 네트워크 워터폴 문제가 발생하지 않기에 로딩한 다음에 빠른 인터랙션이 보장되지만, 부분적으로 데이터를 가져오고 변경하는 등의 구현을 하는 것이 어려워 중복된 데이터 요청을 많이 하게 될 뿐만 아니라, 코드 변경에 취약하다.

  • Render-as-you-fetch: Fetch-then-render 전략처럼 모든 필요한 데이터를 불러오지만, 다른 점은 데이터가 모두 불러와질 때 까지 렌더링을 멈추고 기다리지 않는다는 점이다.

  • 참고: epicreact.dev

React 앱에서 render-as-you-fetch 라우팅을 구현하는 방법

특정 route를 render할 때, 컴포넌트가 '어떤 데이터를 불러오기 시작해야 할 지' 알고있어야 한다.

👉 Route 별 preloading function, loader function 같은 형태로 보통 API를 제공한다.

Yarr에 대한 소개

  • 글을 쓰는 시점의 라이브러리 버젼은 v2.0.4
  • Yet Another React Router의 약자로 Contra에서 Relay와 같이 사용하기 위해 개발하다 공개한 라이브러리이다.
  • 현재 프로젝트 초기인 관계로 매뉴얼과 용례가 다소 부족하나, 기본적으로 react-router 등과 같이 필요한 API들은 모두 갖춰져 있다. 코드를 읽어보니, 아직 리퍼런스 상에 써져있지 않은 동작이나 인터페이스들이 제법 존재했다. (i.e. 가령 router config 중 하나인 assistPreload 라든지)

기본적인 routing example

// 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 해보도록 하겠다.

API 살펴보기

  • 컴포넌트 <RouterProvider />
    • router 오브젝트를 제공하는 Context Provider로, createBrowserRouter 등의 router object를 만드는 함수를 이용해 router 오브젝트를 초기화시 넘겨줄 수 있다.
    • route 오브젝트를 넘겨줄 때에는, awaitComponent awaitPreload 등의 옵션을 줄 수 있는데, 이는 preload 시점과 컴포넌트 로딩 시점을 정의하는 플래그이다. 참고
    • useRouter 훅을 통해 RouterContext 에 접근할 수 있다.
  • 컴포넌트 <RouteRenderer />
    • 실제로 route를 render하는 컴포넌트이며, 실질적인 route transition 등의 동작이 이 컴포넌트에서 일어난다.
    • Render-as-you-fetch 패턴을 달성하기 위해서는 최소한 이 컴포넌트는 Suspense로 감싸줘야 한다.
  • 컴포넌트 <Link /> <Redirect />
    • React router와 거의 동일하다.
  • useRouteProps
    - 컴포넌트 내의 active route에 대한 prop들 - params search 들을 반환하며, react-router-dom의 useLocationuseParams 가 섞인 형태이다.
  • useNavigation
    • react-router의 useHistory 에 해당하는 훅으로, history 조작을 할 수 있다. (back, push, replace 등)
  • useBlockTransition
    - 사용자 이탈 시 표시 메시지 훅 (beforeunload 이벤트를 이용한)

핸즈온

swapi-graphql을 이용해 (별로 실용적이지는 않지만) 각 필드들의 관계들을 wiki처럼 볼 수 있는 예제를 만들었다.

Relay의 usePreloadedQuery 훅 API를 이용한 render-as-you-fetch 패턴의 구현으로, 로딩 상태의 표현이나 데이터 로딩 방식에 대한 특별한 고민 없이 굉장히 쉽고 빠르게 페이지를 구성할 수 있는 점이 인상적이었다.

타 라이브러리/프레임웍과의 비교

위에서 API와 핸즈온까지 포함해, 최신 라우팅 라이브러리인 Yarr를 간단히 살펴보았다. 그 외 현재 비슷한 기능을 가지고 있는 라이브러리와 프레임워크들 또한 비교해보자면:

  • react-location
    • Route Loader 기능으로 preloading을 성취할 수 있다.
    • 'Pending state' 등의 자체 구현 등, React Suspense를 활용하기보다는 자체적인 Suspense-like 메커니즘을 구현하고 있어, 추후 React 18 릴리즈의 수혜를 받지 못할 수도 있을 것 같다.
  • remix
    - loader function을 이용, 훅으로 preload된 데이터를 받아올 수 있음.
    - Relay/GraphQL을 이용하는 상황에는 다소 적합하지 않다.
  • react-resource-router
    - route 별로 필요한 'resource'를 정의하는 점은 비슷하나, Relay같은 방식의 API를 사용하기 약간 불편하다.

결론: Yarr가 어떤 점이 좋은가? 쓸만한가?

코드를 읽어봤을 때, React 18을 고려해서 기존 라이브러리(특히 react-router)들의 구현을 발전시킨 형태라고 느껴져 fresh-baked라는 느낌보다는 구현이 안정적이라고 느껴졌다.

현재로서는 Next나 Remix와 같은 SSR 프레임웍이 아닌, 클라이언트 렌더링만 하는 경우를 고려해 Render-as-you-fetch 를 고려한 라우팅 라이브러리를 찾아보기 힘든 상황이다. Relay 지원을 염두에 두고 만들어졌기 때문에,

  • Relay를 사용한다
  • Next.js 나 Remix와 같은 프레임웍의 사용을 고려하지 않고 있다

와 같은 환경이라면, 비록 신생(?) 라이브러리지만 사용을 충분히 고려해볼만한 하다는 결론이다.

profile
개발자

0개의 댓글