Remix Complete Guide
이 글의 핵심
Remix is a full-stack React framework built on Web Standards. It simplifies data fetching with loaders, form handling with actions, and works without JavaScript through progressive enhancement.
Introduction
Remix is a full-stack web framework built by the creators of React Router. It embraces web standards and progressive enhancement, making your apps fast, resilient, and accessible.
Built by Ryan Florence and Michael Jackson (creators of React Router), Remix was acquired by Shopify in October 2022. Shopify made Remix fully open source and uses it to rebuild their merchant admin dashboard.
Why Remix?
Next.js approach:
// Separate data fetching methods
export async function getServerSideProps() { ... }
export async function getStaticProps() { ... }
// API routes in separate files
Remix approach:
// Data fetching in the same file
export async function loader() { ... }
// Form handling in the same file
export async function action() { ... }
Real-World Adoption and Philosophy
Remix powers production applications at scale:
- Shopify Admin ??Shopify is rebuilding their entire merchant dashboard with Remix (serves millions of merchants)
- NASA ??uses Remix for internal mission-critical applications
- Wayfair ??e-commerce platform adopted Remix for faster page loads
- Vox Media ??uses Remix for editorial tools
Remix’s core philosophy: “Web Standards First”
Unlike other frameworks that abstract away the web platform, Remix embraces it:
- Forms work without JavaScript ??standard HTML
<form>withmethod="POST" - Uses native
fetch(),Response,FormData??no proprietary APIs - Progressive enhancement ??app works with JS disabled, enhances with JS enabled
- HTTP caching ??respects
Cache-Control,ETag, etc.
When to choose Remix:
- Content-heavy sites ??blogs, documentation, e-commerce (better SEO than SPA)
- Forms-heavy apps ??admin panels, SaaS dashboards (Remix excels at forms)
- Need resilience ??app must work even if JS fails to load
- Want simplicity ??collocate data fetching with UI (no separate API routes folder)
When to choose Next.js instead:
- Static sites ??Next.js ISR is more mature for blogs/marketing sites
- Vercel ecosystem ??Next.js has tighter Vercel integration (Edge Runtime, Image Optimization)
- Larger community ??Next.js has 5x more GitHub stars, more tutorials/courses
- Incremental adoption ??Next.js can be added to existing React apps more easily
When to choose Astro instead:
- Mostly static content ??Astro ships zero JS by default
- Multi-framework ??need Vue/Svelte/Solid components in one site
1. Getting Started
Create Project
npx create-remix@latest my-app
cd my-app
npm run dev
Choose deployment target:
- Remix App Server (recommended for learning)
- Vercel
- Cloudflare Pages
- Fly.io
Project Structure
my-app/
?��??� app/
?? ?��??� routes/
?? ?? ?��??� _index.tsx # Home page (/)
?? ?? ?��??� about.tsx # /about
?? ?? ?��??� posts.$id.tsx # /posts/:id
?? ?��??� root.tsx # Root layout
?? ?��??� entry.client.tsx # Client entry
?��??� public/
?��??� remix.config.js
2. Loaders: Server-Side Data Fetching
Basic Loader
// app/routes/posts.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
interface Post {
id: number;
title: string;
content: string;
}
export async function loader() {
const response = await fetch('https://api.example.com/posts');
const posts: Post[] = await response.json();
return json({ posts });
}
export default function Posts() {
const { posts } = useLoaderData<typeof loader>();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
Loader with Parameters
// app/routes/posts.$id.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
export async function loader({ params }: LoaderFunctionArgs) {
const response = await fetch(`https://api.example.com/posts/${params.id}`);
if (!response.ok) {
throw new Response('Not Found', { status: 404 });
}
const post = await response.json();
return json({ post });
}
export default function Post() {
const { post } = useLoaderData<typeof loader>();
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
3. Actions: Form Handling
Basic Action
// app/routes/posts.new.tsx
import type { ActionFunctionArgs } from '@remix-run/node';
import { json, redirect } 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');
const content = formData.get('content');
// Validation
if (!title || !content) {
return json({ error: 'All fields are required' }, { status: 400 });
}
// Save to database
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
const post = await response.json();
// Redirect to new post
return redirect(`/posts/${post.id}`);
}
export default function NewPost() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<h1>Create New Post</h1>
{actionData?.error && (
<div className="error">{actionData.error}</div>
)}
<div>
<label htmlFor="title">Title</label>
<input id="title" name="title" type="text" required />
</div>
<div>
<label htmlFor="content">Content</label>
<textarea id="content" name="content" required />
</div>
<button type="submit">Create Post</button>
</Form>
);
}
Update with Optimistic UI
import { useFetcher } from '@remix-run/react';
export function LikeButton({ postId, likes }: { postId: number; likes: number }) {
const fetcher = useFetcher();
// Optimistic update
const displayLikes = fetcher.formData
? likes + 1
: likes;
return (
<fetcher.Form method="post" action={`/posts/${postId}/like`}>
<button type="submit" disabled={fetcher.state !== 'idle'}>
?�️ {displayLikes} {fetcher.state !== 'idle' && '...'}
</button>
</fetcher.Form>
);
}
4. Nested Routes
File-Based Routing
app/routes/
?��??� _index.tsx # /
?��??� about.tsx # /about
?��??� blog.tsx # /blog (layout)
?��??� blog._index.tsx # /blog (index)
?��??� blog.$slug.tsx # /blog/:slug
?��??� blog.new.tsx # /blog/new
Nested Layout
// app/routes/blog.tsx (Parent Layout)
import { Outlet } from '@remix-run/react';
export default function BlogLayout() {
return (
<div className="blog-container">
<nav>
<a href="/blog">All Posts</a>
<a href="/blog/new">New Post</a>
</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}
// app/routes/blog._index.tsx (Child)
export default function BlogIndex() {
return <h1>Blog Home</h1>;
}
5. Error Handling
Error Boundary
// app/routes/posts.$id.tsx
import { useRouteError, isRouteErrorResponse } from '@remix-run/react';
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.id);
if (!post) {
throw new Response('Not Found', { status: 404 });
}
return json({ post });
}
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>
);
}
export default function Post() {
// Component code
}
6. Form Validation
With Zod
npm install zod
import { z } from 'zod';
const PostSchema = z.object({
title: z.string().min(1, 'Title is required').max(100),
content: z.string().min(10, 'Content must be at least 10 characters'),
});
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const result = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!result.success) {
return json({
errors: result.error.flatten().fieldErrors
}, { status: 400 });
}
// Save validated data
const post = await createPost(result.data);
return redirect(`/posts/${post.id}`);
}
7. Database Integration
Prisma Example
// app/routes/users.tsx
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function loader() {
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
},
});
return json({ users });
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const user = await prisma.user.create({
data: {
name: formData.get('name') as string,
email: formData.get('email') as string,
},
});
return redirect(`/users/${user.id}`);
}
8. Authentication
Session Management
// app/utils/session.server.ts
import { createCookieSessionStorage, redirect } from '@remix-run/node';
const sessionStorage = createCookieSessionStorage({
cookie: {
name: '__session',
httpOnly: true,
path: '/',
sameSite: 'lax',
secrets: [process.env.SESSION_SECRET!],
secure: process.env.NODE_ENV === 'production',
},
});
export async function createUserSession(userId: string, redirectTo: string) {
const session = await sessionStorage.getSession();
session.set('userId', userId);
return redirect(redirectTo, {
headers: {
'Set-Cookie': await sessionStorage.commitSession(session),
},
});
}
export async function getUserId(request: Request): Promise<string | null> {
const session = await sessionStorage.getSession(
request.headers.get('Cookie')
);
return session.get('userId');
}
export async function requireUserId(request: Request) {
const userId = await getUserId(request);
if (!userId) {
throw redirect('/login');
}
return userId;
}
Protected Route:
// app/routes/dashboard.tsx
export async function loader({ request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const user = await getUserById(userId);
return json({ user });
}
9. Optimistic UI
useOptimistic Hook
'use client';
import { useFetcher } from '@remix-run/react';
import { useOptimistic } from 'react';
export function TodoList({ todos }: { todos: Todo[] }) {
const fetcher = useFetcher();
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
);
return (
<div>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<fetcher.Form
method="post"
onSubmit={(e) => {
const formData = new FormData(e.currentTarget);
addOptimisticTodo({
id: Date.now(),
title: formData.get('title') as string,
});
}}
>
<input name="title" />
<button type="submit">Add</button>
</fetcher.Form>
</div>
);
}
10. File Uploads
import { unstable_parseMultipartFormData } from '@remix-run/node';
import { writeFile } from 'fs/promises';
import path from 'path';
export async function action({ request }: ActionFunctionArgs) {
const uploadHandler = async ({ name, data }: any) => {
if (name !== 'file') return undefined;
const chunks = [];
for await (const chunk of data) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
const filename = `${Date.now()}-${Math.random()}.png`;
const filepath = path.join('public/uploads', filename);
await writeFile(filepath, buffer);
return `/uploads/${filename}`;
};
const formData = await unstable_parseMultipartFormData(request, uploadHandler);
const fileUrl = formData.get('file');
return json({ fileUrl });
}
11. Meta Tags & SEO
import type { MetaFunction } from '@remix-run/node';
export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
{ title: data.post.title },
{ name: 'description', content: data.post.excerpt },
{ property: 'og:title', content: data.post.title },
{ property: 'og:description', content: data.post.excerpt },
{ property: 'og:image', content: data.post.image },
];
};
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.id);
return json({ post });
}
12. Resource Routes (API Endpoints)
// app/routes/api.posts.ts
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
export async function loader({ request }: LoaderFunctionArgs) {
const posts = await getPosts();
return json({ posts });
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const post = await createPost(formData);
return json({ post }, { status: 201 });
}
13. Prefetching
import { Link } from '@remix-run/react';
export default function PostList({ posts }: { posts: Post[] }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{/* Prefetch on hover */}
<Link to={`/posts/${post.id}`} prefetch="intent">
{post.title}
</Link>
</li>
))}
</ul>
);
}
Prefetch options:
none: No prefetchintent: Prefetch on hover/focusrender: Prefetch when link rendersviewport: Prefetch when in viewport
14. Environment Variables
// .env
DATABASE_URL=postgresql://...
SESSION_SECRET=your-secret-here
// app/utils/env.server.ts
export const env = {
DATABASE_URL: process.env.DATABASE_URL!,
SESSION_SECRET: process.env.SESSION_SECRET!,
};
15. Best Practices
1. Use Type-Safe Loaders
export async function loader() {
return json({ message: 'Hello' } as const);
}
// TypeScript knows the exact shape
const { message } = useLoaderData<typeof loader>();
2. Separate Concerns
app/
?��??� routes/
?��??� models/ # Database queries
?��??� utils/ # Utilities
?��??� services/ # Business logic
3. Handle Loading States
import { useNavigation } from '@remix-run/react';
export default function App() {
const navigation = useNavigation();
const isLoading = navigation.state === 'loading';
return (
<div>
{isLoading && <div>Loading...</div>}
<Outlet />
</div>
);
}
4. Use fetcher for Non-Navigation Actions
const fetcher = useFetcher();
// Update without navigation
<fetcher.Form method="post" action="/api/update">
<input name="field" />
<button type="submit">Update</button>
</fetcher.Form>
16. Deployment
Cloudflare Pages
npm install @remix-run/cloudflare
// remix.config.js
module.exports = {
serverModuleFormat: 'esm',
server: './server.ts',
serverBuildPath: 'functions/[[path]].js',
serverPlatform: 'neutral',
};
Vercel
npm install @remix-run/vercel
// remix.config.js
module.exports = {
server: '@remix-run/vercel',
};
Summary
Remix simplifies full-stack React development:
- Loaders handle server-side data fetching
- Actions process forms without API routes
- Progressive Enhancement works without JavaScript
- Nested Routes enable complex layouts
- Web Standards based on fetch, FormData, Response
Key Takeaways:
- Co-locate data fetching with components (loaders)
- Handle forms declaratively with actions
- Use nested routes for complex UIs
- Embrace web standards for resilience
- Progressive enhancement by default
Next Steps:
- Build a full app with [Next.js 15](/en/blog/nextjs-15-complete-guide/
- Learn [React 18](/en/blog/react-18-deep-dive/ features
- Master [TanStack Query](/en/blog/tanstack-query-complete-guide/ for client-side
Resources:
?�주 묻는 질문 (FAQ)
Q. ???�용???�무?�서 ?�제 ?�나??
A. Complete Remix guide for building full-stack web apps. Master loaders, actions, nested routes, forms, error boundaries, ???�무?�서????본문???�제?� ?�택 가?�드�?참고???�용?�면 ?�니??
Q. ?�행?�로 ?�으�?좋�? 글?�?
A. �?글 ?�단???�전 글 ?�는 관??글 링크�??�라가�??�서?��?배울 ???�습?�다. C++ ?�리�?목차?�서 ?�체 ?�름???�인?????�습?�다.
Q. ??깊이 공�??�려�?
A. cppreference?� ?�당 ?�이브러�?공식 문서�?참고?�세?? 글 말�???참고 ?�료 링크???�용?�면 좋습?�다.
같이 보면 좋�? 글 (?��? 링크)
??주제?� ?�결?�는 ?�른 글?�니??
- [React Router Complete Guide | Client-Side Routing for React](/en/blog/react-router-complete-guide/
- [Next.js Complete Guide ??App Router Internals, RSC, Caching](/en/blog/nextjs-complete-guide/
- [Qwik Complete Guide | Resumable JavaScript Framework](/en/blog/qwik-complete-guide/
??글?�서 ?�루???�워??(관??검?�어)
Remix, React, Full Stack, SSR, Web Framework, Progressive Enhancement, React Router ?�으�?검?�하?�면 ??글???��????�니??