프레임워크는 주장의 묶음이다
현대 최고의 웹 애플리케이션 프레임워크를 제1원리에서 정의하려는 시도 — 웹 앱의 정의에서 도출한 8가지 문제, 각각 '뒤집는 조건'을 붙인 9가지 설계 판단, 그리고 거의 아무도 출하하지 않는 7가지 게이트.
질문을 제대로 세우기
“최고의 웹 프레임워크는 무엇인가”는 대개 대리전이다. 사람들이 내놓는 답은 자신이 이미 쓰고 있는 프레임워크이고, 논쟁은 매몰 비용의 변호가 된다. 여기서는 반대로 해보고 싶다. 기존의 무언가와 일치하는지 확인하기 전에 최고의 웹 애플리케이션 프레임워크를 정의한다. 답이 내 스택에 유리하게 나오는지도 신경 쓰지 않는다.
그러려면 먼저 “최고”라는 말을 분해해야 한다. 이 말은 여러 질문을 묶고 있기 때문이다:
- 천장 — 그 프레임워크로 도달할 수 있는 앱 품질의 상한(레이턴시·오프라인·실시간)
- 바닥 — 평범한 팀이 특별히 애쓰지 않아도 보장되는 하한(기본값으로 올바르고·안전하고·빠름)
- 검사 가능성 — 프레임워크가 세우는 주장 중 기계가 검사하는 것의 비율
세 번째가 이 글 전체의 척추이므로, 정의로 먼저 써 둔다:
프레임워크란 애플리케이션에 대한 주장의 묶음이며, 최고의 프레임워크란 가장 강한 주장을, 그것을 검사하는 게이트와 짝지어 출하하는 것이다.
현행 세대의 주장은 거의 전부가 선언이지 검사가 아니다. “캐시는 revalidateTag 로 무효화된다” — 태그를 잊은 읽기는 오래된 데이터를 조용히 계속 반환하고, 아무도 아무 말도 듣지 못한다. “서버와 클라이언트는 같은 트리를 그린다” — React 는 개발 중에는 경고하고, 프로덕션에서는 조용히 고친다. 오라클은 갖춰져 있는데 검사는 한 번도 만들어지지 않았다.
웹 애플리케이션이란 무엇인가
정의는 거기서 열거할 수 있을 때 생산적이 된다. 내 정의는 이렇다:
웹 애플리케이션이란, 작성자가 제어하지 않는 실행 환경(브라우저)에서 UI 가 동작하고, 작성자가 제어하는 환경에 기록의 본체가 있으며, 신뢰할 수 없는 네트워크가 그 사이에 있고, 여러 주체가 동시에 사용하며, 사용되는 도중에 계속 다시 만들어지는 프로그램이다.
이 문장의 각 절에서 문제가 정확히 하나씩 나온다. 여기가 대충 넘어가면 안 되는 부분인데, 기존 프레임워크의 기능 목록에서 출발하면 아무도 답하지 않는 문제는 처음부터 시야에 들어오지 않는다.
| # | 문제 | 정의의 어느 절에서 나오는가 |
|---|---|---|
| P-1 | 상태와 그 사본 | “기록의 본체” — 모든 캐시와 레플리카의 일관성·무효화 |
| P-2 | 경계 | “신뢰할 수 없는 네트워크” — 지연·부분 장애·적대적 입력·양단의 버전 차 |
| P-3 | 주체 | “여러 주체” — 모든 효과는 누군가로서 실행된다 |
| P-4 | 표시와 갱신 | “UI 가 동작” — 첫 렌더링·갱신 입도·스트리밍 |
| P-5 | 배포(전달) | “작성자가 제어하지 않는 실행 환경” — 코드 자체가 매번 네트워크 너머로 도착하고, 전부 도착하지 않을 때가 있다 |
| P-6 | 진화 | “계속 다시 만들어진다” — 스키마 마이그레이션·롤링 배포·버전 스큐·설치 기반 |
| P-7 | 시간과 동시성 | “동시에 사용” — 낙관적 갱신·충돌·실시간 |
| P-8 | 검사 가능성 | (메타) P-1~P-7 에 대한 답이 주장으로 끝나 있지 않은가 |
2026년을 “답안지”로 읽기
프레임워크의 각 계열은 이 8문항에 대한 답안지다. 대부분의 칸은 빈 채로 있다:
| 계열 | 가장 좋은 답 | 빈 칸 |
|---|---|---|
| Next.js (RSC) | P-4 스트리밍은 현재 최고. P-1 은 절반 | P-2(경계가 함수의 옷을 입고 있다)·P-3(권한이 ambient)·P-6·P-8 |
| React Router / Remix | P-5(사상으로서의 progressive enhancement)·P-2 의 명시성(loader/action) | P-1·P-6 — 사상은 있지만 검사는 없다 |
| TanStack Start | P-2 의 타입(끝에서 끝까지) | 타입이 장애와 버전을 말하지 않는다 — T 의 옷을 입은 Remote<T> |
| SvelteKit / SolidStart | P-4 의 갱신 입도(시그널) | P-1·P-6 |
| Qwik | P-4 의 기동(resumability — 같은 일을 두 번 하지 않는다) | 생태계: 공개 웹에서 672 사이트(2026-07) |
| Astro | P-5(zero JS 기본·islands) | 애플리케이션보다 문서용 |
| HTMX / Hotwire | “분할하지 않음”으로 P-1·P-2 를 지운다 | P-7 의 낙관적 UI·오프라인·레이턴시의 바닥 |
| Phoenix LiveView | P-7(server push·diff 전송) + 분할하지 않음 | 접속마다 서버 상태를 드는 비용·오프라인 불가 |
| Rails / Laravel / Django | P-6 중 데이터 마이그레이션은 어디보다 성숙. 관례라는 바닥 | P-2·P-4 는 한 세대 전 그대로 |
| Convex | P-1·P-7(transactional reactive queries) | P-5. DB 가 프레임워크의 일부 |
| Zero / Electric / PowerSync | P-1·P-7 의 최전선(query-driven replication. Zero 는 2026-06 에 1.0) | P-3 이 가장 어렵고, 클라이언트 상주 스키마로 P-6 도 어려워진다 |
| Lamdera | P-6 에 타입 검사된 마이그레이션으로 답하는 유일한 존재. P-2 도 한 언어로 타입이 통한다 | 규모와 생태계 — 다만 선례로서의 가치와는 무관 |
| Meteor(역사) | 아래 D-3 과 같은 주장을 2012년에 세웠다 | 구현(oplog tailing)이 스케일에서 죽었다. 주장이 틀린 게 아니라, 스케일 검사 없이 출하되었다 |
그리고 거의 아무도 답하지 않는 문제가 3가지 있다.
P-6, 버전 스큐. 배포한 순간, 옛 HTML 을 든 클라이언트가 새 서버를 호출한다. 모든 앱에서 매 배포마다 반드시 일어나는데, 경계에 버전이 없다. 오늘날의 실질적인 답은 Vercel 의 Skew Protection 뿐 — 유료 플랜의 인프라 기능이지, 어느 프레임워크의 의미론에도 들어 있지 않다. 데이터 쪽 절반에 답하는 것은 Lamdera 뿐이고, 영속화된 타입을 바꾸면 컴파일러가 마이그레이션 함수를 요구한다.
P-3, 주체. 권한 부여가 middleware 와 context 에 ambient 하게 놓여 있다 — 경로의 속성이 되어 있지만, 필요한 것은 효과의 속성이라는 점이다. CVE-2025-29927 에서는 헤더 하나로 권한 레이어를 통째로 지나칠 수 있었다. CVE-2025-66478 — RSC 와이어 프로토콜의 CVSS 10.0 역직렬화 RCE — 는 경계를 적대적 입력의 파서로 다루지 않았을 때 무슨 일이 일어나는지의 실례다.
P-8, 검사. 서버/클라이언트 렌더링 일치·무효화의 건전성·권한 커버리지 — 전부 오라클을 얻을 수 있는데, 전부 게이트로 출하되지 않는다.
9가지 판단
여기부터가 정의의 본체다. 9가지 판단에 이유와, 중요하게는 “무엇이 관측되면 이 판단을 버릴 것인가”를 붙인다. 반증 조건을 말할 수 없는 정의는 정의가 아니라 기분이다.
D-1. 배치는 “앱의 선택”이 아니라 “기능별 선언”
오늘날 상태를 어디에 둘지 — 전부 서버(LiveView)·분할(RSC)·클라이언트 상주(local-first) — 는 프레임워크를 고른 순간 전 기능에 일괄로 정해진다. 하지만 실제 앱은 혼합이다. 설정 화면은 server-only 로 충분하고, 에디터는 local-first 가 필요하며, 결제는 server-only 이어야 한다. 최고의 프레임워크는 server-only ↔ streamed ↔ interactive ↔ local-first 의 스펙트럼을 기능 단위 선언으로 펼치고, 쓰지 않는 배치의 코드는 출력에 일절 나타나지 않는다. “분할하지 않음” 학파는 스펙트럼 끝의 일급 시민으로 흡수된다. 대립하는 아키텍처가 아니다.
뒤집는 조건: 배치 선언이 실제로는 안정되지 않는다는 것 — 같은 기능이 런타임 문맥에 따라 다른 배치를 요구하는 것 — 이 다수파라고 실측되면.
D-2. 경계는 함수가 아니라 프로토콜
server function 은 경계를 함수 호출로 판다. 그 의상이 숨기는 것은 4가지다: 부분 장애·재시도·양단의 버전 차·적대적 입력. 경계는 (1) 이름과 버전을 갖고, (2) 스키마를 가지며(직렬화 가능성은 타입으로 판정), (3) 장애가 타입에 나타나고 — T 가 아니라 Remote<T> — 멱등성을 선언해 재시도 의미론을 거기서 도출하며, (4) 호환성 게이트(후술 G-2)를 통과한다. 설탕으로서의 함수 구문은 좋다. 거짓말하는 타입은 안 된다.
뒤집는 조건: 타입에 드러낸 장애의 의식 비용이, 잡아낸 장애 버그의 비율에 걸맞지 않다고 실측되면. 그때는 기본값을 “암묵적 경계 + 일률적 재시도 의미론”으로 눕힌다.
D-3. 읽기는 구독이고, 무효화는 손으로 쓰게 하지 않고 도출한다
인간의 주석에 의한 캐시 무효화 — revalidateTag·staleTime — 는 검사 없는 주장이다. 잊힌 태그는 오래된 데이터를 조용히 반환한다. 최고의 프레임워크에서 읽기는 기본적으로 살아 있는 쿼리가 된다: 어느 쓰기가 어느 쿼리에 영향을 주는지 시스템이 도출해 갱신한다. 도출할 수 없는 쿼리에는 “썩어도 된다”고 명시적으로 선언하게 한다. 기본값의 방향이 뒤집힌다 — 썩는 읽기야말로 표시가 붙은 특별한 경우가 된다.
Meteor 가 2012년에 정확히 같은 주장을 세우고 oplog tailing 위에서 죽었다. 하지만 전제가 바뀌었다. logical replication 과 CDC 는 이제 DB 쪽의 표준 장비이고, query-driven replication(Zero)과 transactional reactive queries(Convex)는 프로덕션 구현으로 존재한다.
뒤집는 조건: 실제 앱의 쿼리 분포에서 도출 가능한 비율이 너무 낮다고 실측되면. escape hatch 가 다수파라면 그것을 기본값이라 부를 수 없다.
D-4. 주체를 ambient 로 하지 않는다
경계를 넘는 모든 효과는 principal 을 명시적으로 — capability 로서 — 받는다. “권한 검사에 도달하지 않고 실행할 수 있는 효과”는 컴파일 에러여야 하고, 최소한 후술할 G-3 이 런타임에 센다. CVE-2025-29927 은 이 실패의 교과서적 형태다: 권한을 경로 도중의 레이어에 두면, 레이어를 우회하는 입력은 권한째로 우회한다. 노출은 효과의 속성이지 경로의 속성이 아니다.
뒤집는 조건: 이 의식 때문에 개발자가 전능한 principal 을 하나 만들어 전부에 넘기게 되면 — ambient authority 의 재발명이다. (이것 자체도 게이트가 감시할 수 있다: 모든 효과의 principal 이 동일하면 경고하면 된다.)
D-5. HTML 이 토대, JavaScript 는 강화
이것은 이데올로기가 아니라 부분 장애 공학 — P-5 에 대한 답이다. 코드는 매번 네트워크 너머로 도착하고, 도착하지 않을 때가 있다. 열악한 회선, 회사 프록시, 확장 프로그램, 혹은 그냥 느림. form 이 HTML 로서 일하는 앱은 전달 장애에 대해 우아하게 낮아지고, 낮아질 곳이 없는 앱은 전손한다. 첫 표시와 주요 쓰기 경로는 HTML + HTTP 로 완결되고, JavaScript 는 그것을 빠르게 한다.
뒤집는 조건: 그 앱 부류에 의미 있는 축퇴형이 존재하지 않으면(에디터·게임·지도). 이것은 D-1 이 흡수한다 — 해당 기능만 “강화 필수”를 선언하면 된다.
D-6. 같은 계산을 두 번 하지 않는다
hydration — 서버가 이미 실행한 것을, 이미 받은 결과를 재구축하기 위해 클라이언트가 다시 실행하는 것 — 과 VDOM 리렌더 — 값 하나가 바뀌었다고 부분 트리를 재실행해 diff 를 뜨는 것 — 는 같은 병의 두 얼굴이다. 의존을 추적하지 않았기 때문에 재계산으로 구멍을 메우고 있다. 기동은 서버가 하던 일의 이어서 재개하고(resume), 갱신은 의존 그래프(시그널)를 흐른다. 재계산은 모델이 아니라 예외다.
정직한 주석 하나. resumability 의 최전선인 Qwik 은 공개 웹에서 672 사이트다. 보급 부진은 설계의 반증이 아니지만, “직렬화 가능한 경계를 인간에게 관례로 유지시키는” 것이 성립하지 않는다는 증거이긴 하다. 그래서 D-9 없는 D-6 은 Qwik 의 재연이 된다.
뒤집는 조건: 직렬화 + 복원 비용이 재실행을 웃도는 형태가 지배적이라고 실측되면. 그때는 재실행이 옳다.
D-7. 진화는 일급 주제다
애플리케이션이란 “지금의 코드”가 아니다. 지금 살아 있는 모든 버전의 코드와 모든 기존 데이터다. 따라서: (1) 스키마 마이그레이션은 기존 데이터 전건에 대해 타입 검사된다 — 선례는 Lamdera — (2) 경계는 스큐 윈도우(어느 버전의 클라이언트까지 살릴 것인가)를 선언하고, 게이트가 윈도우 안의 전 조합을 검사하며, (3) expand/contract 2단 배포는 부족의 지식이 아니라 프레임워크가 아는 절차가 된다.
뒤집는 조건: 없다. 이것은 트레이드오프가 아니라 현실의 기술이다. 너무 비싼 구현은 있을 수 있지만, 문제 자체는 사라지지 않는다.
D-8. 게이트는 프레임워크에 동봉된다
위의 모든 주장은 그것을 검사하는 게이트(다음 절의 7가지)와 짝으로 출하된다. 게이트는 pin 된 기지(旣知)의 괴리가 고쳐졌다는 것도 검출한다. 그렇지 않으면 더는 참이 아닌 선언이 쌓여 간다.
D-9. 이것은 라이브러리로는 만들 수 없다
여기까지의 판단은 언어 자체에 4가지 성질을 요구한다: (1) 직렬화 가능성이 타입으로 판정 가능 — 어느 값이 경계를 넘어도 되는가, (2) 배치가 효과로서 타입에 보인다 — server-only 효과는 client 배치 코드에서 호출 불가, (3) 렌더링이 결정적 — G-1 의 전제, (4) 스키마 버전이 언어의 값 — D-7 의 전제. 현행 세대는 (1)과 (2)를 문자열 "use server" 와 lint 로 근사하고 있다. 문자열의 존재는 성질의 성립이 아니다 — CVSS 10.0 의 경계 CVE 는 그 근사의 청구서다. 그러므로 최고의 프레임워크는 컴파일러 협조형이다: 언어 그 자체이거나, 타입 있는 서브셋 + 코드 생성이거나.
뒤집는 조건: TypeScript 의 타입 시스템(혹은 미래의 이펙트에 상당하는 언어 기능)이 (1)과 (2)를 건전하게 표현할 수 있게 되면.
떨어져 나오는 형태
9가지 판단을 하나의 물체로 다시 쓰면 7층이 된다:
- 컴파일러 층 — 하나의 프로그램을 여러 타깃(server / client / edge / worker)으로 분할해 배포. 배치는 기능별 선언. 경계를 넘는 값은 타입으로 검사
- 프로토콜 층 — 경계의 와이어 형식: 이름·버전·스키마·타입에 나타나는 장애·멱등성. 적대적 입력의 파서로 설계된다
- 데이터 층 — 기록의 본체 + 살아 있는 쿼리. 무효화는 도출. 트랜잭션은 낙관적 갱신의 되감기까지 포함해 끝에서 끝까지
- 주체 층 — capability 가 “누구로서”를 모든 효과에 나른다
- 표시 층 — HTML 토대·resume 기동·의존 그래프 갱신·스트리밍이 평상 운영
- 진화 층 — 검사된 마이그레이션·선언된 스큐 윈도우·설치 기반 관리
- 검사 층 — 게이트들. 이 층이 다른 6층의 주장을 검사한다
7가지 게이트
| # | 게이트 | 무엇을 검사하는가 | 오늘 누가 출하하고 있는가 |
|---|---|---|---|
| G-1 | 렌더링 일치 | 동일 입력 ⇒ 서버와 클라이언트가 동일한 트리를 낸다. 진단 불가한 차이는 기지의 괴리로 pin 하고, 그 해제 조건도 기계가 검사한다 | 아무도(React 는 개발 시 경고·프로덕션은 조용히 수복) |
| G-2 | 경계 호환 | 배포 후보가 스큐 윈도우 안의 모든 클라이언트 버전의 호출을 받을 수 있다 — protobuf 의 호환 게이트를 UI 경계에 적용한 형태 | 아무도(Vercel Skew Protection 은 문제를 우회하는 라우팅이지 검사가 아니다) |
| G-3 | 권한 커버리지 | 경계를 넘는 모든 효과가 principal 검사에 도달한다 — grep 이 아니라 런타임에 센다 | 아무도 |
| G-4 | 무효화 건전성 | 쓰기 뒤에 오래된 읽기가 돌아오는 경로가 존재하지 않는다. 도출 쿼리는 구성적으로 성립하고, “썩어도 됨” 선언이 붙은 읽기에는 독이 든 입력으로 테스트를 댄다 | 아무도 |
| G-5 | 마이그레이션 | 마이그레이션 함수가 기존 데이터 전건을 먹을 수 있다. 그리고 되돌릴 수 있다 | Lamdera 가 타입 레벨만 |
| G-6 | 예산 | 번들 크기·조작 레이턴시·접근성이 숫자로 CI 를 떨어뜨린다 | 외장만(Lighthouse CI 등) |
| G-7 | JS 없이 통과 | form 과 주요 동선이 JavaScript 를 끈 상태로 일을 한다 — D-5 의 검사 | 아무도 |
7가지 전부에 공통하는 설계 규칙이 2가지. 어느 게이트든 몇 건의 검사가 돌았는지를 assert 한다 — “전부 초록”이 빈 테스트 집합에서 생성되어서는 안 된다. 그리고 어느 게이트든 “오라클이 틀렸다” 칸을 가진다 — 그것이 없으면 첫 정당한 괴리가 테스트 삭제로 해결된다.
모르는 것
- Q-1. 배치 선언은 안정되는가. 같은 기능이 오프라인에서는 local-first, 온라인에서는 서버 뒷받침을 요구한다면, 정적인 선언은 도구로서 틀렸을지도 모른다
- Q-2. 실세계 쿼리 중 무효화를 도출할 수 있는 비율은 얼마인가. 아무도 재지 않았다. escape hatch 가 다수파라면 D-3 은 기본값이 될 수 없다
- Q-3. local-first × 권한. 클라이언트가 낙관 적용을 끝낸 쓰기를 서버가 거부할 수 있다. “서버는 거부할 수 있다”와 “클라이언트는 기다리지 않는다”의 합성이 낳는 UI 의미론을 일반적으로 정의한 사람은 아직 없다
- Q-4. 스큐 윈도우의 권위는 누구에게 있는가. 열어 둔 탭은 무한히 산다. 윈도우를 선언해도(D-7) 윈도우 밖에서의 동작 — 강제 리로드인가, 축퇴인가 — 는 미정의인 채다
- Q-5. 언어는 비결정성(시각·난수·로케일·부동소수)을 어떻게 봉하는가. G-1 의 오라클은 그것이 전제이고, 한 곳이라도 새면 괴리 리스트가 채워진다
- Q-6. 가장 큰 미검사 주장은 이 글 자신의 척추다. “게이트를 동봉하면 타입과 보통의 테스트로는 나오지 않는 비율로 결함이 나온다” — 이것은 주장이다. 누군가 이것을 만들고 독이 든 입력을 먹일 때까지, 주장인 채로 남는다
이 문서는 어떻게 틀릴 수 있는가
각 판단에는 반증 조건을 붙였다. 전체에 대해서도 2가지:
- 게이트를 구현하고, 실재하는 앱 하나를 옮겨 싣고, 독이 든 입력을 흘린다 — G-1~G-7 이 타입과 기존 테스트를 넘는 것을 아무것도 잡지 못하면, 이 글의 척추는 웹에는 틀린 것이었다
- 브라우저가 상태 동기화와 버전 관리를 플랫폼 기능으로 갖게 되면, 혹은 공유 메모리 있는 WASM 이 경계의 형태 자체를 바꾸면, P-1~P-8 의 열거는 거짓 전제에서 출발한 것이다. 다시 해야 한다
나는 취미로 프로그래밍 언어를 만들고 있고, 이 정의가 거기에 맞는가가 자명한 다음 질문이다. 그 질문은 여기서는 의도적으로 세우지 않았다. 답에 유리하게 쓰인 정의는 정의가 아니기 때문이다.
Sources
- React: Critical Security Vulnerability in React Server Components (CVE-2025-66478) · Next.js advisory · OX Security analysis
- Vercel security bulletin: CVE-2025-55184 / CVE-2025-55183
- Next.js Version Skew Protection — the complete guide
- PkgPulse: The State of Server Components in 2026
- Kanopy: TanStack DB vs ElectricSQL vs Zero (2026) · PkgPulse: Reactive Sync Engines 2026
- Alternatives to Qwik 2026 · Qwik docs: Resumable
- Lamdera · Lamdera v1.1.0: auto-generated migrations
- Convex: The “full-stack framework” fallacy
- HTMX in Production 2026 · How does Phoenix LiveView work