AI가 프레임워크를 선택하는 방식을 어떻게 바꾸었는가, 그리고 React가 살아남은 두 가지 사례
요약
AI가 코드 작성 비용을 낮추면서 프레임워크 선택 기준이 개발자 경험에서 런타임 크기와 리뷰 효율성으로 변화하고 있음을 설명합니다. 저자는 AI 시대에 더 적합한 스택으로 React 대신 SolidJS와 같은 가벼운 프레임워크를 선택하는 이유를 분석합니다.
핵심 포인트
- AI로 인해 코드 작성 비용이 낮아지며 프레임워크 선택 기준이 변화함
- 새로운 기준은 런타임 바이트 크기와 인간의 코드 리뷰 시간임
- React의 런타임 오버헤드는 AI가 해결할 수 없는 물리적 비용임
- AI 에이전트 활용 시 코드 일관성 유지가 더욱 중요해짐
KeyEcho 1.0을 출시했던 그 130개 파일 규모의 PR에는 제가 한 번도 글로 쓰지 않았던 결정이 포함되어 있었습니다. 바로 데스크톱 앱을 Tauri 1 + Vue에서 Tauri 2 + SolidJS로 전환했다는 점입니다.
같은 여름, upweb.dev는 Nuxt 4에서 SolidStart로 이동했고, keyecho.app (SSR, 5개 언어, Stripe 결제 지원)은 처음부터 SolidStart를 기반으로 구축되었습니다. 세 번의 별개 결정이었지만, 매번 결과는 같았습니다.
범위(Scope)를 먼저 정하겠습니다. 이 포스트는 제 개인 프로젝트를 위한 기본 설정(defaults)에 관한 것입니다. 글의 마지막 부분에는 제가 여전히 React를 사용하는 두 가지 사례를 다룰 것이며, 그중 하나는 매우 훌륭하게 작동합니다.
기준이 바뀌었다
이제 제 저장소(repos)에 있는 대부분의 코드는 AI가 생성합니다. 제 업무는 코드를 작성하는 것에서 코드를 리뷰하는 것으로 옮겨갔습니다.
과거의 프레임워크 체크리스트는 학습 곡선(learning curve)과 개발자 경험(developer experience)에 가장 큰 비중을 두었습니다. 하지만 이제 이 두 가지의 가치는 0에 가깝습니다. 모델은 다섯 가지 프레임워크를 유창하게 작성하며, 대부분의 인간보다 Hooks의 규칙을 더 잘 알고 있습니다. AI는 코드 작성 비용을 파괴했습니다. 하지만 두 가지 비용은 변하지 않았습니다. 바로 모든 사용자가 다운로드하는 런타임 바이트(runtime bytes)와 리뷰에 소비되는 인간의 시간입니다. 새로운 비용 체계에 맞춰 스택을 선택하십시오.
런타임 비용은 아키텍처에 존재합니다. 어떤 프롬프트로도 이를 제거할 수 없으므로, 이것이 프레임워크를 결정합니다. 리뷰 대역폭(bandwidth)은 유한하며, AI의 처리량(throughput)은 구두 합의로 유지되는 일관성을 갉아먹을 것이기에, 이것이 스타일링 레이어(styling layer)를 결정합니다. 저는 몇 년 된 React 코드베이스에서 한두 개 이상의 사례를 보았습니다. 두 개의 애니메이션 라이브러리, 두 개의 캐러셀(carousel) 컴포넌트, 세 개의 스타일링 시스템이 각각 어떤 티켓에서 완벽하게 타당한 이유로 도입되었지만, 그 합계는 아무도 정리할 수 없는 엉망진창인 상태가 됩니다. AI가 이러한 드리프트(drift)를 만들어낸 것은 아닙니다. AI는 그것을 수십 배 더 빠르게 만들었을 뿐입니다.
React는 더 이상 나의 기본값이 아니다: 크기와 성능
먼저 약한 논거들은 버리겠습니다. 의존성 배열(Dependency arrays), 오래된 클로저(stale closures), 리렌더링 폭풍(re-render storms): 저는 이 중 어떤 것도 다시 논쟁하지 않을 것입니다. 모델은 Hooks의 규칙을 완벽하게 숙지하고 있고, eslint-plugin-react-hooks가 대부분의 실수를 잡아내며, 이제 React Compiler가 메모이제이션(memoization)을 처리합니다. 인체공학(ergonomics)에 대한 논쟁은 더 이상 승자를 만들어내지 못합니다.
여전히 승자를 만들어내는 것은 클라이언트입니다. react-dom은 압축 시 약 40 kB이며, 가상 DOM (virtual DOM)과 리콘실러 (reconciler)로 구성되어 모든 사용자의 모든 콜드 방문 (cold visit) 시마다 다운로드, 파싱 및 실행됩니다. React 컴파일러 (React Compiler)는 낭비되는 리렌더링 (re-renders)을 제거할 수 있지만, 런타임 (runtime) 자체를 제거할 수는 없습니다. 개발자들은 이 비용을 결코 느끼지 못하지만, 휴대폰을 사용하는 사용자들은 이를 끊임없이 지불합니다.
저의 프로젝트는 한 명의 사람과 에이전트 (agents) 군단으로 이루어집니다. 채용 풀 (hiring pools)은 저에게 도움이 되지 않으며, 콘텐츠 사이트들은 컴포넌트 라이브러리 (component-library) 생태계에 거의 영향을 미치지 않습니다. 일단 가중치 (weights)가 그렇게 이동하고 나면, 저의 개인적인 스택 (stack)에서 React가 차지할 자리는 남아있지 않습니다.
동일한 앱을 열 번 작성하기
Loren Stewart는 작년에 실험을 진행했습니다: 동일한 칸반 (kanban) 앱을 열 가지 프레임워크로 구현하였고, 기능은 동일하게 (드래그 앤 드롭, 댓글, 태그, 서버 검증) 구성한 뒤, 보드 페이지의 압축된 JS를 비교했습니다.
| 프레임워크 (Framework) | 압축된 JS (Compressed JS) |
|---|---|
| Next.js 16 (React 19) | 176.1 kB |
| ... |
두 가지 비교 결과가 눈에 띕니다. 동일한 메타 프레임워크 (meta-framework)인 TanStack Start에서 React를 Solid로 교체했을 때: 118.2 kB가 60.4 kB가 되었습니다. Next.js와 SolidStart를 비교하면: 4.2배의 격차가 발생합니다. 그의 결론은 React의 아키텍처 비용 (architectural cost)은 어떤 메타 프레임워크도 최적화하여 없앨 수 없는 요소라는 것이었습니다.
언제나 그렇듯 경계선이 존재합니다. 열 가지 구현체 모두 FCP (First Contentful Paint)가 35에서 71 ms 사이였으며, 체감상 구분이 불가능했습니다. 실제 사이트의 병목 현상 (bottlenecks)은 대개 이미지와 비디오이며, 프레임워크 JS는 전체 바이트 (bytes)에서 차지하는 비중이 작습니다. 하지만 두 비용은 다르게 작동합니다. 미디어 최적화는 사이트가 유지되는 동안 지속적인 노동이 필요하지만, 프레임워크의 무게는 pnpm create 시점에 영원히 고정됩니다. AI는 이미 코드를 작성하는 비용을 평탄화했습니다. 동일한 기능 세트에 대해, 네 배나 더 무겁게 시작할 이유는 없습니다.
퍼스트 파티(First-party) 수치, 동일한 사이트, 전후 비교입니다. Nuxt 4 환경에서 upweb.dev는 모바일 PageSpeed 점수 70/100/100/100을 기록했습니다. Cloudflare 레이어에서 여러 차례 튜닝을 시도했지만 성능 점수는 움직이지 않았습니다. 실험실 환경(lab)의 병목 지점은 계속해서 JS 무게(JS weight)를 가리켰습니다. SolidStart로 전환한 후에는 99/100/100/100을 기록했습니다(느린 4G 환경의 Moto G Power 시뮬레이션 기준, FCP 1.5s, LCP 1.8s, CLS 0.006, TBT 0ms). 저 0ms의 TBT(Total Blocking Time)야말로 세밀한 반응성 (fine-grained reactivity)이 가장 큰 보상을 받는 지점입니다. 메인 스레드에서 조정(reconcile)해야 할 가상 트리 (virtual tree)가 없기 때문입니다.
이 비교 수치를 인용하기 전에 전제 조건을 명확히 해야 합니다: Lighthouse 실험실 데이터이며, 단일 페이지 세션이고, 단 하나의 사이트 샘플이며, 프레임워크만이 유일한 변수는 아니었습니다. 사람들은 실험실 점수가 실제 사용자 경험 (real user experience)은 아니라고 지적할 것이며, 그들의 말이 맞습니다. RUM (Real User Monitoring)은 오늘날 사용자들이 어떻게 경험하고 있는지를 알려주지만, 실험실 데이터는 아키텍처가 얼마나 많은 여유 공간 (headroom)을 남겨두는지를 알려줍니다. 동일한 도메인, 동일한 CDN 설정에서 프레임워크만 교체했을 뿐인데 점수가 70에서 99로 뛰었습니다. 공짜 점수였고, 저는 그것을 챙겼습니다.
데스크톱도 동일한 논리가 적용됩니다: Tauri WebView 내부에서는 런타임 (runtime)에서 줄인 모든 킬로바이트(KB)가 콜드 스타트 (cold-start) 시간과 메모리 절약으로 돌아옵니다.
Svelte, Vue, Qwik, 그리고 Marko도 살펴보았습니다
Svelte 5의 룬 (runes) 역시 시그널 (signals) 방식이며, SvelteKit의 54.1 kB는 충분히 작습니다. Vue의 반응성 (reactivity) 또한 같은 계열이며, Nuxt의 74.7 kB는 빅3 메타 프레임워크 (meta-frameworks) 중 가장 좋은 결과였습니다. 이들을 제외하게 만든 결정적인 요인은 언어 레이어 (language layer)였습니다.
.svelte와 .vue는 커스텀 파일 형식입니다. TypeScript는 파일 내부의 한 영역이며, 타입 체크 (type checking)는 브릿지 (bridge)를 통해 실행됩니다. 한쪽에는 svelte-language-tools가, 다른 쪽에는 Volar가 있습니다. 이 브릿지는 일상적인 업무에서는 잘 작동합니다. 하지만 툴체인 (toolchain)이 이동할 때 비용이 발생합니다. 이번 달 8일에 TypeScript 7.0이 GA (General Availability) 되었는데, 이는 공식적으로 약 10배 더 빠른 네이티브 Go 컴파일러로, VS Code 코드베이스의 타입 체크 시간이 125.7초에서 10.6초로 단축되었습니다. JSX는 순수 TS (plain TS)이므로, Solid 프로젝트는 출시 당일에 변경 사항 없이 바로 전환됩니다. 반면 Vue와 Svelte의 템플릿 체크가 의존하는 컴파일러 API (compiler API)는 7.0 버전에 안정적인 버전이 없습니다. 그들은 10월의 7.1 버전을 기다려야 합니다. 추가되는 모든 언어 레이어 (language layer)는 업그레이드 대기열의 또 다른 항목이 됩니다.
Nuxt에 반하는 또 다른 점은, 스트리밍 SSR (streaming SSR)이 여전히 실험적인 ssrStreaming 플래그 뒤에 묶여 있다는 것입니다. 반면 SolidStart의 스트리밍은 createHandler의 세 가지 내장 렌더링 모드 중 하나입니다. 뒤집어야 할 플래그가 필요 없습니다.
Qwik의 재개 가능성 (resumability)은 상호작용이 적은 거대한 랜딩 페이지 문제를 해결하지만, 제 사이트들은 그렇지 않으며, 저는 그 멘탈 모델 (mental model)을 가져가고 싶지 않았습니다. Marko의 28.8 kB는 10개 중 가장 작지만, 제가 수년간 유지 관리할 계획인 두 사이트에게 그 생태계는 너무 작은 도박입니다.
반응성 모델 (reactivity models)은 수렴하고 있습니다. Vue의 refs, Svelte 5의 runes, Angular와 Preact의 signals, 그리고 TC39 Signals 제안까지 모두 같은 방향을 가리키고 있습니다. React는 유일한 예외로, 컴파일러를 통해 이를 임시방편으로 해결하고 있습니다. Signals, JSX, 순수 TS: 이 세 가지를 동시에 요구한다면 남는 것은 Solid입니다.
Astro는 SolidStart보다 작습니다. 왜 Astro가 아닌가요?
해당 표의 한 행은 솔직한 답변을 요구합니다. Astro + HTMX는 34.3 kB로, SolidStart보다 7 kB 더 작습니다. 제 두 사이트 모두 콘텐츠 중심의 마케팅 사이트입니다. 오직 바이트 (byte) 논리로만 따지면, Astro가 승리합니다.
Astro의 모델은 콘텐츠 우선(content first)이며, 기본적으로 자바스크립트(JS)가 없는 제로 JS(zero JS) 방식이고, 인터랙티브한 기능은 아일랜드(islands)를 통해 추가됩니다. 문제는 인터랙티브한 부분을 작성할 때, 각 아일랜드가 어쨌든 컴포넌트 프레임워크(component framework)를 호스팅하게 된다는 점입니다. 결과적으로 프로젝트는 두 가지 방언을 사용하게 됩니다. 페이지 구조를 위한 .astro 템플릿과 동작을 위한 아일랜드 프레임워크이며, 상태(state)는 그 경계(seam)를 통해 전달됩니다. 또한 .astro는 또 하나의 커스텀 파일 형식이므로, 이전 섹션에서 언급한 업그레이드 대기열(upgrade-queue) 문제도 여기서 동일하게 적용됩니다.
만약 당신의 사이트가 순수 블로그나 문서(docs) 형태이고 일 년에 세 번 정도만 변경된다면, SolidStart가 아닌 Astro가 더 나은 답입니다. 팀 규모가 커진다면 가장 좋은 관행은 완전히 분리하는 것입니다. 랜딩 페이지는 Astro나 SolidStart로 구축하고, 콘솔(console)은 자체 스택을 가진 별도의 서브도메인으로 운영하는 것입니다.
생태계 비용: TanStack이 최악의 상황을 해결해주며, 컴포넌트는 직접 키워나가야 한다
SolidStart는 만능 해결책(silver bullet)이 아니며, 가장 큰 약점은 생태계입니다. React가 가진 대부분의 기능에 대해 Solid도 대응하는 기능이 있지만, 성숙도 단계가 한 단계 낮고 선택지도 더 적습니다. 그중 일부는 감수해야 하며, 일부는 우회 방법(workarounds)이 있습니다.
가장 넓은 범위를 커버하는 우회 방법은 TanStack입니다. Router, Query, Form, Table, Virtual: 모든 패키지가 코어(core)와 어댑터(adapters)로 구성되어 있으며, Solid는 일급 시민(first-class) 수준의 어댑터를 제공받습니다. 저는 difflore.dev에서 Solid 버전의 TanStack Start를 프로덕션 환경에서 운영하고 있습니다: solid-router, solid-query, 그리고 인증, 데이터베이스, AI 호출을 포함한 풀 백엔드인 oRPC를 사용하며, 지금까지 누락된 부분은 없었습니다. 이 하나의 벤더가 목록에서 가장 무거운 항목들을 지워줍니다.
더 큰 흐름 또한 도움이 됩니다. 인프라가 프레임워크에 구애받지 않는(framework-agnostic) 방향으로 가고 있습니다. Vite, Nitro, Drizzle, Zod, oRPC, better-auth는 무엇이 UI를 렌더링하는지 상관하지 않으며, 헤드리스 UI(headless UI)는 React 전용에서 모든 사용자를 위한 어댑터를 갖춘 상태 머신(state-machine) 코어 중심으로 이동했습니다. 생태계에서 프레임워크 전용이 차지하는 비중은 줄어들고 있습니다. 하지만 그 이면을 소리 내어 말할 필요가 있습니다: React 전용 라이브러리에서 얻는 모든 편의성은, 당신이 언젠가 그곳을 떠나기 위해 넘어야 할 벽을 높입니다. 생태계는 자산인 동시에 족쇄이기도 합니다.
남아 있는 격차는 세련된 컴포넌트 라이브러리(component library)입니다. React 측의 표준은 shadcn/ui입니다. Solid에는 커뮤니티 포팅(ports) 버전들이 있지만, 영향력(traction) 면에서는 아주 미미한 수준입니다. 이것이 저의 작업을 늦추지는 않는데, shadcn 자체가 그 이유를 증명하기 때문입니다. shadcn은 npm 패키지를 배포하지 않습니다. 대신 컴포넌트 소스 코드를 당신의 저장소(repo)로 복사하며, 이를 통해 컴포넌트를 의존성(dependency)이 아닌 당신 자신의 코드로 변환합니다. 이러한 정의야말로 AI가 빠르게 생성해낼 수 있는 바로 그것입니다. 하나의 컴포넌트를 React 버전과 Solid 버전으로 두 번 작성하는 것은 단 몇 분의 작업일 뿐입니다. 현재 제가 사용하는 인터페이스 중 사전 구축된 라이브러리를 사용하는 것은 하나도 없습니다. 두 사이트 모두 토큰(tokens) 위에 수작업으로 작성된 컴포넌트들입니다. React 관리자 페이지는 Base UI를 사용하고, Solid 측은 Kobalte를 사용하는데, 이들은 모두 외형과 동작을 제가 정의한 헤드리스 프리미티브(headless primitives)들입니다. AI는 출력(output) 문제를 해결했습니다. 하지만 AI가 해결하지 못한 것은 컴포넌트 API를 설계하는 방법, 상태(state)가 어디에 위치해야 하는지, 그리고 토큰이 어떻게 강제되는지입니다. 그 기술은 React와 Solid 사이에서 완전히 전이됩니다. 컴포넌트 라이브러리의 해자(moat)가 얕아질수록, 그 기술을 연습하는 가치는 더욱 높아집니다.
스타일링(Styling): 유틸리티 클래스(utility classes)가 기본 자리를 잃다
비난처럼 들리지 않도록 현재 상태를 먼저 짚고 넘어가겠습니다. KeyEcho 데스크톱은 UnoCSS 기반으로 1.0 버전을 출시했고, 지금도 여전히 그것을 사용 중이며, 이를 들어낼 계획도 없습니다. 해당 UI는 규모가 작고 고정되어 있으며, 맞출 디자인 파일도 없고, 한 번 정해지면 패널이 거의 바뀌지 않기 때문에 유틸리티 클래스가 잘못될 여지가 거의 없습니다. 하지만 관리자 플랫폼과 마케팅 사이트는 형태가 다릅니다. 페이지는 계속 늘어나고, 스타일은 리디자인(redesign)을 따라가야 하며, 수호해야 할 디자인 언어(design language)가 존재합니다. 아래의 두 가지 실패 사례는 바로 이 두 번째 형태에 해당합니다.
AI의 손에서 유틸리티 클래스는 두 가지 지점에서 실패합니다.
첫 번째는 토큰 드리프트 (token drift)이며, Tailwind와 UnoCSS는 똑같이 이 문제에 노출되어 있습니다. 프로젝트는 자체적인 디자인 토큰 (design tokens)을 가지고 있지만, 모델은 여전히 학습 메모리에 있는 기본 스케일 (default scale)을 찾으려 하거나 임의의 값을 입력해 버립니다. 리뷰 단계에서 gap-3와 gap-[12px]는 거의 동일해 보이며, 오늘날 이들은 정확히 똑같이 렌더링됩니다. 차이점은 두 번째 방식이 토큰 시스템을 벗어났기 때문에, 다음 스케일 변경 시 이를 반영하지 못한다는 것입니다. 모든 코드는 문법적으로 유효하지만, PR (Pull Request)이 거듭될수록 디자인 시스템은 조금씩 갈라져 나갑니다 (fork). 저는 이런 일이 일어나는 것을 한두 번 본 것이 아닙니다. Tailwind v4의 @theme는 토큰 정의를 깔끔하게 정리하지만, 임의의 값 (arbitrary values)은 영원히 유효한 상태로 남습니다.
두 번째는 크기입니다. 원자적 CSS (Atomic CSS)는 클래스 재사용에 기반합니다. 즉, 페이지 수가 늘어나도 스타일시트 (stylesheet)가 계속 커지지 않아야 한다는 전제가 있습니다. 하지만 임의의 값이 충분히 많아지면 그 전제는 무너집니다. 모든 w-[347px]는 일회성 규칙이 되며, 스타일시트는 PR 횟수에 따라 커지고, HTML 내의 클래스 문자열도 마찬가지로 늘어납니다. 이토록 허용적인 제약 모델 (constraint model)은 모든 것을 컴파일하며, 그 비용은 다른 곳에서 청구됩니다.
vanilla-extract는 토큰을 TypeScript export로 변환합니다. 스타일은 .css.ts 파일에 존재하며, 색상은 반드시 테마 (theme)에서 임포트 (import)해야 합니다. 오타가 난 변수는 타입 에러 (type error)가 발생하며, 기억에 의존해 임의로 만든 헥스 값 (hex value)은 타입 체크 (type checking)를 통과할 수 없습니다. upweb.dev의 테마 계약 (theme contract)은 createThemeContract를 통해 16개의 토큰을 정의합니다. 라이트 모드와 다크 모드는 각각 모든 키를 할당해야 하며, 다크 모드 값이 하나라도 누락되면 빌드 (build)가 실패합니다. 이제 규칙은 누군가의 기억력이나 모델이 문서를 읽었는지 여부에 의존하지 않습니다. 출력물은 런타임 (runtime) 비용이 없는 순수 CSS이며, 라우트 (route)별로 분할됩니다. 방문자는 사이트 전체 시트가 아닌, 현재 페이지에서 사용하는 스타일만 다운로드합니다.
CSS-in-TS에 대한 가장 오래된 불만은 명명 (naming) 문제였습니다. AI가 이 문제를 뒤집었습니다. 모델이 이름을 짓고 저는 리뷰만 하면 됩니다. 그리고 이름이 붙은 블록 (named block)은 30자 길이의 유틸리티 문자열보다 더 빠르게 읽힙니다.
CSS 분야의 나머지
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기