본문으로 건너뛰기
Previous
Next
Remix Complete Guide

Remix Complete Guide

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> with method="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 prefetch
  • intent: Prefetch on hover/focus
  • render: Prefetch when link renders
  • viewport: 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:

  1. Co-locate data fetching with components (loaders)
  2. Handle forms declaratively with actions
  3. Use nested routes for complex UIs
  4. Embrace web standards for resilience
  5. 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 ?�으�?검?�하?�면 ??글???��????�니??