서론
Next.js와 Remix는 무엇인가?
현대 웹 개발에서는 사용자 경험과 성능을 최적화하기 위해 다양한 프레임워크들이 사용됩니다. 그 중에서도 Next.js와 Remix는 React 기반의 프레임워크로, 각각 고유한 방식으로 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 그리고 클라이언트와 서버 간의 데이터를 효율적으로 관리하는 방식 등을 제공합니다.
Next.js는 Vercel에서 개발한 풀 스택 웹 프레임워크로, React 애플리케이션의 서버 사이드 렌더링과 정적 사이트 생성 기능을 쉽게 구현할 수 있도록 해줍니다. 파일 기반 라우팅, 이미지 최적화, API 라우팅 같은 기능을 제공해 성능 향상과 SEO 최적화에 유리한 옵션으로 알려져 있습니다.
Remix는 풀 스택 웹 프레임워크로, 데이터 페칭과 서버와 클라이언트 간의 상호작용을 개선하여 더 나은 성능과 사용자 경험을 제공하는 것을 목표로 하고 있습니다. 특히 서버와 클라이언트 간의 데이터를 효율적으로 로드하고 캐시하는 방법을 세밀하게 관리할 수 있으며, Next.js와 마찬가지로 SEO를 최적화할 수 있는 기능도 제공합니다. Remix는 페이지 로드 시 필요한 데이터를 미리 준비해 사용자 경험을 극대화하는 데 초점을 맞추고 있습니다.
이 아티클을 통해 배우게 될 내용
이 아티클에서는 Next.js와 Remix의 핵심 기능을 비교하면서, 각 프레임워크가 어떻게 다른 방식으로 성능을 최적화하고 개발자 경험을 개선하는지 살펴봅니다.
- Next.js와 Remix의 주요 기능 및 철학
- 두 프레임워크의 성능 비교 (SSR, SSG, 데이터 페칭)
- 개발자 경험과 코드 구조 차이
- SEO와 접근성에서의 차이점
- 언제 Next.js 혹은 Remix를 선택하는 것이 적합한지에 대한 가이드
이 아티클을 읽고, 두 프레임워크의 차이점을 명확히 이해하고 자신의 프로젝트에 적합한 프레임워크를 선택하는 데 필요한 지식을 얻게 되었으면 합니다.
Next.js와 Remix란 무엇인가?
Next.js와 Remix는 둘 다 현대 웹 애플리케이션을 구축하는 데 최적화된 프레임워크로, 풀스택 개발을 지원합니다. 서버사이드 렌더링(SSR)과 정적 사이트 생성(SSG)을 기본적으로 제공하여, SEO 최적화 및 성능 향상을 도와줍니다. 두 프레임워크 모두 React를 기반으로 하며, 서버와 클라이언트 간의 데이터를 효율적으로 처리하기 위한 기능들을 갖추고 있습니다.
하나씩 살펴보도록 하겠습니다.
Next.js 소개
Next.js는 Vercel에서 개발한 React 기반의 풀스택 프레임워크로, 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 그리고 클라이언트 사이드 렌더링(CSR)을 모두 지원합니다. Next.js는 뛰어난 성능 최적화와 쉬운 배포 프로세스를 제공하여 대규모 웹 애플리케이션에 적합합니다. SSR을 통해 SEO 최적화에 강점이 있으며, 이미지 최적화 기능과 함께 성능 향상을 위한 다양한 도구를 제공합니다.
특히 Next.js는 생산성과 유연성을 바탕으로 다양한 프로젝트에 빠르게 적용할 수 있는 범용성을 제공합니다. 복잡한 애플리케이션부터 정적 사이트까지 폭넓게 대응할 수 있어 개발자의 효율적인 작업을 지원합니다.
Next.js의 주요 기능
파일 기반 라우팅
Next.js는 두 가지 라우터(App Router, Pages Router)를 제공하며, 각각의 라우터는 파일 시스템 기반으로 동작합니다. App Router는 React의 최신 기능인 서버 컴포넌트(Server Components)와 스트리밍(Streaming)을 지원하며, 디렉터리 구조로 라우트를 정의합니다./app
디렉터리 내의 파일 구조가 곧 라우팅 구조가 되며, 예를 들어page.tsx
파일이 페이지 컴포넌트로 빌드됩니다. 중첩된 라우팅을 위해 폴더 내에 폴더를 중첩하여 계층적인 라우트를 만들 수 있습니다.
next.js docsSSR(서버사이드 렌더링)과 SSG(정적 사이트 생성)
Next.js는 다양한 데이터 페칭 방법을 제공하여 유연하게 SSR 및 SSG를 구현할 수 있습니다.getServerSideProps
: 각 요청마다 서버에서 데이터를 받아 페이지를 렌더링합니다. 이는 동적 데이터를 필요로 하는 경우에 유용하며, 페이지가 요청될 때마다 새로운 데이터를 받아 처리합니다.import type { InferGetServerSidePropsType, GetServerSideProps } from "next"; type Repo = { name: string; stargazers_count: number; }; export const getServerSideProps = (async () => { // Fetch data from external API const res = await fetch("https://api.github.com/repos/vercel/next.js"); const repo: Repo = await res.json(); // Pass data to the page via props return { props: { repo } }; }) satisfies GetServerSideProps<{ repo: Repo }>; export default function Page({ repo }: InferGetServerSidePropsType<typeof getServerSideProps>) { return ( <main> <p>{repo.stargazers_count}</p> </main> ); }
App Router에서는 ServerComponent와 Server Action을 사용하여 구현합니다.
Upgrading: App Router MigrationgetStaticProps
: 빌드 시 데이터를 페칭하여 정적 HTML 페이지를 생성합니다. 이는 데이터가 자주 변경되지 않는 경우에 적합하며, 빠른 로딩 속도를 제공합니다.import type { InferGetStaticPropsType, GetStaticProps } from "next"; type Repo = { name: string; stargazers_count: number; }; export const getStaticProps = (async (context) => { const res = await fetch("https://api.github.com/repos/vercel/next.js"); const repo = await res.json(); return { props: { repo } }; }) satisfies GetStaticProps<{ repo: Repo; }>; export default function Page({ repo }: InferGetStaticPropsType<typeof getStaticProps>) { return repo.stargazers_count; }
App Router에서는 ServerComponent와 Server Action을 사용하여 구현합니다.
Upgrading: App Router Migration
API 라우트
Next.js는 자체적으로 API 라우팅을 지원하여 서버리스 함수 형태로 API를 구현할 수 있습니다. 별도의 서버 구축 없이 애플리케이션에서 직접 API 요청을 처리할 수 있는 이 기능은 데이터 관리 및 비즈니스 로직을 간단하게 처리하는 데 유용합니다.
next.js docs//app/api/route.ts export async function GET(request: Request) {}
이미지 최적화 및 성능 기능
Next.js는next/image
컴포넌트를 통해 이미지 최적화 기능을 제공하여 웹사이트 성능을 향상시킵니다. 이미지 리사이징, 포맷 변경(WebP 등), 지연 로딩(lazy loading) 등의 기능을 사용하여 웹페이지 로딩 속도를 크게 개선할 수 있습니다. 또한, 자동 캐싱 및 코드 분할을 통해 성능 최적화에 중점을 둡니다.
Optimizing: ImagesSEO 설정 방법
Next.js 13의 App Router에서는 메타데이터 API를 통해 SEO 관련 설정을 할 수 있습니다. 이는 정적 및 동적 메타데이터 모두를 지원합니다.import type { Metadata } from "next"; // either Static metadata export const metadata: Metadata = { title: "...", }; // or Dynamic metadata export async function generateMetadata({ params }) { return { title: "...", }; }
Remix 소개
Remix는 2022년에 React Router의 창립자들이 만든 비교적 새로운 풀스택 웹 프레임워크로 서버와 클라이언트 간의 원활한 상호작용을 통해 성능을 극대화하는 것을 목표로 합니다. 서버에서 클라이언트로의 데이터 페칭, 캐싱, 그리고 폼 처리 방식을 개선하여 사용자 경험을 더욱 부드럽고 빠르게 만듭니다. React 생태계의 현대적인 풀스택 솔루션으로 주목받고 있으며, 서버 중심의 데이터 관리와 강력한 라우팅 기능을 제공하여 성능과 개발 효율성을 모두 갖추고 있습니다.
특히 Remix는 웹의 본질(web fundamentals)과 사용자 경험에 초점을 맞추어 근본적인 접근 방식을 제시합니다. 웹 표준을 철저히 준수하고 서버와 클라이언트의 통합을 통해 보다 자연스러운 웹 애플리케이션을 구축할 수 있게 합니다.
Remix의 주요 기능
파일 기반 라우팅
Remix는/app/routes
폴더 내의 파일들을 기반으로 라우팅을 구성합니다. 새로운 파일을 생성하면 자동으로 해당 경로에 대응하는 라우트가 만들어집니다. 중첩 라우트를 생성할 때는 파일명에 마침표(.
)를, 동적 라우트를 생성할 때는 달러($
)를 사용하여 구조화할 수 있으며 이를 통해 복잡한 라우팅도 간단하게 처리할 수 있습니다. Remix는 직관적인 파일 시스템 기반 라우팅을 사용하여 개발자가 라우트를 쉽게 관리할 수 있도록 합니다.ex) app/routes/contacts.$contactId.tsx → /contacts/1 으로 접근 가능. app/ ├── routes/ │ ├── _index.tsx │ ├── about.tsx │ ├── concerts.$city.tsx │ └── concerts.trending.tsx └── root.tsx
데이터 로딩과 캐싱
Remix는 서버 중심의 데이터 로딩을 강조하며,loader
함수와useLoaderData
훅을 통해 데이터를 서버에서 가져와 렌더링합니다. 모든 데이터는 서버에서 페칭되며, 로더 함수가 반환하는 데이터는 컴포넌트에 직접 전달됩니다. 이 방식은 SSR과 유사하지만, Remix는 로더가 비동기 함수로 작동하며 서버에서만 실행되므로 데이터 로딩과 의존성 관리가 매우 간단해집니다. 또한 Remix는 데이터의 자동 캐싱 및 재검증을 통해 항상 최신 상태의 데이터를 제공하는 것을 목표로 합니다.import { json } from "@remix-run/node"; import { Form, Link, Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from "@remix-run/react"; // existing imports import { getContacts } from "./data"; // existing exports export const loader = async () => { const contacts = await getContacts(); return json({ contacts }); }; export default function App() { const { contacts } = useLoaderData(); return ( <html lang="en"> {/* other elements */} <body>{/* other elements */}</body> </html> ); }
SPA와 서버 통합 방식
Remix는 SPA(Single Page Application) 방식이면서도 서버와 밀접하게 통합되어 동작합니다. 각 상호작용마다 데이터를 서버와 동기화하여 클라이언트와 서버의 일관성을 유지하며, 이를 통해 서버 상태를 기반으로 한 성능 최적화를 구현합니다.Form과 Action 사용 방식
Remix는 데이터 변경 및 처리에 있어 전통적인 HTML 폼과 서버 액션 방식을 사용합니다. 사용자가 폼을 제출하면 서버의 액션 함수가 호출되고, 그 후 라우트의 데이터가 자동으로 갱신됩니다. 이 과정은 사용자가 명시적으로 업데이트를 요청하지 않아도 항상 UI와 서버 데이터를 일치시킵니다. 이러한 풀스택 데이터 흐름은 폼 전송을 통해 서버와 클라이언트가 지속적으로 연결되도록 하여 사용자 경험을 향상시킵니다.import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno import { json, redirect } from "@remix-run/node"; // or cloudflare/deno import { Form } from "@remix-run/react"; import { TodoList } from "~/components/TodoList"; import { fakeCreateTodo, fakeGetTodos } from "~/utils/db"; export async function action({ request }: ActionFunctionArgs) { const body = await request.formData(); const todo = await fakeCreateTodo({ title: body.get("title"), }); return redirect(`/todos/${todo.id}`); } export async function loader() { return json(await fakeGetTodos()); } export default function Todos() { const data = useLoaderData<typeof loader>(); return ( <div> <TodoList todos={data} /> <Form method="post"> <input type="text" name="title" /> <button type="submit">Create Todo</button> </Form> </div> ); }
SEO 설정 방법
Remix에서는 각 라우트 모듈에서meta
함수를 export하여 메타데이터를 설정합니다. 이 함수는 loader 데이터에 접근할 수 있어 동적 메타데이터 생성이 가능합니다.export async function loader({ params }: LoaderFunctionArgs) { return json({ task: await getTask(params.projectId, params.taskId), }); } export const meta: MetaFunction<typeof loader> = ({ data }) => { return [{ title: data.task.name }]; };
두 프레임워크의 주요 차별점
개발자 경험과 성능 비교
Data Mutation의 차이
Remix는 웹 표준을 중요시하며, HTML form과 FormData API를 활용하여 데이터 변경을 처리합니다. 이 접근 방식은 HTTP 메서드와 상태 코드를 적극적으로 활용합니다. 또한 라우트 모듈에action
함수를 정의하여 POST, PUT, PATCH, DELETE 등의 HTTP 메서드를 처리합니다. 이 함수는 서버에서 실행되며, 폼 제출이나 다른 데이터 변경 요청을 처리합니다.
https://remix.run/docs/en/main/discussion/data-flow// app/routes/posts/new.tsx import { Form, useActionData } from "@remix-run/react"; export default function NewPost() { const actionData = useActionData(); return ( <Form method="post"> <input type="text" name="title" /> <textarea name="content"></textarea> <button type="submit">Create Post</button> {actionData?.error ? <p>{actionData.error}</p> : null} </Form> ); } export async function action({ request }) { const formData = await request.formData(); const title = formData.get("title"); const content = formData.get("content"); // 데이터 유효성 검사 및 저장 로직 // ... return redirect("/posts"); }
이 방법의 장점은 다음과 같습니다.
- JavaScript가 비활성화되어도 기본적인 폼 제출이 작동합니다.
- 점진적 향상(Progressive enhancement)을 쉽게 구현할 수 있습니다.
- 웹 표준을 따르므로 다양한 클라이언트와 호환됩니다.
반면 Next.js는 데이터 변경에 대해 더 유연한 접근 방식을 제공합니다. 일반적으로 API 라우트를 정의하고, 클라이언트에서 이를 호출하는 방식을 사용합니다.
// app/api/posts/[id]/route.js
import { NextResponse } from "next/server";
export async function DELETE(_, { params }) {
try {
// await deletePost(params.id);
return NextResponse.json({ message: "Deleted" }, { status: 200 });
} catch {
return NextResponse.json({ error: "Failed" }, { status: 500 });
}
}
// app/posts/[id]/page.js
import DeleteButton from "./DeleteButton";
async function getPost(id) {
// 실제 데이터 fetching 로직 대신 예시 데이터 반환
return { id, title: "Post Title", content: "Post content..." };
}
export default async function Post({ params }) {
const post = await getPost(params.id);
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
<DeleteButton postId={post.id} />
</div>
);
}
// app/posts/[id]/DeleteButton.js
"use client";
import { useRouter } from "next/navigation";
export default function DeleteButton({ postId }) {
const router = useRouter();
async function deletePost() {
await fetch(`/api/posts/${postId}`, { method: "DELETE" });
router.push("/posts");
router.refresh();
}
return <button onClick={deletePost}>Delete Post</button>;
}
이 방식의 장점은 다음과 같습니다.
- API 라우트와 페이지 컴포넌트가 분리되어 있어 더 모듈화된 구조를 가집니다.
- 클라이언트 사이드에서 더 많은 제어가 가능합니다 (예: 로딩 상태 관리).
- RESTful API 설계와 유사하여 기존 백엔드 개발자들에게 친숙할 수 있습니다.
결론
Remix는 웹 표준과 HTTP를 충실히 따르며, 서버와 클라이언트의 긴밀한 통합을 제공합니다. 반면 Next.js는 더 유연한 접근 방식을 제공하며, 클라이언트 사이드에서 더 많은 제어를 할 수 있습니다.
빌드 속도와 HMR
아래 표는 개발 모드 기준 도구 차이를 보여줍니다.
Next.js | Remix | |
---|---|---|
모듈 번들러 | Webpack | Vite |
트랜스 파일러 | SWC | ESbuild |
최적화 도구 | SWC | ESbuild |
- Vite의 경우 esbuild를 개발 모드에서 사용, 프로덕션 모드 에서는 rollup을 사용합니다.
- 개발 모드 기준으로 Next.js는 기본 모듈 번들러로 Webpack을 사용하는 반면, Remix는 Vite를 사용합니다.
- Next.js는 12버전부터 트랜스 파일러와 최적화 도구로 SWC를 사용합니다. 기존에는 트랜스 파일러로 Babel, 최적화 도구로 Terser를 사용하였습니다.
- SWC는 Rust로 작성되었고, ESbuild는 GO언어로 작성되어 빠른 속도를 보여줍니다.
속도 차이가 나는 이유?
SWC와 ESbuild의 차이는 미미합니다. SWC는 Rust로 작성되었고, ESbuild는 Go언어로 작성되었는데, 언어의 차이라고 한다면 Rust가 조금 더 빠르다고 할 수 있지만 그 차이는 미미합니다.
따라서 Next.js와 Remix의 속도차이는 모듈 번들러의 차이에서 올 수 있다고 말할 수 있겠습니다.
모듈 번들러의 동작 방식
- Vite는 개발 모드에서 번들링 하지 않습니다. Vite는 브라우저에서 필요한 모듈을 직접 요청하는 방식으로 작동합니다.
브라우저는 Vite가 제공한 ESM 모듈을 받아들여, 필요할 때마다 import 구문을 통해 모듈을 HTTP 요청으로 가져옵니다. (Native ESM을 채택)
이 과정에서 Vite는 브라우저가 요청한 모듈만 트랜스파일링해서 제공하므로, 전체 애플리케이션을 미리 번들링할 필요가 없습니다. 이것이 Vite가 빠르게 동작하는 이유 중 하나입니다. - Webpack은 Vite와 다르게 ‘번들러’입니다. 개발서버로 실행할 때에도 프로젝트에 관련된 모든 모듈을 번들링하여 브라우저에 제공하는 방식을 취합니다.
- 현재 Next.js는 Turbo라는 자체 모듈 번들러를 탑재중입니다. (24.09 기준 베타버전, 24.10 기준 Next v15에서 Turbo를 도입하였습니다.) Getting started Turbo
결론
기본적으로 Next가 Remix보다 더 느립니다. Remix가 빠릅니다 그 이유는 모듈 번들러의 차이가 있기 때문입니다.
언제 어떤 프레임워크를 선택할까?
Next.js와 Remix의 특징 요약
- Next.js
- 렌더링 방식
- 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 클라이언트 사이드 렌더링(CSR) 지원
- 각 페이지마다 렌더링 방식을 선택할 수 있는 유연성
- 파일 기반 라우팅
/pages
또는/app
디렉토리 구조를 통한 직관적인 라우팅- 동적 라우트와 중첩 라우트 지원
- API 라우트
- 같은 프로젝트 내에서 API 엔드포인트 생성 가능
- 서버리스 함수로 동작하여 별도의 백엔드 서버 불필요
- 이미지 최적화
next/image
컴포넌트를 통한 자동 이미지 최적화- 지연 로딩, 크기 조정, 포맷 변환 등 지원
- 자동 코드 분할
- 페이지별 자동 코드 분할로 초기 로딩 시간 최적화
- Vercel 통합
- Vercel 플랫폼과의 긴밀한 통합으로 쉬운 배포 및 스케일링
- 풍부한 생태계
- 다양한 플러그인, 예제, 커뮤니티 지원
- 렌더링 방식
- Remix
- 웹 표준 중심 접근
- 브라우저와 웹 API의 기본 기능을 최대한 활용
- 프로그레시브 인핸스먼트 철학 adopted
- 서버-클라이언트 데이터 흐름
loader
와action
함수를 통한 효율적인 데이터 관리- 서버에서 클라이언트로의 원활한 데이터 전달
- 중첩 라우팅
- 파일 시스템 기반의 강력한 중첩 라우팅 구조
- 각 라우트 컴포넌트가 자체 데이터 로딩 로직을 가짐
- 폼 처리
- HTML 폼 기반의 직관적인 데이터 제출 및 처리
- JavaScript 없이도 동작 가능한 프로그레시브 인핸스먼트
- 에러 핸들링
- 각 라우트 레벨에서의 세밀한 에러 처리 가능
- 사용자 경험을 해치지 않는 우아한 에러 복구
- 성능 최적화
- 효율적인 데이터 로딩 및 캐싱 전략
- 필요한 데이터만 서버에서 클라이언트로 전송
- 유연한 배포
- 다양한 호스팅 환경에 배포 가능
- 서버리스 환경부터 전통적인 서버 환경까지 지원
- 웹 표준 중심 접근
독자의 상황에 맞는 선택 가이드
- Next.js: 기능이 풍부한 프레임워크, 광범위한 지원으로 빠르게 성과를 내야 하는 경우에는 Next.js가 적합할 수 있습니다.
- Remix: 성능이 중요한 프로젝트, 부드러운 사용자 경험, 현대적인 접근 방식을 탐구할 의향이 있다면 Remix가 적합할 수 있습니다.
결론적으로, 프로젝트의 특성과 팀의 상황을 고려하여 선택하는 것이 중요합니다. Next.js는 다양한 렌더링 옵션과 풍부한 생태계를 제공하며, Remix는 웹 표준에 충실하면서도 효율적인 데이터 처리에 강점을 가지고 있습니다. 두 프레임워크 모두 현대적인 웹 개발에 적합한 선택이 될 수 있으므로, 프로젝트의 요구사항을 신중히 검토하고 결정하시기 바랍니다.
'Front End' 카테고리의 다른 글
SSR로 사용자 경험 향상시키기 (0) | 2024.12.28 |
---|---|
[FE] 디자인 시스템에 아토믹 디자인 패턴 도입하기 (1) | 2024.09.15 |
[웹 최적화] 웹 페이지 번들 사이즈 줄이기 (1) | 2024.09.08 |
MSW 사용, 테스트 환경 세팅, TextEncoder is not defined 트러블 슈팅 기록 (0) | 2024.07.21 |