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>
);
}
라우트 구조:
/posts→posts.tsx+posts/index.tsx/posts/123→posts.tsx+posts/$postId.tsx/posts/new→posts.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 등 많은 기업에서 사용합니다.