Remix 2.0 완벽 가이드 — Vite 통합, 타입 안정성, v1→v2 마이그레이션
이 글의 핵심
Remix 2.x는 Classic Compiler 대신 Vite를 중심으로 한 개발·빌드 경로를 권장하며, Future Flags로 React Router v7로의 이행을 준비합니다. 본 글은 패키지 업그레이드, vite.config.ts 전환, CSS·타입·라우팅(Flat Routes)까지 실무에서 바로 쓰는 마이그레이션 순서와 함께 정리합니다.
이 글의 핵심
Remix 2.0은 단순한 메이저 버전 상승이 아니라, 개발 서버·번들링 스택을 Vite 중심으로 재정렬하고 React Router v7(및 Single Fetch 등)로 이어지는 Future Flags를 도입하는 전환점입니다. 팀 입장에서는 “기능 추가”보다 빌드 파이프라인·타입·라우트 규칙을 한 번에 정리할 기회로 보는 편이 안전합니다.
본 가이드는 v1 앱을 최신 v2.x로 올린 뒤, 공식 문서가 권장하는 대로 한 단계씩 커밋·배포하며 Vite 플러그인, 타입 설정, Flat Routes·routes.ts까지 연결하는 흐름을 다룹니다. 이미 Loader·Action·Nested Routes에 익숙하고, Node·npm 생태계를 다룰 수 있다는 전제입니다.
참고: Remix와 React Router는 긴밀하게 연동되며, 공식 문서에서는 Future Flags를 통해 다음 메이저로의 차이를 점진적으로 흡수하라고 안내합니다. 버전 번호는 시점에 따라 달라질 수 있으므로, 실제 작업 시에는
npm info @remix-run/dev version등으로 최신 2.x를 확인하십시오.
1. Remix 2.0의 주요 변경사항
1.1 컴파일러: Classic → Vite
기존 Classic Remix Compiler(내부적으로 별도의 번들 파이프라인)는 Vite 플러그인(@remix-run/dev의 vitePlugin as remix)으로 대체되는 방향입니다. Vite를 쓰면 개발 서버 기동·HMR, Rollup 기반 프로덕션 빌드, 풍부한 플러그인 생태계를 동일한 설정 축에서 다룰 수 있습니다.
1.2 런타임: installGlobals 제거와 Node 20+
과거에는 fetch 등을 위해 installGlobals()를 호출하는 패턴이 흔했습니다. 이후 메이저에서는 Node 20 이상을 전제로 내장 fetch를 활용하며, 마이그레이션 가이드에서는 installGlobals 제거를 명시적으로 안내합니다. Cloudflare Workers 등을 쓰는 경우 호환 날짜(compatibility date) 조건을 함께 확인해야 합니다.
1.3 루트 UI: LiveReload 제거
Vite 개발 모드에서는 HMR이 담당하므로, 루트 레이아웃에서 LiveReload 컴포넌트는 제거하고 Scripts 등만 유지하는 형태로 정리합니다. 이를 누락하면 불필요한 중복 또는 빌드 경고가 남을 수 있습니다.
1.4 스타일: @remix-run/css-bundle·cssBundleHref
Vite는 CSS 사이드 이펙트 import, PostCSS, CSS Modules 등을 기본적으로 처리합니다. 따라서 @remix-run/css-bundle은 제거하고, links에서 참조하는 CSS는 Vite 관례에 맞게 ?url 접미사 등으로 명시하는 식으로 맞춥니다(공식 Vite 마이그레이션 문서의 “Fix up CSS imports” 절 참고).
1.5 Future Flags와 유틸리티 변화
json, defer, SerializeFrom 등은 React Router v7·Single Fetch 방향과 맞추기 위해 단계적으로 대체가 권장됩니다. 예를 들어 json에 의존하던 응답 구성은 data 유틸이나 Response.json() 등으로 옮기는 식의 점검이 필요합니다. 팀에서는 경고 로그를 기준으로 호출부를 목록화한 뒤, 릴리스 단위로 줄이는 방식이 안전합니다.
2. Vite 기반 빌드 시스템 이해하기
2.1 최소 vite.config.ts
루트에 vite.config.ts를 두고 Remix 플러그인을 등록합니다. Classic 시절의 remix.config.js에 있던 옵션 중 일부는 플러그인 인자로 직접 넘깁니다.
// vite.config.ts
import { vitePlugin as remix } from '@remix-run/dev';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [
remix({
// 예: 특정 파일을 라우트에서 제외
ignoredRouteFiles: ['**/*.css'],
future: {
// 팀 상황에 맞게 순차 적용
// v3_fetcherPersist: true,
},
}),
tsconfigPaths(),
],
server: {
port: 3000,
},
});
위 설정에서 vite-tsconfig-paths는 Classic 컴파일러가 암묵적으로 맞춰 주던 ~ → app 별칭 등을 tsconfig.json과 동기화할 때 특히 유용합니다. Vite는 기본적으로 경로 별칭을 자동 생성하지 않으므로, 기존 코드가 ~/ import에 의존한다면 이 플러그인 도입을 우선 검토하십시오.
2.2 package.json 스크립트
공식 가이드에서 제시하는 형태는 다음과 유사합니다(프로젝트 템플릿에 따라 미세하게 다를 수 있음).
{
"scripts": {
"dev": "remix vite:dev",
"build": "remix vite:build",
"start": "remix-serve ./build/server/index.js"
}
}
산출물 경로가 Classic 때와 다르므로, Dockerfile·PM2·CI의 start 명령을 반드시 함께 수정합니다. “로컬에서는 되는데 서버에서 404/모듈 not found” 유형의 이슈는 대부분 빌드 산출 경로 불일치에서 발생합니다.
2.3 선택적 unstable_optimizeDeps
의존성 사전 번들 최적화가 필요하면 future.unstable_optimizeDeps를 검토할 수 있습니다. 다만 unstable이므로, 팀 정책상 기능 플래그로 감싸거나 스테이징에서 충분히 검증한 뒤 적용하는 편이 좋습니다.
3. v1에서 v2로 마이그레이션: 권장 순서
아래는 운영 트래픽이 있는 서비스를 가정한 보수적인 순서입니다. 각 단계마다 테스트·배포를 권장합니다.
3.1 1단계: 최신 v2.x로 먼저 올리기
@remix-run/* 패키지를 동일한 메이저(2.x) 안에서 최신 마이너·패치로 맞춥니다. 이 시점에서 deprecation 경고가 다수 출력될 수 있으며, 이는 다음 단계의 작업 목록이 됩니다.
3.2 2단계: Node·런타임 정렬
- Node 20 LTS(또는 팀이 고정한 짝수 LTS)로 개발·CI·프로덕션을 통일합니다.
- 서버 진입점에서
installGlobals()호출을 제거합니다.
3.3 3단계: Vite 도입
vite,@remix-run/dev등을package.json에 맞게 정리합니다.type: "module"요구 사항이 있다면 템플릿 지침에 따릅니다.remix.config.js→vite.config.ts로 이전하고,remix()플러그인에 기존 Remix 설정의 해당 옵션을 이식합니다.- 루트에서
LiveReload제거,@remix-run/css-bundle정리,links용 CSS는?url등으로 정리합니다. tsconfig.json의types에vite/client를 포함하고,module,moduleResolution을 Bundler/ESNext 계열로 맞춥니다. 공식 예시를 그대로 비교하는 것이 빠릅니다.remix.env.d.ts에 남아 있는 불필요한 reference가 있으면 제거하고, 파일이 비면 삭제합니다.
3.4 4단계: Future Flags를 한 번에 켜지 말 것
v3_fetcherPersist, v3_relativeSplatPath, v3_routeConfig 등은 성격이 서로 다릅니다. 한 번에 전부 켜면 디버깅 범위가 넓어지므로, 플래그별로 브랜치를 나누거나 스테이징에서 순차 검증하는 편이 낫습니다. 특히 스플랫 라우트·상대 링크를 쓰는 앱은 v3_relativeSplatPath 영향 분석이 필요합니다.
4. 타입 안정성 개선
4.1 컴파일러 관점: moduleResolution: "Bundler"
Vite는 번들러 해석 규칙에 가깝습니다. tsconfig의 moduleResolution이 구식 값으로 남아 있으면, 타입은 통과해도 런타임 해석이 어긋나는 경우가 생깁니다. Remix 공식 가이드가 제시하는 Bundler/ESNext 조합을 기준선으로 삼으십시오.
4.2 Loader·Action 반환 타입을 “계약”으로 두기
팀 규모가 커질수록 loader가 반환하는 객체 형태를 단일 소스(스키마 또는 타입)와 맞추는 패턴이 유효합니다. Zod 등으로 런타임 검증을 붙이면, 프로덕션에서의 예외 데이터까지 줄일 수 있습니다.
// app/routes/example.tsx (개념 예시)
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node'; // 팀 정책에 따라 data/Response로 대체
type LoaderData = { userId: string };
export async function loader({ request }: LoaderFunctionArgs) {
const userId = new URL(request.url).searchParams.get('uid') ?? 'guest';
return json({ userId } satisfies LoaderData);
}
// 컴포넌트에서는 useLoaderData<typeof loader> 패턴으로 좁힙니다.
위는 개념 예시이며, 실제 코드베이스에서는 json 대체 정책(Single Fetch·data 유틸)에 맞춰 반환부를 조정해야 합니다. 중요한 점은 “로더가 주는 키 집합”을 문서화하고, 프론트 훅에서 제네릭으로 고정한다는 것입니다.
4.3 SerializeFrom 사용처 점검
SerializeFrom에 의존해 loader 데이터의 언랩을 하던 코드는 향후 제거를 전제로 리팩터링 계획을 세웁니다. 공식 가이드에서는 대체 타입 패턴을 예시로 제공하므로, 팀 내 공통 타입 유틸로 추출해 두면 중복을 줄일 수 있습니다.
5. Flat Routes 패턴
5.1 왜 Flat Routes인가
전통적인 app/routes 트리는 규모가 커질수록 폴더 깊이·파일명 규칙 부담이 커집니다. Flat Routes는 파일명 접두·접미 규칙으로 URL 구조를 표현해, 라우트 추가·리네이밍을 단순화합니다. 팀 컨벤션과 코드 리뷰 규칙을 함께 정하는 것이 중요합니다.
5.2 @remix-run/fs-routes의 flatRoutes()
Future Flag v3_routeConfig를 켠 뒤 app/routes.ts를 도입하는 흐름이 공식적으로 정리되어 있습니다. @remix-run/fs-routes를 설치한 뒤, 최소 구성은 다음과 같이 시작할 수 있습니다.
// app/routes.ts (개념 예시 — 공식 문서의 최신 API와 버전을 확인하세요)
import type { RouteConfig } from '@remix-run/route-config';
import { flatRoutes } from '@remix-run/fs-routes';
export default flatRoutes() satisfies RouteConfig;
이미 routes 옵션으로 remix-flat-routes 같은 대체 파일 시스템 라우팅을 쓰고 있었다면, @remix-run/routes-option-adapter로 연결하는 예시가 문서에 제시되어 있습니다. 즉, “기존 관례를 유지하면서도 routes.ts 시대로 이행”할 수 있습니다.
5.3 코드 기반 라우트와의 혼합
대규모 앱에서는 일부만 파일 시스템, 일부는 코드 기반 route() 정의가 필요할 수 있습니다. 문서에서는 RouteConfig 배열을 병합하는 패턴도 보여 주므로, 점진적 이전에 활용할 수 있습니다.
6. 실전 프로젝트 구조
아래는 Vite + Remix 2.x + (선택) routes.ts를 가정한 현실적인 디렉터리 스케치입니다. 팀·제품에 따라 server·jobs·e2e 등이 추가됩니다.
project/
├── app/
│ ├── routes/ # 기본 파일 기반 라우트 (팀 관례에 따라 유지)
│ ├── routes.ts # v3_routeConfig 도입 시 (선택)
│ ├── root.tsx
│ ├── entry.client.tsx
│ └── entry.server.tsx
├── public/
├── vite.config.ts
├── tsconfig.json
├── package.json
└── (Dockerfile, .github/, tests/ …)
도메인 경계가 뚜렷한 팀은 app/features/<도메인> 아래에 컴포넌트·서버 유틸·테스트를 모으고, routes 쪽은 얇게 유지하는 패턴도 흔합니다. Remix의 강점인 로더/액션 근접 배치를 해치지 않는 범위에서 폴더 규칙을 문서화하십시오.
7. 트러블슈팅 체크리스트
- 개발 서버는 뜨는데 HMR만 이상하다: 브라우저 캐시·프록시·
server.hmr설정, React 플러그인(SWC 여부)을 점검합니다. - 프로덕션만 스타일이 깨진다:
links의 CSS가?url등 Vite 규약을 따르는지, 빌드 산출물에 실제 URL이 포함되는지 확인합니다. remix-serve가 파일을 못 찾는다:start스크립트의 상대 경로가 CI 산출물과 일치하는지, 작업 디렉터리가 맞는지 확인합니다.- 타입은 맞는데 런타임만 터진다:
loader가 환경 변수·권한·외부 API에 의존하는 경우가 많습니다. 스테이징 데이터로 재현 테스트를 분리합니다.
8. 정리
Remix 2.0 계열은 Vite로의 이전, Node 20+, Future Flags, routes.ts·Flat Routes라는 축으로 정리하는 것이 가장 덜 고통스럽습니다. 한 번에 바꾸기보다 “런타임 정렬 → Vite 전환 → 플래그 순차 적용 → 라우팅 현대화” 순서를 지키면, 롤백 지점도 명확해집니다.
공식 문서의 Vite 가이드와 Future Flags 페이지를 작업 체크리스트로 삼고, 팀에서는 경고 로그 제로를 릴리스 기준에 포함하는 것을 권장합니다.