티스토리 뷰

💛 .client modules

흔하지는 않지만 브라우저에서 모듈 부작용을 사용하는 파일이나 종속성이 있을 수 있다.

서버 번들에서 강제로 제외하기 위해 파일 이름에 *.client.ts를 사용하거나 .client 디렉토리 내에 파일을 중첩할 수 있다.

// feature-check.client.ts
export const supportsVibrationAPI =
  "vibrate" in window.navigator;

이 모듈에서 내보낸 값은 서버에 있으므로 모두 undefined가 될 것이기 때문에 이를 사용할 수 있는 유일한 장소는 useEffect와 클릭 핸들러와 같은 유저 이벤트이다.

import { supportsVibrationAPI } from "./feature-check.client.ts";

console.log(supportsVibrationAPI);
// server: undefined
// client: true | false

 

.client 디렉토리는 Remix Vite를 사용할 때만 지원된다.

Classic Remix Compiler는 오직 .client 파일만 지원한다.

 

 

 

 

💛 .server modules

꼭 필요한 것은 아니지만 .server 모듈은 전체 모듈을 서버-전용으로 명시적으로 표시하는 좋은 방법이다.

.server 파일이나 .server 디렉토리의 코드가 실수로 클라이언트 모듈 그래프에 표시되면 빌드가 실패한다.

app

├── .server 👈 이 디렉토리 안의 모든 파일은 서버-온리

│   ├── auth.ts

│   └── db.ts

├── cms.server.ts 👈 이 파일은 서버-온리

├── root.tsx

└── routes

    └── _index.tsx

.server모듈은 Remix app 디렉토리 내에 있어야 한다.

 

.server 디렉토리는 Remix Vite를 사용할 때만 지원된다.

Classic Remix Complier는 .server 파일만 지원한다.

Classic Remix Complier를 사용할 때 .server 모듈은 빈 모듈로 대체되며 컴파일 오류가 발생하지 않는다. 이로 인해 런타임 오류가 발생할 수 있다.

 

 

 

 

💛 Assets Imports

App 폴더 내의 모든 파일은 모듈로 가져올 수 있다.

  1. 파일을 브라우저 빌드 디렉토리에 복사한다.
  2. 장기 캐싱을 위해 파일을 확인(핑거프린트)
  3. 렌더링하는 동안 사용할 모듈에 공개 URL을 반환한다.

이는 스타일시트에 가장 일반적이지만 정의된 loader가 있는 모든 파일 유형에 사용할 수 있다.

import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

import banner from "./images/banner.jpg";
import styles from "./styles/app.css";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: styles },
];

export default function Page() {
  return (
    <div>
      <h1>Some Page</h1>
      <img src={banner} />
    </div>
  );
}

 

 

 

 

 

💛 entry.client

기본적으로 Remix는 클라이언트에서 앱 수화를 처리한다.

이 동작을 커스터마이즈하려면, npx remix reveal을 실행해 우선적으로 처리되는 app/entry.client.tsx(또는 .jsx)를 생성할 수 있다.

이 파일은 브라우저의 진입점이며 server entry module에서 서버가 생성한 마크업을 수화하는 역할을 한다. 그러나 여기에서 다른 클라이언트 측 코드를 초기화할 수도 있다.

 

일반적으로 이 모듈은 server entry module에서 이미 서버에서 생성된 마크업을 수하하기 위해 ReactDOM.hydrateRoot를 사용한다.

// app/entry.client.tsx
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <RemixBrowser />
    </StrictMode>
  );
});

이것은 브라우저에서 실행되는 첫 번째 코드 조각이다. 클라이언트 측 라이브러리를 초기화하고 클라이언트 전용 공급자를 추가하는 등의 작업을 수행할 수 있다.

 

 

 

 

💛 entry.server

기본적으로 Remix는 HTTP 응답 생성을 처리한다.

이 동작을 커스터마이즈하려면, npx remix reveal을 실행해 우선적으로 처리되는 app/entry.server.tsx(또는 .jsx)를 생성할 수 있다.

이 모듈의 default export는 HTTP 상태, 헤더 및 HTML을 포함한 응답을 생성할 수 있는 함수이고, 마크업이 생성되어 클라이언트에 전송되는 방식을 완전히 제어할 수 있다.

 

이 모듈은 현재 요청에 대한 컨텍스트와 url을 가지고 <RemixServer> 요소를 사용해 현재 페이지에 대한 마크럽을 렌더링해야 한다.

이 마크업은 브라우저 입력 모듈을 사용하여 브라우저에 javaScript가 로드되면 (선택적으로)다시 수화된다.

 

- handleDataRequest

데이터 요청의 응답을 수정할 수 있도록 할 수 있는 선택적인 handleDataRequest 함수를 내보낼 수 있다.

이는 HTML을 렌더링하지 않고 클라이언트 측 하이드레이션이 발생한 후 로더 및 action 데이터를 브라우저에 반환하는 요청이다.

export function handleDataRequest(
  response: Response,
  {
    request,
    params,
    context,
  }: LoaderFunctionArgs | ActionFunctionArgs
) {
  response.headers.set("X-Custom-Header", "value");
  return response;
}

 

 

 

- handleError

기본적으로 Remix는 마주한 서버 측 오류를 콘솔에 기록한다.

기록을 더 강력하게 제어하고 싶거나, 이러한 오류를 외부 서비스에 보고히려는 경우에는 제어권을 부여하고 내장된 오류 로깅을 비활성화하는 선택적인 handleError 함수를 내보낼 수 있다.

export function handleError(
  error: unknown,
  {
    request,
    params,
    context,
  }: LoaderFunctionArgs | ActionFunctionArgs
) {
  if (!request.signal.aborted) {
    sendErrorToErrorReportingService(error);
    console.error(formatErrorForJsonLogging(error));
  }
}

Remix의 취소 및 경쟁 상태(race-condition) 처리로 인해 많은 요청이 중단될 수 있으므로, 일반적으로 요청이 중단될 때 로깅을 피하고 싶어한다는 것을 알아두어라!

 

 

 

▪︎ Streaming Rendering Error

renderToPipeableStream 또는 renderToReadableStream을 통해 HTML 응답을 스트리밍할 때, 자체 handleError 구현에서는 초기 셀 렌더링 중에 발생한 오류만 처리한다. 후속 스트리밍 렌더링 중에 렌더링 오류가 발생하는 경우 Remix 서버가 해당 시점까지 이미 응답을 보냈으므로 이러한 오류를 수동으로 처리해야 한다.

  • renderToPipeableStream의 경우, onError 콜백 함수에서 이러한 에러를 처리할 수 있다. onShellReady에서 오류가 셸 렌더링 오류(무시 가능)인지 아니면 비동기 렌더링 오류(처리해야만 함)인지 알 수 있도록 불리언을 토글해야한다.
    • 예시) Node를 위한 entry.server.tsx 기본값 참조
  • renderToReadableStream의 경우, onError 콜백 함수에서 이러한 에러를 처리할 수 있다.
    • 예시) Cloudflare를 위한 entry.server.tsx 기본값 참조

 

 

 

▪︎ Thrown Responses

handleError는 loader/action 함수로부터 발생한 Response 인스턴스를 처리하지 않는다는 것을 알아두어라!

이 핸들러의 목적은 코드에서 예상치 못한 오류가 발생한 버그를 찾는 것이다. 시나리오를 감지하고 loader/action의  401/404/등 Response를 던지는 경우 이것은 코드에서 처리되는 예상 흐름이다. 로그에 기록을 하거나 외부 서비스로 보내길 원한다면, 응답을 던질 때 수행해야 한다.

 

 

 

 

💛 Root Route

루트 라우트(app/root.tsx)는 routes/directory에 있는 모든 라우트의 상위 경로이고 루트 <html> 문서 렌더링을 담당하기 때문에 Remix 애플리케이션에서 유일하게 필요한 경로이다.

 

루트 라우트는 문서를 관리하기 때문에 Remix가 제공하는 소수의 "문서 수준" 컴포넌트를 렌더링하기에 적합한 위치이다.

이러한 구성 요소는 루트 경로 내에서 한 번 사용되며 페이지가 제대로 렌더링되도록 Remix가 파악하거나 구축한 모든 것을 포함한다.

// app/root.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

import globalStylesheetUrl from "./global-styles.css";

export const links: LinksFunction = () => {
  return [{ rel: "stylesheet", href: globalStylesheetUrl }];
};

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1"
        />

        {/* 모든 라우트에 있는 meta 내보내기가 여기에서 렌더링됨 */}
        <Meta />

        {/* 모든 라우트에 있는 link 내보내기가 여기에서 렌더링됨 */}
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

 

 

- Layout Export

루트 라우트는 모든 경로에 대한 문서를 관리하므로 추가적인 선택적 Layout 내보내기도 지원한다.

레이아웃 라우트는 두 가지 목적으로 사용된다.

  • root 컴포넌트, HydrateFallback, ErrorBoundary에서 문서'/app shell'을 복제하는 것을 피하기
  • React가 <Links> 컴포넌트에서 <link rel="stylesheet"> 태그를 제거하고 다시 추가하는 경우 FOUC가 발생할 수 있는 root 컴포넌트/HydrateFallback/ErrorBoundary 사이를 전환할 때 React가 app shell 요소를 다시 마운트하는 것을 피하기

 

 

Layout 컴포넌트의 useLoaderData에 대한 참고 사항

useLoaderData는 happy-path 라우트 렌더링을 위한 것이므로 ErrorBoundary 컴포넌트에서 사용할 수 없으며, 해당 입력에는 로더가 성공적으로 실행되어 무언가를 반환했다는 가정이 내장되어 있다. 그 가정은 경계를 던지고 촉발한 loader일 수 있기 때문에 ErrorBoundary에 적용되지 않는다! ErrorBoundary의 loader 데이터에 접근하는 대신, loader 데이터가 잠재적으로 undefined로 설명하는 useRouteLoaderData를 사용할 수 있다.

 

Layout 컴포넌트가 성공 및 오류 플로우 모두에서 사용되기 때문에, 이와 동일한 제한 사항이 적용된다. 

성공적인 요청인지 여부에 따라 Layout에서 로직을 분기해야하는 경우에는 useRouteLoaderData("root")와 useRouteError()를 사용할 수 있다.

// app/root.tsx
export function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const data = useRouteLoaderData("root");
  const error = useRouteError();

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1"
        />
        <Meta />
        <Links />
        <style
          dangerouslySetInnerHTML={{
            __html: `
              :root {
                --themeVar: ${
                  data?.themeVar || defaultThemeVar
                }
              }
            `,
          }}
        />
      </head>
      <body>
        {data ? (
          <Analytics token={data.analyticsToken} />
        ) : null}
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

 

728x90