월간 후론트
8월의 프론트엔드는 8월 4일 하루로 요약된다. 그날 npm에서 벌어진 일 앞에서 나머지 뉴스는 전부 배경음이 됐다. 그러거나 말거나 프레임워크들은 뻔뻔하게도 자기 자랑을 계속했다. Next.js는 dev 메모리 90%를 덜어냈다고 자랑한 직후 무인증 RCE 두 건을 데리고 나왔다. Rust로 다시 쓴 것들은 약속이라도 한 듯 한꺼번에 stable에 도착했다.
이달의 표지는 그중 어느 것도 아니다.
Shai-Hulud, 두 번째 등장
8월 4일 UTC 기준, keyv 메인테이너 계정이 털렸다. 여기까지는 흔한 이야기다. 흔하지 않은 건 그다음이다.
감염된 패키지의 dist 폴더는 직전 정상 릴리스와 바이트 단위로 동일했다. 라이브러리 코드에는 손끝 하나 대지 않았다. 악성 동작은 오직 package.json에 한 줄 추가된 "preinstall": "node setup.mjs"에만 있었다. 설치하고 나면 패키지는 멀쩡히 잘 돌아간다. 아무도 눈치채지 못한다.
setup.mjs는 시스템에 bun이 있으면 그걸 쓰고 없으면 GitHub 공식 릴리스에서 Bun 1.3.13을 다운받아 710KB짜리 진짜 페이로드를 실행하고 흔적을 지운다. 호스트 Node 버전과 Node 레벨 모니터링을 통째로 우회하려고 그렇게 짰다. 훔쳐 가는 목록을 읽어보면 차라리 감탄이 나온다. npm·Yarn·PyPI 토큰, AWS/Azure/GCP/알리바바/텐센트/Hetzner 설정, Kubernetes·Helm·Vault, .env, 셸 히스토리, SSH 키, VPN 프로필, 브라우저 자격증명 저장소, 크립토 월렛, 거기에 OpenAI·Anthropic·Cursor·Codex·Gemini 크리덴셜까지 쓸어 간다.
그다음이 진짜 문제다. 훔친 npm 토큰으로 자기를 복제한다. bypass_2fa가 켜져 있고 write 권한이 있는 토큰을 찾으면, 그 토큰으로 배포 가능한 모든 패키지의 tarball을 받아 scripts를 통째로 덮어쓰고 patch 버전을 올려 다시 퍼블리시한다. JFrog 집계로 400개 이상 패키지, 1,700개 이상 버전. 외부 집계로는 최대 868개, 월 다운로드 합산 20억 회다.
여기서 당신이 남 일이라고 생각하면 안 된다. flat-cache(월 5.8억)와 file-entry-cache(월 5.71억)는 ESLint 의존성 체인에 들어 있다. 프론트엔드 레포 대부분이 간접 의존한다. 직접 설치한 적이 없어도 노출됐다.
이 웜은 npm만 노리지도 않았다. 저장소당 최대 50개 브랜치에 .vscode/tasks.json(폴더 열면 실행)과 .claude/settings.json(세션 시작 시 실행) 훅을 심는다. 커밋 메시지는 chore: update config, 작성자는 claude로 위장했다. npm install을 치지 않아도 감염된 저장소를 VS Code로 열거나 Claude 세션을 시작하는 것만으로 발동한다.
마지막으로 이번 사건의 진짜 교훈. @opensearch-project/opensearch 케이스에서 웜은 release-drafter.yml 워크플로 컨텍스트 안에서 Actions OIDC 토큰을 npm publish 토큰으로 교환하고 Fulcio·Rekor를 거쳐 정상 provenance가 붙은 악성 tarball을 만들어냈다. 서명을 위조하지 않았다. 정당하게 발급받았다. 침해가 빌드 상류에서 일어났으니 서명 검증은 경고 한 줄 낼 이유가 없었다.
provenance 배지가 초록불이면 그 tarball이 그 워크플로에서 나왔다는 사실만 보증한다. 그 워크플로가 정상인지는 아무도 보증하지 않는다.
당신이 지금 해야 할 일은 아래 즉시 섹션 첫 항목에 있다. 읽고 넘기지 말고 진짜로 해라.
이번 달을 여덟 개로 압축하면
| 숫자 | 무엇 |
|---|---|
| 400+ | Shai-Hulud 웜에 감염된 npm 패키지 (버전 기준 1,700+) |
| 20억 | 감염 패키지들의 합산 월 다운로드 |
| 21.5GB → 2GB | Next.js 16.3의 dev 서버 메모리 (vercel.com 대시보드 기준, 약 90%↓) |
| 2.71GB → 380MB | TanStack Table V9의 retained heap (100만 행 × 8열, 최대 86%↓) |
| 1,517개 | Bun 1.4가 추가로 통과한 Node.js 테스트 |
| 7.5kb → 784B | Zod 4.5에서 z.string() 스키마 하나가 잡는 힙 |
| 24,975ms → 70ms | Solid 2.0 Rust 컴파일러의 1MB 단일 모듈 처리 (355배) |
| 2건 | 8월 말 Next.js가 패치한 무인증 RCE |
표만 보고 닫아도 이번 달 할 일은 다 파악된다
| 판정 | 항목 | 한 줄 |
|---|---|---|
| 즉시 | Next.js 16.3.3 / 15.5.24 | 무인증 RCE 2건. 우회 방법 없음 |
| 즉시 | keyv 계열 침해 대응 | 회전 말고 폐기부터 |
| 즉시 | Node 26.8.1 | 26.8.0은 버전 문자열이 깨져 나왔다 |
| 즉시 | Express body-parser | DoS 보안 릴리스 |
| 검토 | Next.js 16.3 | 업그레이드는 공짜, Instant Navigations는 유료 |
| 검토 | TanStack Table V9 | 수만 행 다루면 무조건, 수백 행이면 무의미 |
| 검토 | pnpm 12 | Rust 재작성인데 명령어가 그대로다 |
| 검토 | Zod 4.5 | z.compile() 한 줄로 3~9배 |
| 검토 | Oxlint React Compiler 룰 | 컴파일러 안 켜도 룰만 쓸 값어치 |
| 검토 | Bun 1.4 | 호환성이 드디어 논의할 수준 |
| 검토 | Better Auth 1.7 | OAuth를 제대로 하려면 |
| 검토 | vlt 1.0 | 갈아타지 않고 vlt query만 써도 이득 |
| 검토 | Baseline 의존성 감사 | gzip 60~90KB가 그냥 놀고 있다 |
| 관망 | Solid 2.0 RC | async가 리액티브 그래프 안으로 |
| 관망 | React browser() | Canary. mounted 플래그의 사망 선고 |
| 관망 | Vitest 5.0 RC | 브레이킹 30개 이상 |
| 관망 | Waku 1.0 RC / SvelteKit 3 RC / Preact 11 RC | RC가 동시에 셋 |
| 관망 | Node node:zlib ZIP API | 의존성 하나 걷어낼 날이 온다 |
| 관망 | Kitesurf / TermDOM / Octane | 지금 쓰면 안 되지만 재밌다 |
| 참고 | Lovable·Yelp 마이그레이션기, SPA soak test, 코드 제거론 외 | 아래 참조 |
즉시. 이번 주 안에 끝내라. 미루면 그게 사고다.
무인증 RCE 2건, 우회 없음
8월 25일, 크리티컬 2건이 동시에 패치됐다. 원래 예고된 릴리스보다 앞당겨졌다. 업스트림에서 추가 크리티컬이 나왔기 때문이다. 좋은 신호는 아니다.
① AVIF 이미지 최적화 RCE (GHSA-2xp9-vwfh-vxw4). sharp가 내부적으로 쓰는 libheif 결함이라 근본 원인은 Next.js 밖에 있다. 공격자가 제어하는 AVIF를 Image Optimization API가 최적화하는 순간 RCE가 터진다. 영향 범위가 넓다. 10.0.0부터 15.5.23, 그리고 16.3.2까지의 모든 16.x. 패치는 AVIF 최적화 자체를 꺼버린다. 고쳤다기보다 막았다.
② Windows 호스팅 RCE (CVE-2026-75604). Pages Router와 App Router를 함께 쓰면서 Cache Components를 안 쓰는 앱이 Windows 파일시스템에서 돌 때 발생. Linux·macOS는 무관하다.
npm install next@16.3.3 # Active LTS npm install next@15.5.24 # Maintenance LTS
버전만 올리고 끝내지 마라. next.config의 images.formats에 image/avif가 있는지 확인해라. 패치 후에는 AVIF 최적화가 꺼지니 이미지 용량과 LCP 지표가 달라진다. WebP fallback이 제대로 도는지 배포 전에 봐야 한다. remotePatterns를 넓게 열어둔 서비스일수록 ①의 노출도가 높은 것도 기억해라. 공격자가 임의 AVIF를 최적화 파이프라인에 밀어 넣을 수 있으니까.
Windows 이슈는 국내 서비스 대부분(리눅스 컨테이너)에는 무관하다. 다만 알려진 우회 방법이 없다. 해당 환경이면 업그레이드 말고는 길이 없다.
회전 말고 폐기부터
npm ls keyv flat-cache file-entry-cache cacheable cacheable-request cache-manager를 지금 쳐봐라. 8월 4일 이후 이 중 하나라도 설치했거나 CI 빌드가 돌았다면 그 호스트는 감염된 것으로 간주하는 게 기본이다.
순서가 중요하다.
bypass_2fa + write 권한 npm 토큰, GitHub PAT·OAuth·Actions 토큰, 그다음이 AWS·GCP·K8s·Vault·DB·SSH·VPN·AI 서비스다..vscode/tasks.json, .claude/settings.json, chore: update config 커밋, ${{ toJSON(secrets) }}를 직렬화하는 워크플로를 찾아라.npm ci --ignore-scripts를 기본값으로 두고 예외만 허용해라. npm v12·pnpm·vlt는 이게 이미 기본 정책이다.한 가지 더. 감염 규모 집계가 벤더마다 다르다(Endor Labs 373개, SafeDep 79개 이름/353개 버전, Wiz·OX 400~440개, Aikido 최대 868개). 단일 숫자를 확정치로 인용하지 말고 IOC 기준으로 직접 스캔해라. 2차 확산본은 파일명이 Math_Symbol.js에서 math_init.js로 바뀐다. 파일명 하나로 탐지 룰을 짜면 놓친다.
짧게 끝나는 것들
Node 26.8.0은 버전 문자열이 깨진 채로 나가서 몇 시간 만에 26.8.1이 뒤따랐다. 26.8.0은 그냥 건너뛰어라. Express 팀의 body-parser 보안 릴리스도 나왔다. DoS 취약점이고 Express를 쓰는 서비스는 의존성 버전만 확인하면 된다.
검토. 올릴 값어치는 있는데 전부 조건이 붙는다.
업그레이드는 공짜, 헤드라인은 유료
이 릴리스는 두 개의 서로 다른 물건이 한 이름으로 묶여 있다. 헷갈리면 손해다.
공짜 쪽. npm i 한 번의 대가로 얻는 것들이다. dev 서버 메모리가 21.5GB에서 2GB로 떨어진다(nextjs.org는 4,600MB에서 840MB). Turbopack 디스크 캐싱이 next build까지 확대돼 vercel.com/geist가 30초에서 5.5초. App Router 렌더링 레이어를 web streams에서 네이티브 Node.js streams로 교체해 부하 상황 SSR 처리량이 22% 늘었다. pnpm add -D typescript@^7 한 줄이면 next build의 타입 체크가 약 10배 빨라진다. 대형 모노레포에서 next dev 켤 때마다 노트북 팬이 이륙하던 팀은 이것만으로 체감이 다르다.
유료 쪽. 이름값 하는 Instant Navigations는 cacheComponents: true와 partialPrefetching: true 두 플래그가 필요하고 그 전에 Cache Components 마이그레이션이 끝나 있어야 한다. 토글 하나로 끝나지 않는다. 캐싱 모델을 갈아엎는 일이다.
작동 원리는 Vercel의 실전 가이드에 오픈소스 데모 네 개(Next Beats·Drop·Flow·Huddle)와 함께 정리돼 있다. Cache Components가 라우트마다 즉시 보여줄 App Shell을 보장하고 Partial Prefetching이 클릭 전에 그 shell을 가져온다. 캐싱은 'use cache'에 cacheLife('minutes')와 cacheTag('drops', 'drop-{id}')를 얹는 조합이고 뮤테이션 후엔 Server Action에서 updateTag()로 만료시킨다.
실무 팁을 셋만 골라두면 이렇다.
updateTag 누락으로 stale UI가 남는 사고를 막는다.prefetch={true}를 목록에 무분별하게 붙이지 마라. 뷰포트에 들어오는 순간 서버를 호출한다. 미리 준비할 값어치가 있는 링크에만 선별 적용이다.그리고 next/root-params. i18n 하는 팀이라면 이게 조용한 최대 수혜다. [locale] 같은 최상위 세그먼트를 트리 깊은 Server Component에서 await locale()로 바로 읽는다. next-intl은 setRequestLocale 우회책을 통째로 걷어낼 수 있게 됐고 setRequestLocale 누락으로 정적 렌더링이 조용히 깨지던 고질병도 구조에서 사라진다. 단 Route Handler와 Server Action에서는 아직 동작하지 않는다. Client Component에서도 못 쓴다.
4년 만에, 그리고 2년을 갈아 넣어서
2년 넘는 작업 끝에 stable이 됐다. V8과의 간격이 4년. 헤드리스 렌더링 모델은 그대로고 그 아래 아키텍처가 통째로 교체됐다.
숫자부터. 100만 행 × 8열 페이지네이션 벤치에서 retained heap 2.71GB → 약 380MB(최대 86%↓). row·column·cell·header API를 인스턴스마다 재생성하지 않고 공유 프로토타입에 두면서 얻은 결과다. 2만 행 이상 구간 평균 처리 시간은 core row model 79%, expanding 74%, grouping/aggregation 52%, faceting 44%, selection 43%, sorting 37%, filtering 34% 단축(총 1.5~3.9배).
구조는 tableFeatures()로 필요한 기능만 명시 등록하는 방식이다.
import { createPaginatedRowModel, rowPaginationFeature, tableFeatures, useTable, } from '@tanstack/react-table'; const features = tableFeatures({ rowPaginationFeature, paginatedRowModel: createPaginatedRowModel(), }); const table = useTable({ features, columns, data });
상태는 alien-signals 기반 TanStack Store 위로 옮겨가 table.Subscribe로 필요한 슬라이스만 구독한다. Cell Selection(사각 범위·드래그·Shift 확장·다중 비연속), Cell Spanning, 컬럼별 다중 aggregation도 새로 들어왔다. 어댑터는 10종. React는 18+ 지원이고 React Compiler 아래에서 정상 동작한다.
함정. 패키지 총 크기는 14kb에서 25kb로 오히려 커졌다. tree shaking 이득은 기능을 명시적으로 골라 등록했을 때만 나온다. V8 코드를 무지성으로 옮기면 손해다. 테이블 하나만 쓰는 앱이면 특히 그렇다. 그리고 수백 행짜리 테이블에는 이 마이그레이션이 아무 의미 없다. 페이지네이션은 양쪽 다 5ms 미만이라 벤치마크에서 아예 제외됐다. Solid 2는 미지원(V10 목표), Angular 19+, Svelte 5+, Ember 5.8+ 등 어댑터별 최소 버전이 제각각이다.
여러 화면에서 테이블을 반복해 쓰는 제품이라면 tableOptions()와 createTableHook()으로 사내 표준 테이블 훅을 만들어두는 게 이번 버전의 진짜 활용법이다. 기본 정렬·공통 필터·셀 렌더러를 한곳에 묶고 개별 테이블 코드는 짧게 유지한다.
Rust로 다시 썼는데 아무것도 안 바뀌었다
Rust 재작성판이 stable이 됐다. 그런데 이건 의도적으로 마이그레이션이 아니다. v11의 명령어·플래그·설정·락파일 포맷이 전부 그대로라 문서 하나가 두 버전을 동시에 설명한다. 재작성했다는 사실을 쓰는 쪽에서 눈치챌 일이 거의 없다.
pnpm self-update next-12 # npm latest 태그는 아직 pnpm 11을 가리킨다
실무에서 제일 큰 건 락파일 결정성이다. 순환 의존성 멤버를 package id로 정렬해 항상 같은 지점에서 엣지를 끊으니, 락파일이 의존성 그래프의 순수 함수가 된다. importer 순서나 반복 설치와 무관하게 byte-identical. 순환이 많은 대형 워크스페이스는 peer resolution이 2~3배 빨라지고 메모리도 약 25% 덜 쓴다. CI 머지 충돌과 캐시 미스가 줄어든다. 단 첫 재resolve 커밋의 락파일 diff는 크게 잡힐 걸 각오해라.
Linux에서 packageImportMethod: auto가 hardlink 우선으로 바뀐 것도 공짜 이득이다. btrfs에서 reflink는 새 inode를 만들고 extent 메타데이터를 복사하는데 hardlink는 디렉터리 엔트리 하나면 끝난다. warm store 기준 node_modules 구성이 대략 절반으로 준다. ext4는 변화 없다.
그리고 project-aware global bins. 전역 설치된 node·deno·bun이 현재 프로젝트가 pin한 버전을 따라간다. nvm/fnm/volta를 셸 훅으로 굴리던 팀은 대체를 검토할 만하다.
가장 파급 큰 브레이킹: git 의존성이 전부 호스트의 canonical HTTPS URL로 resolve되고 락파일에 SSH URL이 기록되지 않는다. 프라이빗 저장소를 SSH로 받던 파이프라인은 깨진다. CI 이미지마다 git config --global url."git@github.com:".insteadOf https://github.com/를 넣어야 한다.
z.compile(), 그리고 조용한 지뢰
8월 28일 공개. 간판은 z.compile()이다. 임의 스키마를 사전 컴파일해 파싱을 3~9배 빠르게 만든다. 20키 객체 301ns → 38ns(7.8배), 객체 10개 배열 377ns → 68ns(5.5배), discriminated union 92ns → 27ns(3.4배). 순수 z.string() 같은 단순 스칼라는 이득이 거의 없다. 스키마가 복잡할수록 이득이 커지는 구조라 API 라우트나 Server Action의 큰 바디 검증에 제일 잘 맞는다.
도입은 어이없을 만큼 싸다. 엔트리포인트 최상단에 import "zod/compile" 한 줄이면 이후 생성되는 모든 스키마가 첫 파싱 시점에 자동 컴파일된다. 빌드 시스템 통합이 전혀 필요 없다. 단 스키마를 정의하는 모듈보다 먼저 실행돼야 한다.
메모리도 크게 줄었다. 4.4까지는 모든 스키마 메서드가 인스턴스에 바인딩돼 z.string() 하나가 7.5kb를 잡았는데 메서드 메모이제이션으로 784바이트가 됐다(최대 9.8배). .safeParse()가 스택 트레이스를 안 잡으면서 실패 경로도 6.3µs에서 840ns로 내려갔다. 폼 검증처럼 실패가 잦은 경로는 z.validate(schema, input)로 바꾸면 boolean만 돌려받으며 최대 16배 빠르다.
여기서부터가 진짜다. 브레이킹 체인지가 조용히 위험하다.
z.iso.datetime()이 RFC 3339에 맞춰 초(seconds)를 필수로 요구한다. 2020-01-01T06:15Z는 이제 거부된다.__proto__ 키는 항상 제거된다.{ strict: true }를 넘겨라.컴파일러를 안 켜도 이득
Oxlint에 React Compiler 검증 패스를 그대로 돌리는 22개 룰이 들어갔다. 별도 패키지 oxc-transform-react는 자동 메모이제이션 자체를 수행하며 Babel 대비 10배 이상을 주장한다. 파일당 약 100ms에서 약 10ms.
구현 뒷이야기가 재밌다. React 팀이 올해 초 컴파일러를 Rust로 포팅했는데 그 포트는 Babel 형태 AST를 그대로 유지하고 있었다. Oxc는 처음엔 fork를 동기화하다가 AST 변환 왕복 비용 때문에 결국 컴파일러를 vendoring해 Oxc AST 위에서 직접 돌게 바꿨다. 원본 Rust 포트 대비 2배 빨라졌고 바이너리는 8.66 MiB에서 3.97 MiB로 줄었다. 100개 이상 리포와 10만 개 이상 파일에서 Babel과 출력이 동일함을 확인했다고 한다.
{ "plugins": ["react"], "categories": { "correctness": "error" } }
컴파일러를 실제로 켜지 않아도 린트 룰만 먼저 도입하는 게 현실적이다. purity, immutability, set-state-in-effect 같은 룰은 React Compiler와 무관하게 잠재 버그를 잡아준다. 다만 recommended 룰이 전부 correctness에 들어가 있어서 레거시 코드베이스에 바로 켜면 경고가 쏟아진다. 디렉터리 단위 점진 적용을 권한다. 기존 nursery의 react/react-compiler 룰을 켜뒀다면 제거해야 한다.
Vite 통합은 experimental이며 Vite 8과 @vitejs/plugin-react v6.1.0을 요구한다. vendored 컴파일러에 TODO 마커 10개와 Todo 진단 경로 57개가 남아 있다는 것도 알아두자.
이제는 논의할 수준
Zig에서 Rust로의 전면 재작성판이 나왔다. 1.3.14가 마지막 Zig 판이었다. 언어 교체보다 중요한 건 Node.js 테스트 스위트를 1,517개 더 통과했다는 대목이다. Bun 1.0 이후 가장 큰 호환성 도약이다. worker_threads, node:cluster, node:repl, node:domain처럼 빠져 있던 API가 채워졌고 이슈 2,900건이 정리됐다. Next.js·Playwright·Vitest·Nuxt 호환성도 명시적으로 손봤다.
성능은 유휴 CPU 5배 감소, 메모리 최대 35% 감소, 리눅스 시작 속도 50% 향상(hello-world 10.9ms → 5.1ms). mimalloc 일원화, partial page clearing, 유휴 시 회수하는 scavenger 스레드가 그 실체다. 번들러는 큰 그래프에서 code splitting 14배, browser field 리매핑 개선으로 jszip이 459KB에서 149KB로 줄었다.
단서를 셋 달아둔다. ① 이 재작성은 AI 주도로 진행됐고 초기 머지에서 Rust unsafe가 13,000회 이상 사용됐다는 지적이 있었다(비교군 uv는 수십 건). 1.4 시점의 정리 상태는 공개 자료로 확인되지 않는다. Rust니까 메모리 안전하다는 추론은 여기서 성립하지 않는다. ② 벤치마크는 전부 벤더 자체 측정이다. ③ 메이저 재작성 직후 릴리스는 통계적으로 회귀가 잦다. 프로덕션 전환은 1.4.x 패치가 몇 번 쌓인 뒤가 안전하다.
CI 러너나 테스트 러너처럼 프로세스를 자주 띄우고 내리는 곳부터 재평가하는 게 순서다.
Better Auth, vlt, 그리고 의존성 다이어트
max_age 강제, device authorization(RFC 8628). MCP 인증은 @better-auth/mcp로 분리됐고 SCIM은 Groups·role projection 포함 사실상 재작성됐다. Next.js 앱이라면 hydrateSession 하나만으로도 초기 렌더 후 세션 재요청을 없앨 수 있다. 브레이킹이 많다. deprecated된 oidcProvider는 제거됐고 SCIM은 계획된 cutover가 필요하며 Cloudflare D1은 트랜잭션 미지원으로 아예 안 된다.vlt install은 다운로드만 하고 실행은 vlt build로 분리된다. 당장 갈아탈 생각이 없어도 vlt query를 감사 도구로 쓰는 건 비용이 거의 없다. :malware, :cve, :vuln, :eval, :fs, :unmaintained 같은 60개 이상의 CSS 유사 셀렉터 중 절반이 보안용이고 vlt query ':host(local) #react:v(<19)'처럼 머신 전체를 훑을 수도 있다. npm 대비 38% 빠름, 27.5만 버전 플래그 같은 수치는 전부 자체 집계다.timeago.js→Intl.RelativeTimeFormat, numeral→Intl.NumberFormat), HTTP(~17KB: axios→fetch + AbortSignal.timeout), UI 프리미티브(~24KB: <dialog> + showModal() 하나가 모달 라이브러리와 focus-trap을 동시에 대체하고 body-scroll-lock은 body:has(dialog:modal){overflow:hidden} 한 줄로 끝난다), lodash(~8KB: clonedeep→structuredClone, groupby→Object.groupBy). 반대 사례까지 정직하게 다루는 게 이 글의 미덕이다. Temporal은 지금 바꾸면 dayjs 3KB가 44KB 폴리필로 늘어난다. debounce와 throttle은 여전히 네이티브 대체재가 없다.관망. 지금 쓰면 안 되지만 6개월 뒤에 "그거 왜 몰랐지" 소리 안 들으려면.
Solid 2.0 RC. 이번 달 가장 급진적인 물건이다. 설계 결정 하나가 전부다. async를 리액티브 시스템의 속성으로 만든다. 지금까지 모든 프레임워크는 비동기를 동기 코어가 견뎌내야 할 외부 조건으로 취급했는데 Solid 2.0은 그래프 안으로 끌어들였다. computation이 Promise를 반환할 수 있고 하위 그래프 전체가 그걸 이해한다. 그 결과 createResource, batch, startTransition, useTransition, on, createComputed가 전부 제거됐다. 파생 상태·에러 처리·트랜지션·낙관적 업데이트가 이 아이디어 하나에서 자동으로 따라 나오니까. 컴파일러는 OXC 위에 Rust로 재작성돼 1MB 단일 모듈이 24,975ms에서 70ms로 줄었다(355배). SolidStart는 은퇴하고 Vite 플러그인의 start: true 플래그로 흡수됐다. React 팀에도 시사점이 크다. 그래프가 loading 플래그와 null 체크를 대신 처리하게 만드는 방향은 React Suspense와 use()가 향하는 곳과 같은 좌표계에 있다.
React browser() API. Canary에 문서가 올라왔다. use(browser())를 호출하면 서버 렌더 시점에 그 컴포넌트 렌더링이 중단되고 가장 가까운 Suspense fallback이 HTML에 남으며 브라우저에서는 정상 렌더된다. typeof window 체크와 mounted state를 세팅하는 Effect의 사망 선고다. 조건부 호출과 커스텀 훅 내부 호출이 허용돼서 초기 데이터가 있으면 SSR, 없으면 클라이언트로 가는 하이브리드 전략을 훅 하나에 캡슐화할 수 있다. 반드시 <Suspense> 안에 있어야 하고 RSC 앱에서는 Client Component에서만 호출 가능하다.
Vitest 5.0 RC. 브레이킹 30개 이상. Vite ≥ 6.4.0, Node ≥ 22.12.0이 필수다. 조용히 넘어가던 것들을 이제 대놓고 실패시키는 쪽으로 방향을 잡았다. clearMocks가 기본 true가 되고 await 안 한 비동기 assertion이 이제 테스트를 실패시킨다(v4는 자동 await 후 경고만 찍었다). 워커·풀 ID가 0-based에서 1-based로 바뀌니 VITEST_WORKER_ID를 테스트 DB 샤딩이나 포트 할당에 쓰고 있다면 오프바이원이 난다. 인덱스 계산식은 전수 검토 대상이다.
RC 삼종. Waku 1.0 RC(RSC 우선 미니멀 프레임워크, API 동결), SvelteKit 3 RC, Preact 11 RC. 지난달 베타였던 것들이 나란히 RC에 도착했다.
Node node:zlib의 ZIP API. ZipEntry·ZipFile·ZipBuffer 세 클래스가 26.8.0에 들어왔다. adm-zip이나 archiver를 걷어낼 날이 온다. ZipBuffer는 기존 버퍼 위의 zero-copy view라 생성자가 central directory만 파싱한다. 사용자 업로드 ZIP의 목록만 보고 싶으면 압축 해제 없이 keys()만 읽으면 끝난다. 함정도 zero-copy에서 나온다. ZipBuffer가 살아 있는 동안 원본 메모리를 수정하거나 재사용하면 나중 읽기가 손상된 데이터를 돌려준다. 버퍼 재사용 여지가 있으면 Buffer.from(source)로 복사해서 넘겨라. Stability 1.0(Early development)이고 릴리스 직후 악의적 입력 하드닝 작업이 이어졌으니, 신뢰할 수 없는 업로드를 다룰 거면 26.8.1 이상에 후속 패치까지 확인해야 한다.
나머지 구경거리. Kitesurf(Cloudflare가 Workers 위에 만든 브라우저 엔진. 사람 말고 에이전트를 타깃으로 설계했다. 페이지당 하나의 V8 isolate로 돌아 Chromium 대비 CPU·메모리를 3~7배 아끼는 대신 더 느리다). Octane(Inferno 창시자 Dominic Gannaway의 신작. React 프로그래밍 모델을 유지하되 사전 컴파일로 리액티비티를 생성한다. VDOM 없음, rules of hooks 없음, 의존성 배열 없음. 훅 슬롯이 호출 순서 대신 call site 기준이라 조건문 뒤에도 훅을 둘 수 있다. RSC만 호환 안 된다). TermDOM(DOM·cascade·레이아웃 엔진을 직접 구현해 HTML과 CSS만으로 TUI를 짠다. Ink는 좋은데 React는 싫은 사람용. v0.1.5에서 CSS Grid 지원). celld(Ryan Dahl이 만든 Durable Objects의 셀프호스팅 버전. 합의 계층 없이 S3 버킷으로 조율한다).
이 목록만 알아도 하루는 번다
| 지뢰 | 왜 |
|---|---|
| Next.js 16.3.2 이하 / 15.5.23 이하 | 무인증 RCE. 16.3.0은 별개로 프리페치 루프·캐시 조기 폐기·headers() stale 버그도 있었다 |
| Node 24.19.0 | 24.18.1에서 올린 뒤 OOM 크래시 보고 다수. 롤백하면 사라진다. 원인 미확정 |
| Node 26.8.0 | 버전 문자열이 깨져서 몇 시간 만에 26.8.1이 나왔다 |
| provenance 초록불 | Shai-Hulud는 서명을 위조하지 않았다. 정당하게 발급받았다 |
| Zod 4.5의 문자열 길이 검사 | code unit에서 code point로. 한글·이모지 검증이 조용히 뒤집힌다 |
Zod 4.5의 z.iso.datetime() | 초가 필수다. 2020-01-01T06:15Z가 거부된다 |
| pnpm 12 + SSH git 의존성 | 락파일에서 SSH URL이 사라진다. CI마다 git URL rewrite 필요 |
| pnpm 12 + sudo | pnpm setup·self-update가 sudo 아래에서 실패한다. Docker 빌드 스크립트 점검 |
| TanStack Table V9 무지성 이관 | 패키지가 14kb에서 25kb로 커졌다. feature를 골라 등록하지 않으면 손해 |
Vitest 5의 VITEST_WORKER_ID | 0-based에서 1-based로. 샤딩·포트 할당 오프바이원 |
| Bun 1.4 프로덕션 직행 | AI 주도 재작성에 자체 벤치마크에 메이저 직후. 셋 다 켜져 있다 |
{ ... }[keyof T] 관용구 | optional 프로퍼티 하나만 있어도 undefined가 유니온에 샌다 (아래 참조) |
마지막 항목은 Inngest의 추적기가 원본이다. 미들웨어를 하나 넣으면 멀쩡한데 둘 넣으면 step.run 반환 타입이 {}로 붕괴하는 버그였다. 범인은 우리 대부분이 쓰는 키 필터링 관용구다. [Key in keyof T] 매핑 타입이 ? 수식어를 보존하니까 optional 프로퍼티를 값으로 읽으면 undefined가 딸려 나온다. 오염된 유니온이 Pick에 들어가는데 제네릭 T에 대해서는 선언 시점에만 제약이 검사되고 인스턴스화 시점에 재검사되지 않아 조용히 통과한다. 두 번째 적용에서 T[undefined]가 계산되고 error type이 NaN처럼 전파돼 Pick이 {}를 뱉는다. 경고 한 줄 없이.
교훈은 셋이다. ① 값 유니온을 거쳐 Pick하지 말고 as 절로 키 단계에서 걸러내라. ② 타입 변환 함수가 자기 자신과 합성될 수 있다면 f(f(x)) === f(x)를 테스트해라. ③ 타입 테스트를 alias 비교로만 짜면 거짓 통과한다. TypeScript가 지연 상태의 두 alias를 완전히 평가하지 않으니까. 프로퍼티 접근으로 평가를 강제해야 실제 실패를 잡는다.
당장 코드를 바꾸진 않지만 다음 분기 의사결정에 쓰일 것들
page.clock으로 시계를 압축하면 1시간치 타이머를 몇 분에 시뮬레이션할 수 있다. 매 PR 말고 야간 잡에 넣는 게 권장 위치다. 플로우는 반드시 왕복형이어야 한다(무한 스크롤은 대상이 아니다). 목 응답 크기도 실제 API와 비슷해야 한다. 실제 50kB를 200바이트로 목킹하면 누수가 수백 배 작아져 그냥 통과해버린다.require()는 값의 복사본만 쥔다. 이 차이가 순환 의존성과 배포된 패키지에서 실제 버그로 드러난다.Object.keys와 JSON.stringify에서 완전히 사라진다. 라이브러리 설계에 바로 써먹을 패턴이다.프론트엔드가 읽어야 할 이유가 있다
AGENTS.md(비용 대비 효과 최고), 문서 사이트 llms.txt, MCP 서버, Agent Skill 순이다. llms.txt는 처음부터 컨텍스트 예산 단위로 쪼개라(HeroUI는 8개 파일, Ant Design은 3종). 생성만 시키지 말고 검증 훅을 붙이는 것도 실효적이다.props: any, 자유 문자열 className, 문서화 안 된 slot은 그대로 에이전트의 오답 경로가 된다.isLoading boolean 하나로는 표현이 안 된다. 진행 신호도 스피너 하나로 때우지 말고 서버가 tool call 단위 이벤트를 SSE로 흘려주고 클라이언트가 평문으로 렌더하는 구조여야 한다. 피드백이 없으면 사용자는 같은 프롬프트를 반복 입력한다.이번 달 이적 시장과 뒷말
unsafe가 13,000회 이상 나왔다는 지적에 대해, 1.4 시점의 정리 상태는 여전히 공개 자료로 확인되지 않는다.start: true 플래그로 흡수된다. 프로덕션 운영 중이라면 당장 깨지진 않지만 유지보수 릴리스만 이어진다.