웹 개발 데일리 브리핑 — 2026-10-26
Soshy··조회수 —

기준 시점: 2026-10-26 06:00 KST
조사 범위: 2026-10-25 06:00 ~ 2026-10-26 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
React, Next.js, Vercel, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro, Remix의 공식 블로그와 릴리스 채널을 조사했지만, 이번 24시간에 새롭게 공개된 주요 정식 릴리스나 렌더링 API 변경은 확인하지 못했다.
Next.js의 최근 릴리스에는 16.4.3과 16.5.0 Canary 계열이 포함돼 있지만, 확인된 주요 변경은 이번 조사 기간 이전에 공개됐다.
개발 버전의 개별 Commit이나 사소한 유지보수 변경은 새로운 안정화 기능의 출시와 구분했다.
이번 추천 글에서는 Vue와 Nuxt를 이용해 여러 사용자가 참여하는 실시간 인터랙티브 화면을 구현한 실제 프로젝트를 소개한다. 특히 WebSocket의 메시지 중복과 누락, Vue 컴포넌트의 재사용, Web Worker를 통한 카메라 영상 처리 등 프론트엔드 아키텍처와 관련된 내용을 자세히 살펴볼 수 있다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome Developers·Chromium, Mozilla·Firefox, WebKit·Safari의 공식 발표와 JavaScript, TypeScript, CSS, 웹 성능 및 접근성 관련 정보를 확인했지만, 이번 24시간에 해당하는 중요한 웹 표준 변경이나 새로운 브라우저 API의 정식 출시를 확인하지 못했다.
WebKit에서는 개발 빌드가 계속 생성되고 있지만, 개발 빌드의 생성 사실과 새로운 웹 플랫폼 기능이 안정화됐다는 사실은 구분했다.
Firefox와 관련된 새로운 콘텐츠도 확인했지만, 이번 기간에 공개된 일부 게시물은 제품 홍보 및 커뮤니티 콘텐츠로서 브라우저 엔진이나 웹 API의 중요한 변경을 설명하는 발표는 아니었다.
이번 조사에서는 새로운 웹 표준을 발표한 소식보다 기존 브라우저 기술을 실제 애플리케이션에 적용한 사례가 더 유의미했다.
특히 추천 글에 포함한 Vue 프로젝트에서는 SVG, Web Animations API, Canvas, Web Audio API, Web Worker를 함께 사용하면서 각 기술의 책임을 명확하게 분리했다.
3️⃣ 백엔드·인프라
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
Cloudflare, Vercel, AWS, Google Cloud, Microsoft Azure의 공식 발표와 Node.js, Deno, Bun, 주요 서버리스·엣지 및 데이터베이스 관련 정보를 검토했다.
Cloudflare의 공식 Changelog에서는 10월 25일 새로운 WAF 긴급 변경 공지를 확인했다. 그러나 새로운 백엔드 실행 기능이나 서버리스 플랫폼의 변경이 아니라 보안 규칙 업데이트이므로 4️⃣ 개발 도구 및 보안 섹션에서 다룬다.
Vercel의 최근 주요 업데이트는 10월 23일에 공개된 기능들이다. 이번 조사 기간에 새롭게 공개된 주요 서버리스·배포 기능은 확인하지 못했다.
Node.js 역시 10월 21일 보안 업데이트 이후 이번 조사 범위에 해당하는 별도의 주요 신규 릴리스는 확인하지 못했다.
앞선 브리핑에서 소개한 Vercel WebSocket 정식 출시, Cloudflare의 Origin Range Requests 및 최근 보안 패치 등은 반복하지 않았다.
4️⃣ 개발 도구 및 보안
Cloudflare WAF, Node.js Brotli 취약점 대응 긴급 규칙 2개 추가 예고 — 기본값은 Log이며 Next.js 이미지 요청 차단에 주의
- 발표일: 2026-10-25 — 정확한 게시 시각 미확인
- 발표 주체: Cloudflare
- 적용 대상: Cloudflare Managed Ruleset
- 관련 취약점: CVE-2026-111497
- 변경 유형: 긴급 WAF 탐지 규칙 추가
- 기본 동작: Log
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-25-emergency-waf-release/
Cloudflare가 Node.js의 Brotli Decoder 취약점 CVE-2026-111497과 관련된 HTTP 요청을 탐지하기 위해 두 개의 새로운 WAF 규칙을 추가하겠다고 발표했다.
이번 변경은 새로운 취약점이 발견됐다는 발표가 아니다.
CVE-2026-111497 자체는 10월 21일 Node.js의 긴급 보안 업데이트에서 이미 공개됐으며, 이전 브리핑에서 다뤘다.
이번에는 해당 취약점에 대응하기 위해 Cloudflare가 어떤 요청을 탐지하고, 그 요청에 어떤 동작을 적용할 것인지를 새롭게 발표했다.
특히 주의해야 할 점이 있다.
두 규칙 모두 기본적으로 요청을 차단하지 않는다.
규칙은 기본적으로 Log 모드로 활성화될 예정이며, 해당 조건에 일치하는 요청을 기록하는 역할을 한다.
따라서 새로운 규칙이 추가된다는 사실과 실제 공격 요청이 자동으로 차단된다는 사실을 혼동해서는 안 된다.
Brotli Decoder 취약점의 배경
Brotli는 데이터를 압축하기 위한 알고리즘이다.
웹에서는 HTML과 CSS, JavaScript를 비롯한 여러 종류의 데이터를 전송할 때 사용할 수 있다.
예를 들어 브라우저가 서버에 HTML을 요청하면 서버는 Brotli로 압축한 응답을 반환할 수 있다.
브라우저는 전달받은 데이터를 압축 해제한 뒤 사용한다.
반대로 클라이언트가 서버로 전송하는 요청 본문도 Brotli로 압축할 수 있다.
Node.js는 이러한 데이터를 처리하기 위한 Brotli Decoder를 제공한다.
그러나 CVE-2026-111497에서는 특정 Brotli 압축 데이터의 압축 해제 과정에서 메모리 범위를 벗어나는 쓰기가 발생할 수 있었다.
이러한 메모리 손상은 원격 코드 실행으로 이어질 가능성이 있기 때문에 높은 심각도로 분류됐다.
Node.js 프로젝트는 10월 21일 해당 문제를 수정한 보안 버전을 공개했다.
관련 수정 버전은 다음과 같다.
| Node.js 버전 라인 | 보안 수정 버전 |
|---|---|
| 22.x | 22.23.4 |
| 24.x | 24.22.1 |
| 26.x | 26.12.1 |
이번 Cloudflare WAF 규칙은 이러한 취약점과 관련된 HTTP 요청을 관찰하기 위한 추가적인 방어 계층이다.
하지만 런타임에 존재하는 메모리 손상 문제를 수정하는 것은 아니다.
따라서 실제 Node.js 런타임 업데이트와 WAF 정책은 별개로 관리해야 한다.
첫 번째 규칙: Brotli로 인코딩된 요청 본문 탐지
첫 번째 규칙은 HTTP Request Body가 Brotli로 인코딩됐다고 선언하는 요청을 탐지한다.
핵심 조건은 다음 Header다.
Content-Encoding: br
Content-Encoding은 HTTP 메시지의 콘텐츠에 어떤 인코딩이 적용됐는지 나타내는 Header다.
여기서 br은 Brotli를 의미한다.
예를 들어 클라이언트가 Brotli로 압축한 JSON 데이터를 서버에 전송한다고 가정해 보자.
클라이언트는 요청에 다음 Header를 포함할 수 있다.
Content-Encoding: br
서버는 이를 확인하고 요청 본문을 Brotli로 압축 해제할 수 있다.
이번 규칙은 이러한 요청을 탐지한다.
다만 중요한 제한사항이 있다.
이 Header를 사용한다는 사실만으로 요청이 악의적이라는 의미는 아니다.
정상적인 애플리케이션에서도 Brotli로 압축한 요청 본문을 사용할 수 있기 때문이다.
따라서 해당 규칙은 취약점 악용 여부를 완전히 판별하는 정밀한 탐지 규칙이라고 보기 어렵다.
오히려 Brotli 요청 본문을 사용하는 트래픽이 얼마나 존재하는지 확인할 수 있는 넓은 범위의 탐지 조건에 가깝다.
Content-Encoding과 Accept-Encoding의 차이
이번 규칙에서 특히 주의해야 할 부분은 다음 두 Header의 차이다.
| Header | 의미 |
|---|---|
Content-Encoding: br | 현재 전송하는 콘텐츠가 Brotli로 인코딩돼 있음 |
Accept-Encoding: br | Brotli로 인코딩된 응답을 수신할 수 있음 |
두 Header는 이름이 비슷하지만 역할이 다르다.
Accept-Encoding은 클라이언트가 서버에 어떤 압축 형식의 응답을 처리할 수 있는지 알려주는 데 사용한다.
반면 Content-Encoding은 실제로 전송하는 메시지의 콘텐츠에 어떤 인코딩이 적용됐는지 나타낸다.
일반적인 브라우저는 HTTP 요청에서 Accept-Encoding: br을 사용할 수 있다.
하지만 그렇다고 해당 브라우저가 서버에 Brotli로 압축한 Request Body를 전송하는 것은 아니다.
Cloudflare는 이번 규칙이 Accept-Encoding: br에는 일치하지 않는다고 명시했다.
또한 Cloudflare의 일반적인 Brotli 응답 압축 설정에도 영향을 주지 않는다.
따라서 이번 규칙이 적용된다고 해서 브라우저에서 사용하는 Brotli 응답 압축이 자동으로 비활성화되는 것은 아니다.
이러한 차이를 이해하지 못하면 실제로 영향을 받지 않는 일반적인 HTTP 응답 압축까지 제한해야 한다고 오해할 수 있다.
두 번째 규칙: Next.js 이미지 최적화 경로 탐지
두 번째 규칙은 Next.js의 이미지 최적화 엔드포인트를 대상으로 한다.
탐지 경로는 다음과 같다.
/_next/image
Next.js는 이미지 최적화 기능을 제공한다.
예를 들어 개발자가 next/image를 이용해 이미지를 표시하면 Next.js는 이미지의 크기와 형식을 최적화한 응답을 제공할 수 있다.
이러한 요청은 일반적으로 /_next/image 경로를 통해 처리된다.
이번 규칙은 해당 경로로 들어오는 요청을 탐지한다.
하지만 Next.js 이미지 최적화 요청 전체가 악의적인 요청이라는 의미는 아니다.
정상적으로 동작하는 Next.js 애플리케이션에서도 해당 경로가 자주 사용될 수 있다.
예를 들어 다음과 같은 이미지가 모두 이 경로를 통해 제공될 수 있다.
- 상품 썸네일
- 사용자 프로필 이미지
- 블로그 대표 이미지
- 콘텐츠에 포함된 일반 이미지
따라서 이 규칙을 단순히 Block으로 변경하면 실제 사용자에게 필요한 이미지까지 차단될 수 있다.
Cloudflare 역시 공식 공지에서 해당 위험을 명시했다.
두 규칙의 기본 동작
이번 변경을 정리하면 다음과 같다.
| 규칙 | 탐지 대상 | 기본 동작 |
|---|---|---|
| Brotli Request Content-Encoding | Content-Encoding: br이 포함된 요청 | Log |
| Next.js Image Optimizer Path | /_next/image 경로의 요청 | Log |
두 규칙은 Cloudflare Managed Ruleset에 추가될 예정이다.
기본 설정에서는 조건에 일치한 요청을 기록한다.
그러나 해당 요청을 차단하지 않는다.
따라서 운영자는 Cloudflare의 Analytics에서 실제로 얼마나 많은 요청이 규칙에 일치하는지 확인할 수 있다.
예를 들어 첫 번째 규칙에 정상적인 API 요청이 많이 일치한다면 Brotli Request Body를 사용하는 클라이언트가 존재한다는 사실을 확인할 수 있다.
두 번째 규칙에 요청이 많이 일치한다면 Next.js 이미지 최적화 기능을 사용하고 있을 가능성이 있다.
이러한 정보를 확인한 뒤 특정 규칙을 차단 목적으로 사용할지 결정할 수 있다.
그러나 단순히 조건에 일치한 요청 수가 많다는 이유로 공격이 많이 발생했다고 판단해서는 안 된다.
현재 탐지 조건에는 정상적인 요청도 포함되기 때문이다.
Log와 Block의 차이
WAF에서 Log는 요청을 기록하는 동작이다.
요청 자체는 계속 처리된다.
반면 Block은 요청을 차단하는 동작이다.
따라서 두 동작의 운영상 영향은 매우 다르다.
예를 들어 정상적인 Next.js 이미지 요청이 하루 100만 건 발생한다고 가정해 보자.
새 규칙을 Log 모드로 사용하면 해당 요청들이 기록될 수 있지만 이미지 전달은 계속 이루어진다.
반면 Block으로 변경하면 일치하는 요청들이 차단될 수 있다.
이 경우 실제 사용자는 이미지가 표시되지 않는 문제를 경험할 수 있다.
특히 이미지가 상품 구매나 콘텐츠 이해에 필요한 경우에는 사용자 경험에 큰 영향을 줄 수 있다.
따라서 규칙에 일치하는 트래픽이 실제로 어떤 종류의 요청인지 확인하지 않은 상태에서 Block으로 전환하는 것은 위험할 수 있다.
기존 Ruleset Override가 더 중요한 문제
이번 공지에서 가장 중요한 주의사항은 기존에 설정한 Ruleset 수준의 Action Override다.
Cloudflare는 각 WAF 규칙에 기본 동작을 정의할 수 있다.
하지만 운영자는 규칙 전체에 적용되는 Override를 설정할 수도 있다.
예를 들어 특정 Managed Ruleset에 포함된 모든 규칙을 Block하도록 설정했다고 가정해 보자.
이러한 설정이 존재한다면 이번에 추가되는 두 규칙도 해당 Override의 영향을 받을 수 있다.
문제는 새로운 규칙의 기본 동작이 Log라는 점이다.
운영자는 새로운 규칙이 Log로 출시되므로 실제 요청을 차단하지 않을 것이라고 생각할 수 있다.
그러나 Ruleset 수준에서 Block을 강제하고 있다면 새로운 규칙도 배포되는 즉시 Block 동작을 수행할 수 있다.
Cloudflare는 이러한 상황에서 정상적인 요청이 차단되는 False Positive가 발생할 수 있다고 명시했다.
따라서 다음 두 설정은 구분해야 한다.
- 규칙 자체의 기본 동작
- Ruleset 수준에서 적용되는 Action Override
실제 적용되는 동작은 규칙의 기본값만으로 결정되지 않을 수 있다.
Ruleset 수준 Override와 규칙별 Override
Cloudflare는 이번 규칙을 차단 없이 평가하려면 규칙별 Override를 사용해 Log 동작을 명시적으로 설정하도록 안내한다.
예를 들어 기존 Ruleset 전체에 Block Override가 존재한다고 가정해 보자.
이 경우 새로운 두 규칙에만 별도의 Log Override를 적용할 수 있다.
이렇게 하면 기존 보안 규칙의 차단 정책은 유지하면서 새로운 규칙의 탐지 결과만 먼저 관찰할 수 있다.
이후 요청 내용을 분석하고 실제 서비스에 미치는 영향을 확인한 뒤 특정 규칙의 동작을 변경할 수 있다.
다만 모든 규칙을 무조건 Log로 유지하는 것이 가장 적합한 선택이라는 의미는 아니다.
실제 보안 요구사항과 요청 특성에 따라 차단 정책이 필요할 수 있다.
핵심은 정상적인 요청까지 광범위하게 일치하는 규칙을 실제 차단 목적으로 사용할 때 발생할 영향을 이해하는 것이다.
이번 발표의 안정화 상태와 적용 시점
이번 공지는 2026년 10월 25일에 게시됐다.
다만 정확한 게시 시각은 확인되지 않았다.
공식 문서는 두 규칙을 추가할 것이라고 설명한다.
따라서 공지의 게시 사실과 모든 Cloudflare 환경에 해당 규칙의 배포가 이미 완료됐다는 사실은 구분해야 한다.
이번 조사에서는 공식 문서에 제시된 새 규칙의 동작을 확인했지만, 개별 고객의 Ruleset에 실제로 배포가 완료됐는지까지 확인한 것은 아니다.
또한 Cloudflare는 이번 공지에서 두 규칙의 성능이나 실제 공격 차단율에 관한 독립적인 Benchmark를 제시하지 않았다.
따라서 해당 규칙을 활성화하면 모든 Brotli 관련 공격이 탐지되거나 차단된다고 보장할 수 없다.
실제 취약점 수정과의 관계
이번 두 규칙은 취약한 Node.js 런타임을 직접 수정하지 않는다.
따라서 다음 사항을 구분해야 한다.
첫 번째는 Node.js의 보안 수정 버전을 사용하는 것이다.
이는 실제 Brotli Decoder의 메모리 손상 문제를 해결하는 작업이다.
두 번째는 WAF에서 해당 취약점과 관련된 요청을 관찰하거나 차단하는 것이다.
이는 네트워크에서 특정 요청을 처리하는 방식에 관한 문제다.
세 번째는 애플리케이션이 실제로 어떤 압축 데이터를 처리하는지 확인하는 것이다.
예를 들어 애플리케이션이 외부 API에서 가져온 Brotli 데이터를 직접 압축 해제한다면 해당 응답은 일반적인 인바운드 WAF 요청 검사와 다른 경로로 들어올 수 있다.
따라서 WAF 보호만으로 모든 공격 경로를 차단할 수는 없다.
이번 업데이트는 Node.js의 기존 보안 취약점에 대응하기 위해 넓은 범위의 요청을 먼저 관찰하고, 운영자가 실제 트래픽을 확인한 뒤 차단 여부를 결정할 수 있도록 만든 WAF 규칙 추가 공지다.
특히 새로운 Managed Rule의 기본 동작이 Log이더라도 기존 Ruleset Override 때문에 실제 운영 환경에서는 Block으로 적용될 수 있다는 점이 중요하다.
이번 조사 범위에서 이외에 포함할 만한 주요 신규 개발 도구 및 보안 발표는 확인하지 못했습니다.
GitHub Changelog의 최근 주요 업데이트는 10월 23일에 게시된 발표들이다. 이번 조사 범위에 해당하는 중요한 신규 기능은 확인하지 못했다.
Vite, webpack, Rollup, esbuild, Babel, Biome, ESLint, Prettier, npm, pnpm, Yarn의 공식 릴리스 및 변경 기록에서도 이번 기간에 해당하는 중요한 정식 업데이트는 확인하지 못했다.
다만 앞선 브리핑에서 소개한 PHP와 Node.js의 보안 취약점은 여전히 운영 환경에 영향을 줄 수 있다. 이번 브리핑에서는 새로운 발표가 아닌 기존 취약점 설명을 반복하지 않았다.
📚 추천 글
Vueの好きで、街を作ろう。 - Vue Fes Japan 2026のブース向け企画
- 원제: Vueの好きで、街を作ろう。 - Vue Fes Japan 2026のブース向け企画
- 저자: HarukiExplorer / Finatext Tech Blog
- 게시일: 2026-10-24
- 원문: https://zenn.dev/finatext/articles/vue-fes-japan-2026-town
Vue Fes Japan 2026 행사에서 참가자들이 자신의 스마트폰으로 투표하면 행사장 모니터에 표시된 도시가 실시간으로 성장하는 인터랙티브 웹 애플리케이션을 구현한 과정을 설명한 글이다.
이번 글의 특징은 하나의 화면만 구현한 프론트엔드 예제가 아니라는 점이다.
실제로 행사에 참여하는 여러 사용자의 스마트폰, 행사장의 대형 모니터, 관리자의 운영 화면을 연결한다.
또한 실시간 투표 결과를 반영하면서 애니메이션을 유지하고, 카메라로 여러 참가자의 카드를 인식하는 기능까지 구현했다.
그 과정에서 Vue의 반응성 시스템과 SVG, Web Animations API, WebSocket, Web Worker를 각각 어떤 목적으로 사용했는지 구체적으로 설명한다.
특히 서버에서 관리하는 실제 상태와 브라우저에서 표현하는 애니메이션 상태를 분리한 구조가 흥미롭다.
서비스의 기본적인 사용자 경험
행사 참가자는 스마트폰으로 투표 페이지에 접속한다.
그리고 Vue에서 가장 좋아하는 요소를 선택한다.
예를 들어 다음과 같은 항목을 선택할 수 있다.
- Vue의 반응성
- 컴포넌트 기반 개발 방식
- Single-File Component
- 개발 경험
사용자가 투표하면 행사장 모니터의 도시에 새로운 주민이 등장한다.
투표가 많이 쌓이면 도시의 건물도 변화한다.
처음에는 작은 주택가였던 지역이 상점가로 발전하고, 이후 더 큰 도시처럼 표현된다.
주민들은 도시 안을 걸어 다닌다.
일부 주민은 기차와 버스, 헬리콥터, 비행기, 로켓 등을 타고 도착한다.
이러한 애니메이션은 단순한 장식 이상의 역할을 한다.
참가자의 행동이 행사장 전체의 화면에 즉시 반영된다는 사실을 시각적으로 보여주는 피드백이다.
사용한 기술 구성
프로젝트는 세 가지 UI로 구성된다.
첫 번째는 참가자의 스마트폰 화면이다.
두 번째는 행사장 모니터에 표시되는 도시 화면이다.
세 번째는 운영자가 사용하는 관리 화면이다.
세 UI는 모두 Nuxt로 구현했다.
전체 기술 구성은 다음과 같다.
| 영역 | 사용 기술 |
|---|---|
| 웹 애플리케이션 | Nuxt 4, Vue 3, VueUse |
| 도시와 주민 렌더링 | SVG |
| 주민 이동 애니메이션 | Web Animations API |
| 불꽃놀이 | Canvas 2D, Web Audio API |
| 카메라 마커 인식 | js-aruco2, Web Worker |
| 일반 HTTP API | AWS API Gateway, Lambda |
| 실시간 업데이트 | API Gateway WebSocket API, Lambda |
| 데이터 저장 | DynamoDB |
| 인증 | Amazon Cognito |
| 정적 파일 전달 | CloudFront, S3 |
| 인프라 및 보안 | Terraform, AWS WAF |
이러한 구성에서는 프론트엔드와 백엔드가 서로 다른 책임을 가진다.
서버는 실제 투표 수와 카드 발급 기록을 관리한다.
브라우저는 해당 상태를 바탕으로 도시의 모습을 렌더링한다.
따라서 도시의 시각적 표현과 실제 투표 데이터의 저장을 분리했다.
SVG와 Vue 컴포넌트로 도시 구성
도시는 SVG로 그렸다.
SVG는 벡터 그래픽을 표현하는 웹 기술이다.
Canvas와 달리 각 그래픽 요소를 DOM과 유사한 구조로 관리할 수 있다.
따라서 Vue 컴포넌트와 연결하기 쉽다.
저자는 도시를 다음과 같은 단위로 분리했다.
- 건물
- 가로등
- 주민
- 배경
- 계절별 장식
각 요소는 자신의 상태에 따라 SVG 속성과 스타일을 변경한다.
예를 들어 투표 수가 증가하면 건물의 종류가 달라질 수 있다.
시간대가 변경되면 창문과 가로등의 밝기가 달라진다.
계절이 바뀌면 눈이 쌓인 지붕이나 배경색이 달라질 수 있다.
이러한 구조에서는 전체 SVG 문자열을 매번 새로 생성할 필요가 없다.
Vue가 변경된 상태에 따라 필요한 컴포넌트를 업데이트할 수 있다.
v-for와 안정적인 Key
실시간으로 투표 수가 증가하면 도시의 주민 수도 계속 늘어난다.
하지만 새로운 주민이 등장할 때마다 기존 주민의 이동 애니메이션까지 처음부터 다시 실행된다면 화면이 부자연스럽게 보일 수 있다.
예를 들어 주민 A가 도시의 왼쪽에서 오른쪽으로 이동하고 있다고 가정해 보자.
이때 새로운 투표가 발생해 주민 B가 등장한다.
기존 주민 A의 컴포넌트가 새로 생성된다면 주민 A는 이동 중이던 위치를 잃어버릴 수 있다.
저자는 이를 방지하기 위해 주민마다 안정적인 ID를 부여했다.
그리고 v-for의 key에 해당 ID를 사용했다.
또한 주민의 이동 경로도 ID별로 캐싱했다.
이렇게 하면 투표 집계가 변경되더라도 기존 주민의 컴포넌트와 이동 경로를 재사용할 수 있다.
이는 Vue의 리스트 렌더링에서 Key가 단순히 경고를 없애기 위한 속성이 아니라는 점을 보여준다.
Key는 상태를 가진 컴포넌트의 정체성을 유지하는 데 영향을 준다.
특히 애니메이션이나 내부 상태가 존재하는 리스트에서 중요하다.
도시의 시간과 계절 변화
도시는 시간에 따라 모습이 변화한다.
저자는 세 가지 시간대를 구현했다.
- 낮
- 저녁
- 밤
각 시간대는 5분 간격으로 변경된다.
세 시간대를 모두 지나면 도시의 하루가 완료된다.
즉 하나의 하루는 15분이다.
계절은 하루가 지날 때마다 다음 계절로 이동한다.
따라서 60분이 지나면 봄, 여름, 가을, 겨울을 모두 경험할 수 있다.
이러한 시간 변화는 실제 투표 데이터와 별도의 상태로 관리한다.
저자는 시작 시각을 기준으로 경과 시간을 계산한다.
해당 계산은 1초마다 수행된다.
그리고 다음 정보를 shallowRef에 저장한다.
- 현재 계절
- 현재 시간대
- 눈이 쌓이는 정도
shallowRef는 Vue의 반응형 참조 기능 가운데 하나다.
일반적인 ref와 달리 내부 객체의 모든 중첩 속성을 깊게 반응형으로 처리하지 않는다.
따라서 하나의 상태 객체를 교체하는 방식으로 업데이트할 때 활용할 수 있다.
저자는 해당 상태를 바탕으로 computed에서 CSS Custom Property를 계산한다.
그리고 도시의 Root Element에 :style로 전달한다.
예를 들어 다음 시각 효과가 CSS 변수에 따라 변경된다.
- 하늘 색상
- 건물 색상
- 창문의 밝기
- 가로등의 밝기
- 눈의 표시량
이렇게 하면 각 건물 컴포넌트가 시간 계산을 개별적으로 수행할 필요가 없다.
시간과 계절을 나타내는 공통 상태를 계산하고, 실제 시각적 표현은 CSS를 통해 변경하도록 한 것이다.
WebSocket으로 실시간 투표 결과 전달
사용자가 투표하면 행사장 모니터가 새로운 결과를 반영해야 한다.
일반적인 방법 가운데 하나는 WebSocket을 사용하는 것이다.
WebSocket은 하나의 연결을 유지하면서 클라이언트와 서버가 양방향으로 메시지를 교환하도록 하는 기술이다.
하지만 단순히 투표가 발생할 때마다 다음과 같은 메시지를 보내는 방식에는 문제가 존재한다.
vote_added: 1
행사장 모니터가 이 메시지를 받을 때마다 화면의 투표 수를 1씩 증가시킨다고 가정해 보자.
문제는 네트워크 연결이 불안정한 상황에서 발생한다.
예를 들어 메시지 하나가 누락되면 화면의 투표 수가 실제 값보다 작아진다.
반대로 같은 메시지를 두 번 처리하면 실제 값보다 커질 수 있다.
따라서 브라우저가 메시지의 수신 횟수만으로 실제 투표 수를 관리하면 서버와 다른 상태가 될 가능성이 있다.
저자는 이를 해결하기 위해 WebSocket 메시지를 실제 상태 변경 명령이 아니라 상태 갱신을 알리는 신호로 사용했다.
Revision 기반 상태 동기화
서버가 전송하는 WebSocket 메시지에는 다음 정보가 포함된다.
eventKeyroomIdrevision
revision은 실제 저장된 상태의 버전을 나타낸다.
예를 들어 다음과 같은 메시지다.
{"eventKey":"vue-fes-2026","roomId":"product-town","revision":123}
행사장 모니터는 WebSocket 메시지를 받으면 투표 수를 직접 증가시키지 않는다.
대신 HTTP API를 호출해 최신 집계 결과를 다시 가져온다.
이렇게 하면 WebSocket 메시지가 중복으로 전달되더라도 실제 집계는 서버에 저장된 값을 기준으로 유지된다.
또한 이전 응답이 나중에 도착하는 상황도 고려했다.
예를 들어 다음과 같은 순서를 생각해 보자.
- Revision 123에 대한 데이터를 요청한다.
- 새로운 투표가 발생해 Revision 124가 된다.
- Revision 124에 대한 데이터를 요청한다.
- Revision 124의 응답이 먼저 도착한다.
- Revision 123의 응답이 나중에 도착한다.
아무런 검증 없이 마지막에 도착한 응답을 적용하면 화면이 이전 상태로 돌아갈 수 있다.
저자는 이를 방지하기 위해 이미 적용한 Revision보다 오래된 응답을 무시하도록 했다.
따라서 실제 응답이 도착한 순서와 관계없이 최신 상태를 유지할 수 있다.
이러한 설계는 실시간 공동 작업 애플리케이션에서도 활용할 수 있다.
예를 들어 여러 사용자가 같은 문서의 변경 알림을 받더라도 실제 문서의 현재 상태는 서버에서 다시 조회하도록 구성할 수 있다.
다만 모든 실시간 서비스에 HTTP 재조회 방식이 적합한 것은 아니다.
매우 높은 빈도로 상태가 변경된다면 WebSocket 이벤트마다 전체 상태를 가져오는 방식의 비용이 커질 수 있다.
이번 프로젝트는 행사장에서 발생하는 투표 이벤트를 처리하는 상황에 맞춰 설계됐다.
불안정한 행사장 Wi-Fi 대응
행사장에는 여러 사용자의 스마트폰이 동시에 연결된다.
따라서 네트워크 연결이 불안정해질 가능성이 있다.
저자는 WebSocket이 끊어진 상황에서 재연결하는 방법도 구체적으로 구현했다.
재연결 정책은 다음과 같다.
| 상황 | 처리 방식 |
|---|---|
| WebSocket 연결 종료 | 2~4초 뒤 재시도 |
| Heartbeat 응답 없음 | 2~4초 뒤 재시도 |
| 10초 안에 WebSocket이 열리지 않음 | 연결을 닫고 다시 시도 |
| 연결 Ticket 발급 실패 | 8초 뒤 재시도 |
여기서 중요한 부분은 재시도 간격을 완전히 동일하게 고정하지 않았다는 점이다.
예를 들어 행사장 Wi-Fi가 잠깐 끊겼다고 가정해 보자.
여러 기기의 연결이 동시에 종료된다.
이때 모든 클라이언트가 정확히 2초 뒤 다시 연결하면 서버에 연결 요청이 한꺼번에 집중될 수 있다.
저자는 이러한 현상을 줄이기 위해 2~4초 범위 안에서 재시도 시점을 분산했다.
이는 Jitter를 적용한 재시도 방식이다.
Jitter는 재시도 간격에 작은 무작위성을 추가해 여러 클라이언트의 요청이 동시에 집중되는 문제를 줄이는 방법이다.
또한 다음 브라우저 이벤트를 활용했다.
onlinevisibilitychange
online은 브라우저가 네트워크 연결 상태의 변화를 감지했을 때 발생할 수 있다.
visibilitychange는 사용자가 다른 탭으로 이동하거나 다시 돌아오는 상황을 처리하는 데 사용할 수 있다.
저자는 이러한 이벤트를 최신 상태를 다시 가져오는 계기로 사용했다.
따라서 WebSocket 연결이 끊어진 동안 일부 투표 알림이 누락됐더라도 네트워크가 복구되면 서버의 현재 집계 결과를 가져올 수 있다.
Vue Single-File Component를 모티브로 한 카드
이번 프로젝트에는 행사 참가자 간의 협력을 유도하는 기능도 있다.
참가자는 투표를 완료한 뒤 카드를 하나 받는다.
카드의 종류는 다음과 같다.
<template><script><style>
이는 Vue의 Single-File Component 구조를 모티브로 만든 것이다.
Vue의 Single-File Component는 하나의 파일 안에서 컴포넌트의 Template과 Script, Style을 구성할 수 있는 방식이다.
다만 실제 Vue 컴포넌트에서 세 영역이 모두 필수인 것은 아니다.
이번 행사는 세 가지 카드를 모두 모으면 하나의 컴포넌트가 완성된다는 규칙을 만들었다.
서로 다른 카드를 가진 참가자 세 명이 함께 행사장 카메라 앞에 카드를 보여주면 도시에서 불꽃놀이가 발생한다.
따라서 실제 사용자들은 같은 종류의 카드를 가진 사람끼리만 모이는 것이 아니라 서로 다른 카드를 가진 참가자를 찾아야 한다.
이러한 구조는 기술적 구현과 행사 참여 경험을 연결한다.
카메라 영상의 Web Worker 처리
카드에는 AR Marker가 표시된다.
행사장 카메라는 해당 Marker를 읽어 어떤 카드인지 확인한다.
하지만 카메라 영상 분석은 CPU를 많이 사용할 수 있다.
특히 이미지의 각 픽셀을 분석하거나 Marker의 기울기를 보정하는 작업은 메인 스레드에 부담을 줄 수 있다.
브라우저의 메인 스레드에서 이러한 작업을 계속 수행하면 SVG 애니메이션이나 UI 입력이 지연될 수 있다.
저자는 이를 방지하기 위해 Web Worker에서 카메라 영상을 분석하도록 했다.
Web Worker는 웹 애플리케이션의 메인 스레드와 분리된 실행 환경이다.
복잡한 계산 작업을 메인 스레드에서 분리하는 데 사용할 수 있다.
저자는 js-aruco2를 사용해 Marker의 사각형 영역을 찾고 기울기를 보정했다.
이후 자체적으로 정의한 Marker 형식에 맞춰 비트 데이터를 분석했다.
64비트 Marker 구조
이번 프로젝트의 Marker는 8×8 비트로 구성된다.
전체 64비트는 다음과 같이 나뉜다.
| 데이터 | 비트 수 |
|---|---|
| 발급 세대 번호 | 12 |
| 세대 안의 카드 일련번호 | 27 |
| BCH 오류 검증 및 정정 정보 | 24 |
| 전체 검증 비트 | 1 |
| 합계 | 64 |
Marker에는 단순히 카드 종류만 포함되지 않는다.
카드의 발급 세대와 일련번호도 포함된다.
이러한 구조를 이용하면 동일한 이미지가 반복적으로 사용되더라도 실제로 유효한 카드인지 서버에서 확인할 수 있다.
저자는 BCH(63,39,9) 코드를 사용했다.
그리고 Marker를 읽는 과정에서 최대 2비트의 오류 정정을 허용했다.
카메라로 화면을 촬영하는 환경에서는 빛 반사나 흔들림 때문에 일부 비트가 정확하게 읽히지 않을 수 있다.
따라서 작은 인식 오류를 보정할 수 있도록 구성한 것이다.
Transferable로 이미지 데이터 전달
카메라에서 가져온 RGBA 데이터는 Web Worker로 전달된다.
RGBA는 이미지 픽셀의 Red, Green, Blue, Alpha 값을 표현하는 방식이다.
일반적인 객체를 Worker로 전달하면 데이터가 복사될 수 있다.
하지만 이미지 데이터는 크기가 크므로 매번 전체 데이터를 복사하는 것은 불필요한 비용을 만들 수 있다.
저자는 Transferable을 이용해 이미지 데이터의 소유권을 Worker에 전달했다.
이렇게 하면 불필요한 복사 비용을 줄일 수 있다.
영상 분석은 약 100ms 간격으로 시도한다.
하지만 이전 Frame의 처리가 완료되지 않았다면 새로운 Frame을 보내지 않는다.
이러한 방식은 분석 작업이 밀려 Queue에 오래된 영상이 쌓이는 문제를 방지한다.
예를 들어 Marker 하나를 인식하는 데 200ms가 걸리는데 매 100ms마다 새로운 Frame을 계속 전달하면 처리하지 못한 Frame이 누적될 수 있다.
저자는 이전 작업이 완료됐을 때만 새로운 Frame을 전달하도록 해 이러한 문제를 줄였다.
Marker 인식의 안정성
참가자가 스마트폰을 손으로 들고 있기 때문에 카메라 영상이 흔들릴 수 있다.
Marker가 한순간 인식되지 않았다고 해서 즉시 실패 처리하면 사용자는 카드를 완전히 고정해야 한다.
하지만 실제 행사장에서 세 사람이 동시에 스마트폰을 정확하게 고정하기는 어렵다.
저자는 이를 해결하기 위해 인식한 카드에 300ms의 유예 시간을 부여했다.
예를 들어 Marker가 한순간 사라지더라도 300ms 동안은 인식 중인 상태로 유지한다.
이후 서로 다른 세 종류의 카드가 3초 동안 함께 인식된 상태라면 성공으로 판단한다.
이러한 방식은 카메라 인식 결과의 일시적인 불안정성을 사용자 경험에 그대로 반영하지 않도록 만든 것이다.
중요한 점은 센서나 영상 분석 결과가 잠깐 달라졌다는 이유로 UI 상태까지 즉시 변경할 필요는 없다는 사실이다.
실제로 카메라를 사용하는 웹 애플리케이션에서는 이와 같은 시간 기반 안정화 처리가 유용할 수 있다.
서버에서 카드 검증
브라우저에서 Marker를 인식했다고 해서 자동으로 성공 처리하지는 않는다.
브라우저는 Marker에서 읽은 번호만 서버로 전달한다.
서버는 다음 사항을 확인한다.
- 실제 발급한 카드인지
- 현재 발급 세대에 속하는지
- 어떤 종류의 카드인지
- 어떤 브라우저에 발급했는지
- 이미 사용한 카드인지
이러한 구조에서는 카메라가 카드 번호를 읽는 작업과 해당 카드가 실제로 유효한지 판단하는 작업을 분리한다.
특히 브라우저가 전달한 카드 번호를 그대로 신뢰하지 않는다는 점이 중요하다.
브라우저에서 실행되는 JavaScript는 사용자가 직접 수정하거나 임의의 HTTP 요청을 생성할 수 있기 때문이다.
따라서 실제 보상이나 상태 변경을 수행하기 전에 서버에서 데이터를 검증해야 한다.
구현의 한계와 트레이드오프
이번 글은 행사에 사용한 실제 구현을 설명하지만 대규모 운영 환경에서의 독립적인 성능 Benchmark를 제공하지는 않는다.
예를 들어 다음 수치는 공개되지 않았다.
- 최대 동시 접속자 수
- WebSocket 메시지 처리량
- 카메라 Marker 인식의 평균 정확도
- Web Worker 사용 전후의 메인 스레드 점유율
- AWS 인프라 운영 비용
따라서 이번 구조가 수십만 명의 동시 사용자를 처리할 수 있다고 단정할 근거는 없다.
또한 WebSocket 알림을 받을 때마다 HTTP API에서 최신 상태를 가져오는 방식은 투표 빈도가 극도로 높은 서비스에서는 추가적인 서버 부하를 만들 수 있다.
하지만 행사장 모니터가 실제 서버 상태와 일치해야 하는 상황에서는 메시지 누락과 중복에 강한 설계가 된다.
이번 글은 Vue 컴포넌트의 재사용, 브라우저 애니메이션, 실시간 상태 동기화, Web Worker를 이용한 영상 처리, 서버 측 검증을 하나의 사용자 경험 안에서 연결한 구체적인 개발 사례다.
Claude Code にテストを渡しても差はゼロだった。作者の「最重要のコツ」を 72 回試してわかった効く条件
- 원제: Claude Code にテストを渡しても差はゼロだった。作者の「最重要のコツ」を 72 回試してわかった効く条件
- 저자: かくしごと
- 매체: Zenn
- 게시일: 2026-10-24
- 원문: https://zenn.dev/kakushigoto/articles/claude-code-verification-loop
AI 코딩 에이전트에게 테스트 코드를 제공하면 실제 구현의 정확도가 얼마나 개선되는지 총 72회의 실험으로 검증한 글이다.
이번 글의 특징은 일반적인 AI 코딩 경험담과 다르다는 점이다.
저자는 테스트를 제공하면 결과가 좋아질 것이라는 가설을 먼저 세웠다.
그리고 테스트가 없는 조건과 테스트가 있는 조건을 비교했다.
하지만 예상과 다른 결과가 나타났다.
문제의 요구사항이 충분히 명확한 경우에는 테스트를 추가로 제공하더라도 숨겨진 테스트의 통과율이 달라지지 않았다.
반면 요구사항의 일부가 테스트에만 포함된 경우에는 테스트 제공 여부에 따라 큰 차이가 발생했다.
저자는 이러한 결과를 바탕으로 테스트가 반드시 자동으로 코드 품질을 개선하는 것은 아니며, 어떤 정보를 모델에 전달하는지가 중요하다고 설명한다.
실험의 배경
Claude Code의 개발자인 Boris Cherny는 AI 코딩 에이전트의 결과를 개선하는 방법으로 작업 결과를 검증할 수 있는 수단을 제공하는 것을 강조한 바 있다.
예를 들어 AI가 코드를 작성한 뒤 실제 테스트를 실행하도록 하는 것이다.
웹 애플리케이션이라면 브라우저를 열어 실제 화면을 확인할 수 있다.
백엔드라면 API 테스트를 실행할 수 있다.
CLI라면 명령을 실행해 결과를 확인할 수 있다.
이러한 과정은 Feedback Loop를 형성한다.
Feedback Loop는 작업을 수행한 뒤 실제 결과를 확인하고 필요한 경우 다시 수정하는 반복적인 구조다.
하지만 저자는 이러한 방법이 실제로 어느 정도 효과가 있는지 확인하고 싶었다.
특히 작은 프로그래밍 문제에서도 테스트를 제공하는 것이 유의미한 차이를 만드는지 실험했다.
실험의 두 조건
저자는 두 가지 조건을 비교했다.
첫 번째는 테스트를 제공하지 않는 조건이다.
이를 A 조건이라고 했다.
A 조건에서는 Claude Code에 문제 설명만 제공한다.
두 번째는 테스트를 제공하는 조건이다.
이를 B 조건이라고 했다.
B 조건에서는 문제 설명과 함께 다음 파일을 제공한다.
test_public.py
또한 Claude Code에 해당 테스트를 실행하도록 요청했다.
대표적인 실행 명령은 다음과 같다.
python test_public.py
두 조건에서 사용할 수 있는 도구와 권한은 동일하게 설정했다.
즉 테스트를 제공한 조건에서만 더 많은 실행 권한을 부여한 것은 아니다.
차이는 테스트 파일과 테스트 실행 지시가 존재하는지 여부였다.
사용한 모델과 실험 규모
실험에 사용한 환경은 다음과 같다.
| 항목 | 설정 |
|---|---|
| AI 코딩 도구 | Claude Code 2.1.309 |
| 모델 | Claude Sonnet 5.5 |
| 프로그래밍 언어 | Python |
| 테스트 대상 문제 | 12개 |
| 조건 | 테스트 없음 / 테스트 있음 |
| 문제별 반복 | 조건마다 3회 |
| 전체 실행 | 72회 |
12개 문제에 대해 두 조건을 적용했다.
그리고 각 조건을 세 번씩 실행했다.
따라서 전체 실행 횟수는 다음과 같다.
12개 문제 × 2개 조건 × 3회 = 72회
실제 결과의 정확성은 Claude Code에 보여주지 않은 별도의 숨겨진 테스트로 평가했다.
각 문제에는 15~38개의 숨겨진 테스트가 존재했다.
이는 에이전트가 제공된 공개 테스트만 통과하도록 코드를 작성하는 상황과 실제 요구사항을 충족하는 상황을 구분하기 위한 것이다.
첫 번째 실험: 요구사항이 충분히 명확한 문제
처음에는 요구사항이 모두 문장으로 설명된 문제를 사용했다.
예를 들어 다음과 같은 문제였다.
- 겹치는 구간 병합
- 날짜에 개월 수 더하기
- Semantic Version 비교
- CSV 한 줄 파싱
- 단어 줄바꿈
- 바이트 수 표시
이러한 문제들은 경계 조건에서 오류가 발생하기 쉬운 작업이다.
하지만 저자는 입력과 출력, 처리 규칙을 명확하게 설명했다.
실험 결과는 다음과 같았다.
| 조건 | 숨겨진 테스트 실패 |
|---|---|
| A: 테스트 없음 | 0 / 327 |
| B: 테스트 있음 | 0 / 327 |
두 조건 모두 모든 숨겨진 테스트를 통과했다.
여기서 흥미로운 점은 A 조건의 실행 방식이다.
A 조건에서는 18회 가운데 17회가 실제 Python 코드를 한 번도 실행하지 않은 상태에서 작업을 완료했다.
그럼에도 숨겨진 테스트를 모두 통과했다.
반면 B 조건에서는 테스트 실행이 정상적으로 이루어졌다.
하지만 최종 정확도에는 차이가 없었다.
이 결과는 작은 문제에서 요구사항이 충분히 명확하다면 모델이 코드를 실행하기 전부터 올바른 구현을 생성할 수 있다는 가능성을 보여준다.
그러나 이것이 모든 소프트웨어 개발에서 테스트가 불필요하다는 뜻은 아니다.
이번 실험의 문제가 상대적으로 작고 요구사항이 명확했기 때문이다.
두 번째 실험: 기존 코드의 버그 수정
저자는 조금 더 복잡한 문제도 추가했다.
예를 들어 다음과 같은 작업이다.
- Cron 표현식에서 다음 실행 시각 계산
- 수식 평가
- Unified Diff 적용
- 청구서 계산 버그 수정
- 영업일 계산 버그 수정
일부 문제는 여러 파일로 구성된 기존 코드를 수정해야 했다.
다만 요구사항은 문제 설명이나 Repository 안의 문서 및 Docstring에 존재했다.
결과는 다음과 같았다.
| 조건 | 숨겨진 테스트 실패 |
|---|---|
| A: 테스트 없음 | 0 / 363 |
| B: 테스트 있음 | 0 / 363 |
이번에도 두 조건 사이의 차이가 없었다.
처음 여섯 문제와 다음 다섯 문제를 합치면 요구사항이 명확한 총 11개 문제에서 다음 결과를 얻었다.
A 조건과 B 조건 모두 690개의 숨겨진 테스트에서 실패가 0건이었다.
따라서 이번 실험에서는 테스트 제공이 최종 구현의 정확도를 높였다는 증거가 나타나지 않았다.
하지만 그 이유는 테스트가 아무런 가치가 없기 때문이라고 단정할 수 없다.
이미 테스트를 제공하지 않은 조건에서도 모든 문제를 통과했기 때문이다.
이를 Ceiling Effect라고 한다.
Ceiling Effect는 측정 대상이 이미 최고 수준에 도달해 추가적인 개선 효과를 구분할 수 없는 현상이다.
예를 들어 두 학습 방법을 비교하는 시험에서 모든 학생이 100점을 받았다면 어느 학습 방법이 더 우수한지 판단하기 어렵다.
이번 실험도 비슷한 상황이었다.
세 번째 실험: 요구사항이 테스트에만 존재하는 경우
저자는 마지막으로 조건을 변경했다.
이번 문제는 문자열을 URL Slug로 변환하는 작업이었다.
예를 들어 게시글 제목을 URL에서 사용할 수 있는 문자열로 변경하는 것이다.
하지만 일부 처리 규칙은 문제 설명에 포함하지 않았다.
대신 공개 테스트에만 존재하도록 했다.
대표적으로 다음과 같은 규칙이었다.
&를and로 변경- Apostrophe 처리
- 일본어 등 일부 문자 제거
- 최대 길이 제한
- 결과가 비어 있으면
untitled반환
A 조건의 Claude Code는 이러한 규칙을 알 수 없었다.
반면 B 조건은 공개 테스트를 읽어 요구사항을 추론할 수 있었다.
결과는 다음과 같았다.
| 조건 | 숨겨진 테스트 실패 | 실패율 |
|---|---|---|
| A: 테스트 없음 | 38 / 51 | 74.5% |
| B: 테스트 있음 | 4 / 51 | 7.8% |
이번에는 큰 차이가 나타났다.
테스트를 제공한 조건의 실패율이 크게 낮아졌다.
하지만 저자는 이 결과를 단순히 테스트 실행의 효과라고 해석하지 않았다.
이유는 테스트 파일이 실제로 누락된 요구사항을 전달하는 역할을 했기 때문이다.
A 조건의 모델은 해당 규칙을 전달받지 못했다.
따라서 숨겨진 테스트에서 실패한 것이 자연스러웠다.
반면 B 조건에서는 공개 테스트가 사실상 추가적인 요구사항 문서 역할을 했다.
즉 이번 결과는 테스트를 실행하는 행위의 효과와 테스트가 새로운 정보를 제공하는 효과를 완전히 구분하지 못한다.
저자는 이러한 한계를 명시했다.
테스트에도 모호한 요구사항이 존재할 수 있다
B 조건에서도 네 개의 숨겨진 테스트가 실패했다.
모두 문자열의 최대 길이를 처리하는 문제였다.
원인은 공개 테스트의 예제가 모호했기 때문이다.
예를 들어 긴 제목을 Slug로 변환할 때 단순히 39글자로 잘라도 공개 테스트를 통과할 수 있었다.
하지만 실제 의도는 단어의 중간에서 잘리지 않도록 처리하는 것이었다.
공개 테스트만으로는 두 해석을 명확하게 구분하기 어려웠다.
그 결과 모델은 공개 테스트를 통과하지만 실제 숨겨진 요구사항과 다른 코드를 생성했다.
이는 테스트를 제공하더라도 해당 테스트가 요구사항을 충분히 명확하게 표현하지 못하면 잘못된 구현을 만들 수 있다는 점을 보여준다.
또한 테스트 자체가 잘못됐다면 모델이 해당 테스트에 맞춰 잘못된 동작을 구현할 가능성도 존재한다.
예상하지 못한 발견: 테스트 실행 권한 문제
이번 글에서 가장 흥미로운 부분은 실행 권한과 관련된 문제다.
저자는 Claude Code를 비대화형 모드로 실행했다.
비대화형 모드는 도구 사용을 승인할 사람이 실시간으로 응답하지 않는 실행 환경이다.
예를 들어 CI에서 자동으로 에이전트를 실행하는 상황과 비슷하다.
저자는 Python 실행을 허용하기 위해 다음과 같은 권한 패턴을 사용했다.
Bash(python *)
겉으로 보기에는 Python 명령을 실행할 수 있도록 설정한 것이다.
하지만 실제 Claude Code는 다음과 같은 명령을 생성할 수 있었다.
PYTHONPATH=. python -c "..."
이 명령은 Python을 실행하지만 명령의 앞부분에 환경 변수 설정이 존재한다.
따라서 단순한 Bash(python *) 패턴과 일치하지 않을 수 있다.
저자의 실험에서는 실제로 이러한 명령이 거부됐다.
허용된 도구와 실제 실행 가능 여부는 다르다
저자는 테스트가 없는 조건에서 Claude Code가 스스로 코드를 실행하려고 시도한 기록을 분석했다.
그 결과 Python 실행을 시도한 15회 가운데 7회는 권한 설정 때문에 실제 검증을 완료하지 못했다.
그럼에도 Claude Code의 전체 작업은 종료됐다.
그리고 최종 보고서에는 검증할 수 없었다는 내용이 기록됐다.
즉 에이전트가 작업을 완료했다고 해서 실제로 테스트까지 수행했다는 의미는 아니었다.
여기서 중요한 차이는 다음과 같다.
- 에이전트가 테스트를 실행하려고 시도했다.
- 테스트 실행 명령이 권한 검사를 통과했다.
- 테스트가 실제로 실행됐다.
- 테스트가 정상적으로 통과했다.
이 네 가지는 서로 다른 상태다.
자동화 시스템에서는 이러한 상태를 구분해야 한다.
단순히 에이전트의 프로세스가 정상적으로 종료됐다는 사실만으로 검증이 완료됐다고 판단해서는 안 된다.
permission_denials 분석
Claude Code의 실행 결과에는 권한 거부 기록이 포함될 수 있다.
대표적인 필드는 다음과 같다.
permission_denials
저자는 이 값을 이용해 어떤 명령이 거부됐는지 확인했다.
주요 거부 사례는 다음과 같았다.
| 실행 형태 | 거부 사례 |
|---|---|
PYTHONPATH=. python ... | 5회 |
Unicode 공백이 포함된 python -c ... | 2회 |
sed -i와 테스트 실행을 결합한 명령 | 2회 |
| 특정 문자가 포함된 Here-document 파일 작성 | 2회 |
이러한 결과는 허용할 명령을 너무 단순한 패턴으로 정의하면 예상하지 못한 실행 실패가 발생할 수 있다는 점을 보여준다.
하지만 그렇다고 모든 Shell 명령을 무조건 허용하는 것이 적절한 해결 방법은 아니다.
에이전트의 실행 권한을 과도하게 넓히면 다른 보안 문제가 발생할 수 있기 때문이다.
저자는 대신 테스트 실행 명령을 명시적으로 지정하고 실제 거부된 명령을 로그에서 확인하는 방식을 제안한다.
예를 들어 테스트를 제공한 조건에서는 다음 명령을 직접 지정했다.
python test_public.py
이렇게 하면 에이전트가 임의의 실행 방법을 선택하는 경우보다 명령의 형태가 일정해진다.
실험의 한계
이번 실험은 다음 범위에서 수행됐다.
- Claude Sonnet 5.5 한 종류
- Python의 작은 프로그래밍 문제 12개
- 조건별 문제당 3회 반복
- 숨겨진 테스트를 통한 기능적 정확성 측정
따라서 결과를 대규모 React 프로젝트나 복잡한 UI 개발에 그대로 적용할 수는 없다.
또한 다음 항목은 평가하지 않았다.
- 코드의 유지보수성
- 아키텍처 품질
- 성능
- 접근성
- 실제 브라우저의 사용자 경험
- 장시간 작업에서의 오류 복구
특히 실제 브라우저에서 여러 사용자 흐름을 검증하는 작업은 작은 Python 함수의 결과를 검사하는 작업과 다르다.
따라서 이번 실험으로 AI 코딩 에이전트의 테스트 기능이 전체적으로 무의미하다고 결론 내릴 수 없다.
오히려 테스트의 가치가 문제의 크기와 요구사항의 명확성, 실제 실행 가능 여부에 따라 달라질 수 있다는 점을 확인한 실험이다.
이번 글은 AI 코딩 에이전트를 평가할 때 자연어로 작성된 성공 보고서보다 실제 실행 기록과 숨겨진 테스트 결과를 확인해야 한다는 점을 보여준다.
또한 테스트 코드를 제공하는 것과 테스트 실행 권한을 올바르게 구성하는 것이 서로 다른 문제라는 점을 구체적인 실험으로 설명한다.
AIコードレビューの不合格率が51%→13%に。3モデル中2つは判定していなかった
- 원제: AIコードレビューの不合格率が51%→13%に。3モデル中2つは判定していなかった
- 저자: yuki takamura
- 매체: Zenn
- 게시일: 2026-10-24
- 원문: https://zenn.dev/taka8823/articles/2df36d66844c15
개인 개발자가 여러 AI 에이전트로 애플리케이션을 개발하면서 서로 다른 세 모델을 이용한 코드 리뷰 시스템을 운영했지만, 실제로는 두 모델이 검토에 참여하지 않고 있었다는 사실을 로그 분석으로 발견한 사례다.
이번 글은 단순히 AI 코드 리뷰의 효과를 소개하는 내용이 아니다.
오히려 모니터링 지표가 개선되고 있어도 실제 시스템의 품질 검증 기능은 약화되고 있을 수 있다는 점을 보여준다.
저자는 약 4개월 반 동안 자율 AI 에이전트를 이용해 개인용 작업 관리 애플리케이션을 개발했다.
해당 애플리케이션의 기술 구성은 다음과 같다.
- 프론트엔드: Next.js
- 백엔드: Bun, Elysia
- 데스크톱 애플리케이션: Tauri
개발 에이전트는 작업 목록에서 다음 작업을 선택한다.
이후 별도의 Git Worktree에서 코드를 분석하고 수정한다.
수정한 코드는 테스트와 검증을 거친다.
검증에 성공하면 PR을 생성한다.
이후 CI 결과를 확인하고 문제가 없다면 병합하는 구조다.
이 과정에서 코드의 품질을 확인하기 위해 세 개의 AI 리뷰 모델을 사용했다.
세 개의 모델을 사용하는 리뷰 시스템
코드 리뷰에 사용하는 모델은 다음과 같다.
- Claude
- Gemini
- ChatGPT
각 모델은 Git Diff를 전달받는다.
그리고 해당 변경 사항을 승인할 수 있는지 판단한다.
모델은 다음 세 가지 결과 가운데 하나를 반환한다.
passfailunknown
pass는 변경 사항을 승인할 수 있다는 의미다.
fail은 문제가 존재해 수정이 필요하다는 의미다.
unknown은 정상적인 판단을 수행하지 못했다는 의미다.
예를 들어 모델 호출이 실패하거나 응답을 올바르게 해석하지 못했다면 unknown이 반환될 수 있다.
이렇게 하면 세 모델의 판단을 종합해 실제 코드를 병합할지 결정할 수 있다.
이론적으로는 하나의 모델만 사용하는 것보다 여러 모델의 판단을 활용할 수 있다.
그러나 실제 구현에서는 중요한 문제가 존재했다.
월별 코드 리뷰 실패율
저자는 SQLite에 저장된 리뷰 기록을 분석했다.
전체 리뷰 기록은 876건이었다.
월별 결과는 다음과 같았다.
| 월 | 리뷰 수 | Fail 수 | Fail 비율 |
|---|---|---|---|
| 2026년 7월 | 47 | 24 | 51.1% |
| 2026년 8월 | 314 | 87 | 27.7% |
| 2026년 9월 | 306 | 71 | 23.2% |
| 2026년 10월 19일까지 | 209 | 28 | 13.4% |
겉으로 보면 매우 긍정적인 변화다.
7월에는 리뷰의 절반 이상이 실패했다.
하지만 10월에는 실패율이 약 13%로 감소했다.
이 수치만 보면 AI 에이전트가 생성하는 코드의 품질이 점진적으로 개선됐다고 해석할 수 있다.
하지만 저자가 추가로 로그를 분석하자 전혀 다른 문제가 나타났다.
실제로는 하나의 모델만 판단하고 있었다
저자는 각 리뷰에서 몇 개의 모델이 실제로 유효한 판단을 반환했는지 확인했다.
결과는 다음과 같았다.
| 월 | 유효한 판단 1개 | 유효한 판단 2개 | 유효한 판단 3개 |
|---|---|---|---|
| 2026년 7월 | 1 | 18 | 27 |
| 2026년 8월 | 37 | 136 | 135 |
| 2026년 9월 | 207 | 20 | 57 |
| 2026년 10월 | 209 | 0 | 0 |
10월에 기록된 209개의 리뷰는 전부 하나의 모델만 유효한 판단을 반환한 상태였다.
나머지 두 모델은 모두 unknown을 반환했다.
특히 다음 결과가 확인됐다.
| 모델 | 10월 Unknown 수 |
|---|---|
| Claude | 0 / 209 |
| Gemini | 209 / 209 |
| ChatGPT | 209 / 209 |
즉 10월에는 Claude만 실제 판단을 반환했다.
Gemini와 ChatGPT는 유효한 판단을 제공하지 못했다.
하지만 리뷰 시스템은 계속 정상적으로 결과를 반환하고 있었다.
이유는 unknown을 제외한 나머지 모델의 판단만으로 최종 결과를 결정하는 구조였기 때문이다.
따라서 Claude가 pass를 반환하면 전체 리뷰도 통과할 수 있었다.
원래 의도했던 세 모델의 교차 검토는 사실상 이루어지지 않고 있었다.
기존 Aggregation 로직
저자는 실제 코드에 사용한 결과 집계 규칙을 공개했다.
기본적인 규칙은 다음과 같았다.
unknown을 반환한 모델은 집계에서 제외한다.- 유효한 판단이 하나도 없으면 전체 결과를
unknown으로 처리한다. - 유효한 판단 가운데
fail이pass이상이면 실패 처리한다. - 그 외에는 통과 처리한다.
특히 세 번째 규칙이 중요하다.
예를 들어 두 모델이 판단했다고 가정해 보자.
첫 번째 모델은 pass를 반환한다.
두 번째 모델은 fail을 반환한다.
이 경우 두 결과의 수가 같다.
저자의 시스템은 이러한 상황을 실패로 처리했다.
반면 하나의 모델만 판단한다면 결과가 갈릴 가능성이 없다.
그 모델이 pass를 반환하면 그대로 통과한다.
따라서 유효한 모델의 수가 줄어드는 것 자체가 전체 실패율을 낮추는 방향으로 작용할 수 있다.
이는 실제 코드 품질이 개선되지 않았더라도 발생할 수 있는 변화다.
판단에 참여한 모델 수에 따른 실패율
저자는 유효한 판단을 반환한 모델 수에 따라 결과를 다시 분석했다.
| 유효한 모델 수 | 리뷰 수 | Fail 수 | Fail 비율 |
|---|---|---|---|
| 1개 | 454 | 97 | 21.4% |
| 2개 | 174 | 60 | 34.5% |
| 3개 | 219 | 53 | 24.2% |
두 모델이 판단한 경우의 실패율이 가장 높았다.
이는 두 모델의 판단이 서로 다를 때 실패 처리하도록 만든 규칙의 영향을 받는다.
실제로 38건의 리뷰가 동률일 때 실패로 처리하는 규칙 때문에 fail이 됐다.
반면 하나의 모델만 판단하면 의견이 갈릴 가능성이 없다.
따라서 동일한 코드 변경이라도 참여한 모델 수에 따라 최종 결과가 달라질 수 있다.
그러나 이 표만으로 하나의 모델이 판단할 때 코드 품질이 가장 좋았거나 가장 나빴다고 결론 내릴 수는 없다.
서로 다른 시기에 서로 다른 코드 변경을 리뷰했기 때문이다.
동일한 Diff를 모든 모델이 평가한 통제 실험은 아니었다.
저자는 이러한 한계를 명확하게 설명한다.
모델들이 서로 다른 판단을 반환한 비율
두 개 이상의 모델이 유효한 판단을 반환한 리뷰는 393건이었다.
그 가운데 91건에서 pass와 fail이 동시에 등장했다.
비율은 23.2%다.
즉 여러 모델이 실제로 판단에 참여한 리뷰 가운데 약 네 건 중 한 건에서 의견이 달랐다.
이러한 결과는 서로 다른 모델을 사용하는 것이 단순한 중복 검사가 아닐 수 있다는 점을 보여준다.
모델마다 다른 문제를 발견하거나 같은 코드를 다르게 해석할 수 있기 때문이다.
하지만 어떤 모델의 판단이 실제로 더 정확했는지는 확인되지 않았다.
저자는 여러 모델의 판단이 서로 달랐다는 사실만 확인했다.
실제 정답을 독립적으로 검증한 자료는 충분하지 않았다.
모델 호출 경로가 변경되면서 발생한 문제
저자는 Gemini와 ChatGPT의 결과가 unknown으로 변경된 이유를 조사했다.
처음에는 Gemini CLI가 실행 환경의 PATH에 존재하지 않는 것이 원인이라고 생각했다.
하지만 실제 코드를 분석하자 해당 시스템이 Gemini CLI를 호출하지 않는다는 사실을 발견했다.
원인은 다른 곳에 있었다.
저자는 AI 모델 호출 비용을 줄이기 위해 보조 AI 실행 경로를 Claude CLI 중심으로 통합했다.
그 결과 Gemini와 ChatGPT의 실행 경로가 더 이상 연결되지 않는 구조가 됐다.
Gemini와 ChatGPT는 실제 모델 호출이 시작되기 전에 거부됐다.
하지만 시스템은 이를 unknown으로 기록했다.
따라서 세 모델의 결과 레코드는 계속 생성됐다.
실제 모델이 실행되지 않았더라도 로그에는 세 모델의 항목이 존재할 수 있었다.
이는 결과를 기록했다는 사실과 실제 모델이 실행됐다는 사실이 다르다는 문제를 보여준다.
Circuit Breaker가 문제를 숨긴 과정
기존 시스템에는 일시적인 오류에 대응하기 위한 로직도 존재했다.
예를 들어 특정 모델이 반복적으로 Timeout을 발생시키면 전체 리뷰가 오래 지연될 수 있다.
이를 방지하기 위해 저자는 다음 규칙을 추가했다.
같은 모델이 세 번 연속 Timeout을 발생시키면 30분 동안 해당 모델의 호출을 중단한다.
이러한 구조는 Circuit Breaker와 유사하다.
Circuit Breaker는 외부 시스템이 반복적으로 실패할 때 계속 요청을 보내지 않고 일정 시간 동안 호출을 중단하는 방식이다.
문제는 호출을 중단하는 동안에도 unknown을 반환하도록 구현했다는 점이다.
따라서 다음 네 가지 상황이 모두 같은 결과로 기록될 수 있었다.
| 실제 상황 | 기록된 값 |
|---|---|
| 모델 호출 경로가 존재하지 않아 실행 거부 | unknown |
| Circuit Breaker에 의해 실행 중단 | unknown |
| 모델 호출 Timeout | unknown |
| 응답을 Parsing하지 못함 | unknown |
이렇게 되면 나중에 로그를 분석하더라도 어떤 문제가 발생했는지 구분하기 어렵다.
예를 들어 9월에 Gemini가 230번 unknown을 반환했다고 가정해 보자.
이 가운데 몇 번이 실제 Timeout이었는지, 몇 번이 실행 경로 거부였는지 구분할 수 없다.
저자는 이러한 정보 손실 때문에 원인을 정확하게 추적하기 어려웠다고 설명한다.
unknown의 의미를 세분화해야 한다
저자는 문제를 해결하기 위해 단순한 unknown 외에 실제 실패 이유를 기록하는 방식을 제안한다.
예를 들어 다음과 같은 값을 사용할 수 있다.
refusedrestingtimeoutunparsable
refused는 호출이 거부됐다는 의미다.
resting은 Circuit Breaker에 의해 일시적으로 호출하지 않았다는 의미다.
timeout은 실제 요청이 제한 시간 안에 완료되지 않았다는 의미다.
unparsable은 모델이 반환한 결과를 정상적으로 해석할 수 없었다는 의미다.
이렇게 구분하면 로그에서 각 문제의 발생 빈도를 확인할 수 있다.
예를 들어 특정 모델의 timeout이 증가했다면 해당 제공업체의 응답 지연을 조사할 수 있다.
반면 refused가 증가했다면 API 설정이나 실행 경로 문제를 조사할 수 있다.
단순한 unknown 하나로는 이러한 차이를 확인하기 어렵다.
코드의 실패와 리뷰 시스템의 실패를 분리
저자가 제안한 또 다른 개선은 유효한 판단 수를 확인하는 조건이다.
예를 들어 세 모델 가운데 최소 두 모델이 판단해야 리뷰가 성립한다고 가정해 보자.
하지만 실제로 하나의 모델만 판단했다면 어떻게 해야 할까?
기존 구현에서는 해당 모델의 결과로 통과 여부를 결정했다.
저자는 이를 변경해야 한다고 설명한다.
최소 두 모델이 필요하다면 하나만 판단한 경우 다음 상태를 반환하도록 제안한다.
review_incomplete
이 상태는 fail과 다르다.
fail은 코드에 문제가 있다는 판단이다.
반면 review_incomplete는 코드의 품질을 판단하기 위한 검사 자체가 충분히 수행되지 않았다는 의미다.
따라서 두 상황을 같은 상태로 처리해서는 안 된다.
예를 들어 코드가 정상적이더라도 모델 제공업체의 장애 때문에 검토를 완료하지 못할 수 있다.
이러한 경우 코드가 잘못됐다고 판단하는 것은 적절하지 않다.
반대로 검토가 완료되지 않았는데 정상적인 코드라고 판단해 병합하는 것도 위험할 수 있다.
리뷰 시스템의 상태를 별도로 모니터링
저자는 기존에 리뷰 실패율만 관찰했다.
하지만 앞으로는 리뷰 시스템 자체의 상태도 별도로 측정해야 한다고 설명한다.
대표적인 지표는 다음과 같다.
- 리뷰당 유효한 판단 수
- 모델별
unknown비율 - Timeout 발생 횟수
- Circuit Breaker로 인한 호출 중단 횟수
- 최소 판단 수를 충족하지 못한 리뷰 비율
이러한 지표를 함께 확인하면 실제 코드 리뷰의 실패율이 낮아졌을 때 그 이유를 구분할 수 있다.
예를 들어 실패율이 감소한 동시에 유효한 판단 수가 줄었다면 리뷰 시스템의 기능이 약화됐을 가능성을 의심할 수 있다.
반대로 유효한 판단 수가 유지되고 동일한 검사 조건에서 실패율이 감소했다면 실제 코드 품질의 개선 가능성을 더 신중하게 검토할 수 있다.
실제 오류 상황을 테스트해야 한다
저자는 모델이 정상적으로 응답하는 상황만 테스트해서는 안 된다고 설명한다.
다음과 같은 실패 상황도 의도적으로 발생시켜야 한다.
- 특정 모델 실행 경로가 존재하지 않음
- API 호출 Timeout
- 모델이 올바르지 않은 JSON 반환
- Circuit Breaker 활성화
- 일부 모델만 결과 반환
이러한 상황에서 코드 리뷰 시스템이 어떤 최종 결과를 반환하는지 확인해야 한다.
예를 들어 세 모델 가운데 두 모델을 사용할 수 없도록 설정했는데도 PR이 자동으로 통과한다면 실제 리뷰 정책과 다른 동작이 발생하고 있을 수 있다.
검사 시스템의 정상 동작뿐 아니라 검사 시스템 자체가 실패했을 때의 동작도 검증해야 한다는 것이다.
공개한 수치의 한계
이번 글은 실제 운영 로그를 바탕으로 한다.
하지만 코드 리뷰가 얼마나 정확했는지를 검증하는 자료는 충분하지 않았다.
저자에 따르면 실제로 판단의 옳고 그름을 추적한 기록은 12건뿐이었다.
따라서 전체 876건의 리뷰 가운데 어떤 fail이 실제 버그를 발견한 것인지, 어떤 pass가 잘못된 승인인지 판단할 수 없다.
또한 월별로 코드 변경의 종류와 난이도가 달랐을 수 있다.
따라서 51.1%에서 13.4%로 감소한 실패율을 실제 품질 개선이나 악화의 증거로 사용할 수 없다.
이번 글이 보여주는 것은 다음 사실이다.
AI 에이전트의 품질을 측정하는 지표가 좋아지고 있더라도 해당 지표를 생성하는 시스템 자체가 정상적으로 작동하는지 확인해야 한다.
특히 여러 모델의 판단을 결합하거나 자동으로 PR을 검토하는 시스템에서는 모델의 품질만큼 Aggregation 로직과 실패 처리 정책이 중요하다는 점을 보여준다.
OpenClaw got 591 advisories. 1 in 4 top npm installs is the real hole
- 원제: OpenClaw got 591 advisories. 1 in 4 top npm installs is the real hole
- 저자: Matt Busel
- 매체: Tensorust
- 게시일: 2026-10-24
- 원문: https://tensorust.com/articles/openclaw-vulnerabilities-npm-advisories/
오픈소스 패키지의 보안 권고가 급격히 증가하는 상황에서 취약점의 발표 건수와 실제 사용 중인 취약한 패키지 버전의 비율을 구분해 분석한 데이터 기반 기술 글이다.
저자는 여러 공개 보안 데이터베이스와 npm 다운로드 통계를 직접 수집했다.
그리고 패키지별로 실제 다운로드된 버전이 알려진 취약점의 영향을 받는지 분석했다.
특히 이번 글은 단순히 특정 패키지에 몇 개의 취약점이 존재하는지 세는 방식의 한계를 보여준다.
예를 들어 어떤 프로젝트에 보안 권고가 500건 존재한다고 가정해 보자.
하지만 해당 프로젝트의 최신 버전에서는 모든 알려진 취약점이 수정됐을 수 있다.
반대로 다른 프로젝트에는 보안 권고가 3건만 존재하지만 많은 사용자가 취약한 이전 버전을 계속 설치하고 있을 수도 있다.
이 경우 어떤 프로젝트가 실제로 더 위험한지 보안 권고의 개수만으로 판단하기 어렵다.
저자는 이러한 문제를 실제 패키지 버전별 다운로드 데이터와 취약점 데이터의 결합으로 분석했다.
조사에 사용한 데이터
저자는 크게 세 가지 출처를 사용했다.
첫 번째는 OSV.dev다.
OSV는 오픈소스 소프트웨어의 취약점 정보를 제공하는 데이터베이스다.
저자는 OSV가 제공하는 전체 데이터셋을 다운로드했다.
조사 대상은 다음 여덟 개 패키지 생태계였다.
- npm
- PyPI
- Go
- crates.io
- Packagist
- Maven
- RubyGems
- NuGet
두 번째는 GitHub Advisory Database다.
저자는 OSV에 포함된 여러 정보 가운데 GitHub에서 검토한 GHSA 형식의 보안 권고를 기준으로 집계했다.
GitHub Advisory Database는 오픈소스 패키지에 영향을 주는 보안 취약점 정보를 제공한다.
다만 모든 취약점이 이 데이터베이스에 포함되는 것은 아니다.
검토되지 않은 취약점이나 공개되지 않은 취약점은 집계에서 빠질 수 있다.
세 번째는 npm의 패키지 버전별 다운로드 통계다.
저자는 단순히 패키지 전체의 다운로드 수만 확인하지 않았다.
어떤 버전이 실제로 얼마나 다운로드됐는지 조회했다.
그리고 해당 버전이 OSV에 등록된 취약점의 영향을 받는지 검사했다.
데이터 처리 방식
저자는 약 300줄의 Python 코드를 작성해 데이터를 수집하고 분석했다.
전체 작업에는 약 40분이 걸렸다.
시간의 상당 부분은 npm API의 요청 제한으로 인해 대기하는 데 소비됐다.
원본 데이터에서 다음 항목을 제외했다.
- 철회된 보안 권고
- MAL 형식의 악성 패키지 보고서
그 결과 약 50,537개의 데이터를 처리했다.
이후 생태계별로 GitHub에서 검토한 보안 권고를 집계했다.
또한 NVD(National Vulnerability Database)의 CVE 데이터를 별도로 조회했다.
NVD는 다양한 소프트웨어의 보안 취약점 정보를 제공하는 데이터베이스다.
이렇게 하면 GitHub Advisory Database 한 곳의 집계 변화만 보고 전체 보안 취약점의 증가 추세를 판단하는 문제를 줄일 수 있다.
그러나 NVD와 GHSA는 서로 다른 범위의 정보를 수집한다.
따라서 두 데이터베이스의 건수를 단순히 더해서 전체 고유 취약점 수로 취급할 수는 없다.
2026년 보안 권고의 증가
저자가 공개한 결과는 다음과 같다.
| 항목 | 2025년 | 2026년 10월 23일까지 |
|---|---|---|
| 8개 생태계의 GitHub 검토 보안 권고 | 3,542건 | 11,198건 |
| npm 보안 권고 | 606건 | 3,314건 |
| crates.io 보안 권고 | 177건 | 483건 |
2026년은 아직 종료되지 않았다.
하지만 10월 23일까지 확인된 GitHub 검토 보안 권고 수는 2025년 전체의 약 3.2배였다.
npm만 비교하면 약 5.5배였다.
저자는 이러한 증가의 원인 가운데 하나로 AI 기반 보안 분석 도구의 사용 확대를 제시한다.
하지만 중요한 점은 보안 권고의 발표 건수가 증가했다는 사실만으로 실제 소프트웨어의 취약점이 동일한 비율로 증가했다고 판단할 수 없다는 것이다.
예를 들어 기존에 존재했지만 발견되지 않았던 취약점이 새로운 분석 도구를 통해 발견됐을 수 있다.
또한 보안 연구자가 이전보다 더 많은 프로젝트를 분석하고 있을 수 있다.
보안 권고를 검토하고 공개하는 정책이 변경됐을 가능성도 있다.
따라서 이러한 수치는 실제 위험뿐 아니라 취약점을 발견하고 공개하는 활동의 변화도 반영한다.
AI 관련 패키지의 보안 권고 증가
저자는 패키지 이름을 이용해 AI 관련 도구와 라이브러리를 분류했다.
대표적인 대상은 다음과 같다.
- OpenClaw
- n8n
- LangChain
- LiteLLM
- LlamaIndex
- MCP 서버
- AI 에이전트 프레임워크
이러한 패키지에 영향을 주는 보안 권고가 전체에서 차지하는 비율을 계산했다.
결과는 다음과 같았다.
| 연도 | AI 관련 패키지의 보안 권고 비율 |
|---|---|
| 2024년 | 4.1% |
| 2025년 | 8.8% |
| 2026년 | 15.5% |
즉 저자의 분류 기준에서는 AI 관련 패키지가 전체 보안 권고에서 차지하는 비중이 증가했다.
그러나 이 분석에는 제한사항이 있다.
저자는 패키지의 이름을 기준으로 AI 관련 여부를 분류했다.
따라서 실제로 AI 기능을 포함하고 있지만 패키지 이름에 관련 정보가 없는 경우는 누락될 수 있다.
반대로 이름은 AI 관련 패키지처럼 보이지만 실제 사용 방식은 다른 경우도 존재할 수 있다.
따라서 이번 수치는 저자가 정의한 분류 기준에 따른 추정이다.
모든 AI 관련 소프트웨어를 완전히 분류한 결과는 아니다.
OpenClaw의 591개 보안 권고
이번 조사에서 눈에 띄는 프로젝트는 OpenClaw였다.
OpenClaw는 오픈소스 개인용 AI 에이전트다.
저자의 조사에 따르면 2026년에 공개된 OpenClaw 관련 보안 권고는 591건이었다.
이는 같은 기간 crates.io 전체에 대해 집계한 483건보다 많았다.
하지만 저자는 이 숫자만으로 OpenClaw가 모든 Rust 패키지보다 더 위험하다고 판단하지 않았다.
이유는 보안 권고가 여러 버전에 걸쳐 누적된 기록이기 때문이다.
예를 들어 오래된 OpenClaw 버전에는 수많은 취약점이 존재할 수 있다.
하지만 최신 버전에서는 해당 취약점이 수정됐을 수 있다.
따라서 실제로 어떤 버전을 사용하는지가 중요하다.
저자는 이를 확인하기 위해 OpenClaw의 버전별 다운로드 수를 조사했다.
실제 다운로드 버전의 취약점 비율
저자의 조사 기간 동안 OpenClaw의 npm 다운로드 수는 약 182만 건이었다.
그중 알려진 보안 권고의 영향을 받는 버전을 다운로드한 비율은 약 3.1%였다.
반대로 약 96.9%의 다운로드는 조사 당시 알려진 보안 권고가 없는 버전에서 발생했다.
여기서 주의해야 할 점은 다음과 같다.
알려진 보안 권고가 없다는 사실이 해당 버전에 실제 취약점이 존재하지 않는다는 보장은 아니다.
아직 발견되지 않았거나 공개되지 않은 문제가 존재할 수 있기 때문이다.
따라서 96.9%를 완전히 안전한 설치의 비율로 해석하는 것은 적절하지 않다.
또한 npm 다운로드 수는 고유 사용자 수와 다르다.
하나의 CI 시스템이 같은 패키지를 여러 번 다운로드할 수 있다.
자동화된 빌드 서버와 캐시 시스템도 다운로드 통계에 영향을 줄 수 있다.
따라서 해당 수치는 실제 사용자의 설치 비율이 아니라 npm에서 기록한 다운로드 요청의 버전별 분포다.
가장 많이 다운로드되는 npm 패키지 40개 분석
저자는 OpenClaw뿐 아니라 일반적인 JavaScript 개발에 사용하는 패키지도 조사했다.
특히 2026년에 보안 권고가 공개된 패키지 가운데 다운로드 수가 많은 40개를 선택했다.
대표적인 패키지는 다음과 같다.
- minimatch
- brace-expansion
- picomatch
- ajv
- zod
- postcss
- esbuild
- js-yaml
- uuid
- ws
- axios
- vite
- lodash
- next
이 패키지들은 웹 개발 프로젝트에서 직접 사용하거나 다른 패키지의 의존성으로 설치되는 경우가 많다.
저자는 2026년 10월 16일부터 22일까지의 버전별 다운로드 수를 분석했다.
전체 다운로드 수는 약 55억 8,000만 건이었다.
그중 10월 1일 이전에 이미 수정된 것으로 알려진 취약점을 포함하는 버전의 다운로드 비율을 계산했다.
결과는 다음과 같았다.
| 항목 | 비율 | 다운로드 수 |
|---|---|---|
| 알려진 취약점이 수정된 이전 버전의 다운로드 | 35.2% | 약 19억 7,000만 건 |
| High 또는 Critical 취약점에 해당하는 이전 버전의 다운로드 | 24.7% | 약 13억 8,000만 건 |
이 수치는 많은 설치 요청이 이미 수정된 취약점을 포함하는 이전 버전에 집중돼 있을 수 있다는 점을 보여준다.
하지만 npm 전체 설치 요청의 35.2%가 취약하다는 의미는 아니다.
저자는 2026년에 보안 권고가 존재했던 패키지 가운데 다운로드 수가 많은 40개를 선택했다.
즉 애초에 보안 권고가 존재하는 패키지를 대상으로 구성한 표본이다.
따라서 npm 생태계 전체의 취약한 다운로드 비율로 일반화할 수 없다.
또한 같은 프로젝트에서 여러 패키지를 설치하면 각 패키지 다운로드가 별도로 집계된다.
따라서 다운로드 수를 실제 취약한 애플리케이션의 수와 동일하게 취급해서도 안 된다.
패키지별 차이
저자는 개별 패키지의 취약한 버전 다운로드 비율도 공개했다.
일부 결과는 다음과 같다.
| 패키지 | 주간 다운로드 | 알려진 수정 전 취약 버전 비율 |
|---|---|---|
| minimatch | 약 4억 5,920만 | 21.7% |
| brace-expansion | 약 4억 | 56.4% |
| picomatch | 약 3억 3,890만 | 18.2% |
| ajv | 약 2억 4,930만 | 23.7% |
| zod | 약 2억 2,160만 | 0.2% |
| postcss | 약 1억 9,980만 | 42.7% |
| esbuild | 약 1억 9,700만 | 34.5% |
| js-yaml | 약 1억 8,870만 | 54.9% |
| ws | 약 1억 8,430만 | 30.7% |
| axios | 약 7,020만 | 58.9% |
| vite | 약 1억 2,720만 | 30.7% |
| next | 약 4,460만 | 45.2% |
이러한 수치는 패키지마다 버전 업데이트의 분포가 크게 다르다는 점을 보여준다.
예를 들어 같은 npm 생태계에 속해 있어도 Zod와 Axios의 비율에는 큰 차이가 있다.
하지만 이 차이를 특정 패키지의 개발 품질이나 사용자들의 보안 의식 차이로 바로 해석해서는 안 된다.
패키지마다 다음 조건이 다르기 때문이다.
- 보안 권고가 공개된 버전 범위
- 취약점의 심각도
- 최신 버전으로 업데이트하기 위한 호환성 비용
- 전이적 의존성에서 사용되는 비율
- 실제 패키지 업데이트 주기
- 다운로드를 발생시키는 자동화 시스템의 특성
따라서 수치를 해석할 때는 단순한 비율뿐 아니라 각 패키지의 취약점과 수정 버전, 영향을 받는 기능을 함께 확인해야 한다.
Lockfile의 역할
저자는 이번 분석에서 Lockfile의 중요성을 강조한다.
JavaScript 프로젝트에서는 다음과 같은 Lockfile을 사용할 수 있다.
- package-lock.json
- pnpm-lock.yaml
- yarn.lock
Lockfile은 실제 설치할 패키지의 버전을 기록한다.
예를 들어 package.json에 다음과 같은 의존성 범위가 존재한다고 가정해 보자.
"axios": "^1.0.0"
이 설정은 특정 버전 범위를 허용한다.
하지만 실제 프로젝트에서 설치하는 버전은 Lockfile에 고정돼 있을 수 있다.
따라서 package.json만 확인하면 현재 설치되는 버전을 정확하게 알기 어려울 수 있다.
특히 다른 라이브러리가 간접적으로 사용하는 전이적 의존성도 중요하다.
예를 들어 개발자가 직접 설치한 패키지는 모두 최신 버전일 수 있다.
하지만 내부적으로 사용하는 다른 패키지가 오래된 취약한 라이브러리를 계속 참조하고 있을 수 있다.
이 경우 실제 설치된 의존성 트리 전체를 확인해야 한다.
보안 권고와 실제 악용 가능성의 차이
이번 글에서 저자가 직접 인정한 가장 중요한 한계는 취약한 버전을 설치했다는 사실과 실제 공격이 가능한 상태라는 사실이 동일하지 않다는 점이다.
예를 들어 특정 패키지에 정규식 처리로 인한 ReDoS 취약점이 존재한다고 가정해 보자.
ReDoS는 악의적으로 구성한 입력으로 정규식 처리 시간을 크게 증가시키는 공격이다.
하지만 해당 패키지가 개발 환경에서만 사용되고, 공격자가 영향을 줄 수 있는 입력을 처리하지 않는다면 실제 운영 서비스에서의 악용 가능성은 낮을 수 있다.
반대로 같은 패키지가 외부 사용자의 입력을 직접 처리하는 API에서 사용된다면 위험이 커질 수 있다.
즉 다음 세 가지를 구분해야 한다.
첫 번째는 패키지에 알려진 취약점이 존재하는가다.
두 번째는 실제 설치한 버전이 해당 취약점의 영향을 받는가다.
세 번째는 애플리케이션이 취약한 코드 경로를 공격자가 사용할 수 있는 방식으로 실행하는가다.
저자의 분석은 주로 첫 번째와 두 번째 질문을 다뤘다.
세 번째 질문에 해당하는 실제 도달 가능성(Reachability)은 패키지별로 검증하지 않았다.
따라서 24.7%라는 High/Critical 비율이 곧바로 실제 원격 공격이 가능한 설치의 비율을 의미하는 것은 아니다.
AI로 발견한 취약점에 관한 분석
저자는 보안 권고의 설명에서 AI가 취약점 발견에 참여했다는 표현도 조사했다.
그 결과 2026년에는 최소 80개의 보안 권고에서 AI의 취약점 발견 또는 분석 참여가 명시적으로 언급됐다.
하지만 이 역시 완전한 수치는 아니다.
저자는 보안 권고의 문장에 포함된 표현을 검색했다.
따라서 AI를 사용했지만 해당 사실을 보안 권고에 기록하지 않은 경우는 포함되지 않는다.
반대로 보안 권고에 AI를 언급했다고 해서 실제 취약점 발견 과정에서 AI가 어느 정도 기여했는지 정확하게 평가할 수 있는 것도 아니다.
따라서 이 수치는 AI 보안 분석 도구의 실제 사용량보다 공개적으로 AI의 기여를 명시한 보안 권고의 최소 건수에 가깝다.
조사 결과의 한계
이번 글은 공개 데이터를 직접 수집해 분석했지만 다음과 같은 한계를 가진다.
첫 번째로 보안 권고의 수는 발견과 공개 활동의 영향을 받는다.
두 번째로 npm 다운로드 수는 실제 사용자 수나 운영 중인 서버 수와 다르다.
세 번째로 취약한 패키지 버전을 다운로드했다는 사실이 실제 공격 가능성을 보장하지 않는다.
네 번째로 AI 관련 패키지의 분류는 저자가 정의한 이름 기반 규칙에 의존한다.
다섯 번째로 각 패키지에 대한 보안 권고가 실제로 어떤 코드 경로에 영향을 주는지 모두 검증한 것은 아니다.
따라서 이번 수치는 절대적인 안전성을 판단하는 지표보다 패키지의 취약점 공개 현황과 실제 버전 사용 분포를 함께 분석하기 위한 자료로 이해하는 것이 적절하다.
실제 프로젝트의 의존성 점검과 연결
저자는 JavaScript 프로젝트에서 다음 명령을 사용할 수 있다고 안내한다.
npm audit
npm Audit은 프로젝트의 의존성 정보를 분석하고 알려진 보안 취약점과 비교한다.
필요한 경우 운영 환경에 포함되는 의존성만 검사할 수도 있다.
예를 들어 다음 명령이다.
npm audit --omit=dev
이 명령은 개발 의존성을 제외한 범위를 검사한다.
하지만 개발 의존성에 보안 문제가 존재해도 안전하다는 의미는 아니다.
빌드 도구나 CI 환경에서 해당 패키지를 실행한다면 별도의 공급망 위험이 존재할 수 있기 때문이다.
따라서 실제로 어떤 범위를 검사할지는 프로젝트의 실행 환경을 고려해야 한다.
또한 npm Audit에서 취약점이 발견됐다고 해서 모든 패키지를 무조건 최신 버전으로 변경하는 것이 최선은 아니다.
메이저 버전 업데이트로 인해 애플리케이션의 동작이 변경될 수 있기 때문이다.
수정 가능한 버전과 실제 취약한 코드 경로의 사용 여부, 업데이트에 따른 호환성 문제를 함께 검토해야 한다.
이번 글은 오픈소스 보안 위험을 평가할 때 프로젝트에 누적된 취약점 발표 건수보다 실제 설치 버전과 취약한 코드의 사용 여부가 중요하다는 점을 대규모 npm 다운로드 데이터로 보여준다.
특히 React와 Next.js, Vite 기반 프로젝트에서 직접 설치한 라이브러리뿐 아니라 Lockfile에 기록된 전이적 의존성까지 함께 살펴봐야 한다는 점을 구체적으로 이해할 수 있는 자료다.