Next.js와 Remix를 비교할 때 “Next.js는 서버 컴포넌트, Remix는 웹 표준”이라는 구분만으로 선택하면 실제 차이를 놓치기 쉽다. 두 계열 모두 서버 렌더링, 데이터 로딩, 폼 제출, 클라이언트 전환을 지원한다. 선택에 영향을 주는 것은 서버와 클라이언트를 나누는 방식, 데이터 변경 흐름, 캐시와 배포 조건이다.
비교 기준은 Next.js 15 App Router와 Remix v2에서 이어지는 React Router v7 Framework Mode다. 아래 React Router 코드는 v7 API를 사용한다. Remix v2 패키지의 import와 혼용하지 않는다. 이후 버전이나 별도 실험 기능을 이 비교의 기본 동작으로 간주하지 않는다. Remix에서 React Router로 전환
Next.js App Router에서는 기본적으로 Server Component에서 서버 데이터에 접근하고, 브라우저 상호작용이 필요한 부분을 Client Component로 나눌 수 있다.
Server Component는 라우트와 캐시 설정에 따라 빌드 시점, 요청 시점, 재검증 시점 등에 서버에서 실행될 수 있다. “빌드 때 실행해서 HTML만 보내고 끝난다”는 설명으로 모든 동작을 포괄할 수 없다.
초기 로드에는 화면을 보여 주는 HTML, 컴포넌트 결과를 표현하는 RSC payload, 상호작용에 필요한 Client Component의 JavaScript가 관여한다. 서버 컴포넌트 자체의 코드가 클라이언트 번들에 그대로 포함되는 것은 아니지만, 페이지 전체에 JavaScript가 없다는 뜻은 아니다. 'use client'도 해당 부분이 초기 서버 렌더링을 전혀 하지 않는다는 뜻으로 읽으면 안 된다. Next.js 서버·클라이언트 컴포넌트
React Router의 Framework Mode에서는 route의 loader로 데이터를 읽고 action으로 변경 요청을 처리하는 모델을 중심으로 구성한다. Form과 useFetcher는 제출과 대기 상태를 화면에 연결한다. RSC 지원 여부를 비교할 때는 채택한 버전·모드와 실험 기능을 별도로 확인해야 한다.
Next.js에서도 Server Component의 Server Action 폼은 JavaScript가 아직 로드되지 않았거나 비활성화된 상태에서 제출할 수 있다. 따라서 “Next.js는 폼에 JS가 필수이고 fallback이 없다”는 비교는 맞지 않는다. 다만 클라이언트에서 별도로 구성한 상태·이벤트 처리까지 모두 JS 없이 동작한다는 뜻은 아니다. Next.js Server Actions와 폼
아래는 두 환경에서 검색어를 서버에서 검증하고 결과 URL로 이동하는 같은 작업을 보여 준다. 데이터 저장 예제는 아니며, 인증이 필요한 변경 작업에는 요청자·소유권 검사를 추가해야 한다.
공통 검증 함수는 다음과 같다.
// lib/search-term.ts
export function parseSearchTerm(value: FormDataEntryValue | null): string | null {
if (typeof value !== 'string') return null;
const term = value.trim();
return term.length >= 1 && term.length <= 80 ? term : null;
}
// app/search/page.tsx
import { redirect } from 'next/navigation';
import { parseSearchTerm } from '../../lib/search-term';
async function search(formData: FormData) {
'use server';
const term = parseSearchTerm(formData.get('q'));
if (term === null) redirect('/search?error=invalid');
redirect('/results?q=' + encodeURIComponent(term));
}
export default async function SearchPage({ searchParams }: {
searchParams: Promise<{ error?: string }>;
}) {
const { error } = await searchParams;
return (
<form action={search}>
<label htmlFor="q">검색어</label>
<input id="q" name="q" required maxLength={80} />
<button type="submit">검색</button>
{error === 'invalid' && <p role="alert">검색어는 1~80자로 입력해 주세요.</p>}
</form>
);
}
서버 액션은 폼 데이터를 서버에서 처리한다. required와 maxLength는 사용자 입력을 도와주지만, 서버 검증을 대체하지 않는다. redirect는 제어 흐름을 종료하므로 일반 예외 처리로 감싸서 삼키지 않도록 주의한다. /results는 별도의 결과 페이지가 있어야 한다.
// app/routes/search.tsx — /search에 등록한 route module
import { data, Form, redirect, useActionData, type ActionFunctionArgs }
from 'react-router';
import { parseSearchTerm } from '../lib/search-term';
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const term = parseSearchTerm(formData.get('q'));
if (term === null) {
return data({ error: '검색어는 1~80자로 입력해 주세요.' }, { status: 400 });
}
return redirect('/results?q=' + encodeURIComponent(term));
}
export default function SearchRoute() {
const result = useActionData<typeof action>();
return (
<Form method="post">
<label htmlFor="q">검색어</label>
<input id="q" name="q" required maxLength={80} />
<button type="submit">검색</button>
{result?.error && <p role="alert">{result.error}</p>}
</Form>
);
}
React Router 예제의 공통 함수는 app/lib/search-term.ts에 둔다. Next 예제와 루트 구조가 다르므로 상대 경로도 다르다. action을 import한 다음 같은 이름의 함수를 다시 선언하지 않는다. 서버 렌더링 환경의 <Form>은 기본 HTML 제출을 바탕으로 하고, JavaScript가 로드되면 클라이언트 전환과 대기 상태 관리가 결합된다. React Router Actions
Next.js에서는 같은 렌더링 과정의 동일한 GET fetch를 합치는 request memoization과, 요청을 넘어 데이터를 재사용하는 Data Cache를 구분한다. 라우트 결과 캐시와 브라우저의 라우터 캐시도 범위가 다르다.
Next.js 15에서 서버 fetch는 기본적으로 Data Cache에 저장되지 않는다. 필요한 경우 cache: 'force-cache', next.revalidate 등의 옵션을 명시하고, 사용자별 데이터가 공유되지 않도록 설계한다. 요청 헤더에 Cache-Control을 넣는 것만으로 Next의 모든 캐시 정책을 제어하는 것은 아니다. Next.js 15 업그레이드의 캐시 변경
React Router의 loader 데이터 재검증은 route 전환과 action 결과 등의 흐름에 연결된다. 이것을 서버의 영구 데이터 캐시와 같다고 생각하면 안 된다. HTTP 캐시, CDN, 외부 데이터 캐시를 사용할지는 응답 헤더·서버 구성과 함께 결정한다.
| 비교할 작업 | 확인할 질문 |
|---|---|
| 사용자별 대시보드 | 데이터가 어느 요청·사용자 범위로 캐시되는가? |
| 상품 목록 | 갱신 후 어느 화면과 캐시를 재검증하는가? |
| 폼 제출 | 오류·중복 제출·대기 UI를 어디에서 관리하는가? |
| 외부 API 호출 | 인증·타임아웃·실패 응답을 어떤 서버 계층에서 처리하는가? |
Next.js는 Vercel 외의 Node.js 서버·컨테이너 등에도 배포할 수 있다. 다만 이미지 처리, 캐시 공유, 스트리밍, 여러 인스턴스의 동작은 배포 방식에 맞게 구성해야 한다. 정적 export에서는 요청 시점의 서버 기능을 같은 방식으로 사용할 수 없다.
React Router도 선택한 서버·배포 어댑터와 렌더링 모드에 따라 사용 가능한 기능이 달라진다. SPA 모드와 SSR Framework Mode를 섞어 비교하면 판단이 흐려진다.
서버 컴포넌트로 화면의 서버·클라이언트 경계를 세밀하게 나누고 싶다면 Next App Router의 모델을 검토할 만하다. route loader·action 중심의 데이터 흐름과 폼 처리 모델이 팀에 잘 맞는다면 React Router Framework Mode를 검토할 수 있다. 어느 쪽이든 팀이 디버깅할 수 있는 구조와 실제 배포 환경이 중요하다.
성능은 같은 화면, 데이터, 네트워크, 캐시 조건으로 비교한다. 초기 HTML 응답뿐 아니라 클라이언트 JavaScript 양, 상호작용 가능 시점, 폼 제출 오류 처리, 운영 복잡성을 함께 측정한다. 프레임워크 이름만으로 번들이 더 작다거나 항상 더 빠르다고 결론 내릴 수는 없다.