본문으로 건너뛰기
Previous
Next
Remix 2.0 완벽 가이드 — Vite 통합, 타입 안정성, v1→v2 마이그레이션

Remix 2.0 완벽 가이드 — Vite 통합, 타입 안정성, v1→v2 마이그레이션

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/devvitePlugin 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.jsvite.config.ts로 이전하고, remix() 플러그인에 기존 Remix 설정의 해당 옵션을 이식합니다.
  • 루트에서 LiveReload 제거, @remix-run/css-bundle 정리, links용 CSS는 ?url 등으로 정리합니다.
  • tsconfig.jsontypesvite/client를 포함하고, module, moduleResolutionBundler/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는 번들러 해석 규칙에 가깝습니다. tsconfigmoduleResolution이 구식 값으로 남아 있으면, 타입은 통과해도 런타임 해석이 어긋나는 경우가 생깁니다. 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-routesflatRoutes()

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 vite:build서버 번들클라이언트 자산을 분리해 냅니다. remix-serve는 Node 진입점을 실행하는 가장 단순한 경로이고, 실서비스에서는 프로세스 매니저(systemd, PM2)·컨테이너·플랫폼 PaaS 위에 올립니다. 중요한 것은 작업 디렉터리build/server/index.js 상대 경로가 배포 스크립트와 일치한다는 점입니다.

어댑터 선택

Cloudflare Workers, Deno Deploy, AWS Lambda@Edge 등은 파일 시스템·스트리밍·타이머 제약이 다릅니다. loader에서 Node 전용 모듈을 import하면 빌드는 통과해도 런타임에서만 터지는 경우가 있어, 타깃 어댑터 문서의 제약을 CI에서 검증하는 스텝을 두는 편이 안전합니다.

HTTP 스트리밍과 프록시

HTML·데이터 스트리밍을 쓰는 경우, Nginx/Cloudflare버퍼링이 켜져 있으면 TTFB 이후에도 한 번에 flush되는 것처럼 보일 수 있습니다. proxy_buffering off·캐시 규칙·gzip 조합을 점검하고, SSE/스트림은 타임아웃을 넉넉히 잡습니다.

관측성

서버 측에서는 요청 ID·라우트 이름·loader 지연을 메트릭으로 남기고, 클라이언트는 Web Vitals와 함께 하이드레이션 오류를 수집합니다. Remix는 에러 바운더리·ErrorBoundary로 사용자 경로를 보호하므로, 프로덕션 빌드에서 스택이 숨겨져도 서버 로그에 원인이 남도록 연결합니다.


9. 정리

Remix 2.0 계열은 Vite로의 이전, Node 20+, Future Flags, routes.ts·Flat Routes라는 축으로 정리하는 것이 가장 덜 고통스럽습니다. 한 번에 바꾸기보다 “런타임 정렬 → Vite 전환 → 플래그 순차 적용 → 라우팅 현대화” 순서를 지키면, 롤백 지점도 명확해집니다.

공식 문서의 Vite 가이드Future Flags 페이지를 작업 체크리스트로 삼고, 팀에서는 경고 로그 제로를 릴리스 기준에 포함하는 것을 권장합니다.

내부 동작과 핵심 메커니즘

이 글의 주제는 「Remix 2.0 완벽 가이드 — Vite 통합, 타입 안정성, v1→v2 마이그레이션」입니다. 여기서는 앞선 설명을 구현·런타임 관점에서 한 번 더 압축합니다. 구성 요소 간 책임 분리와 관측 가능한 지점을 기준으로 생각하면, “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)이 어디서 터지는가”가 한눈에 드러납니다.

처리 파이프라인(개념도)

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]

알고리즘·프로토콜 관점에서의 체크포인트

  • 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(예: 버퍼 경계, 프로토콜 상태, 트랜잭션 격리)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 동일 입력에 동일 출력이 보장되는 순수한 층과, 시간·네트워크에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합처럼 “한 번의 호출이 아니라 누적되는 비용”을 의심 목록에 넣습니다.

프로덕션 운영 패턴

실서비스에서는 기능 구현과 함께 관측·배포·보안·비용이 동시에 요구됩니다. 아래는 팀에서 자주 쓰는 최소 체크리스트입니다.

영역운영 관점에서의 질문
관측성요청 단위 상관 ID, 에러율/지연 분위수, 주요 의존성 타임아웃이 보이는가
안전성입력 검증·권한·비밀 관리가 코드 경로마다 일관적인가
신뢰성재시도는 멱등한 연산에만 적용되는가, 서킷 브레이커·백오프가 있는가
성능캐시 계층·배치 크기·풀링·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리, 마이그레이션 호환성이 문서화되어 있는가

운영 환경에서는 “개발자 PC에서는 재현되지 않던 문제”가 시간·부하·데이터 크기 때문에 드러납니다. 따라서 스테이징의 데이터 양·네트워크 지연을 가능한 한 현실에 가깝게 맞추는 것이 중요합니다.


문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스 컨디션, 타임아웃, 외부 의존성 불안정최소 재현 스크립트 작성, 분산 트레이스·로그 상관관계 확인
성능 저하N+1 쿼리, 동기 I/O, 잠금 경합, 과도한 직렬화프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 클로저/이벤트 구독 누수, 대용량 객체의 불필요한 복사상한·TTL·스냅샷 비교(힙 덤프/트레이스)
빌드·배포만 실패환경 변수·권한·플랫폼 차이CI 로그와 로컬 diff, 컨테이너/런타임 버전 핀(pin)

권장 디버깅 순서: (1) 최소 재현 만들기 (2) 최근 변경 범위 좁히기 (3) 의존성·환경 변수 차이 확인 (4) 관측 데이터로 가설 검증 (5) 수정 후 회귀·부하 테스트.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Remix 2.0의 Vite 플러그인, Classic Compiler 종료 방향, Node 20+, Future Flags, Flat Routes, 실무 마이그레이션 절차를 한글로 정리한 고급 가이드입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

Remix, React, SSR, Vite, Migration 등으로 검색하시면 이 글이 도움이 됩니다.