[번역] vite-plugin-ssr React Tour

MOON HEE·2023년 2월 2일
0

번역하게 된 이유

새로 시작하는 프로젝트에서 클라이언트 세팅을 어떻게 할지 고민했다. 데브스토어에서 vite을 사용해보고 빌드 속도가 빠르기 때문에 이번에도 사용하려고 했다.
하지만 이번 프로젝트에서는 화면 성능까지 고려해보려고 Next.js 도입을 고민했다(Next.js를 쓰면 vite을 못씀). Next에 대해서 공부하다가 어떤 개발자분의 구글 강연 후기를 읽게 됐다.

Google I/) 2019:Day3 후기

이 글을 읽기 전에는 Next.js를 쓰는 이유가 명확하지 않았다. Next.js라는 도구는 Hydration 아키텍처를 구현하기 위한 프레임워크다. Hydration이라는 건 프로젝트 하나를 'CSR 아니면 SSR!' 이렇게 이분법적으로 나누는 게 아니라, 첫 페이지를 SSR로, 나머지 페이지를 CSR로 이루어지게 함으로써 각 렌더링 방식의 단점을 보완하는 도구다.

여기까지 읽어보고 Next.js 쓰려고 했다 ㅎㅎㅎ
하지만 여기에 추가되는 내용도 있었다. Hydration을 사용한다고 해서 웹 성능이 좋아지지는 않는다는 것이다(TTI를 예로 든다).

그래서 Progressive Hydration이라는 개념을 소개한다. Progressive Hydration는 레이지 로딩처럼 필요한 부분만 Hydration을 수행한다. React Suspense를 통해 추후 지원될 예정이라고 나와 있는데, 2019년도 글이라 리액트 공식 문서에서 확인해본 후 이 방법도 고려해야 할 것 같다.

라인 기술 블로그에서 JS 크기가 사이트 성능에 나쁜 영향을 미친다는 내용의 글을 본 적 있다(읽어 보기). 리액트는 라이브러리 자체에 js 파일이 많기도 하지만 실제 기기에서는 js 파일이 더욱 많아진다.

리액트, 리덕스 사용을 생각하고 있는데... 또 설치해야 할 파일이 생길 수도 있다. 그래서 vite의 플러그인으로 Next.js를 대체하기로 했다. 나처럼 vite을 포기하지 못하겠는 여러 개발자들이 있었다 ㅋㅋㅋ

다시 Next.js로 마음을 바꿀 수도 있지만 주요 기능 공부를 위해 번역했다.

1. Routing

Next.js와 유사하게 새로운 .page.jsx 파일을 생성하여 새 페이지를 정의한다.

// /pages/index.page.jsx
// Environment: 브라우저, Node.js

import React, { useState } from "react";
import { Counter } from "../components/Counter";

export { Page };

function Page() {
  return <>
    This page is rendered to HTML and interactive: <Counter />
  </>;
}

기본적으로 vite-plugin-ssr은 파일 시스템 라우팅(Filesystem Routing)을 수행한다.

FILESYSTEM                  URL
/pages/index.page.jsx        /
/pages/about.page.jsx        /about

또한 Route String(/movies/@id와 같은 매개변수화된 경로의 경우) 또는 Route Function(완전한 프로그래밍 유연성을 위한 경우)을 사용하여 페이지의 경로를 정의할 수 있다.

// /pages/index.page.route.js
// Environment: Node.js (Client Routing을 선택한 경우엔 브라우저)

// 두 파일이 동일한 기본 `/pages/index.page.`을 공유하는 방법에 유의하기. 이것이 `vite-plugin-ssr` 방법이다.
// `/pages/index.page.route.js`가 `/pages/index.page.jsx`의 경로를 정의한다.

// Route Function
export default pageContext => pageContext.urlPathname === '/';

// `.page.route.js` 파일을 만들지 않는 경우 vite-plugin-ssr이 파일 시스템 라우팅을 수행한다.

2. Render Control

Next.js와 달리 페이지가 렌더링되는 방식을 제어한다.

// /renderer/_default.page.server.jsx
// Environment: Node.js

import ReactDOMServer from "react-dom/server";
import React from "react";
import { escapeInject, dangerouslySkipEscape } from "vite-plugin-ssr";

export { render };

async function render(pageContext) {
  const { Page, pageProps } = pageContext;
  const viewHtml = ReactDOMServer.renderToString(
    <Page {...pageProps} />
  );

  const title = "Vite SSR";

  return escapeInject`<!DOCTYPE html>
    <html>
      <head>
        <title>${title}</title>
      </head>
      <body>
        <div id="page-view">${dangerouslySkipEscape(viewHtml)}</div>
      </body>
    </html>`;
}
// /renderer/_default.page.client.jsx
// Environment: Browser

import ReactDOM from "react-dom";
import React from "react";

export { render };

async function render(pageContext) {
  const { Page, pageProps } = pageContext
  ReactDOM.hydrate(
    <Page {...pageProps} />,
    document.getElementById("page-view")
  );
}

이 제어를 통해 원하는 모든 도구(Redux, GraphQL, Service Worker, Preact 등)를 쉽고 자연스럽게 통합할 수 있다.

4가지 page file 접미사가 있다:

  • page.js: Node.js뿐만 아니라 브라우저에서 실행
  • page.client.js: 브라우저에서만 실행
  • page.server.js: Node.js에서만 실행
  • page.route.js: 페이지의 Route String 또는 Route Function을 정의.

각 페이지에 대해 .page.client.js.page.server.js 파일을 만드는 대신, 모든 페이지에 대해 기본으로 적용되는 /renderer/_default.page.client.js/renderer/_default.page.server.js를 만들 수 있다.

우리가 만든 마지막 두 파일은 실제로 /renderer/_default.page.client.jsx/renderer/_default.page.server.jsx다. 즉, 이제 새로운 .page.jsx 파일을 정의해서 새로운 페이지를 만들 수 있다(.page.route.js 파일은 선택 사항이다).

_default.page. 파일들은 재정의할 수 있다. 예를 들어 React가 아니라 Vue와 같은 완전히 다른 UI 프레임워크로 일부 페이지를 렌더링하기 위해 render() hooks를 재정의할 수 있다.


3. Data Fetching

이제 데이터를 가져오는 방법을 살펴보자.

// /pages/star-wars/movie.page.jsx
// Environment: 브라우저, Node.js

import React from "react";

export { Page };

function Page(pageProps) {
  const { movie } = pageProps;
  return <>
    <h1>{movie.title}</h1>
    <p>Release Date: {movie.release_date}</p>
    <p>Director: {movie.director}</p>
  </>;
}
// /pages/star-wars/movie.page.route.js
// Environment: Node.js

// Route String
export default "/star-wars/@movieId";
// /pages/star-wars/movie.page.server.js
// Environment: Node.js

import fetch from "node-fetch";

export async function onBeforeRender(pageContext) {
  // `/star-wars/@movieId`의 경로 파라미터는 `pageContext.routeParams`에서 사용할 수 있다.
  const { movieId } = pageContext.routeParams;

  // `.page.server.js` 파일은 항상 Node.js에서 실행된다. 여기서 SQL/ORM 쿼리를 사용할 수 있다.
  const response = await fetch(`https://swapi.dev/api/films/${movieId}`);
  let movie = await response.json();

  // 이전에 정의한 render 함수와 hydrate 함수는 `pageContext.pageProps`를 다음으로 전달한다.
  // 루트 React component `Page`; 이것은 우리가 `pageProps`를 정의한 곳이다.
  const pageProps = { movie };

  // `pageProps`를 `pageContext.pageProps`로 사용할 수 있도록 만들어 준다.
  return {
    pageContext: {
      pageProps
    }
  };
}

// 기본적으로 `pageContext`는 서버에서만 사용할 수 있다.
// 그러나 이전에 정의한 hydrate 함수는 브라우저에서 실행되며 `pageContext.pageProps`가 필요하다.
// 우리는 `passToClient`를 사용하여 `vite-plugin-ssr`가 `pageContext.pageProps`를 직렬화하고 
// 브라우저에서 `pageContext.pageProps`를 사용할 수 있도록 한다.
export const passToClient = ["pageProps"];

4. 통합 > 리덕스

✅ vite-plugin-ssr을 사용하면 앱 아키텍처를 계속 제어할 수 있다. Redux SSR Docs를 따라 간단하게 통합할 수 있다.


상위 수준에서 SSR 통합은 다음과 같이 작동한다.

  1. 서버 측에서 store의 초기 상태를 설정한다(초기 상태가 HTML로 렌더링되도록 서버 측에서 수행한다).
  2. 초기 상태를 pageContext.initialStoreState로 사용할 수 있도록 한다.
  3. 브라우저에서 pageContext.initialStoreState를 사용할 수 있도록 passToClientinitialStoreState를 추가한다.
  4. pageContext.initialStoreState를 사용하여 브라우저 측에서 store를 초기화한다.
profile
자고 일어나면 해결되는게 더 많은 듯 그럼 잘까

0개의 댓글