Remix Run 완벽 가이드 | 풀스택 프레임워크·Loader·Action·Nested Routes·실전 활용

Remix Run 완벽 가이드 | 풀스택 프레임워크·Loader·Action·Nested Routes·실전 활용

이 글의 핵심

Remix Run으로 풀스택 웹 앱을 구축하는 완벽 가이드입니다. Loader, Action, Nested Routes, Form, Error Boundary까지 실전 예제로 정리했습니다.

실무 경험 공유: Next.js에서 Remix로 전환하면서, 데이터 페칭이 간소화되고 Form 처리가 훨씬 직관적이 된 경험을 공유합니다.

들어가며: “데이터 페칭이 복잡해요”

실무 문제 시나리오

시나리오 1: 클라이언트 상태 관리가 어려워요
useEffect가 많습니다. Remix는 서버에서 처리합니다.

시나리오 2: Form 처리가 복잡해요
수동 API 호출이 필요합니다. Remix는 Form을 네이티브로 처리합니다.

시나리오 3: 에러 처리가 일관적이지 않아요
각 컴포넌트마다 다릅니다. Remix는 Error Boundary를 제공합니다.


1. Remix란?

핵심 특징

Remix는 풀스택 React 프레임워크입니다.

주요 장점:

  • Loader: 서버 데이터 페칭
  • Action: Form 처리
  • Nested Routes: 레이아웃 공유
  • Progressive Enhancement: JavaScript 없이도 작동
  • Error Boundary: 에러 처리

2. 프로젝트 설정

설치

npx create-remix@latest

프로젝트 구조

my-remix-app/
├── app/
│   ├── routes/
│   │   ├── _index.tsx
│   │   ├── login.tsx
│   │   └── posts/
│   │       ├── $postId.tsx
│   │       └── index.tsx
│   ├── root.tsx
│   └── entry.server.tsx
└── public/

3. Loader

기본 Loader

// app/routes/posts/index.tsx
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';

export async function loader() {
  const posts = await db.post.findMany();
  return json({ posts });
}

export default function Posts() {
  const { posts } = useLoaderData<typeof loader>();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

파라미터 사용

// app/routes/posts/$postId.tsx
import { json, LoaderFunctionArgs } from '@remix-run/node';

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await db.post.findUnique({
    where: { id: params.postId },
  });

  if (!post) {
    throw new Response('Not Found', { status: 404 });
  }

  return json({ post });
}

export default function Post() {
  const { post } = useLoaderData<typeof loader>();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

4. Action

Form 처리

// app/routes/posts/new.tsx
import { json, redirect, ActionFunctionArgs } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  if (!title || title.length < 3) {
    return json({ error: 'Title must be at least 3 characters' }, { status: 400 });
  }

  const post = await db.post.create({
    data: { title, content },
  });

  return redirect(`/posts/${post.id}`);
}

export default function NewPost() {
  const actionData = useActionData<typeof action>();

  return (
    <Form method="post">
      <input name="title" required />
      {actionData?.error && <span>{actionData.error}</span>}

      <textarea name="content" />

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

5. Nested Routes

레이아웃

// app/routes/posts.tsx
import { Outlet } from '@remix-run/react';

export default function PostsLayout() {
  return (
    <div>
      <nav>
        <a href="/posts">All Posts</a>
        <a href="/posts/new">New Post</a>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

라우트 구조:

  • /postsposts.tsx + posts/index.tsx
  • /posts/123posts.tsx + posts/$postId.tsx
  • /posts/newposts.tsx + posts/new.tsx

6. Error Boundary

// app/routes/posts/$postId.tsx
import { useRouteError, isRouteErrorResponse } from '@remix-run/react';

export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>Error</h1>
      <p>Something went wrong</p>
    </div>
  );
}

7. 인증

세션

// app/sessions.ts
import { createCookieSessionStorage } from '@remix-run/node';

export const { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    cookie: {
      name: '__session',
      secrets: [process.env.SESSION_SECRET!],
      sameSite: 'lax',
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
    },
  });

로그인 Action

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;

  const user = await verifyLogin(email, password);

  if (!user) {
    return json({ error: 'Invalid credentials' }, { status: 401 });
  }

  const session = await getSession(request.headers.get('Cookie'));
  session.set('userId', user.id);

  return redirect('/dashboard', {
    headers: {
      'Set-Cookie': await commitSession(session),
    },
  });
}

정리 및 체크리스트

핵심 요약

  • Remix: 풀스택 React 프레임워크
  • Loader: 서버 데이터 페칭
  • Action: Form 처리
  • Nested Routes: 레이아웃 공유
  • Error Boundary: 에러 처리
  • Progressive Enhancement: JavaScript 없이도 작동

구현 체크리스트

  • Remix 설치
  • Loader 구현
  • Action 구현
  • Nested Routes 설정
  • Error Boundary 추가
  • 인증 구현
  • 배포

같이 보면 좋은 글

  • Next.js App Router 가이드
  • React Native 완벽 가이드
  • tRPC 완벽 가이드

이 글에서 다루는 키워드

Remix, React, Fullstack, SSR, Loader, Action, Web Framework

자주 묻는 질문 (FAQ)

Q. Next.js와 비교하면 어떤가요?

A. Remix가 Form 처리와 데이터 페칭이 더 간단합니다. Next.js는 더 많은 기능을 제공합니다.

Q. SPA를 만들 수 있나요?

A. 네, 하지만 Remix는 서버 렌더링에 최적화되어 있습니다.

Q. 배포는 어디에 하나요?

A. Vercel, Cloudflare Pages, Fly.io 등 다양한 플랫폼을 지원합니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, Shopify 등 많은 기업에서 사용합니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3