Remix Jam 2025에서 공개된 Remix v3의 방향성을, 현대 React가 안고 있는 복잡성과 대비해 살피며 프런트엔드/백엔드 모델, 라우팅, 이벤트·시그널 기반 상호작용, 웹 플랫폼 중심 접근의 의미를 정리합니다.
Remix는 React의 언더독 가장 인기 있는 React 패키지의 저자들이 만든 웹 프레임워크다. 며칠 전 Remix Jam 2025에서 Ryan과 Michael이 Remix v3의 스니크 피크를 공개했다. 아직 공식 블로그 포스트나 문서는 없다. 여기서는 그것이 무엇인지 내 방식대로 설명해 보려고 한다.
Remix v1은 데이터 로딩과 서버 사이드 렌더링을 관리하는 React 프레임워크였다. 가장 큰 성과는 마케팅 웹사이트였다. 초창기 Remix가 흥미로웠던 이유는 Next.js의 지배적 위치에 도전하는 첫 실질적 경쟁자로 보였기 때문이다.
Remix v2는 메시지와 정체성에서 어려움을 겪었다. 저자 중 한 명은 이를 이렇게 설명했다: “Remix v2는 Vite 플러그인으로, Route Module API를 통해 React Router v6 기능 접근을 더 편리하게 해 준다”. 이걸로는 Vercel의 마케팅 머신을 이길 수 없다. 프레임워크에는 사용자가 있었지만, Next.js의 시장 점유율에 영향을 줄 만큼의 성장 궤적은 확실히 아니었다.
Remix v3는… 매우 다르다. 웹 개발 전반의 정서 변화와 맞닿아 있고, 주목할 가치가 있다. 왜 그런지 이해하려면, 먼저 모던 React의 현주소를 보자.
2025년의 React는 복잡하게 느껴진다. React Server Components는 컴포넌트를 서버에서 렌더링하고 클라이언트에서 하이드레이트하는 더 정교한 방식을 도입했다. 무엇이 허용되는지에 대한 규칙도 미묘하다. 서버 컴포넌트는 이제 async로 정의할 수 있고 클라이언트 컴포넌트로 데이터를 전달할 수 있지만(모든 종류의 데이터를 다 전달할 수 있는 건 아니다), 일반 서버 함수도 마법 같은 RPC 액션을 통해 클라이언트에서 사용할 수 있게 되었다. 개발자는 "use client"와 "use server"를 배워야 한다.
특히 대형 앱에서 속도가 공통된 우려가 되기 시작했다. Svelte 같은 프레임워크는 가상 DOM 디핑을 피해 가장 효율적으로 요소를 갱신하는 방식을 도입하면서도 선언적 모양새를 어느 정도 유지했다. 이에 React 팀은 8년 가까이 Meta 내부에서 다듬어 온 React 컴파일러 접근을 공개했다. 이는 컴포넌트와 훅을 자동으로 메모이제이션한다. 다만 모든 컴포넌트를 그렇게 하지는 못하는데, 미묘한 버그를 유발할 수 있기 때문이다. 이제는 "use memo"와 "use no memo" (별칭으로는 "use forget"과 "use no forget")을 두고 헷갈리지 말아야 한다.
React의 코어 API 표면적은 계속 넓어지고 있다. 반응성을 높이기 위해 비동기 렌더링 API들(Suspense, startTransition, useDeferredValue, useActionState)이 추가되면서, 사용자 상호작용을 덜 긴급하지만 비용이 큰 컴포넌트 트리 업데이트보다 우선시할 수 있게 되었지만, 이제 “active”와 “pending” 상태를 함께 추론해야 한다. 다른 새로운 API들도 복잡하고 틈새적이라는 인상이 있다. useEffectEvent 문서를 한 번 보라(클릭하기 전에 무엇을 하는지 먼저 맞혀보라).
물론 아무도 개발자에게 이런 기능들을 반드시 쓰라고 강요하지는 않는다. 사실 가장 오래된 createReactClass와 믹스인조차 공식 create-react-class npm 패키지와 함께 여전히 잘 작동한다. 오늘날에도 그런 스타일의 React를 사용할 수 있고, 최신 기능을 모두 채택할 필요는 없다.
그런데도 어느 정도는 해야 한다. 이 기능들이 Next.js를 통해 개발자들의 목구멍에 억지로 밀어 넣어지기 때문이다. React 자체는 프레임워크가 되려는 시도를 포기했다. 빠르게 변하는 JS 도구 생태계를 따라가는 일이 너무 벅찼고, Facebook은 오픈소스화할 가치가 없는 자신들만의 셋업을 갖고 있었기 때문이다.
Next.js 자체도 고통스러운 전환기(pages 라우터 vs app 라우터)를 겪으며 최전선에 있던 개발자들을 상처 입혔다. 여기에 API 변경의 소용돌이 위로 Vercel이 초래한 또 다른 복잡성 레이어가 있다. Vercel은 풀스택 Next.js 앱을 고유한 방식으로 배포한다. 일부 코드는 브라우저에서, 일부는 AWS Lambda(3배 마진)에서, 또 일부는 자사 워커 런타임에서 실행된다(“미들웨어”는 수십 개발자-년의 혼란과 좌절을 야기한 가장 큰 거짓말 중 하나일 것이다).
이 모든 게 그냥… 너무 과하다. 사실, 이런 기능들과 프레임워크가 해결하려는 문제들을 설명하는 데만도 Dan이 여러 컨퍼런스와 블로그 글을 할애해야 했다. 이해하는 사람은 이해한다. RSC, Suspense, React Compiler는 실제로 강력하고, 어떤 회사들이 가진 진짜 문제들을 해결한다.
게다가, LLM은 React에 약하다. 앱을 만들어 보라고 하면 다들 React를 집어드는데, 결과물은 useEffect 범벅과 미묘한 버그를 우회하려는 임시방편 해킹으로 가득한 보기 흉한 컴포넌트가 되기 쉽다.
이런 복잡성과 좌절의 한가운데에서 Remix v3가 태어났다. Remix Jam 녹화를 보고(댓글의 타임스탬프를 참고) 두 저자의 X 타임라인을 훑어보면 감을 잡을 수 있다.
프런트엔드에서 Remix는 여전히 JSX를 사용하지만 React 런타임은 없다. React처럼 상태를 추적하지 않고, 대신 개발자에게 무언가 변경되었음을 Remix에 알리는 this.update() 함수를 제공한다. 명시적 상태 대신 무엇이든 사용할 수 있는데, 가장 흔한 것은 클로저로 캡처한 변수다:
function Counter(this: Remix.Handle) {
// 상태는 그냥 클로저
let count = 0;
return () => (
<div>
<div>{count}</div>
<button
class="p-2 text-green-500"
on={dom.click((event, signal) => {
count++;
this.update();
})}
>
Inc
</button>
</div>
);
}
이 접근은 코드를 더 명령형으로, 기계적으로 단순하게 만든다. 이걸 하고, 그다음 저걸 한다. 즉, React가 약속한 view = f(state) 대신 전이(transition)를 직접 다룬다.
이벤트는 일급이며, 웹에 내장된 이벤트 메커니즘을 사용한다. onClick 대신 범용 "on" 프롭이 있고 표준 dom 이벤트 라이브러리가 있으며, 개발자는 자신만의 CustomEvent를 정의할 수 있다.
비동기 작업 제어에는 시그널을 사용한다. 예를 들어 컴포넌트가 언마운트될 때 fetch를 취소하려면 다음처럼 할 수 있다:
function Cities(this: Remix.Handle) {
let list = [], isLoading = true;
fetch("https://api.remix.run/cities.json", {
signal: this.signal // <-
})
.then(response => response.json())
.then(data => {
list = data;
isLoading = false;
this.update();
});
return () => …
}
Remix는 컴포넌트 라이브러리를 동봉할 예정이다. React에는 Remix에서 동작하지 않는 방대한 컴포넌트 생태계가 있으므로, 경쟁력을 유지하기 위해 팀은 고품질의 기본 제공 컴포넌트를 준비 중이다. 디테일과 접근성을 갖춘 메뉴, 폼 같은 것들이다. 또한 React 대비 은근히 유용한 품질 개선도 제공한다. 예를 들어 내장 css 프롭, className 대신 class 등이다.
백엔드에서는 Remix가 웹 플랫폼에 더 강하게 기댄다. 핸들러는 웹의 Request를 받고 Response를 반환한다. 또한 서버로 FormData, File 같은 것들을 가져온다. 이렇게 하면 서버 런타임은 구현 상세가 된다. Node, Deno, Bun 등은 이러한 웹 API에 대한 네이티브 지원이나 어댑터를 제공하므로 Remix는 어디서나 실행될 수 있다.
v3에서는 파일 기반 라우팅을 포기했다. 지원하려는 범위를 파일 이름만으로 담아내기에는 너무 골치 아팠기 때문이다. 대신 TypeScript 기반 라우트 정의 방식을 도입한다. TypeScript는 모든 라우트가 구현되었음을 보장하고, URL 파라미터가 핸들러로 전달되며, 링크가 절대 깨지지 않도록 한다.
let routes = route({
home: "/",
about: "/about",
books: {
index: { method: "GET", pattern: "/" },
create: { method: "POST", pattern: "/" },
show: "/books/:slug",
},
});
// 서버에서의 구현
router.map(routes.books, booksHandlers)
// 클라이언트에서의 참조
<a href={routes.home.href()}>…</a>;
서버에는 로깅, 파일 저장 같은 건전한 기본 기능도 내장되어 있다.
The gap은 클라이언트와 서버 사이에서 일어나는 일이다. Remix v3의 접근은 RSC보다 훨씬 덜 마법 같다. 커스텀 JSON 하이드레이션 스트림이 없다. 대신 Remix는 비동기 로딩 경계를 만들고, HTML을 전송 포맷으로 사용하며 HTMX 스타일로 사실상 iFrame을 재발명한다. 또한 웹 컴포넌트를 활용해 HTML과 JS를 결합하려는 힌트도 보인다.
Remix는 웹 플랫폼과 TypeScript에 계속 기대고, 풀스택의 더 넓은 부분을 직접 통제하며, React의 미묘한 부분을 단순한 this.update()로 대체한다. 덕분에 코드는 덜 마법 같아지고, 사람과 LLM 모두에게 이해하기 쉬워진다. Remix는 Next.js가 되었어야 할 것의 Grug Brain 버전에 가깝다.
그러면 이제 어떻게 될까?
소음과 뜨거운 의견이 많아질 것이다(이 글도 포함해서). 유튜브 인플루언서들은 벌써 다음 하이프를 타기 위해 영상을 찍고, 썸네일에 놀란 표정을 합성하고 있을 것이다. 투덜거리는 Hacker News는 또 하나의 JS 프레임워크가 나왔다고 불평할 것이다.
지금 당장 Remix를 배우고 모든 프로젝트를 이주해야 할 것처럼 느껴질지 모른다. 그럴 필요 없다. React는 여전히 괜찮다(React를 선택해서 잘린 사람은 없다). 좋은 부분만 계속 골라 쓰면 된다(아마도 JS 세계의 상수 같은 얘기다).
Remix v3는 React 커뮤니티의 더 강해진 좌절감과 변화를 향한 열망을 보여주는 신호다. 함수형 ↔ 명령형 추의 진자가 다시 반대쪽으로 움직이기 시작했다.
