stylish.log

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

2026.10.06. · 57 min read

26년 10월

월간 후론트

S E R I E S

월간 후론트 2026

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

9월에 나온 큰 결정들은 하나같이 같은 근거를 댔다. 코딩 에이전트 덕분에 비용 계산이 달라졌다는 것. Shopify는 그 이유로 React Native를 버렸고 Linear는 그 이유로 CSS-in-JS를 버렸다. shadcn은 에이전트가 디자인 시스템을 이탈하는 걸 막겠다고 린터를 새로 만들었다. 기술 선택의 기준이 사람이 쓰기 좋은가에서 에이전트가 다루기 좋은가로 옮겨가는 중이다.

이번 호 표지는 그 전환에 가장 비싼 값을 치른 쪽이다.


이달의 표지

Shopify, 6년 만의 네이티브 회귀

2020년 Shopify는 React Native에 올인했다. 그 결정을 발표한 사람이 Mustafa Ali다. 2025년 1월에는 "React Native의 미래는 밝다"는 글도 썼다. 그리고 2026년 9월, 같은 사람이 네이티브로 돌아간다고 발표했다.

먼저 짚어둘 게 있다. 이 글은 React Native가 느리다거나 결함이 있다고 말하지 않는다. 바뀐 건 프레임워크가 아니라 전제다. 크로스 플랫폼을 택한 원래 근거는 같은 기능을 두 번 만들면 비용이 두 배라는 계산이었는데 코딩 에이전트가 그 두 배를 충분히 깎았다는 게 Shopify의 판단이다. 전제가 사라지면 결론도 사라진다.

방식은 greenfield다. 점진 이식을 포기하고 전면 재작성을 택했다. Shop 앱은 엔지니어 6명이 PoC부터 앱스토어 배포까지 12주 만에 Swift·Kotlin으로 다시 만들었다. 다음 차례인 Shopify 앱은 300개 이상 화면에 홈·잠금화면 위젯, Apple Watch 앱, Siri Shortcuts까지 포함하며 연내 출시 예정이다.

여기서 재밌는 건 실패담이다. LLM에 RN 코드베이스를 통째로 던져 한 번에 네이티브로 변환하는 방식은 실패했다. 사양과 태스크 파일로 고정해도 유지보수 불가능한 코드가 대량으로 나왔다고 자체 평가했다.

그래서 만든 게 Helix다. 화면 하나를 지정하면 RN 코드를 읽고 수 분 내에 리뷰 가능한 작은 체크포인트 순서를 제안한다. 각 체크포인트는 네 개의 관문을 전부 통과해야 커밋된다. 테스트로 동작 증명, 실행 중인 앱과 비주얼 대조, 적대적 코드 리뷰어 2개 통과, 그리고 사람 승인. 리뷰 피드백은 누적 기억되어 진행할수록 자율성이 올라간다.

두 번째 축은 피드백 루프 속도였다. 시뮬레이터 제어가 접근성 트리와 스크린샷에 의존해 느리고 불안정한 게 병목이었다. 비즈니스 로직을 UI에서 완전히 떼어내 데스크톱에서 헤드리스로 돌리고 CLI로 에이전트에 노출했다. 분 단위가 밀리초 단위가 됐다.

불완전한 결과가 다음 단계로 넘어가지 못하게 막는 루프. 이게 Helix의 전부이고 모바일이 아닌 어떤 대규모 에이전트 리팩터링에도 그대로 옮겨진다.

오픈소스 쪽 정리도 함께 나왔다. React Native Skia는 2026년 말까지 Shopify가 후원하고 이후 William Candillon이 새 이름으로 포크해 배포하며 원본 저장소는 아카이브된다. 주당 약 200만 다운로드의 FlashList는 호환성을 깨는 치명적 이슈만 수정하며 장기 관리 주체를 찾는 중이다. Restyle은 2026년 말까지 동작을 보장한 뒤 아카이브된다. 이 셋을 쓰는 RN 프로젝트는 지금 의존성 정리 계획을 세워야 한다.

반론도 있다. r/reactnative 스레드에서 Expo 창업자 중 한 명은 이게 기술적이라기보다 정치적 결정이며 구 아키텍처 RN 앱과 새로 만든 네이티브 앱을 비교한 것이라 공정하지 않다고 반박했다. 일리가 있다. 개선 수치의 상당 부분은 재작성 효과일 수 있다.

그리고 Shopify의 전제도 대부분의 조직과 다르다. 대규모 인력, 자체 에이전트 인프라를 만들 역량, 앱 두 개를 플랫폼별로 동시 운영할 여력. 결론만 복사하면 다친다. 가져갈 건 결론이 아니라 질문이다. 우리가 그 기술을 택한 근거는 아직 유효한가.


숫자로 보는 9월

이번 달을 아홉 개로 압축하면

숫자무엇
12주 / 6명Shopify가 Shop 앱을 네이티브로 재작성한 기간과 인원
14.3s → 0.81soxc Rust React Compiler의 컴파일 단계 (1,036개 파일, 17.6배)
3,085ms → 550msclaude.ai 신규 로드 시간 (5.6배)
80만 줄GitHub이 14.5주에 작성한 Copilot 런타임 Rust 코드
55%GitHub이 CSS Modules 전환으로 줄인 Primer SSR 시간
약 32,000건8월 한 달 관측된 Vite dev server 파일 읽기 시도
80.5%Bun 1.4의 Node.js 테스트 스위트 통과율
88%React Router 8.4 새 매처의 네비게이션 시간 단축 (라우트 1,000개)
10.0vm2 샌드박스 탈출 CVSS 점수

이달의 판정표

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

판정항목한 줄
즉시Next.js 16.3.6next/og RCE. Critical
즉시Vite 7.3.2 / 8.0.5이미 대규모 스캐닝 중
즉시vm2 3.11.7CVSS 10.0. 패치해도 안심 못 함
즉시Fastify 5.12.2validation·auth 우회 포함 4건
즉시Node 26.8.2 / 24.21.0Undici 보안 릴리스
즉시Moment.js 2.31.0경로 순회. 주간 2,500만 다운로드
즉시Express 4.22.3 계열hbs·multer·morgan 포함 6건
검토React 19.3View Transitions·Fragment Refs stable
검토Vitest 5.0RC에서 정식으로. 최대 53%
검토oxc React CompilerBabel 대비 17.6배, 패턴 커버리지도 넓다
검토@shadcn/lint에이전트가 디자인 시스템 이탈하는 걸 막는다
검토Zod 4.6.validate()로 거절 경로 35배
검토React Router 8.4플래그 없이 리렌더가 준다
검토Turbopack 청킹 옵션Next.js 16.3, 켜기만 하면 되는 둘
검토npm stage-only 토큰2027년 1월 전에 옮겨야 한다
검토Node 26.10 util.debouncelodash 하나 걷어낼 수 있다
검토Vite+ 1.0베타였던 게 정식으로
검토Rslib 1.0앱이 Rspack이면 설정을 공유한다
관망Remix 3 RCReact가 필요 없는 Remix
관망TanStack Redact v0.1React 호환 런타임. 라이선스가 아직 없다
관망Vinext 1.0Vite 위에 올린 Next.js
관망OJRust dev server. Evan You가 벤치마크에 의문
관망htmx 4.02년 만의 메이저. fetch로 갈아탔다
관망node:ffi 기본 활성화26.9부터. 보안 정책 재검토 대상
관망TinyJS / node:bench / Jotai 3.0 / Vidact지금 쓰면 안 되지만 재밌다
참고Anthropic 3배, GitHub CSS 탈출, Linear 두 편 외아래 참조

즉시

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

Next.js

next/og RCE, 그리고 예고된 9건 더

9월 22일, 정기 릴리스 밖에서 긴급 패치가 나왔다. next/og의 Node.js ImageResponse 구현에서 Critical 등급 원격 코드 실행이 발견됐다. 영향 범위는 v16.2.0부터 v16.3.5까지이고 v16.3.6에서 수정됐다. Edge 런타임의 ImageResponse는 영향받지 않는다.

근본 원인은 Next.js 밖에 있다. SVG를 이미지로 바꾸는 업스트림 라이브러리 Satori의 출력 이스케이프 처리가 미흡했고 다른 업스트림 의존성 취약점과 결합하면서 RCE로 이어졌다. GHSA는 Next.js 쪽 GHSA-vcvr-r3jv-pc5j, Satori 쪽 GHSA-wx4j-mvgx-mqwp다.

npm install next@16.3.6   # 16.x
npm install next@15.5.26  # 15.5.x (하드닝만, RCE 영향 없음)

OG 이미지 API가 외부 입력을 파라미터로 받는 구조라면 우선순위를 더 올려라. 그리고 Next.js가 후속 릴리스에서 9건을 추가로 수정하겠다고 예고했다. 이번 패치로 끝났다고 보면 안 된다.

Vite

패치 다음에 할 일은 포트 점검이다

F5 허니넷이 8월 한 달에만 Vite 대상 파일 읽기 시도 약 32,000건을 관측했다. 세션 기준 807건. 직전 3개월 합계가 1,732건이었으니 폭증이다.

CVE-2026-39364는 인증 없이 @fs 라우트로 server.fs.deny 차단 목록을 우회하는 파일 노출이다. 우회 방법이 어이없을 만큼 간단하다. ?raw, ?import&raw, ?import&url&inline 같은 쿼리 파라미터를 붙이면 경로 검증에서 쿼리 스트링이 잘리거나 오해석돼 deny 체크가 발동하지 않는다.

공격자가 노리는 건 앱이 아니다. 클라우드 계정이다. 워드리스트가 그걸 말해준다. .env.local·.env.production·.env.staging, /root/.aws/credentials와 /home/{ec2-user,ubuntu,node,www-data}/.aws/credentials, terraform.tfstate, serverless.yml, .azure/accessTokens.json. 특히 /proc/self/cwd/.env는 절대 경로를 몰라도 실행 중인 프로세스 기준으로 .env를 읽는 수법이다.

스캐너는 User-Agent를 Googlebot·ClaudeBot·GPTBot으로 위조하고 X-Forwarded-For도 조작한다. 봇 화이트리스트를 User-Agent 문자열로 판단하고 있다면 그게 곧 우회 경로다. 역방향 DNS 검증으로 바꿔라.

할 일은 넷이다.

  1. Vite를 7.3.2 / 8.0.5 이상, 또는 4.5.x·5.4.x·6.x 계열 최신 패치로 올린다.
  2. 취약점보다 노출이 더 위험하다. --host, server.host, Docker 포트 매핑을 전수 점검해라. 5173 포트가 공인 IP에 물려 있는지 docker-compose·k8s ingress·보안그룹까지 본다.
  3. 프리뷰·스테이징 컨테이너를 특히 보라. dev server를 프리뷰 용도로 띄워둔 관행이 실제 사고 경로다. vite build + vite preview나 정적 서빙으로 바꾸는 게 낫다.
  4. 8월 중 외부에서 도달 가능한 미패치 Vite dev server가 있었다면 .env 값, AWS 키, Azure 토큰, Terraform state를 유출된 것으로 간주하고 전부 로테이션한다.

한 가지 더. F5 리포트의 타깃 국가 통계에서 한국이 센서당 이벤트 1위다. 남 얘기가 아니다.

vm2

패치가 나왔는데도 안심할 수 없다

GitLab Threat Research Group이 vm2에서 CVSS 10.0 샌드박스 탈출을 찾았다. 공격 설정이 vm2 README의 Quick Examples 첫 번째 예제 그대로다. 설정 실수로 생긴 구멍이 아니다. 공식 문서를 따른 개발자가 기본으로 취약하다.

구조가 교묘하다. new NodeVM({ require: { external: true, root: './' } })에서 root는 방어선이 아니다. 일반적인 npm 프로젝트는 ./node_modules가 ./ 안에 있으니까. 그래서 require('./node_modules/vm2')가 검사를 통과하고 vm2 자체 로더를 건너뛰고 Node의 진짜 hostRequire()로 로드된다. 공격자는 그렇게 얻은 진짜 vm2 모듈로 자기 규칙을 가진 두 번째 NodeVM을 만든다. 바깥 샌드박스 규칙은 안쪽에 적용되지 않는다.

주당 약 125만 다운로드, 월 500만 이상. 3.11.7에서 이 경로는 막혔다. 그런데 수정이 생각보다 좁다. vm2 자체 lib/ 폴더와 메인 파일 require만 차단한다. 같은 README 설정을 다른 파일로 겨누면 3.11.7에서도 완전한 RCE가 난다. require.external: true에 root가 없으면 경고 한 줄만 찍고 코드는 그대로 실행된다.

플러그인 시스템, 코드 플레이그라운드, CI 러너, AI가 생성한 코드를 실행하는 도구가 전형적인 노출 지점이다. vm2 의존성부터 grep해봐라. 새 프로젝트라면 vm2 대신 컨테이너나 별도 프로세스 격리가 훨씬 튼튼하다.

나머지 넷

한 줄씩 끝나는 것들

  • Fastify 5.12.2: high-severity 4건 수정. validation 우회와 auth 우회가 포함돼 있다. 즉시 올려라.
  • Node 26.8.2 / 24.21.0: 다수 취약점을 고친 Undici 보안 릴리스와 OpenSSL 3.5.8 업데이트를 포함한다. LTS 라인도 대상이다.
  • Moment.js 2.31.0: moment.locale()을 통한 경로 순회(CVE-2026-17495, CVSS 5.9). 2.29.2부터 2.30.1까지가 대상이고 2022년 CVE-2022-24785의 불완전한 수정이었다. 가드는 입력이 문자열이라 가정하는데 match()는 통과하고 toString()이 순회 경로를 반환하는 객체를 넘기면 내부 require()까지 도달한다. 주간 2,500만 다운로드가 아직 유지보수 모드로 돌고 있다.
  • Express 보안 릴리스: hbs·multer·morgan 세 패키지에 걸친 취약점 6건. Express 4 사용자는 4.22.3도 함께.

검토

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

React 19.3

작년의 실험이 올해의 정식 API로

9월 9일 공개. 작년에 실험 API로 나왔던 View Transitions와 Fragment Refs가 stable이 됐다. 여기에 react-dom의 browser(), Trusted Types 지원, Server Component에서 Context를 직접 렌더링하는 기능이 붙었다.

<ViewTransition>은 Transition으로 마킹된 업데이트에서만 동작한다. startTransition, <Suspense> reveal, useDeferredValue가 트리거고 긴급 업데이트는 애니메이션되지 않는다. 애니메이션 종류는 트리 변화로 자동 결정된다. enter(추가), exit(제거), update(스타일·내용 변경), share(named ViewTransition이 다른 위치로 이동).

여기서 쓸모 있는 게 addTransitionType이다. state 업데이트와 함께 addTransitionType('next')를 호출하면 같은 업데이트라도 원인별로 다른 애니메이션을 지정할 수 있다. enter={{ 'next': 'from-right', 'previous': 'from-left' }} 형태다. 캐러셀이나 탭처럼 방향성 있는 전환이 정확히 이 케이스다. React가 이 타입을 브라우저 view transition type으로도 심어주니 CSS에서 :active-view-transition-type(...)으로 스코핑할 수 있다.

<ViewTransition> 안에 <img>나 @font-face <style>을 넣으면 해당 리소스가 로드될 때까지 Suspense가 걸린다. 이미지와 폰트가 제각각 튀어나오는 flicker를 막는 용도로 쓸 수 있다.

Fragment Refs는 래퍼 <div> 없이 여러 DOM 노드를 다루게 해준다. <Fragment ref={...}>로 FragmentInstance를 받으면 addEventListener·dispatchEvent(1단계 자식 대상), focus·focusLast·blur(깊이 우선), observeUsing·unobserveUsing(IntersectionObserver·ResizeObserver), getClientRects·scrollIntoView·compareDocumentPosition을 쓸 수 있다. 라이브러리 컴포넌트가 ref를 노출하지 않아 래퍼 div를 끼워 넣던 상황이 여기서 해소된다. 레이아웃을 건드리지 않고 뷰포트 감지를 붙일 수 있다.

**use(browser())**는 서버에서만 suspend한다. SSR HTML에는 가장 가까운 Suspense fallback이 들어가고 클라이언트 하이드레이션 이후에는 suspend하지 않는다. use 계열이라 조건문 안이나 early return 뒤에서도 호출 가능하다. localStorage나 타임존처럼 브라우저 전용 값을 위해 쓰던 mounted state + useEffect 패턴을 이걸로 대체할 수 있다. 조건부 호출이 되니까 initialData가 없을 때만 use(browser())를 부르는 useBrowserQuery 래퍼 패턴이 특히 쓸모 있다.

주의할 것 넷. <ViewTransition>은 현재 DOM 전용이고 React Native는 작업 중이다. Suspense와 조합할 때 기본값 그대로 두면 이미 로드된 캐시 UI까지 매번 애니메이션돼 앱이 굼떠 보인다. <ViewTransition update="auto" default="none">이 권장 패턴이다. Strict Mode에서 하이드레이션 중에도 Effect가 double invoke되도록 바뀌었으니 멱등하지 않은 Effect가 있으면 드러난다. 그리고 use(browser())를 쓰는 컴포넌트는 fallback이 초기 HTML에 들어가므로 SEO나 LCP가 중요한 영역에는 신중해야 한다.

oxc의 React Compiler

Babel을 떠날 이유가 속도만은 아니다

1,036개 파일 규모 React Router 앱에서 컴파일 단계가 14.3초에서 0.81초로 줄었다. 약 17.6배. 다만 전체 빌드는 22.1초에서 9.3초로 약 2.4배에 그친다. 컴파일 외 작업이 대부분이라서다.

속도보다 중요한 건 패턴 커버리지다. Babel 기반 컴파일러 1.0이 건너뛰던 것들이 Rust 버전에서는 이미 처리된다. try/catch 블록 안의 조건 로직(1.0의 대표적 블로커), 구조분해된 prop을 재할당한 뒤 중첩 클로저에서 쓰는 경우, 계산된 객체 프로퍼티 키. 필자 앱에서는 이것만으로 컴파일 대상 함수가 7개 늘었다.

@vitejs/plugin-react v6.1.0부터 react({ compiler: true })로 켠다(Vite 8+ 필요). React Router framework 모드처럼 Vite React 플러그인을 못 쓰면 @acusti/vite-plugin-react-compiler를 쓴다. vite-plugin-babel, babel-plugin-react-compiler, @babel/preset-typescript 세 개를 걷어내고 plugins: [reactRouter(), reactCompiler()]로 끝난다.

함정 하나. Oxlint의 React Compiler 규칙을 쓰고 있다면 빌드 쪽도 같은 oxc-transform-react 버전으로 맞춰야 한다. 버전이 어긋나면 린트는 통과하는데 컴파일은 건너뛰는 조용한 구멍이 생긴다(필자가 실제로 겪고 오탐 이슈를 제출했다). 그리고 아직 미지원 패턴이 남아 있다. try 블록 안의 throw, 논리 할당 연산자(??=, &&=, ||=)는 컴포넌트나 훅을 통째로 건너뛰게 만든다. compiler: true는 아직 experimental 표기다.

@shadcn/lint

에이전트에게 규칙을 말로 하지 말고 린터로 줘라

두 뉴스레터에 동시에 올라온 이번 달 최대 화제작. 출시 직후 GitHub 스타 2.2k를 넘겼다.

문제의식이 명확하다. 에이전트가 짠 UI는 bg-[var(--oh-border)], rounded-[6px], p-[3px], 생 hex 컬러 같은 일회성 arbitrary value로 디자인 시스템을 계속 이탈한다. shadcn이 택한 해법은 린터다. 에러 메시지 자체가 뭐가 잘못됐고 대신 뭘 쓰고 그게 어디 있는지를 프로젝트의 컴포넌트·variant·테마에서 뽑아 알려준다.

"p-4" is not allowed on <Button>: <Button> owns its spacing.
Use a size (sm, lg), or margin here or gap on the parent...

TypeScript로 style prop을 좁히면 TS2353만 뜨고 끝난다. 차이가 여기 있다.

규칙은 6종이다. no-restyle(className으로 컴포넌트 재스타일링), no-raw-colors, no-arbitrary-values, no-inline-styles, no-unknown-classes, require-static-classes. 컴포넌트별 contract를 설정할 수 있다.

"shadcn/no-restyle": ["error", {
  "allow": ["layout"],
  "contracts": [
    { "pattern": "^Button$", "allow": ["w-full", "mt-*", "mb-*"] },
    { "pattern": "^CardTitle$", "allow": ["layout", "typography"], "deny": ["font-*"] }
  ]
}]

Tailwind v4 프로젝트면 되고 shadcn/ui는 필요 없다. ESLint와 Oxlint 양쪽 지원. 공개된 eval은 150회 이상 태스크 런에서 Sonnet 5 8/8(위반 69→0), Haiku 4.5 8/8(66→0), Opus 5 8/8(42→0), GPT 5.6 Terra 8/8(117→0), GPT 5.6 Sol 6/8(98→0)이다. Claude 대조군에서 수정 비용이 규칙 문서만 줬을 때보다 10~48% 저렴했다.

도입 순서가 중요하다. no-restyle + allow: ["layout"] 하나로 시작하고 no-arbitrary-values, no-raw-colors를 순차로 켜라. require-static-classes는 런타임에 클래스를 조립하는 기존 코드를 대거 잡아내니 초기에는 끄고 시작하는 편이 현실적이다. 그리고 AGENTS.md에 "변경 후 npm run lint 실행하고 에러 전부 수정" 한 줄을 넣는 게 사실상 필수 마무리다.

Oxlint의 JS 플러그인 API가 아직 alpha라는 점, eval이 벤더 자체 수치라는 점은 감안해라.

Vitest 5.0

지난 호 관망이던 게 정식으로

9월 4일 정식 릴리스. 이번 메이저의 초점은 성능이고 마이크로 벤치마크 대신 5파일 유틸 패키지부터 1,280 모듈 엔터프라이즈 모놀리스까지 생성한 레퍼런스 앱 세트로 측정했다.

수치(Apple M4 10코어, Node 24, 3회 중앙값, 4.1.10 → 5.0): deps-heavy vmThreads 1.59s → 0.74s(−53%), long-haul vmForks 5.43s → 4.06s(−25%), enterprise-monolith 7.24s → 5.83s(−19%), react-spa Browser Mode 2.40s → 2.01s(−16%). 반대로 forks+jsdom+isolation처럼 환경 셋업이 지배적인 조합은 ±3% 이내로 변화가 없다.

개선 원인은 sharedViteServer(인라인 프로젝트가 루트 Vite 서버 공유), fsModuleCache 정식 승격(변환 모듈을 디스크에 영속해 프로세스 간 재사용), vm 풀의 컴파일 코드 재사용과 모듈 그래프 프리웜이다.

Trace View가 이번 릴리스의 진짜 물건이다. browser.traceView: true로 켜면 모든 인터랙션·어서션·page.mark를 DOM 스냅샷으로 기록해 사후에 단계별로 되감아 볼 수 있다. 실패한 액션은 빨간색으로 강조되고 에디터 패널에서 소스 위치가 열린다. Playwright Traces와 달리 provider 비의존이고 별도 뷰어가 필요 없다. HTML 리포터의 singleFile 옵션과 조합하면 CI 아티팩트 하나만 받아 재생할 수 있다.

진단 기능도 좋아졌다. 리포터 duration이 environment 79%, import 13%, transform 6%, tests 1%, setup 1% 식으로 퍼센트를 보여준다. 새 vitest doctor는 대안 설정으로 실제 실행해 보고 더 빠른 옵션을 추천한다. 업그레이드 전에 이것부터 돌려봐라.

신규 vi.when은 인자별 mock 동작을 선언한다. vi.when(fn).calledWith(1).thenResolve(...) 형태고 deep equality 매칭에 expect.any() 같은 비대칭 매처를 지원한다. mockImplementation 안에 if를 쌓던 코드가 정리된다.

브레이킹이 많다. Vite ≥ 6.4.0, Node.js ≥ 22.12.0이 필수다. clearMocks가 기본 true가 되고 await 안 한 resolves/rejects/toMatchFileSnapshot은 경고로 끝나지 않고 실패한다. locator가 strict 기본이라 getByText('Item')이 더 이상 Item 1을 매칭하지 않는다. 마이그레이션에서 가장 시끄러운 지점이 여기다. 워커·풀 ID는 0-based에서 1-based로 바뀌었고 벤치마킹 API는 재작성이라 기존 .bench.ts를 전부 손봐야 한다.

Zod 4.6

거절 비용을 깎는 릴리스

9월 9일 공개. 헤드라인은 .validate()다. ZodError를 만들지 않고 유효한지만 답하는 불리언 검증 API인데, 핵심은 첫 번째 이슈에서 short-circuit한다는 점이다. 전체 ZodIssue[] 배열을 모으지 않으니 거절 비용이 극적으로 싸진다.

z.compile()과 함께 쓰면 invalid 입력 기준 .safeParse().success 대비 최대 35배 빠르다. 호출당 시간으로 보면 3개 객체 union 28ns(34.9배), 문자열 10개 배열 23ns(24.6배), 키 5개 객체 21ns(16.3배). 반환 타입이 타입 가드라서 if (Player.validate(data)) 안에서 narrowing이 그대로 된다.

CJS 환경이라면 업그레이드만으로 3배를 가져간다. 4.5의 CJS 엔트리포인트는 255개 export 중 252개가 getter였고 V8이 getter 뒤의 callee를 상수로 볼 수 없어 인라이닝이 불가능했다. 4.6은 plain property로 emit하고 exports 객체를 freeze한다. ESM 빌드는 변화 없다.

재귀 스키마 메모리 회귀도 고쳤다. 4.5는 마지막 파스의 input/output을 다음 파스까지 붙들고 있어 장수명 스키마가 만진 모든 객체를 pin했다. 4.6은 파스 상태를 weak하게 유지해 29k 노드 트리 1회 파스 기준 retain이 10.1MB에서 2.2MB로 줄었다(재귀 파스 성능은 약 6% 희생). 카테고리 트리나 AST를 다루다 4.5에서 OOM을 겪었다면 이번 릴리스가 바로 그 수정이다.

적용 지점은 명확하다. 검증 결과는 받되 에러 메시지는 안 쓰는 곳. API 게이트웨이의 요청 필터, 미들웨어 스키마 체크, 큐 컨슈머의 페이로드 선별, 서버 액션 초입 가드. 반대로 폼 검증처럼 필드별 에러를 보여줘야 하는 곳에는 맞지 않는다. short-circuit이 곧 어디가 왜 틀렸는지 모른다는 소리니까.

브레이킹은 아래 손대지 말 것에 따로 모아뒀다. 조용한 것들이 섞여 있다.

React Router 8.4

플래그 없이 얻는 것과 플래그가 필요한 것

9월 15일 릴리스. API 추가 없이 런타임 성능 두 가지를 손봤다.

첫째는 플래그 없이 적용된다. 내부 데이터 라우터 컨텍스트를 하나에서 4개로 분리했다. 현재 위치(useLocation, useSearchParams, useMatches), 진행 중 네비게이션(useNavigation, useRevalidation), loader·action 데이터, fetcher. useLocation만 쓰는 컴포넌트는 navigation.state가 loading으로 바뀌어도 리렌더되지 않는다. 네비게이션마다 트리 전체가 흔들리던 앱은 업그레이드만으로 체감이 다르다.

둘째는 opt-in이다. @remix-run/route-pattern 기반 새 매처가 Chromium 합성 벤치마크 기준 라우트 100개에서 19~38%, 1,000개에서 71~88% 단축을 낸다. future: { unstable_routePatternMatching: true }로 켜되 그 전에 unstable_preloadRoutePattern()을 호출해야 한다. 라우트가 100개 미만이면 이득이 작다.

unstable_validateParams도 들어왔다. 파라미터에 정규식 검증을 걸고 실패하면 매칭이 뒤쪽 라우트로 이어진다. path: "/:drink"에 { drink: /^(wines|whiskeys|sakes|beers)$/ }를 걸고 그다음에 /:food를 두는 식이다. loader 안에서 파라미터 검증 후 404를 던지던 패턴을 라우팅 레이어로 올릴 수 있다.

컨텍스트 분리는 UNSAFE_ export에 breaking change를 포함한다. 테스트 유틸이나 커스텀 라우터 래퍼가 이걸 쓰고 있다면 먼저 확인해라. 그리고 레거시 매칭 API(matchRoutes, matchPath, useMatch)는 여전히 기존 정규식 매처를 쓰므로 플래그를 켠 라우터와 결과가 미묘하게 달라질 수 있다.

Turbopack 청킹

켜기만 하면 되는 둘, 측정이 필요한 셋

Next.js 팀이 청킹 전략을 공개했다. 청킹은 요청 수를 줄이는 것과 코드 전송량을 줄이는 것이 정면으로 충돌하는 문제다. 전부 한 청크면 캐시 적중률은 최고지만 안 쓰는 코드까지 매번 받고 모듈마다 쪼개면 과전송은 없지만 수백 개 요청과 압축 효율 저하가 생긴다.

Turbopack은 같은 chunk group 안의 청크만 병합한다. 어차피 함께 로드되니 병합해도 과전송이 없다. 병합이 손해가 되는 지점은 소프트 네비게이션이다. A+B 병합 파일을 받은 뒤 A만 필요한 페이지로 이동하면 A를 다시 받는다. Turbopack은 세션의 2/3가 단일 페이지 방문이라고 가정하고 확률 가중으로 병합 여부를 정한다.

nextjs.org 실측(8단계 네비게이션 합계): 병합 없음 561.6 KiB / 96 요청, 기본값 554.8 KiB / 38 요청, 그룹당 1청크 610.0 KiB / 15 요청. 기본값이 요청을 절반 이하로 줄이면서 코드량도 조금 적다. 최대 병합은 요청은 더 줄지만 코드가 10% 더 나간다.

지금 바로 켤 만한 건 둘이다. experimental.turbopackSharedRuntime(페이지별 런타임 청크를 하나로 통합, 첫 로드 이후 매 네비게이션마다 블로킹 요청 1개와 약 10KB 절약)과 experimental.turbopackCjsTreeShaking. 둘 다 추후 기본값이 될 예정이라 미리 켜서 검증해 두는 게 낫다.

나머지는 측정이 필요하다. generateComponentChunks는 병합본과 비병합본을 함께 emit해 런타임이 더 싼 쪽을 고르게 한다(SPA성 이동이 잦으면 유효). firstPageLoadPriority(기본 0.67)는 자사 애널리틱스의 실제 이탈률로 맞춰라. clusters는 함께 방문되는 라우트를 정규식으로 묶는데 잘못 정의하면 역효과다. A만 쓰는 페이지와 A+B를 쓰는 페이지를 한 클러스터에 섞으면 오히려 병합이 줄어든다.

그 외 넷

npm 토큰, Node 내장, 그리고 빌드 도구 둘

  • npm stage-only 토큰: "Read and write (stage only)" 권한이 생겼다. CI가 npm stage publish로 버전을 제출만 하고 실제 배포는 메인테이너가 2FA로 승인하는 구조다. npm이 2027년 1월부터 bypass-2FA 토큰의 직접 퍼블리시를 없앤다고 예고했다. 사내 라이브러리를 CI에서 자동 배포하는 팀은 지금 토큰 권한을 감사해라. 마이그레이션을 안 마치면 파이프라인이 갑자기 막힌다. 다만 stage-only 토큰도 dist-tag 이동이나 deprecate 같은 write 권한은 그대로 가지므로 일반 write 토큰처럼 보호해야 하고 더 근본적인 해결책은 OIDC 기반 trusted publishing이다.
  • Node 26.10.0: util.debounce()와 util.throttle()이 AbortSignal 지원과 함께 내장으로 들어왔다. .catch(() => {}) 땜질을 대체할 util.markPromiseAsHandled()도 생겼다. 빌드 스크립트나 SSR 로직에서 lodash 의존성 하나를 걷어낼 수 있다. 다만 Current 라인이라 프로덕션은 LTS 백포트를 기다리는 게 안전하고 무엇보다 이건 Node 런타임 API이지 브라우저에서 돌아가는 클라이언트 코드에는 쓸 수 없다. Flavio Copes가 정리한 12가지 Node 내장 대체재도 함께 볼 만하다. 각 대체재의 한계를 정직하게 짚어준다. fetch는 4xx/5xx에서 reject하지 않고 --env-file은 ${HOST} 변수 확장을 지원하지 않으며 타입 스트리핑은 enum·decorator를 지원하지 않는다.
  • Vite+ 1.0: 7월호에서 베타로 다뤘던 게 정식이 됐다. vp 단일 CLI 아래 create·install·dev·check·test·build·pack·run·env 9개 명령을 두고 vite.config.ts 하나로 전부 설정한다. 내부적으로 Vite 8, Vitest 5, Rolldown, Oxc, tsdown을 한 팀이 유지보수한다. vp migrate가 실행 전에 변경 계획을 먼저 보여주고 tsup 마이그레이션까지 처리한다. Node 런타임 관리까지 vp가 떠맡는 구조라 nvm·volta와 역할이 겹친다. 팀 컨벤션 정리가 먼저다.
  • Rslib 1.0: Rspack 진영의 라이브러리 빌드 도구가 1.0에 도달했다. React 컴포넌트 1만 개 벤치마크에서 0.7.0 대비 무캐시 빌드 24.3%, 캐시 빌드 56.7% 단축. 선언 파일 생성이 TypeScript 6에서 9.7s, TypeScript 7(tsgo 자동 사용)에서 4.1s, Isolated Declarations에서 2.3s다. 사내 디자인 시스템을 tsc/tsup로 빌드 중이고 앱 쪽이 이미 Rsbuild/Rspack이면 설정과 플러그인을 공유할 수 있어 이득이 크다. Isolated Declarations 모드는 타입 체크를 하지 않으니 별도 프로세스를 반드시 붙여야 한다.

관망

지금 쓰면 안 되지만 6개월 뒤에 "그거 왜 몰랐지" 소리 안 들으려면

Remix 3 RC: React 메타프레임워크였던 과거와 결별했다. 웹 프리미티브 위에 세운 독립 풀스택 프레임워크로, DB 관리·스키마 검증·타입 안전 라우터·번들 없는 에셋 서버·자체 UI 런타임까지 remix 단일 패키지에 담겼다. React Router v7을 쓰고 있다면 이건 업그레이드 경로가 아니다. 별개 프레임워크다. 의존성 하나로 풀스택이 갖춰져 npm 공급망 공격 표면이 줄고 에이전트 코딩에도 잘 맞는다는 게 팀의 주장이다. 조각 교체가 가능해서 @remix-run/data-schema 대신 Zod, @remix-run/data-table 대신 Drizzle을 끼울 수 있고 render 미들웨어를 React 기반으로 바꿀 수도 있다. 정식은 10월 2일 Remix Jam 예정이었다. React 생태계 자산을 그대로 가져올 수 없으니 기존 React 코드가 많으면 비용이 크다.

TanStack Redact v0.1: Tanner Linsley가 5월에 예고했던 React 호환 런타임이 나왔다. React 19.3 API를 지원하고 Vite 플러그인과 호환성 표까지 갖췄다. 런타임 크기는 React보다 훨씬 작지만 동시성 렌더링을 지원하지 않는다. 그리고 라이선스가 아직 없다. 실험 단계로 보는 게 맞다.

Vinext 1.0: Cloudflare가 Vite 기반으로 재구현한 Next.js가 1.0을 냈다. Pages Router와 App Router를 모두 지원하고 기존 앱 마이그레이션 경로도 제공한다.

OJ: Lovable이 하루에 Vite dev server를 백만 개씩 띄우는 부담을 줄이려고 Rust로 dev server를 새로 만들었다. 기존 vite.config.ts를 그대로 읽고 Vite 플러그인도 실행한다. 다만 Evan You가 벤치마크에 의문을 제기했다. AI 때문에 다들 자기만의 슬롭 포크를 유지보수하게 될 거라는 전망도 덧붙였다. 뒷담화 섹션 참고.

htmx 4.0: 2년 만의 메이저. 내부를 XMLHttpRequest에서 fetch()로 전면 교체했지만 사용자 관점에선 htmx 2와 거의 같다. 핵심 변경 셋은 속성 상속의 명시적 opt-in 전환(hx-confirm:inherited="..."), 이벤트명 표준화(htmx:beforeRequest → htmx:before:request), 히스토리 복원 시 localStorage 캐시 제거다. idiomorph 기반 morph swap이 내장됐고 <hx-partial>로 한 응답에서 여러 타깃을 갱신할 수 있다. npm latest는 2027년 초까지 2.x로 유지되고 4.0은 next 태그다. 기존 2.x를 쓰고 있다면 npx htmx.org@4.0.0 upgrade-check -- ./templates부터 돌려라. CSRF 헤더에 :inherited가 빠져 요청이 거부되는 사고를 막을 수 있다.

node:ffi 기본 활성화: Node 26.9.0부터 --experimental-ffi 플래그 없이 node:ffi를 쓸 수 있다. 플래그는 반대로 끄는 용도가 됐다. 컴파일된 addon이나 node-gyp 없이 네이티브 라이브러리 함수를 직접 호출한다. 기본값이 바뀌었으니 네이티브 코드 실행 표면도 그만큼 넓어졌다. 샌드박스나 서드파티 코드 실행 환경이라면 보안 정책을 재검토해야 한다.

나머지 구경거리: TinyJS(txiki.js 기반 10MB 미만 JS 데스크톱 앱, 서명·공증까지 지원). node:bench(James Snell의 실험적 벤치마크 러너가 main에 머지됐다. node:test를 본뜬 설계에 node --bench CLI와 JSON 리포터). Jotai 3.0(ESM 전용, React 18·TS 5.5 최소 요구, atomFamily는 jotai-family로 분리). Vidact(React 스타일 컴포넌트를 Solid처럼 직접적인 DOM 조작 코드로 컴파일, 베타). Expo SDK 58 beta(RN 0.88 RC 위, App Intents 지원).


손대지 말 것

이 목록만 알아도 하루는 번다

지뢰왜
Next.js 16.3.5 이하 (16.2.0+)next/og RCE. 예고된 9건 추가 패치
외부 노출된 Vite dev server패치해도 노출 구조가 남으면 다음 우회에 또 당한다
vm23.11.7의 수정이 좁다. 다른 파일로 겨누면 여전히 RCE
Zod 4.6의 에러 맵safeParse()가 lazy해져서 result.error를 읽을 때 실행된다
Zod 4.6의 z.emoji()"123", "#", 단독 ZWJ가 이제 실패한다
Zod 4.6의 enum .options리버스 매핑 제거. [0,1,2]만 남는다. UI 셀렉트에 쓰고 있었다면 확인
Vitest 5의 locator strictgetByText('Item')이 Item 1을 안 잡는다. 가장 시끄러운 지점
Vitest 5의 VITEST_WORKER_ID0-based에서 1-based로. 샤딩·포트 할당 오프바이원
Node 27의 Intel Mac실험적으로 격하. 공식 x64 macOS 빌드가 사라진다
GitHub Actions의 Node 20러너에서 제거됐다. 액션 유지보수 중이면 runs.using을 node24로
dotenv 18.0-r dotenv/config 프리로딩 제거. dotenv run --으로 대체

Zod 쪽을 조금 더 풀어야겠다. 조용히 위험한 게 섞여 있다.

에러 맵의 실행 시점이 바뀌었다. 4.6에서 safeParse()가 에러를 lazy하게 만들면서 에러 맵(global/locale/per-schema)이 파스 시점이 아닌 result.error를 처음 읽을 때 실행된다. 에러 맵에 사이드 이펙트를 넣어뒀다면 아무도 에러를 읽지 않을 때 그 코드가 아예 실행되지 않는다. 로깅이나 메트릭을 거기 걸어뒀다면 조용히 사라진다.

메타데이터 멤버 8종(.format, .minLength, .maxLength, .minValue, .maxValue, .isInt, .minDate, .maxDate)도 첫 읽기 시점에 materialize된다. Object.keys(schema)나 Object.assign({}, schema)에 의존하는 코드는 결과가 달라진다. 값 자체도 바뀔 수 있다. z.string().min(8).length(5).minLength가 4.5에서는 5였는데 4.6에서는 8이다.

그리고 z.toJSONSchema() 출력이 달라진다. 체인된 체크를 conjunction으로 접어서 z.number().min(0).max(23).int()가 올바른 {minimum: 0, maximum: 23}을 낸다. 런타임 파싱은 원래부터 맞게 동작했고 틀린 건 emit된 스키마뿐이었다. 스냅샷 테스트가 깨질 수 있다.


참고

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

성능 실화 세 편

  • Anthropic이 2주 만에 Claude 앱을 3배 빠르게 만든 과정: claude.ai 신규 로드 3,085ms → 550ms(5.6배), 기존 대화 불러오기 1,557ms → 646ms(2.4배), 메시지 전송 최대 19배(928ms → 48ms). 디버깅 스토리 둘이 특히 재밌다. 하나는 em dash(—)나 곡선 따옴표 같은 비-Latin1 문자가 섞이면 V8이 문자열 전체를 UTF-16으로 저장해 정규식이 느린 2바이트 경로를 탄다는 것. 구문 강조 전에 코드 블록을 1바이트 문자열로 복사하는 것만으로 초기 하이라이팅 블로킹이 1초에서 0.35초가 됐다. 다른 하나는 남아 있던 location.reload() 한 줄이 하루 약 50만 건의 보이지 않는 리로드를 유발하고 있었다는 것. 일반적인 로드 지표로는 안 잡히는 문제였다. 교훈은 순서다. Valgrind로 CPU 명령어 수를, V8 커버리지로 호출 횟수를 재는 결정론적 지표 체계를 먼저 만들고 그 위에서 최적화를 반복했다. 다만 2주에 3,000건 이상을 병합하면서 무중단을 유지한 건 약 200개의 피처 플래그와 자동 코드 리뷰, 14개 뷰포트 통합 테스트가 있어서다. 안전장치 없이 같은 속도로 미는 건 위험하다.
  • GitHub이 CSS-in-JS에서 벗어난 이야기: styled-components를 걷어내고 CSS Modules로 전환해 Primer 컴포넌트의 SSR 시간을 55%, 컴포넌트 초기화 시간을 25% 줄였다. 2026년 6월 전환율 100% 달성. 단계 설계가 참고할 만하다. (1) 디자인 시스템에 CSS Modules 도입하며 피처 플래그로 신구 구현 전환, 비주얼 회귀 테스트로 동일성 검증 (2) 수천 곳에서 쓰이던 sx prop 제거를 위해 과도기용 @primer/styled-react 래퍼와 VS Code 플러그인·코드모드를 만들어 엔지니어 8명이 6개월간 약 6,400곳 이관, 이후 Copilot 에이전트를 투입해 남은 항목을 3주 만에 정리 (3) 테마 로직을 CSS로 이전. 2024년 12월 첫 성과 발표에서 2026년 6월 완료까지 수년이 걸린 프로젝트다.
  • Linear가 StyleX로 옮긴 이유: 6개월간 1,000개 넘는 PR. 동기가 둘인데 둘 다 흥미롭다. 하나는 성능으로, view-heavy 페이지에서 런타임 스타일 주입을 없애 메인 스레드 CPU 작업이 20~35% 감소했고 네비게이션 중 주입되던 수백 개 CSS 규칙이 0이 됐다. 다른 하나는 경계다. styled(Button) 같은 패턴이 컴포넌트를 바깥에서 열어젖히는 걸 당연하게 만들었고 리디자인 이후 UI 회귀의 긴 꼬리를 계속 쫓는 원인이 됐다. 에이전트가 코드베이스에 기여하는 비중이 커지면서 명시적 스타일 계약의 필요가 더 커졌다는 게 결정적이었다. 마이그레이션 순서도 참고할 만하다. 스타일 기반 정의 → leaf 노드부터 이동 → 공유 primitive는 마지막. 그리고 채택률을 끌어올린 건 기술이 아니었다. 가시화였다. dev 툴바의 잔여 styled-components 규칙 카운터, 컴포넌트별 스타일 시스템 하이라이터, 주간 진행 그래프, 새 styled-components를 PR에서 지적하는 봇.

에이전트가 바꾼 것들

  • Linear의 CI 재설계: 에이전트가 주당 약 2,000개씩 테스트를 추가하면서 스위트가 올해 초 대비 거의 4배로 늘었는데도 PR 대기 시간을 6분대에서 5분대로 줄였다. 타입 정보에 의존하던 커스텀 ESLint 규칙을 순수 AST 검사로 재작성해 API 린트 68%, 전체 린트 55%를 줄였고 덕분에 Oxlint 전환도 쉬워졌다. Vitest 샤드를 4개에서 8개로 늘리고 안전하다고 표시한 파일에 한해 isolate:false로 모듈 상태를 공유하게 해 월간 비용 17%를 추가로 절감했다(단일 최대 개선폭). isolate:false는 정합성 리스크가 가장 큰 변경이니 fake timer나 공유 상태를 분리할 수 없는 파일은 격리 모드에 남겨야 한다.
  • 프레임워크는 여전히 중요한가: Remix 팀 Brooks Lybrand가 "AI가 알아서 React 짜주면 프레임워크가 무슨 소용이냐"는 담론을 반박한다. 프레임워크를 안 쓰면 에이전트가 자기만의 조잡한 프레임워크를 즉석에서 만들어낸다는 것. 관련해서 과도한 추상화 비용 실험은 지나치게 추상화된 React 앱이 에이전트 작업 비용을 최대 5배까지 올린다는 결과를 냈다. 추상화를 더 하라는 말도 덜 하라는 말도 아니고 적정선이 있다는 이야기다.
  • open-design-system-bench: 우리 디자인 시스템이 AI에게 얼마나 잘 읽히는지 점수로 답하는 오픈소스 벤치마크. 컴포넌트 이름을 절대 말해주지 않는 의도 수준 프롬프트를 헤드리스 에이전트에 던지고 나온 코드를 6개 차원으로 채점한다(imports 0.10, apiFidelity 0.25, tokenDiscipline 0.15, a11yStatic 0.10, compile 0.10, judgment 0.30). 컴포넌트 환각과 컴파일 에러는 하드 게이트로 fail이다. 시작은 API 키가 필요 없는 audit부터. 7개 Tier-0 체크로 AI-Readiness Score를 매기고 티어를 붙인다(AI-native ≥ 70, Invested ≥ 40, 그 아래 Emerging). CI에 smoke 프로파일을 물리면 prop 이름을 바꿨을 때 프로덕션보다 스위트가 먼저 깨진다. 셀당 대략 1.5달러에 생성 2~5분이고 스타 9개짜리 초기 프로젝트라는 점은 감안해라.
  • UX의 관리인 시대: NN/g. 문제의 본질은 AI 결과물의 품질이 아니다. 생산이 평가보다 싸졌다는 데 있다. 경험을 만드는 시간이 그게 유용한지 판단하는 시간보다 짧아졌다. 그 결과 조직은 아이디어 → AI로 빠른 구현 → UX 평가 누락 → 사용자 문제 발생 → UX가 뒷수습하는 패턴에 빠진다. FE 입장에서 바로 쓸 수 있는 처방은 저장소에 UX.md나 Design.md를 두고 코딩 에이전트 컨텍스트에 물리는 것이다. 승인된 컴포넌트 목록, 금지 패턴, 접근성 체크리스트를 거기 명시하면 매번 리뷰에서 같은 지적을 반복할 필요가 없다. 반복해서 같은 문제가 생성물에 나타난다면 개별 산출물 말고 생성 규칙을 고쳐라. 다만 UX-context 파일은 방치되면 금방 낡아서 오히려 틀린 가이드를 주입하게 된다.
  • 에이전틱 표준을 요구한다: MCP로 연결은 표준화가 끝났는데 에이전트가 화면에 date picker를 띄워야 할 때 누가 무엇을 어떻게 렌더링하느냐는 아직 무법지대다. 네 갈래 분류가 이 글의 실질적 가치다. (1) 선언형 JSON + 클라이언트 렌더(A2UI, Gemini Enterprise·Google Opal) (2) 샌드박스 HTML(ChatGPT Apps SDK, Claude Artifacts·MCP Apps) (3) 선언형 JSON + 벤더 카탈로그(Microsoft Adaptive Cards, Slack Block Kit) (4) 코드 생성 또는 없음(Gemini Consumer, xAI Grok). 필자는 이걸 surface tax(서피스 × 플랫폼)라 부르고 90년대 브라우저 전쟁의 재현으로 본다. 실무 액션 넷이 그대로 백로그가 된다. 서피스 말고 셀을 세라. 화면 말고 카탈로그를 써라. 디자인 전에 Adaptive Cards 스키마를 먼저 읽어라. 중급 안드로이드 실기기에서 TalkBack 켜고 먼저 테스트해라.

성능과 엔진

  • Safari가 top-level await를 고친 이야기: Safari의 모듈 로더는 2016년 1월을 마지막으로 갱신이 멈춘 WHATWG Loader 제안 위에 지어져 있었다. async/await가 ECMAScript에 존재하기도 전의 것이다. 증상이 구체적이다. top-level await를 가진 같은 모듈을 동적으로 3번 연속 import하면 완료 순서가 1, 2, 3이 아닌 2, 3, 1로 뒤집히고 2·3번째에서 Cannot access 'someArray' before initialization이 난다. Safari 27에서 모듈 로더를 바닥부터 다시 써 완전한 스펙 준수를 달성했고 구현도 self-hosted JavaScript builtin에서 네이티브 C++로 전면 전환했다. 검증 방식이 인상적이다. 같은 JavaScriptCore 기반이라 같은 문제를 물려받았던 Bun 팀이 실패 케이스를 제공했고 복잡한 모듈 그래프를 생성하는 퍼저를 만들어 다른 엔진 출력과 바이트 단위로 비교했다. 다만 수정은 Safari 27 이후에만 적용되고 iOS는 OS 업데이트와 묶여 있어 잔존 기간이 길다. 라이브러리 공개 진입점에 top-level await를 두는 건 아직 이르다.
  • V8이 상수 시간 라이브러리를 무너뜨린 이야기: 분기 없는 JS 코드조차 V8의 ToBoolean 변환 로직과 -0/-1 처리 방식, 그리고 true/false Oddball 객체가 서로 다른 캐시 라인에 위치한다는 점 때문에 CPU 캐시 접근 패턴으로 비밀 값을 유출할 수 있다. UBC 연구팀이 발견했고 이미 패치됐다. 토큰 비교나 서명 검증을 순수 JS로 상수 시간이라 믿고 구현했다면 재검토 대상이다. 분기가 없어도 상수 시간은 보장되지 않는다.
  • await fetch()가 기다리는 것: 본문 전체를 기다리지 않는다. 헤더까지만 기다린다. 이 사실이 커넥션 관리, clone(), abort, 타임아웃 처리에 만드는 의외의 결과들을 파고든다.
  • GitHub Copilot 앱의 거대 PR 렌더링: 파일 2,200개, 변경 라인 100만 줄 이상, 인라인 코멘트 400개 이상. 진짜 어려움은 크기에 있지 않았다. 코멘트 높이가 마크다운 줄바꿈·접이식 섹션·이미지·답글 입력창에 따라 예측 불가능하게 달라진다는 점이었다. 해법은 지오메트리를 둘로 나누는 것. 코드 라인처럼 높이가 결정적인 부분은 타입드 배열로 오프셋을 미리 계산하고 코멘트처럼 동적인 블록은 추정치를 쓰다가 뷰포트 근처에 올 때만 지연 측정한다. 겪은 버그도 솔직하게 공유한다. 자신이 관찰 중인 엘리먼트의 레이아웃에 높이를 다시 써 넣는 ResizeObserver가 스스로를 재트리거하는 피드백 루프에 빠져서, 유휴 시간과 스크롤 정지 시점에만 실행되는 단일 배치 측정 패스로 바꿨다. 수백만 DOM 노드급 극단 케이스에서만 정당화되는 복잡도이고 보통은 react-window로 충분하다.

설계와 디테일

  • AI 입력창 해부: prompt-kit을 만든 Julien Thibeaut가 AI 제품 인풋을 여덟 레이어로 분해했다. 관통하는 원칙 하나는 어떤 상태에서도 인풋은 계속 사용 가능해야 한다는 것. 로딩·스트리밍 중에 인풋을 잠그지 말고 전송 직후 send 버튼은 stop 버튼으로 바뀌어야 하며 에러는 생성에 속하지 입력에 속하지 않으니 인풋은 중립 상태로 두고 채팅 컨테이너에 재시도 버튼을 둔다. 모바일에서 Safari 자동 줌을 막으려면 font-size 최소 16px. 한 가지 글이 다루지 않는 게 있는데 한국어 환경에서는 그게 제일 자주 터진다. 한글 입력 중 Enter로 전송하면 조합 중인 글자가 잘리는 문제는 compositionstart/compositionend 또는 isComposing 체크로 별도 방어가 필요하다.
  • Props는 디자인 시스템이 아니다: 임의의 스타일 prop을 남발하면 모든 버튼이 일회성 컴포넌트가 돼버리는 이유. 위 @shadcn/lint와 짝으로 읽으면 좋다.
  • DialKit: 실행 중인 앱 위에 플로팅 컨트롤 패널을 띄워 모션·레이아웃·컬러·타이밍을 실시간으로 조정한다. 컨트롤 타입이 값의 형태에서 자동 추론되는 게 깔끔하다. radius: [24, 0, 64]는 슬라이더(기본값·최소·최대), visible: true는 토글, '#a78bfa'는 컬러 피커가 된다. type: 'spring'으로 Time 모드와 Physics 모드를 전환하고 type: 'easing'으로 큐빅 베지어 핸들을 직접 드래그한다. 스프링 애니메이션 튜닝처럼 숫자 바꾸고 새로고침을 수십 번 반복하던 작업이 드래그로 바뀐다. 프로덕션 빌드에서는 자동으로 숨겨지지만 productionEnabled를 실수로 켜두면 사용자에게 개발용 패널이 노출된다.
  • Mesurer: React 앱에 <Measurer /> 하나만 얹으면 살아 있는 화면 위에서 요소를 선택하고 간격을 재고 가이드를 긋는다. 단축키로 Select/Guides/X-ray/Ruler 모드를 오간다. 개발 빌드에만 조건부 마운트하면 디자이너와 QA가 스테이징에서 직접 간격을 재고 이슈를 적어줄 수 있다. 비슷한 결로 Squints는 크롬 확장이라 React가 아닌 페이지에도 쓸 수 있다.
  • 의사결정을 이끄는 대시보드: 대시보드가 실패하는 이유는 데이터 부족이 아니다. 애초에 인사이트를 전달하도록 설계되지 않았다. 차트가 답해야 할 질문 대신 마침 있는 데이터에서 출발한 결과다. FE 관점에서 가져갈 건 하나다. 색을 컴포넌트 스타일이 아닌 데이터 계층에 고정해라. 디자인 토큰과 시리즈 매핑을 한 곳에 두면 차트가 늘어나도 일관성이 유지된다. 사례도 구체적이다. 예약률 15% 급락을 단일 지표로만 보여준 대시보드는 "앱이 깨졌나"는 오판과 헛된 UX 소방훈련을 유발했는데 트래픽 소스와 캠페인 런칭 축을 함께 매핑하자 신규 대규모 캠페인의 저의도 트래픽이 전체 비율을 희석한 것으로 드러났다.

뒷담화

이번 달 이적 시장과 뒷말

  • Tailwind Labs가 Shopify에 합류한다. 팀은 Tailwind CSS와 다른 오픈소스 프로젝트는 달라지는 게 없다고 못 박았다. Adam Wathan은 Tailwind가 이미 스택의 큰 축인 Shopify에서 실제 제품 문제를 풀면서 Tailwind를 개선하는 게 목표라고 밝혔다. 같은 달에 Shopify가 React Native를 버리고 Tailwind 팀을 샀다.
  • Margelo가 Callstack에 합류한다. Nitro Modules와 VisionCamera를 만든 팀이고 기업가치 2천만 유로 이상으로 평가된 딜이다.
  • TanStack과 Vercel이 공식 협력한다. Vercel을 TanStack Start의 1급 배포처로 만들고 Vercel AI Gateway와 Sandbox용 TanStack AI 어댑터를 새로 제공한다.
  • Evan You가 Lovable의 OJ에 공개적으로 의문을 제기했다. 벤치마크를 믿기 어렵다고 했다. AI 때문에 다들 자기만의 "슬롭 포크"를 유지보수하게 될 거라는 전망도 덧붙였다. 같은 달에 Vite+ 1.0을 내면서 한 말이라 맥락이 묘하다.
  • TeamPCP 용의자 2명이 체포됐다. Shai-Hulud 웜을 복제한 것으로 추정되는 그룹이고 이른바 Mini Shai-Hulud 공격 파도를 일으켰다. WIRED는 구글 애널리스트가 그 그룹 채팅에 잠입해 있었다는 기사를 냈다. 지난달 표지의 후일담이다.
  • OpenJS 재단 CVE 팀이 단체 휴식에 들어갔다. 9월 중순부터 10월 6일까지고 이유는 AI가 생성한 리포트 폭증으로 인한 번아웃이다. 실제 악용 중인 이슈에는 계속 대응한다고 밝혔다.
  • npm 12의 기본 방어 기능이 Node 27로 연기됐다. Release WG가 Node 22/24/26에 넣기엔 너무 파괴적이라고 판단했다. install 스크립트 차단이 그만큼 늦어진다.
  • Angular가 부분 Rust 컴파일러를 만들고 있다. TypeScript 7의 Go 포트가 현재 컴파일러가 의존하는 API를 아직 지원하지 않아서다. TS7 전환이 생각보다 긴 꼬리를 남기고 있다.
  • Node.js TSC 선거가 무투표 당선으로 끝났다. Matteo Collina가 Chair를 유임하고 Rafael Gonzaga가 Vice-Chair를 맡는다.
  • jQuery 1.0이 20주년을 맞았다. InfoQ가 $()가 브라우저 전쟁을 어떻게 우회하게 해줬는지 되짚었다. 그보다 10년 전 Microsoft가 상표 문제를 피하려 JScript라는 이름으로 IE에 JavaScript를 실었다는 이야기도 함께.

이전 시리즈 포스트

26년 09월
  • 이달의 표지
  • 숫자로 보는 9월
  • 이달의 판정표
  • 즉시
    • Next.js
    • Vite
    • vm2
    • 나머지 넷
  • 검토
    • React 19.3
    • oxc의 React Compiler
    • @shadcn/lint
    • Vitest 5.0
    • Zod 4.6
    • React Router 8.4
    • Turbopack 청킹
    • 그 외 넷
  • 관망
  • 손대지 말 것
  • 참고
    • 성능 실화 세 편
    • 에이전트가 바꾼 것들
    • 성능과 엔진
    • 설계와 디테일
  • 뒷담화