티스토리 뷰

💛 풀스택 데이터 흐름

Remix의 주요 기능 중 하나는 UI를 영구 서버 상태와 자동으로 동기화하는 방식이다.

이는 세 단계로 진행된다.

  1. Route loader는 UI에 데이터를 제공한다.
  2. 지속 상태를 업데이트하는 작업을 라우팅하기 위해 post 데이터를 형성한다.
  3. 페이지의 loader 데이터가 자동으로 재검증된다.

 

 

 

- Route 모듈 내보내기

사용자 계정 편집 경로를 예시로 들어보자.

경로 모듈에는 작성하고 설명할 세 가지 내보내기가 있다.

// routes/account.tsx
export async function loader() {
  // provides data to the component
}

export default function Component() {
  // renders the UI
}

export async function action() {
  // updates persistent data
}

 

 

 

- Route Loader

route 파일은 라우터 구성 요소에 데이터를 제공하는 loader 기능을 내보낼 수 있다. 사용자가 일치하는 경로로 이동하면 데이터가 먼저 로드된 다음 페이지가 렌더링된다.

// routes/account.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const user = await getUser(request);
  return json({
    displayName: user.displayName,
    email: user.email,
  });
}

export default function Component() {
  // ...
}

export async function action() {
  // ...
}

 

 

 

- Route 컴포넌트

라우트 파일의 기본 내보내기는 렌더링되는 컴포넌트이다. useLoaderData를 사용해 loader 데이터를 읽는다.

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData, Form } from "@remix-run/react";

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const user = await getUser(request);
  return json({
    displayName: user.displayName,
    email: user.email,
  });
}

export default function Component() {
  const user = useLoaderData<typeof loader>();
  return (
    <Form method="post" action="/account">
      <h1>Settings for {user.displayName}</h1>

      <input
        name="displayName"
        defaultValue={user.displayName}
      />
      <input name="email" defaultValue={user.email} />

      <button type="submit">Save</button>
    </Form>
  );
}

export async function action() {
  // ...
}

 

 

 

- Route Action

마지막으로 폼이 제출될 때 폼의 action 속성과 일치하는 라우트의 작업이 호출된다. 예를 들면 동일한 라우트가 호출되는 것!

폼의 필드의 값은 표준 request.formData( )API에서 사용할 수 있다. 

import type {
  ActionFunctionArgs,
  LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData, Form } from "@remix-run/react";

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const user = await getUser(request);
  return json({
    displayName: user.displayName,
    email: user.email,
  });
}

export default function Component() {
  const user = useLoaderData<typeof loader>();
  return (
    <Form method="post" action="/account">
      <h1>Settings for {user.displayName}</h1>

      <input
        name="displayName"
        defaultValue={user.displayName}
      />
      <input name="email" defaultValue={user.email} />

      <button type="submit">Save</button>
    </Form>
  );
}

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const user = await getUser(request);

  await updateUser(user.id, {
    email: formData.get("email"),
    displayName: formData.get("displayName"),
  });

  return json({ ok: true });
}

name 입력의 속성은 formData.get(fieldName) getter에 결합되어 있다.

 

 

 

- 제출 및 재검증

사용자가 폼을 제출하면 다음이 수행된다.

  1. Remix는 폼 데이터를 fetch을 거쳐 route action으로 보내고 보류 상태는 useNavigation과 useFetcher와 같은 훅을 통해 사용할 수 있는 상태가 된다.
  2. action이 완료된 후 새로운 서버 상태를 가져오기 위해 loader의 유효성을 다시 검사한다.
  3. useLoaderData는 서버로부터 업데이트된 값을 반환하고 보류 상태는 다시 쉬는 상태로 돌아간다.

이런 방식으로 UI는 해당 동기화를 위한 코드를 작성하지 않고도 서버 상태와 동기화된 상태로 유지된다.

 

HTML 폼 요소 외에 폼을 제출하는 방법은 다양하다. 드래그 앤 드롭 또는 onChange 이벤트에 대한 응답 등등...

또한 양식 유효성 검사, 오류 처리, 보류 상태 등 내용이 많지만 여기서는 Remix의 데이터 흐름을 가져가자!!

 

 

 

- JavaScript가 로드되기 전

서버에서 HTML을 보낼 때 JavaScript가 로드되기 전에도 HTML이 작동하도록 하는 것이 가장 좋다.

Remix의 일반적인 데이터 흐름은 이 작업을 자동으로 수행한다. 흐름은 동일하지만 브라우저가 일부 작업을 수행한다.

 

JavaScript가 로드되기 전에 사용자가 폼을 제출하는 경우

  1. 브라우저가 fetch 대신 action에 폼을 제출하고 브라우저의 보류 상태는 활성화된다.(회전하는 파비콘)
  2. action이 완료된 후, loader가 호출된다.
  3. Remix는 페이지를 렌더링하고 HTML을 브라우저에 보낸다.

 

 

 

 


💛 상태 관리

React에서의 상태 관리는 일반적으로 클라이언트 측에서 서버 데이터의 동기화된 캐시를 유지하는 것을 포함한다.

하지만 Remix를 사용하면 기본적으로 데이터 동기화를 처리하는 방식으로 인해 대부분의 기존 캐싱 솔루션이 중복된다.

 

- React의 상태 관리 이해

일반적인 React context에서 상태 관리를 언급할 때, 주로 서버 상태를 클라이언트와 동기화하는 방법을 논의한다.

서버가 정보의 근원이고 클라이언트 상태가 대부분 캐시로 기능하기 때문에 "캐시 관리"라는 용어가 더 적절할 수 있다.

 

React에서 인기 있는 캐싱 솔루션

  • Redux
  • React Query
  • Apollo

 

Remix 응용 프로그램에서는 이러한 것을 완전히 무시한다.

 

 

 

- Remix가 상태를 단순화하는 방법

Remix는 재검증을 통한 자동 동기화를 통해 loader, action, form과 같은 매커니즘을 통해 백엔드와 프론트엔드 사이의 격차를 원활하게 연결한다. 이를 통해 개발자는 캐시, 네트워크 통신 또는 데이터 재검증을 관리하지 않고도 컴포넌트 내에서 서버 상태를 직접 사용할 수 있으므로 대부분의 클라이언트 측 캐싱이 중복된다.

 

일반적인 React 상태 패턴을 사용하는 것이 Remix에서 반대-패턴이 될 수 있는 이유는 다음과 같다.

  1. 네트워크 관련 상태: React 상태가 loader의 데이터, 보류중인 폼 제출, 또는 탐색 상태와 같이 네트워크와 관련된 모든 것을 관리하는 겅우 Remix가 이미 관리하고 있는 상태를 관리하고 있는 가능성이 높다. -> 중복된 상태 관리?!
    • useNavigation: navigation.state, navigation.formData, navigation.location 등에 접근할 수 있도록 하는 훅
    • useFetcher: fetcher.state, fetcher.formData, fetcher.data 등과의 상호작용을 용이하게 함
    • useLoaderData: 라우트에 대한 데이터에 액세스 함
    • useActionData: 최신 action의 데이터에 액세스 함
  2. Remix에 데이터 저장: 개발자가 React 상태에 저장하려고 할 수 있는 많은 데이터는 Remix에서 더 자연스러운 위치를 갖는다.
    • URL Search Params: 상태를 보유하는 URL 내의 매개변수
    • Cookies: 사용자의 장치에 저장되는 작은 데이터 조각
    • Server Sessions: 서버 관리 사용자 세션 
    • Servert Caches: 더 빠른 검색을 위한 서버 측의 캐시된 데이터
  3. 성능 고려 사항: 때로 중복된 데이터 가져오기를 방지하기 위해 클라이언트 상태가 활용된다. Remix를 사용하면 loader 내의 Cache-Control헤더를 사용해서 브라우저의 기본 캐시를 사용할 수 있다. 
    • 하지만 이 접근 방식에는 한계가 있으므로 신중하게 사용해야 한다.일반적으로 백엔드 쿼리를 최적화하거나 서버 캐시를 구현하는 것이 더 유리하다. <- 그러한 변경이 모든 사용자에게 이익이 되고 개별 브라우저 캐시가 필요하지 않기 때문!

 

Remix로 전환하는 개발자로서, 전통적인 React 패턴을 적용하기보다는 고유한 효율성을 인식하고 수용하는 것이 중요하다.

Remix는 상태 관리에 대한 간소화된 솔루션을 제공하여 코드가 적고, 최신 데이터가 있으며, 상태 동기화 버그가 없다.

 

 

 

 

예시)

- 네트워크 관련 상태

네트워크 관련 상태를 관리하기 위해 Remix의 내부 state를 사용하는 예시

-> 페이지 탐색: useNavigation을 사용하여 사용자가 새 페이지로 이동하고 있음을 나타낼 수 있음

import { useNavigation } from "@remix-run/react";

function PendingNavigation() {
  const navigation = useNavigation();
  return navigation.state === "loading" ? (
    <div className="spinner" />
  ) : null;
}

 

 

 

 

- URL 검색 매개변수

사용자가 목록 보기와 세부정보 보기 사이를 맞춤설정할 수 있는 UI를 고려한다면 React state를 선택할 것이다.

하지만 사용자가 보기를 변경할 때 상태 동기화를 하는 대신 오래된 HTML 형식을 사용해 URL에서 직접 상태를 읽고 설정할 수 있다.
import { Form, useSearchParams } from "@remix-run/react";

export function List() {
  const [searchParams] = useSearchParams();
  const view = searchParams.get("view") || "list";

  return (
    <div>
      <Form>
        <button name="view" value="list">
          View as List
        </button>
        <button name="view" value="details">
          View with Details
        </button>
      </Form>
      {view === "list" ? <ListView /> : <DetailView />}
    </div>
  );
}

 

 

 

 

- 지속적인 UI 상태

사이드바의 가시성을 전환하는 UI를 고려했을 때 상태를 처리하는 세 가지 방법이 있다.

  1. React state
  2. 브라우저 local storage
  3. 쿠키

 

1. React state

React state는 임시 상태 저장을 위한 간단한 솔루션을 제공한다.

  • 장점
    • 단순함: 구현하고 이해하기 쉬움
    • 캡슐화됨: 상태의 범위는 컴포넌트로 지정됨
  • 단점
    • 일시적: 페이지 새로 고침, 나중에 페이지로 돌아가기 또는 컴포넌트 마운트 해제 및 다시 마운트하기가 유지되지 않음

 

 

2. Local Storage

컴포넌트 수명 주기 이후에도 상태를 유지하려면 브라우저 로컬 저장소가 한 단계 위다.

  • 장점
    • 지속성: 페이지 새로 고침 및 컴포넌트 마운트/언마운트 시 상태를 유지함
    • 캡슐화됨: 상태의 범위는 컴포넌트로 지정됨
  • 단점
    • 동기화 필요: React 컴포넌트는 현재 상태를 초기화하고 저장하기 위해 로컬 스토리지와 동기화해야함
    • 서버 렌더링 제한 사항: 서버 측 렌더링 중에는 window 및 localStorage 개체에 접근할 수 없어 effect가 있는 브라우저에서 상태를 초기화해야함
    • UI 깜박임: 초기 페이지 로드 시 로컬 저장소의 상태가 서버에서 렌더링된 상태와 일치하지 않을 수 있으며 JavaScript가 로드될 때 UI가 깜빡임

 

 

3. Cookies

쿠키는 이러한 사용 사례에 대한 포괄적인 솔루션을 제공한다. 그러나 이 방법은 컴포넌트 내에서 상태에 액세스할 수 있도록 하기 전에 추가 예비 설정을 도입한다.

  • 장점
    • 서버 렌더링: 렌더링은 물론 서버 작업에 대해서도 서버의 상태를 사용할 수 있음
    • 단일 정보 소스: 상태 동기화의 번거로움 제거
    • 지속성: 페이지 로드 및 컴포넌트 마운트/언마운트 전반에 걸쳐 상태를 유지함. 데이터베이스 지원 세션으로 전환하면 여러 장치에서 상태가 지속될 수도 있음
    • 점진적 향상: JavaScript가 로드되기 전에도 작동함
  • 단점
    • 상용구: 네트워크 때문에 더 많은 코드가 필요함
    • 노출됨: 상태가 단일 컴포너트로 캡슐화되지 않으며 앱의 다른 부분에서 쿠키를 인식해야함

구현 )

1. 쿠키 객체 생성

import { createCookie } from "@remix-run/node";
export const prefs = createCookie("prefs");

 

2. 쿠키를 읽고 쓰도록 서버 액션과 로더 설정

import type {
  ActionFunctionArgs,
  LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno

import { prefs } from "./prefs-cookie";

// 쿠키로부터 상태를 읽기
export async function loader({
  request,
}: LoaderFunctionArgs) {
  const cookieHeader = request.headers.get("Cookie");
  const cookie = (await prefs.parse(cookieHeader)) || {};
  return json({ sidebarIsOpen: cookie.sidebarIsOpen });
}

// 쿠키에 상태를 쓰기
export async function action({
  request,
}: ActionFunctionArgs) {
  const cookieHeader = request.headers.get("Cookie");
  const cookie = (await prefs.parse(cookieHeader)) || {};
  const formData = await request.formData();

  const isOpen = formData.get("sidebar") === "open";
  cookie.sidebarIsOpen = isOpen;

  return json(isOpen, {
    headers: {
      "Set-Cookie": await prefs.serialize(cookie),
    },
  });
}

 

3. 서버 코드 설정 후 UI에서 쿠키 상태 사용

function Sidebar({ children }) {
  const fetcher = useFetcher();
  let { sidebarIsOpen } = useLoaderData<typeof loader>();

  // UI 상태를 즉시 변경하기 위해 낙관적인 UI를 사용
  if (fetcher.formData?.has("sidebar")) {
    sidebarIsOpen =
      fetcher.formData.get("sidebar") === "open";
  }

  return (
    <div>
      <fetcher.Form method="post">
        <button
          name="sidebar"
          value={sidebarIsOpen ? "closed" : "open"}
        >
          {sidebarIsOpen ? "Close" : "Open"}
        </button>
      </fetcher.Form>
      <aside hidden={!sidebarIsOpen}>{children}</aside>
    </div>
  );
}

 

 

정리 )

React state Local Storage Cookies
간단하지만 일시적인 상태 관리를 제공 지속성을 제공하지만 동기화 요구 사항 및 UI 깜박임이 있음 상용구를 추가하여 강력하고 지속적인 상태 관리를 제공

 

 

 

 

- 양식 유효성 검사 및 작업 데이터

클라이언트 측 검증은 사용자 경험을 향상시킬 수 있지만, 서버 측 처리에 더 집중하고 복잡성을 처리하도록 함으로써 유사한 개선을 달성할 수 있다.

Remix 기반 구현에서 action은 일관성을 유지하지만 useActionData를 통해 서버 상태를 직접 활용하고 Remix가 본질적으로 관리하는 네트워크 상태를 활용하므로 컴포넌트가 크게 단순화된다.

 

// app/routes/signup.tsx
import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import {
  useActionData,
  useNavigation,
} from "@remix-run/react";

export async function action({
  request,
}: ActionFunctionArgs) {
  const errors = await validateSignupRequest(request);
  if (errors) {
    return json({ ok: false, errors: errors });
  }
  await signupUser(request);
  return json({ ok: true, errors: null });
}

export function Signup() {
  const navigation = useNavigation();
  const actionData = useActionData<typeof action>();

  // 상태 관리
  const userNameError = actionData?.errors?.userName;
  const passwordError = actionData?.errors?.password;
  const isSubmitting = navigation.formAction === "/signup";

  return (
    <Form method="post">
      <p>
        <input type="text" name="username" />
        {userNameError ? <i>{userNameError}</i> : null}
      </p>

      <p>
        <input type="password" name="password" />
        {passwordError ? <i>{passwordError}</i> : null}
      </p>

      <button disabled={isSubmitting} type="submit">
        Sign Up
      </button>

      {isSubmitting ? <BusyIndicator /> : null}
    </Form>
  );
}

서버 상태에 대한 직접적인 접근은 useActionData를 통해 가능하고, 네트워크 상태는 useNavigation(또는 useFetcher)을 통해 가능하다.

 

 

 

 

 

 

 

728x90