stylish.log

© 2026. @StyleList94(새 창에서 열림)

2026.09.01. · 44 min read

26년 09월

월간 후론트

S E R I E S

월간 후론트 2026

26년 06월26년 07월26년 08월26년 09월26년 10월

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이 그 워크플로에서 나왔다는 사실만 보증한다. 그 워크플로가 정상인지는 아무도 보증하지 않는다.

당신이 지금 해야 할 일은 아래 즉시 섹션 첫 항목에 있다. 읽고 넘기지 말고 진짜로 해라.

  • JFrog 분석: Shai-Hulud is back
  • Socket 분석: keyv/cacheable 네임스페이스 침해
  • Aikido 분석 (IOC 해시 포함)
  • Wiz IOC 목록 (CSV)

숫자로 보는 8월

이번 달을 여덟 개로 압축하면

숫자무엇
400+Shai-Hulud 웜에 감염된 npm 패키지 (버전 기준 1,700+)
20억감염 패키지들의 합산 월 다운로드
21.5GB → 2GBNext.js 16.3의 dev 서버 메모리 (vercel.com 대시보드 기준, 약 90%↓)
2.71GB → 380MBTanStack Table V9의 retained heap (100만 행 × 8열, 최대 86%↓)
1,517개Bun 1.4가 추가로 통과한 Node.js 테스트
7.5kb → 784BZod 4.5에서 z.string() 스키마 하나가 잡는 힙
24,975ms → 70msSolid 2.0 Rust 컴파일러의 1MB 단일 모듈 처리 (355배)
2건8월 말 Next.js가 패치한 무인증 RCE

이달의 판정

표만 보고 닫아도 이번 달 할 일은 다 파악된다

판정항목한 줄
즉시Next.js 16.3.3 / 15.5.24무인증 RCE 2건. 우회 방법 없음
즉시keyv 계열 침해 대응회전 말고 폐기부터
즉시Node 26.8.126.8.0은 버전 문자열이 깨져 나왔다
즉시Express body-parserDoS 보안 릴리스
검토Next.js 16.3업그레이드는 공짜, Instant Navigations는 유료
검토TanStack Table V9수만 행 다루면 무조건, 수백 행이면 무의미
검토pnpm 12Rust 재작성인데 명령어가 그대로다
검토Zod 4.5z.compile() 한 줄로 3~9배
검토Oxlint React Compiler 룰컴파일러 안 켜도 룰만 쓸 값어치
검토Bun 1.4호환성이 드디어 논의할 수준
검토Better Auth 1.7OAuth를 제대로 하려면
검토vlt 1.0갈아타지 않고 vlt query만 써도 이득
검토Baseline 의존성 감사gzip 60~90KB가 그냥 놀고 있다
관망Solid 2.0 RCasync가 리액티브 그래프 안으로
관망React browser()Canary. mounted 플래그의 사망 선고
관망Vitest 5.0 RC브레이킹 30개 이상
관망Waku 1.0 RC / SvelteKit 3 RC / Preact 11 RCRC가 동시에 셋
관망Node node:zlib ZIP API의존성 하나 걷어낼 날이 온다
관망Kitesurf / TermDOM / Octane지금 쓰면 안 되지만 재밌다
참고Lovable·Yelp 마이그레이션기, SPA soak test, 코드 제거론 외아래 참조

즉시. 이번 주 안에 끝내라. 미루면 그게 사고다.

Next.js 보안 릴리스

무인증 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 이슈는 국내 서비스 대부분(리눅스 컨테이너)에는 무관하다. 다만 알려진 우회 방법이 없다. 해당 환경이면 업그레이드 말고는 길이 없다.

keyv 계열

회전 말고 폐기부터

npm ls keyv flat-cache file-entry-cache cacheable cacheable-request cache-manager를 지금 쳐봐라. 8월 4일 이후 이 중 하나라도 설치했거나 CI 빌드가 돌았다면 그 호스트는 감염된 것으로 간주하는 게 기본이다.

순서가 중요하다.

  1. 토큰 폐기. 회전(rotate)이 아니라 폐기(revoke)다. 우선순위는 bypass_2fa + write 권한 npm 토큰, GitHub PAT·OAuth·Actions 토큰, 그다음이 AWS·GCP·K8s·Vault·DB·SSH·VPN·AI 서비스다.
  2. 감염된 머신에서 회전시키면 의미가 없다. 정리 후에 돌려라. CI 러너와 개발 머신은 청소로 안 된다. 클린 이미지로 재구축하는 게 원칙이다.
  3. 마지막 정상 버전으로 pin하고 lockfile 재생성. 캐럿·틸드 범위로 신규 버전이 딸려오지 않게 막아라.
  4. 저장소 전 브랜치 점검. default 브랜치만 보면 안 된다. .vscode/tasks.json, .claude/settings.json, chore: update config 커밋, ${{ toJSON(secrets) }}를 직렬화하는 워크플로를 찾아라.
  5. 예방책은 install 스크립트 차단 하나뿐이다. CI에 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.1과 Express body-parser

짧게 끝나는 것들

Node 26.8.0은 버전 문자열이 깨진 채로 나가서 몇 시간 만에 26.8.1이 뒤따랐다. 26.8.0은 그냥 건너뛰어라. Express 팀의 body-parser 보안 릴리스도 나왔다. DoS 취약점이고 Express를 쓰는 서비스는 의존성 버전만 확인하면 된다.

검토. 올릴 값어치는 있는데 전부 조건이 붙는다.

Next.js 16.3

업그레이드는 공짜, 헤드라인은 유료

이 릴리스는 두 개의 서로 다른 물건이 한 이름으로 묶여 있다. 헷갈리면 손해다.

공짜 쪽. 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가 남는 사고를 막는다.
  • 스켈레톤이 튀면 Suspense 경계를 의심해라. 형제로 나열하지 말고 중첩하면 병렬성은 유지되면서 위에서 아래로 순차 확정돼 CLS가 준다.
  • prefetch={true}를 목록에 무분별하게 붙이지 마라. 뷰포트에 들어오는 순간 서버를 호출한다. 미리 준비할 값어치가 있는 링크에만 선별 적용이다.

그리고 next/root-params. i18n 하는 팀이라면 이게 조용한 최대 수혜다. [locale] 같은 최상위 세그먼트를 트리 깊은 Server Component에서 await locale()로 바로 읽는다. next-intl은 setRequestLocale 우회책을 통째로 걷어낼 수 있게 됐고 setRequestLocale 누락으로 정적 렌더링이 조용히 깨지던 고질병도 구조에서 사라진다. 단 Route Handler와 Server Action에서는 아직 동작하지 않는다. Client Component에서도 못 쓴다.

TanStack Table V9

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()으로 사내 표준 테이블 훅을 만들어두는 게 이번 버전의 진짜 활용법이다. 기본 정렬·공통 필터·셀 렌더러를 한곳에 묶고 개별 테이블 코드는 짧게 유지한다.

pnpm 12

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/를 넣어야 한다.

Zod 4.5

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는 이제 거부된다.
  • 문자열 길이 검사 기준이 UTF-16 code unit에서 Unicode code point로 바뀌었다. 이모지와 한글 조합 문자를 다루는 입력 검증에서 기존 통과·실패가 뒤집힐 수 있다. 국내 서비스면 이건 남 얘기가 아니다.
  • record 키와 intersection 동작이 TypeScript 시맨틱에 맞춰 바뀌었고 __proto__ 키는 항상 제거된다.
  • async refinement나 transform이 있는 스키마는 컴파일이 안 되는데 에러 대신 원본 스키마를 조용히 돌려준다. 컴파일 여부를 확실히 하려면 { strict: true }를 넘겨라.

Oxlint의 React Compiler 지원

컴파일러를 안 켜도 이득

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개가 남아 있다는 것도 알아두자.

Bun 1.4

이제는 논의할 수준

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·의존성 다이어트

Better Auth, vlt, 그리고 의존성 다이어트

  • Better Auth 1.7: OAuth 프로바이더가 릴리스의 중심이다. DPoP(RFC 9449, 액세스 토큰을 클라이언트 키에 바인딩), resource indicators(RFC 8707), back-channel logout, max_age 강제, device authorization(RFC 8628). MCP 인증은 @better-auth/mcp로 분리됐고 SCIM은 Groups·role projection 포함 사실상 재작성됐다. Next.js 앱이라면 hydrateSession 하나만으로도 초기 렌더 후 세션 재요청을 없앨 수 있다. 브레이킹이 많다. deprecated된 oidcProvider는 제거됐고 SCIM은 계획된 cutover가 필요하며 Cloudflare D1은 트랜잭션 미지원으로 아예 안 된다.
  • vlt 1.0: npm 창시자와 코어 멤버들이 만든 패키지 매니저가 하필 Shai-Hulud 사태와 같은 날 1.0을 찍었다. 핵심 주장이 "install을 쳤다는 이유만으로 네 머신에서 아무것도 실행되지 않는다"라 타이밍이 절묘하다. 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만 버전 플래그 같은 수치는 전부 자체 집계다.
  • Baseline으로 JS 덜 보내기: 중형 JS 앱이면 gzip 60~90KB 정도의 의존성이 이미 브라우저가 대신 해줄 수 있는 일을 하고 있다. 개별 패키지를 하나씩 보지 말고 클러스터로 묶어 보라고 한다. Intl(~14KB: 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.024.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 + sudopnpm setup·self-update가 sudo 아래에서 실패한다. Docker 빌드 스크립트 점검
TanStack Table V9 무지성 이관패키지가 14kb에서 25kb로 커졌다. feature를 골라 등록하지 않으면 손해
Vitest 5의 VITEST_WORKER_ID0-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를 완전히 평가하지 않으니까. 프로퍼티 접근으로 평가를 강제해야 실제 실패를 잡는다.


참고

당장 코드를 바꾸진 않지만 다음 분기 의사결정에 쓰일 것들

마이그레이션 실화 두 편

  • Lovable, Next.js에서 TanStack Start로: 약 400개 라우트, 85만 줄 이상. 빅뱅 전환이 아니었다. 프록시 뒤에서 두 프레임워크를 동시에 굴리며 코드 폴더를 공유했다. 대규모 앱 마이그레이션을 계획 중이라면 전략 자체가 참고 자료다.
  • Yelp, Flow 모노레포를 TypeScript로: 140만 줄, 수백 개 패키지, 두 타입체커를 동시에 돌리며 수년. 타입 커버리지 83%에서 96%로. Flow 이야기라기보다 장기 마이그레이션을 어떻게 굴리는지에 대한 실전 가이드다.

성능

  • 당신의 SPA는 메모리가 새고 있다: 2026년 초 500개 인기 React/Vue/Angular 저장소 정적 분석에서 86%가 리스너·타이머·구독을 등록만 하고 해제하지 않았다. 가장 큰 카테고리는 정리 안 된 타이머(약 44%). 방법은 단순하다. 단일 브라우저 컨텍스트에서 왕복형 플로우를 200회 반복하고 DOM 노드 수와 리스너 수를 비교한다. 어서션은 노드 수와 리스너 수로 잡는다. 힙 크기는 런마다 요동치니까. GC는 두 번 돌린다. 1회로는 detached 노드가 6번에 1번꼴로 남아 거짓 실패가 난다. page.clock으로 시계를 압축하면 1시간치 타이머를 몇 분에 시뮬레이션할 수 있다. 매 PR 말고 야간 잡에 넣는 게 권장 위치다. 플로우는 반드시 왕복형이어야 한다(무한 스크롤은 대상이 아니다). 목 응답 크기도 실제 API와 비슷해야 한다. 실제 50kB를 200바이트로 목킹하면 누수가 수백 배 작아져 그냥 통과해버린다.
  • 코드 제거가 스케줄링을 이기는 고약한 이유: Alex Russell. 성능 작업의 상당수가 무엇을 언제 로드할지 재배치하는 데 쓰이지만 진짜 승부는 애초에 안 보내는 것이다. 그가 짚는 실패 모드가 날카롭다. "competitive use-case devaluation", 개별 기능 소유자들이 각자 자기 코드를 먼저 로드해달라고 주장하면서 다른 사용자 여정이 암묵적으로 뒤로 밀리는 현상이다. 그는 이걸 기술 문제보다 조직 문제로 본다. 번들 회의에서 lazy loading으로 해결하자가 기본 답변인 조직이라면 읽어볼 만하다.
  • JS 내부의 Progressive Enhancement: Remy Sharp이 기차에서 자기 도구에 파일을 드롭했더니 페이지가 그냥 이탈해버린 이야기. 원인은 JS가 esm.sh에서 압축 600kB(해제 1.3MB)를 받아 파싱한 다음에야 핸들러가 붙는 구조였다. 해법은 이벤트 핸들러와 큐와 로딩 표시 세 조각만 떼어내 인라인으로 HTML 바로 뒤에 배치하는 것. 무거운 번들을 lazy import하는 화면, 특히 파일 드롭이나 폼 제출처럼 브라우저 기본 동작이 페이지 이탈로 이어지는 상호작용에 바로 적용된다.

모듈과 타입

  • ESM과 CommonJS: import와 require가 실제로 하는 일: 겉보기엔 같지만 ESM import는 export한 쪽 바인딩에 계속 연결돼 있고 구조분해한 require()는 값의 복사본만 쥔다. 이 차이가 순환 의존성과 배포된 패키지에서 실제 버그로 드러난다.
  • TypeScript 객체에 내부 상태 숨기기: export하지 않은 unique symbol을 프로퍼티 키로 쓰면 Object.keys와 JSON.stringify에서 완전히 사라진다. 라이브러리 설계에 바로 써먹을 패턴이다.

디자인 시스템과 AI

프론트엔드가 읽어야 할 이유가 있다

  • State of AI in Design Systems: 오픈소스 디자인 시스템 20개를 동일 스키마로 조사한 7월 필드 스터디. 숫자가 명확하다. 20개 중 19개가 MCP 서버를, 18개가 Agent Skill을, 15개가 리포지토리 agent 파일(AGENTS.md/CLAUDE.md)을 제공한다. 실험 단계를 지나 사실상 표준이 되는 중이다. 반대로 Figma Code Connect는 단 2개(Carbon, Primer)뿐이다. 디자인에서 코드로 가는 바인딩보다 코드에서 에이전트로 가는 바인딩이 압도적으로 먼저 자리 잡았다. 사내 디자인 시스템에 AI 대응을 붙일 때 이 데이터가 그대로 우선순위표다. 리포 루트 AGENTS.md(비용 대비 효과 최고), 문서 사이트 llms.txt, MCP 서버, Agent Skill 순이다. llms.txt는 처음부터 컨텍스트 예산 단위로 쪼개라(HeroUI는 8개 파일, Ant Design은 3종). 생성만 시키지 말고 검증 훅을 붙이는 것도 실효적이다.
  • 마지막 프로덕트 디자인 도구: 저자는 tldraw를 주 인터페이스 설계 도구로 쓴다. 전제는 코드로 구현된 디자인 시스템이다. 버튼에 size·state·prominence가 이미 정의돼 있으면 사각형에 Dialog라고만 써도 생김새와 동작이 확정되니까. FE 입장에서 읽으면 이 글은 에이전트 정확도의 병목이 모델보다 디자인 시스템 contract의 품질에 있다고 말한다. AI codegen을 잘 쓰고 싶다면 프롬프트 튜닝보다 컴포넌트 API를 좁고 명확하게 설계하는 게 선행 투자다. 느슨한 props: any, 자유 문자열 className, 문서화 안 된 slot은 그대로 에이전트의 오답 경로가 된다.
  • AI 출력 하나는 예시일 뿐 평가가 아니다: NN/g. LLM은 비결정적인데 팀들은 한 번 돌려보고 결론을 낸다. 해법은 정량 UX 연구가 이미 쓰던 문법이다. 대표 입력 여러 개에 반복 실행을 곱하고 신뢰구간을 붙인다. 총점이 같아도 실패 분포가 다르면 완전히 다른 시스템이다. 8개 질문은 5/5 성공하고 2개는 0/5 실패하는 시스템 A와 모든 질문이 4/5인 시스템 B는 둘 다 80%다. 그런데 A는 실패 유형을 사람에게 라우팅할 수 있고 B는 사람 검수를 전제해야 한다. LLM 기능의 CI를 결정론적 테스트처럼 짜지 마라. 통과·실패 대신 반복 실행의 pass rate를 추적하고 임계값을 걸어야 한다.
  • 도그푸딩 vs QA vs 사용자 리서치: 도그푸딩이 수치로 잡아주는 값어치는 usability의 상한선 하나뿐이다. 직원이 헤매면 실사용자는 더 헤맨다. 역은 성립하지 않는다. curse of knowledge 때문이다. 사내 베타 결과를 릴리즈 근거로 쓰는 관행이 있다면 재검토할 근거가 된다.
  • 에이전트의 첫 5분 설계하기: 원칙 자체보다 FE 구현 과제로 번역했을 때가 유용하다. 종료 상태를 이진(성공·실패) 대신 4가지(완료·부분완료·입력필요·실패)로 모델링하면 부분 결과 UI가 자연스럽게 붙는다. isLoading boolean 하나로는 표현이 안 된다. 진행 신호도 스피너 하나로 때우지 말고 서버가 tool call 단위 이벤트를 SSE로 흘려주고 클라이언트가 평문으로 렌더하는 구조여야 한다. 피드백이 없으면 사용자는 같은 프롬프트를 반복 입력한다.

뒷담화

이번 달 이적 시장과 뒷말

  • Cursor가 Solid에서 React로 돌아왔다. 마이그레이션이 거의 끝났고 이유는 성능과 유지보수 측면에서 React가 더 안전하고 예측 가능했기 때문이다. 여기에 실무에서 AI 에이전트가 다루기에 더 수월했다는 설명이 붙었다. 프레임워크 선택 기준에 에이전트 친화성이 공개적으로 등장한 사례다.
  • 반대 방향도 있었다. Lovable은 Next.js에서 TanStack Start로 갔다. 같은 달에 정반대 방향의 대규모 마이그레이션 두 건이 나왔다는 게 이 시기의 정확한 온도다.
  • Next.js 팀이 Reddit AMA를 했고 읽을 만한 대목이 많다. 최다 추천 질문은 "왜 제대로 된 middleware나 GET Server Actions가 없나, TanStack Start는 둘 다 있는데"였다. RSC가 "아키텍처적 재앙"이라는 비판에는 복잡도 증가는 인정하되 그 주장은 부당하다고 반박했다. 하이라이트는 프레임워크를 갈아타라는 조롱에 Pete Hunt가 **"TanStack Start도 꽤 좋다"**고 받아친 대목. 내장 페이지네이션은 "작업 중"이고 당분간은 SWR이나 TanStack Query를 쓰라는 게 Andrew Clark의 답변이다.
  • Node.js가 공식 AI 기여 정책을 병합했다. 수개월 논쟁 끝이다. 본인이 이해하지도 테스트하지도 않은 AI 생성 코드 PR은 리뷰 없이 닫힐 수 있고 봇이 여는 PR은 사전 승인이 필요하며 AI로 "good first issue"를 선점하는 것도 금지다. 맥락은 Matteo Collina가 쓴 AI 무리 트리아지에 있다. 매주 20~40건씩 쏟아지는 저품질 보안 제보를 처리하는 실무 이야기다.
  • Bun 1.4의 Rust 재작성 논란은 아직 안 끝났다. AI 주도 포팅이었고 초기 머지에서 unsafe가 13,000회 이상 나왔다는 지적에 대해, 1.4 시점의 정리 상태는 여전히 공개 자료로 확인되지 않는다.
  • Deno vs Oracle JavaScript™ 상표 소송은 다섯 번째 기한 연장에 들어갔다. 이쯤 되면 연재물이다.
  • SolidStart가 은퇴했다. Solid 2.0에서 Vite 플러그인의 start: true 플래그로 흡수된다. 프로덕션 운영 중이라면 당장 깨지진 않지만 유지보수 릴리스만 이어진다.
  • Node.js Interactive가 2019년 이후 처음으로 돌아왔다. RenderATL 행사의 일부로 열렸고 AI·공급망 보안·WinterCG의 Ecma TC55 전환·QUIC/HTTP3·연 1회 메이저 릴리스 로드맵을 다뤘다.

이전 시리즈 포스트

26년 08월

다음 시리즈 포스트

26년 10월
  • 이달의 표지
  • 숫자로 보는 8월
  • 이달의 판정
    • Next.js 보안 릴리스
    • keyv 계열
    • Node 26.8.1과 Express body-parser
    • Next.js 16.3
    • TanStack Table V9
    • pnpm 12
    • Zod 4.5
    • Oxlint의 React Compiler 지원
    • Bun 1.4
    • 나머지 셋 — Better Auth·vlt·의존성 다이어트
  • 손대지 말 것
  • 참고
    • 마이그레이션 실화 두 편
    • 성능
    • 모듈과 타입
    • 디자인 시스템과 AI
  • 뒷담화