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

기준 시점: 2026-10-27 06:00 KST
조사 범위: 2026-10-26 06:00 ~ 2026-10-27 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
React, Next.js, Vercel, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro, Remix의 공식 발표와 릴리스 정보를 조사했지만, 이번 24시간에 공개된 중요한 프레임워크 정식 릴리스나 새로운 렌더링 API는 확인하지 못했다.
Vite, TypeScript, 주요 JavaScript 빌드 도구에서도 프론트엔드 애플리케이션의 동작 방식을 실질적으로 변경하는 새로운 주요 발표는 확인하지 못했다.
다만 Vercel에서는 pnpm의 패키지 설치 시간을 줄이는 플랫폼 업데이트를 공개했다. 이는 React나 Next.js 자체의 변경은 아니지만 프론트엔드 프로젝트의 배포 과정에 영향을 줄 수 있는 내용이므로 개발 도구 및 보안 섹션에서 다룬다.
이번 추천 글에는 Next.js의 Font 최적화가 실제로 어떻게 동작하는지 배포 결과물을 분석한 글과 React 애플리케이션의 API 계층을 재구성하는 글을 포함했다.
특히 next/font의 subsets 설정이 다운로드할 Font 파일을 제한하는 옵션이 아니라 Preload 대상을 지정하는 옵션이라는 사실을 실제 네트워크 데이터로 확인한 사례가 흥미롭다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome Developers·Chromium, Mozilla·Firefox, WebKit·Safari와 JavaScript, CSS, 웹 성능 및 접근성 관련 공식 발표를 조사했지만, 이번 24시간에 새롭게 정식 채택된 주요 웹 표준이나 안정화된 브라우저 API는 확인하지 못했다.
개발 버전의 변경과 기존 브라우저 기능을 활용한 기술 글은 새로운 웹 표준의 출시와 구분했다.
이번 추천 글에서는 PDF.js의 실제 소스코드를 분석해 PDF 문서가 Canvas에 렌더링되는 과정을 설명한 자료를 소개한다.
PDF.js는 웹브라우저에서 PDF를 표시하는 데 널리 사용되는 JavaScript 라이브러리다. 해당 글에서는 PDF 좌표계를 Canvas 좌표계로 변환하는 과정과 Web Worker가 렌더링 명령을 생성하는 구조를 자세히 살펴볼 수 있다.
3️⃣ 백엔드·인프라
Cloudflare Computer 0.7.0, 중단된 에이전트 실행 복구 및 사람의 승인 대기 지원
- 발표일: 2026-10-26 — 정확한 게시 시각 미확인
- 버전:
@cloudflare/computer0.7.0 - 적용 대상: Cloudflare Computer, Durable Objects, WorkerJavaScriptBackend
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-26-computer-durable-javascript/
Cloudflare가 Computer 0.7.0에서 에이전트가 실행하던 JavaScript 작업을 중단 이후 복구하고, 특정 작업에 대한 사람의 승인을 기다렸다가 실행을 재개하는 기능을 추가했다.
이번 업데이트는 10월 20~22일에 공개된 Workspace 구조 변경이나 PiHarness의 일반적인 장애 복구 기능과 별개의 발표다.
핵심은 에이전트가 직접 실행하는 JavaScript의 진행 상태를 복구 가능한 형태로 관리한다는 점이다.
AI 코딩 에이전트를 생각해 보자.
사용자가 GitHub Issue의 내용을 확인하고 관련 코드를 수정한 뒤 PR에 댓글을 작성해 달라고 요청했다고 가정할 수 있다.
에이전트는 다음과 같은 작업을 수행한다.
- GitHub Issue를 조회한다.
- 관련 소스코드를 읽는다.
- 수정할 파일을 결정한다.
- 코드를 변경한다.
- 테스트를 실행한다.
- 결과를 GitHub에 기록한다.
문제는 이 과정에서 실행 환경이 종료될 수 있다는 것이다.
예를 들어 새로운 버전의 Worker가 배포되거나, 실행 중인 프로세스가 충돌할 수 있다.
기존 실행 상태를 메모리에만 유지한다면 작업을 처음부터 다시 시작해야 할 수 있다.
이때 이미 완료한 외부 API 요청이 중복으로 실행되는 문제가 발생할 수 있다.
Cloudflare Computer는 이러한 문제에 대응하기 위해 실행 기록을 이용한 복구 기능을 제공한다.
onInterrupt: "replay"
새로운 기능을 사용하려면 WorkerJavaScriptBackend에 다음 옵션을 설정한다.
onInterrupt: "replay"
이 설정을 사용하면 실행 중인 JavaScript 작업이 중단된 이후 다시 시작될 때 이전 실행 기록을 활용한다.
여기서 중요한 점은 중단된 JavaScript가 정확히 마지막 명령어 위치에서 계속 실행되는 방식은 아니라는 사실이다.
Cloudflare는 실행을 처음부터 다시 시작한다.
하지만 이미 완료한 호출의 결과가 기록돼 있다면 해당 결과를 재사용한다.
예를 들어 다음과 같은 작업이 있었다고 가정해 보자.
- 파일 목록을 조회한다.
- 특정 파일을 읽는다.
- 코드 분석 결과를 생성한다.
- GitHub에 댓글을 작성한다.
세 번째 작업까지 완료된 결과가 기록돼 있다면 복구 과정에서 해당 결과를 다시 사용할 수 있다.
따라서 모든 외부 호출을 다시 수행할 필요가 줄어든다.
또한 이전에 출력한 실행 결과를 복구 과정에서 불필요하게 중복 출력하지 않도록 처리한다.
이러한 구조는 단순한 재시도보다 복잡하다.
실행을 다시 시작하되 완료된 작업의 결과는 재사용하는 방식이기 때문이다.
중단된 시점에 따라 외부 작업이 중복될 수 있다
하지만 이번 기능이 모든 작업의 정확히 한 번 실행을 보장하는 것은 아니다.
Cloudflare는 중요한 제한사항을 명시했다.
외부 작업은 성공했지만 해당 결과가 실행 기록에 저장되기 전에 프로세스가 종료되면 복구 과정에서 같은 작업을 다시 호출할 수 있다.
예를 들어 GitHub에 댓글을 작성하는 작업을 생각해 보자.
GitHub API는 댓글 작성을 정상적으로 완료했다.
하지만 Cloudflare Computer가 성공 결과를 저장하기 직전에 Worker가 종료됐다.
이후 실행을 복구하면 시스템은 댓글 작성이 완료됐다는 기록을 찾지 못할 수 있다.
따라서 같은 API를 다시 호출할 가능성이 있다.
그 결과 동일한 댓글이 두 번 작성될 수 있다.
이러한 문제는 Durable Execution 시스템에서 자주 발생한다.
프로그램 내부의 작업 기록과 외부 서비스에서 실제로 발생한 변경을 하나의 원자적인 트랜잭션으로 묶기 어렵기 때문이다.
Cloudflare는 이러한 상황을 처리하기 위해 unsafeToRepeat()과 Idempotency 관련 기능을 안내한다.
Idempotency는 같은 작업을 여러 번 요청하더라도 최종 결과가 한 번 수행한 것과 같도록 만드는 특성이다.
예를 들어 외부 API가 Idempotency Key를 지원한다면 같은 Key를 가진 중복 요청을 하나의 작업으로 처리할 수 있다.
Cloudflare의 공식 예제에서는 다음 값을 활용한다.
ctx.callId
이 값은 실행 중인 호출을 식별하는 데 사용할 수 있다.
외부 API가 이를 Idempotency Key로 처리하도록 구성하면 복구 과정에서 같은 작업이 중복 수행되는 위험을 줄일 수 있다.
다만 외부 서비스가 실제로 Idempotency Key를 지원하고 해당 값을 올바르게 처리해야 한다.
ctx.callId를 전달했다는 사실만으로 모든 외부 API의 중복 실행이 자동으로 방지되는 것은 아니다.
복구가 시작되는 시점
이번 기능에서 또 하나 중요한 부분은 복구가 자동으로 즉시 시작되는 것은 아니라는 점이다.
Cloudflare Computer는 다음에 workspace.runtime이 호출될 때 복구를 시작할 수 있다.
예를 들어 사용자가 새로운 요청을 보내거나 애플리케이션이 Runtime에 다시 접근하는 상황이다.
하지만 장시간 실행되는 에이전트에서는 사용자의 추가 요청이 없더라도 작업을 복구해야 할 수 있다.
Cloudflare는 이러한 경우 Durable Object Alarm을 사용하도록 안내한다.
Alarm에서 다음 함수를 호출할 수 있다.
workspace.runtime.recover()
이렇게 하면 다음 사용자 요청을 기다리지 않고 복구를 시작하도록 구성할 수 있다.
다만 Alarm의 실행 정책과 실제 복구 시점은 Durable Objects의 실행 환경에 영향을 받는다.
따라서 특정한 밀리초 안에 반드시 작업이 복구된다고 보장하는 구조는 아니다.
사람의 승인을 기다리는 ctx.interrupt()
두 번째 주요 기능은 승인이 필요한 작업에서 실행을 중단하고 외부의 결정을 기다리는 기능이다.
예를 들어 AI 에이전트가 GitHub Issue를 분석했다고 가정해 보자.
에이전트는 문제를 발견하고 다음과 같은 댓글을 작성하려고 한다.
문제를 확인했고 수정 작업을 진행하겠습니다.
하지만 실제 GitHub에 댓글을 작성하기 전에 사용자의 승인을 받아야 한다는 정책이 존재할 수 있다.
기존 구현에서는 승인 대기 상태를 애플리케이션이 직접 관리해야 했다.
예를 들어 데이터베이스에 승인 요청을 저장하고, 사용자가 승인하면 중단된 작업의 다음 단계를 다시 실행해야 했다.
새로운 Computer에서는 Custom Module 안에서 다음 함수를 호출할 수 있다.
ctx.interrupt()
이 함수는 필요한 승인 정보를 전달하고 실행을 일시 중단한다.
예를 들어 다음과 같은 정보를 전달할 수 있다.
- 대상 Issue
- 작성할 댓글
- 필요한 승인 내용
애플리케이션은 대기 중인 실행을 조회할 수 있다.
대표적인 API는 다음과 같다.
workspace.runtime.pending()
사용자가 승인 여부를 결정하면 다음 함수를 사용해 응답을 전달할 수 있다.
workspace.runtime.respond()
이때 원래 호출을 식별하는 ID와 응답 데이터를 제공한다.
승인되면 실행이 계속 진행된다.
거부되면 애플리케이션이 정의한 오류 처리 흐름으로 이동할 수 있다.
승인 대기와 실행 시간 제한
이번 기능의 특징은 승인 대기 시간이 실제 JavaScript 실행 시간과 분리된다는 점이다.
예를 들어 사용자가 승인을 위해 30분 동안 응답하지 않았다고 가정해 보자.
일반적인 서버리스 함수에서 Promise를 유지한 채 30분을 기다리는 방식은 실행 시간 제한과 리소스 사용 문제를 만들 수 있다.
하지만 Computer의 승인 대기는 실행 상태를 보존하는 방식으로 처리된다.
따라서 승인 대기 자체가 실행 시간 제한을 계속 소비하지 않는다.
또한 대기 중에 새로운 Worker 버전이 배포돼도 이후 실행을 이어갈 수 있도록 설계됐다.
다만 실제 승인이 적절한 사용자에 의해 이루어졌는지 확인하는 것은 애플리케이션의 책임이다.
ctx.interrupt()가 존재한다는 사실만으로 승인자의 신원과 권한이 자동으로 검증되는 것은 아니다.
예를 들어 GitHub 댓글 작성 승인이라면 해당 Repository의 작업을 승인할 권한이 있는 사용자인지 별도로 확인해야 할 수 있다.
실행 결과의 안정성과 데이터 저장
이번 기능은 Durable Objects Storage를 사용하는 Workspace 구조와 연결된다.
실행 상태와 호출 결과가 영속적으로 저장되므로 프로세스의 수명과 작업의 진행 상태를 분리할 수 있다.
그러나 장시간 실행되는 모든 외부 시스템의 상태까지 자동으로 보존하는 것은 아니다.
예를 들어 Sandbox 안의 임시 파일이나 외부 서비스의 인증 정보가 별도로 관리된다면 복구 이후 해당 상태를 다시 준비해야 할 수 있다.
따라서 실제 작업 환경의 파일과 실행 기록, 외부 API의 상태는 구분해서 설계해야 한다.
이번 발표에는 실행 시간 감소나 비용 절감에 관한 정량적인 Benchmark가 제공되지 않았다.
따라서 작업 복구 기능을 도입하면 모든 에이전트의 처리 속도가 빨라진다고 단정할 수 없다.
이번 업데이트는 AI 에이전트의 실행을 단순한 함수 호출에서 벗어나 중단과 복구, 승인 대기를 처리할 수 있는 지속적인 작업 흐름으로 확장한 변화다.
Vercel Sandbox, 실행 중인 프로세스에 표준 입력 전달 지원 — stdio MCP 서버와 양방향 통신 가능
- 발표일: 2026-10-26 — 정확한 게시 시각 미확인
- 적용 대상: Vercel Sandbox, TypeScript SDK, Python SDK
- 추가 기능: 실행 중인 프로세스의 Standard Input 지원
- 원문: https://vercel.com/changelog/vercel-sandbox-commands-now-support-stdin
Vercel이 Sandbox에서 실행 중인 명령어와 프로세스에 표준 입력(Standard Input, stdin)을 전달할 수 있도록 기능을 확장했다.
Sandbox는 외부 코드를 격리된 실행 환경에서 처리하기 위한 도구다.
예를 들어 AI 코딩 에이전트가 생성한 코드를 실행하거나 테스트를 수행할 때 사용할 수 있다.
이러한 환경에서는 단순한 명령 실행만 필요한 경우도 있다.
예를 들어 다음 명령을 실행할 수 있다.
npm test
하지만 일부 프로그램은 한 번 실행한 이후에도 계속 입력을 받아야 한다.
예를 들어 다음과 같은 프로그램이다.
- 대화형 CLI
- 장시간 실행되는 개발 서버
- JSON 메시지를 주고받는 외부 도구
- Standard Input과 Output으로 통신하는 MCP 서버
이번 업데이트 이전에는 Sandbox에서 명령을 실행하더라도 이미 실행 중인 프로세스에 새로운 입력을 전달하는 작업에 제한이 있었다.
새로운 기능은 이러한 통신을 지원한다.
Standard Input과 Standard Output
일반적인 프로세스에는 세 가지 기본적인 입출력 경로가 존재한다.
| 경로 | 역할 |
|---|---|
| stdin | 프로그램에 데이터를 입력 |
| stdout | 프로그램의 일반적인 실행 결과 출력 |
| stderr | 오류 및 진단 메시지 출력 |
예를 들어 cat 명령은 파일의 내용을 출력하는 데 사용할 수 있다.
하지만 파일 경로를 지정하지 않고 실행하면 Standard Input으로 받은 데이터를 다시 출력할 수도 있다.
이번 Vercel Sandbox에서는 명령을 실행한 뒤 stdin으로 데이터를 전달할 수 있다.
즉 처음에 실행할 명령과 모든 입력값을 함께 제공해야 하는 구조에서 벗어난 것이다.
TypeScript SDK의 writeStdin()
TypeScript SDK에서는 다음 옵션을 사용한다.
stdin: true
detached: true
두 옵션은 sandbox.runCommand()를 호출할 때 전달한다.
실행 이후에는 다음 함수를 사용할 수 있다.
cmd.writeStdin()
이 함수는 이미 실행 중인 프로세스의 stdin에 데이터를 전달한다.
예를 들어 cat 명령을 실행한 뒤 다음 문자열을 전달할 수 있다.
hello
프로세스는 해당 문자열을 읽고 결과를 출력할 수 있다.
더 이상 입력할 데이터가 없다면 다음 함수를 호출한다.
cmd.closeStdin()
이후 다음 함수로 프로세스가 종료되기를 기다릴 수 있다.
cmd.wait()
마지막으로 stdout을 읽어 실제 결과를 확인할 수 있다.
이러한 방식은 프로세스의 실행과 데이터 전송, 실행 결과 확인을 서로 다른 단계로 분리한다.
Node.js Readable Stream 연결
이번 기능에서는 문자열을 개별적으로 전달하는 것뿐 아니라 Node.js의 Readable Stream도 stdin에 연결할 수 있다.
Readable Stream은 데이터를 한 번에 모두 메모리에 저장하지 않고 순차적으로 읽을 수 있는 인터페이스다.
예를 들어 큰 파일이나 네트워크 데이터를 처리할 때 사용할 수 있다.
Vercel Sandbox는 해당 Stream에서 읽은 데이터를 실행 중인 프로세스의 stdin으로 전달할 수 있다.
Stream이 종료되면 명령의 stdin도 닫을 수 있다.
이러한 구조는 작은 문자열뿐 아니라 지속적으로 생성되는 데이터를 외부 프로그램에 전달하는 데 활용할 수 있다.
다만 데이터가 Stream으로 전달된다는 사실만으로 모든 프로그램이 자동으로 실시간 입력을 처리한다는 의미는 아니다.
실제 프로그램이 stdin을 어떤 방식으로 읽는지에 따라 동작이 달라진다.
Python SDK 지원
Python SDK에서도 동일한 기능을 사용할 수 있다.
Python에서는 다음 설정을 사용한다.
stdin=subprocess.PIPE
이후 실행한 프로세스의 stdin에 데이터를 전달할 수 있다.
대표적인 함수는 다음과 같다.
server.stdin.send()
예를 들어 다음 데이터를 전달할 수 있다.
b"ping\n"
전송 이후에는 communicate()를 이용해 입력을 종료하고 남은 출력 결과를 수집할 수 있다.
한 번에 전달할 모든 데이터가 이미 준비돼 있다면 run_process()의 input 인자를 사용할 수도 있다.
따라서 두 종류의 실행 방식을 구분할 수 있다.
첫 번째는 프로그램을 실행하면서 미리 준비한 입력을 한 번에 전달하는 방식이다.
두 번째는 프로그램을 실행한 이후 필요한 시점마다 데이터를 전달하는 방식이다.
이번 업데이트의 주요 변화는 두 번째 방식의 지원이다.
stdio MCP 서버와의 연결
이번 기능이 AI 에이전트 개발에서 중요한 이유는 stdio 기반 MCP 서버를 Sandbox 안에서 실행하고 통신할 수 있기 때문이다.
MCP(Model Context Protocol)는 AI 에이전트가 외부 도구를 호출할 수 있도록 하는 인터페이스다.
MCP 서버는 여러 전송 방식을 사용할 수 있다.
그중 하나가 Standard Input과 Standard Output을 이용하는 방식이다.
예를 들어 MCP 클라이언트는 stdin으로 JSON 메시지를 전송할 수 있다.
MCP 서버는 해당 메시지를 처리한 뒤 stdout으로 응답을 반환한다.
이러한 구조에서는 프로세스를 계속 실행한 상태로 유지하면서 여러 메시지를 주고받아야 한다.
Vercel은 이번 기능의 사용 사례로 agent-browser 같은 도구를 제시했다.
따라서 AI 코딩 에이전트가 Sandbox 안에서 브라우저 자동화 도구를 실행하고, 필요한 명령을 전달하는 구조를 구성할 수 있다.
하지만 stdin을 지원한다는 사실이 모든 CLI의 대화형 터미널 동작까지 완전히 재현한다는 의미는 아니다.
일부 프로그램은 일반적인 stdin뿐 아니라 TTY나 터미널 제어 기능을 요구할 수 있다.
따라서 특정 CLI를 실제로 실행할 수 있는지는 해당 프로그램의 입출력 요구사항에 따라 확인해야 한다.
외부 코드 실행의 보안 경계
Sandbox는 외부 코드를 실행하는 환경이므로 입력 데이터를 전달할 때도 주의가 필요하다.
예를 들어 AI 에이전트가 생성한 Shell 명령을 실행한다고 가정해 보자.
실행에 필요한 데이터가 포함돼 있더라도 해당 코드가 허용되지 않은 네트워크 요청이나 파일 접근을 시도할 가능성이 있다.
따라서 Sandbox의 격리 설정과 인증 정보의 접근 범위는 별도로 관리해야 한다.
특히 프로세스에 환경 변수를 통해 API Key를 제공하는 경우에는 해당 프로세스가 실제로 어떤 데이터에 접근할 수 있는지 고려해야 한다.
이번 기능은 Sandbox의 모든 보안 정책을 변경한 것이 아니다.
기존 격리 실행 환경에서 프로그램과 데이터를 주고받는 방법을 확장한 업데이트다.
Cloudflare Workers, OpenTelemetry Span 정보·상태·이름 변경 API 추가
- 발표일: 2026-10-26 — 정확한 게시 시각 미확인
- 적용 대상: Cloudflare Workers, Custom Span, OpenTelemetry Export
- 신규 API:
spanContext(),setStatus(),updateName() - 원문: https://developers.cloudflare.com/changelog/post/2026-10-26-span-context-status-update-name/
Cloudflare가 Workers의 Custom Span에서 OpenTelemetry 표준과 연결되는 세 가지 API를 추가했다.
이번 업데이트는 실제 서비스의 요청을 추적하고 오류를 분석하는 Observability 기능과 관련된다.
Observability는 프로그램이 실행되는 동안 어떤 일이 발생했는지 외부에서 확인할 수 있도록 하는 기능이다.
대표적으로 세 가지 데이터를 사용한다.
- Log: 프로그램이 출력한 실행 기록
- Metric: 요청 수와 처리 시간 같은 수치
- Trace: 하나의 요청이 여러 작업을 거쳐 실행되는 흐름
특히 Trace는 API 요청이 여러 서비스와 함수로 나뉘어 처리되는 환경에서 유용하다.
예를 들어 사용자가 상품 상세 페이지를 열었다고 가정해 보자.
서버는 다음 작업을 수행할 수 있다.
- 사용자 인증 상태 확인
- 상품 데이터 조회
- 재고 데이터 조회
- 가격 계산
- JSON 응답 반환
각 작업에 걸린 시간을 개별적으로 확인하면 어떤 부분에서 지연이 발생했는지 분석할 수 있다.
OpenTelemetry에서는 이러한 개별 작업을 Span으로 표현한다.
하나의 요청에 속하는 Span들은 공통된 Trace ID로 연결된다.
이번 업데이트는 이러한 Span의 식별자와 상태, 이름을 더 세밀하게 관리할 수 있도록 한다.
span.spanContext()
첫 번째 추가 API는 다음과 같다.
span.spanContext()
이 함수는 해당 Span의 Context를 반환한다.
주요 값은 다음과 같다.
traceIdspanIdtraceFlags
traceId는 하나의 요청 흐름을 식별한다.
spanId는 해당 요청 안의 개별 작업을 식별한다.
traceFlags는 Trace의 처리 및 샘플링과 관련된 정보를 표현한다.
이번 업데이트의 특징은 반환된 식별자가 Workers Observability와 외부로 Export한 Trace에서 사용하는 값과 일치한다는 점이다.
따라서 애플리케이션 로그에 Trace ID를 함께 기록할 수 있다.
예를 들어 사용자가 HTTP 500 오류를 경험했다고 가정해 보자.
서버는 오류 응답에 Trace ID를 포함할 수 있다.
운영자는 해당 ID로 실제 실행 흐름을 찾아볼 수 있다.
다만 Trace ID는 인증 정보가 아니다.
클라이언트에 전달하는 경우에는 해당 ID가 내부 시스템이나 민감한 데이터에 대한 접근 권한을 제공하지 않도록 구성해야 한다.
또한 Trace ID가 존재한다는 사실만으로 모든 실행 기록이 반드시 저장된다는 보장은 없다.
실제 Trace의 수집과 보존은 Observability 설정에 따라 달라진다.
span.setStatus()
두 번째 API는 다음과 같다.
span.setStatus()
이 함수는 Span의 실행 상태를 기록한다.
지원하는 상태는 다음과 같다.
| 상태 | 의미 |
|---|---|
unset | 명시적인 상태를 지정하지 않음 |
ok | 작업을 정상적으로 처리 |
error | 작업에서 오류 발생 |
오류가 발생한 경우에는 추가적인 메시지를 전달할 수 있다.
예를 들어 데이터베이스 요청에 실패했다면 해당 Span에 error 상태를 기록할 수 있다.
그리고 오류가 발생한 이유를 설명하는 메시지를 추가할 수 있다.
다만 setStatus()와 실제 JavaScript의 Exception 처리는 다르다.
Span 상태를 Error로 설정했다고 해서 JavaScript 실행이 자동으로 중단되는 것은 아니다.
반대로 Exception이 발생했다는 사실만으로 모든 Custom Span의 상태가 원하는 방식으로 기록된다고 가정해서도 안 된다.
개발자는 필요한 경우 다음 두 작업을 함께 수행할 수 있다.
span.recordException()span.setStatus()
첫 번째는 발생한 오류를 기록한다.
두 번째는 해당 작업의 실행 상태를 설정한다.
Dashboard 표시의 제한사항
이번 기능에서 중요한 제한이 있다.
setStatus()로 지정한 Span 상태는 OpenTelemetry로 Export한 Trace에는 포함되지만 현재 Workers Observability Dashboard에는 표시되지 않는다.
따라서 외부 OpenTelemetry 수집 시스템에서는 해당 상태를 확인할 수 있다.
하지만 Cloudflare Dashboard에서 동일한 상태를 즉시 확인할 수 있다고 가정해서는 안 된다.
Cloudflare는 공식 발표에서 이러한 차이를 명시했다.
이는 API 자체가 제공하는 정보와 실제 관리 화면이 지원하는 정보가 서로 다를 수 있다는 사례다.
span.updateName()
세 번째 API는 다음과 같다.
span.updateName()
이 함수는 이미 시작한 Span의 이름을 변경한다.
예를 들어 HTTP 요청을 처음 받았을 때는 실제로 어떤 Route를 처리할지 모를 수 있다.
요청 URL이 다음과 같다고 가정해 보자.
/users/123/orders
실제 애플리케이션의 Route 패턴은 다음과 같을 수 있다.
/users/:id/orders
개별 사용자의 ID를 Span 이름에 직접 포함하면 사용자마다 서로 다른 이름이 생성될 수 있다.
이는 관찰 데이터를 분석할 때 불필요한 구분을 만들 수 있다.
따라서 실제 Route가 결정된 이후 다음과 같은 이름으로 변경할 수 있다.
GET /users/:id/orders
이렇게 하면 같은 Route를 사용하는 요청들을 하나의 논리적인 작업으로 구분할 수 있다.
또한 동적인 사용자 ID를 Span 이름에 직접 포함하지 않아도 된다.
이번 API는 일반적인 Custom Span뿐 아니라 Invocation의 Root Span에도 사용할 수 있다.
Root Span은 다음 함수로 가져올 수 있다.
tracing.getActiveSpan()
이후 updateName()으로 해당 이름을 변경할 수 있다.
실제 적용 범위
이번 변경은 Workers의 요청 처리 속도를 개선하는 기능이 아니다.
또한 새로운 Trace 수집 시스템을 출시한 것도 아니다.
기존 Custom Span API에 다음 기능을 추가한 것이다.
- 실행 식별자 조회
- 작업 상태 기록
- 작업 이름 변경
이를 통해 실제 애플리케이션의 실행 흐름을 더 정확하게 기록하고 외부 OpenTelemetry 시스템과 연결할 수 있다.
이번 발표에는 API 사용 전후의 성능 차이나 Trace 수집 비용에 관한 정량적인 Benchmark가 포함되지 않았다.
기존 Workers Observability의 추적 정보를 애플리케이션의 로그와 오류 처리, 외부 모니터링 시스템에서 더 세밀하게 사용할 수 있도록 확장한 업데이트다.
Cloudflare DNS API, Advanced Nameserver 설정을 새로운 nameservers.type 값으로 통합
- 발표일: 2026-10-26 — 정확한 게시 시각 미확인
- 적용 대상: Cloudflare DNS Settings API
- 배포 방식: 7일에 걸친 점진적 적용
- 변경 유형: API 응답 형식 변경 및 기존 필드 Deprecated
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-26-foundation-dns-nameserver-type/
Cloudflare가 Advanced Nameservers를 표현하는 DNS API의 응답 구조를 변경했다.
기존에는 Advanced Nameservers 설정에 다음 필드를 사용했다.
foundation_dns: true
새로운 방식에서는 다음 값을 사용한다.
nameservers.type: "cloudflare.advanced"
이번 변경은 DNS 서버의 실제 동작을 새롭게 변경하는 기능이 아니다.
기존 설정을 API에서 표현하고 변경하는 방식을 정리한 업데이트다.
하지만 해당 API를 직접 호출하는 애플리케이션에서는 호환성 문제가 발생할 수 있다.
기존 설정 방식
Cloudflare는 DNS 설정 API에서 Nameserver 유형을 반환한다.
예를 들어 일반적인 Cloudflare Nameserver를 사용하는 경우 다음 값이 반환될 수 있다.
nameservers.type: "cloudflare.standard"
Advanced Nameservers는 별도의 Boolean 필드로 표현됐다.
foundation_dns: true
이러한 구조에서는 Nameserver 유형을 확인하기 위해 두 개의 필드를 함께 해석해야 할 수 있었다.
이번 업데이트에서는 Advanced Nameservers를 하나의 유형으로 표현한다.
새로운 값은 다음과 같다.
cloudflare.advanced
따라서 일반적인 Nameserver와 Advanced Nameserver를 동일한 필드에서 구분할 수 있다.
영향을 받는 API
이번 변경은 Zone 단위 설정과 Account 기본 설정에 적용된다.
대표적인 엔드포인트는 다음과 같다.
/zones/{zone_id}/dns_settings
/accounts/{account_id}/dns_settings
두 엔드포인트에서 Advanced Nameservers 설정을 새로운 값으로 반환할 수 있다.
하지만 모든 계정에 즉시 적용되는 것은 아니다.
Cloudflare는 10월 26일부터 약 7일에 걸쳐 점진적으로 변경을 배포한다고 설명했다.
따라서 배포가 완료되기 전에는 계정에 따라 서로 다른 응답이 나타날 수 있다.
예를 들어 실제로 Advanced Nameservers를 사용하는 계정이지만 아직 업데이트가 적용되지 않았다면 다음 값이 반환될 수 있다.
cloudflare.standard
따라서 배포 기간 중에는 응답에 포함된 새로운 유형만으로 모든 계정의 상태를 동일하게 판단하기 어려울 수 있다.
Enum 타입을 사용하는 API Client
이번 변경에서 특히 주의해야 할 부분은 TypeScript의 Union Type이나 Enum으로 API 응답을 제한한 경우다.
예를 들어 기존 클라이언트가 다음 두 값만 허용했다고 가정해 보자.
cloudflare.standard- 다른 기존 Nameserver 유형
새로운 API 응답으로 cloudflare.advanced가 도착하면 런타임 검증에서 오류가 발생할 수 있다.
예를 들어 Zod Schema에서 허용된 Enum 값만 받아들이도록 설정했다면 새로운 값이 거부될 수 있다.
따라서 해당 API를 사용하는 클라이언트는 새로운 유형을 허용하도록 변경해야 한다.
이러한 문제는 단순히 TypeScript 컴파일 오류로만 나타나는 것이 아니다.
실제 네트워크 응답은 런타임에 전달되므로 기존 Schema가 새로운 값을 거부할 수 있다.
기존 필드의 Deprecated 처리
기존 foundation_dns 필드는 즉시 삭제되지 않는다.
Cloudflare는 해당 필드를 호환성 유지를 위한 Alias로 제공한다.
따라서 현재는 기존 필드를 읽고 쓸 수 있다.
하지만 앞으로 제거할 계획이라고 밝혔다.
새로운 코드를 작성할 때는 다음 필드를 사용하도록 권장한다.
nameservers.type
또한 PATCH 요청에 기존 필드와 새로운 필드를 모두 포함할 수 있다.
하지만 두 값은 서로 일치해야 한다.
예를 들어 새로운 필드에서는 Advanced Nameserver를 사용한다고 설정했지만 기존 필드에서는 비활성화하도록 설정했다면 API는 요청을 거부한다.
실제 DNS 설정은 변경되지 않는다
Cloudflare는 이번 업데이트가 다음 사항에 영향을 주지 않는다고 명시했다.
- Foundation DNS 구독 상태
- Advanced Nameservers의 실제 활성화 여부
- 기존 Zone의 Nameserver 구성
따라서 이번 변경 때문에 Zone을 다시 이전하거나 DNS 서버를 새롭게 구성할 필요는 없다.
실제 운영 서비스의 도메인 연결이 변경되는 것도 아니다.
DNS 관리 API를 사용하는 자동화 시스템의 데이터 형식 호환성을 관리하기 위한 변경이다.
4️⃣ 개발 도구 및 보안
Vercel, pnpm 의존성 설치 시간 평균 14% 감소 — 기존 공급망 검증 결과 재사용
- 발표일: 2026-10-26 — 정확한 게시 시각 미확인
- 적용 대상: Vercel Build, pnpm
- 지원 요금제: 모든 Vercel 요금제
- 측정 기준: Build Cache를 복구한 Pro 배포
- 원문: https://vercel.com/changelog/faster-pnpm-installs
Vercel이 pnpm을 사용하는 프로젝트의 의존성 설치 과정에서 이전에 성공한 공급망 검증 결과를 재사용하도록 변경했다.
이번 업데이트는 pnpm 자체의 새로운 버전을 출시한 것이 아니다.
Vercel의 배포 환경에서 pnpm이 의존성을 설치하는 과정에 적용한 최적화다.
일반적인 JavaScript 프로젝트의 빌드 과정에서는 다음 작업이 수행될 수 있다.
- 소스코드 가져오기
- 패키지 매니저 실행
- 의존성 검증
- 필요한 패키지 설치
- 애플리케이션 빌드
- 배포 결과물 생성
이 가운데 패키지 설치는 실제 애플리케이션 코드가 변경되지 않았더라도 반복해서 실행될 수 있다.
예를 들어 컴포넌트의 텍스트 하나만 변경한 경우에도 배포 과정에서 의존성 설치 단계가 실행될 수 있다.
이미 같은 Lockfile을 검증한 적이 있다면 동일한 검증 작업을 반복할 필요가 없을 수 있다.
이번 업데이트는 이러한 상황에서 이전에 성공한 검증 결과를 재사용한다.
Lockfile과 공급망 검증
Lockfile은 프로젝트에서 사용하는 의존성의 실제 버전과 관련 정보를 기록한 파일이다.
pnpm에서는 일반적으로 다음 파일을 사용한다.
pnpm-lock.yaml
예를 들어 React 프로젝트에서 특정 라이브러리를 설치했다고 가정해 보자.
package.json에는 허용하는 버전 범위가 기록될 수 있다.
하지만 실제 설치된 패키지 버전과 전이적 의존성 정보는 Lockfile에 기록된다.
Vercel은 이러한 의존성을 설치하는 과정에서 공급망 정책을 확인한다.
예를 들어 패키지와 버전이 조직의 정책을 충족하는지 검증할 수 있다.
기존에는 같은 의존성에 대한 검증이 여러 배포에서 반복될 수 있었다.
이번 변경 이후에는 이전에 성공한 검증 결과를 다시 사용할 수 있다.
다만 공급망 검증 기능 자체가 제거된 것은 아니다.
이전에 성공한 검증 결과를 재사용하는 방식이다.
따라서 이전 검증을 재사용할 수 없는 상황에서는 필요한 검증 작업이 계속 수행될 수 있다.
평균 설치 시간 14% 감소
Vercel은 실제 운영 데이터를 이용해 이번 최적화의 효과를 측정했다.
공개한 결과는 다음과 같다.
Build Cache를 복구한 Pro 배포에서 pnpm의 평균 의존성 설치 시간이 약 14% 감소했다.
여기서 중요한 점은 측정 대상이다.
전체 애플리케이션의 빌드 시간이 14% 감소했다는 의미가 아니다.
또한 모든 Vercel 프로젝트에서 같은 효과가 발생한다는 의미도 아니다.
Vercel이 공개한 수치는 다음 조건에 해당한다.
- pnpm 사용
- Pro 배포
- 기존 Build Cache 복구
- 의존성 설치 단계 측정
따라서 해당 조건이 아닌 프로젝트에서는 결과가 달라질 수 있다.
예를 들어 빌드 시간이 대부분 TypeScript 컴파일이나 이미지 처리에 소비되는 프로젝트라면 의존성 설치 시간이 감소하더라도 전체 빌드 시간의 변화는 작을 수 있다.
공식 예제의 극단적인 개선 사례
Vercel은 하나의 실제 설치 로그를 비교했다.
예제의 조건은 다음과 같다.
- pnpm 12.3.4
- Lockfile 항목 461개
변경 전 결과는 다음과 같았다.
| 단계 | 실행 시간 |
|---|---|
| 공급망 검증 | 2.5초 |
| 전체 의존성 설치 | 2.8초 |
변경 이후에는 이전 검증 결과를 재사용했다.
이전 검증이 완료된 시점은 약 58초 전이었다.
결과는 다음과 같았다.
전체 설치 시간 135ms
이는 약 2.8초에서 135ms로 감소한 사례다.
하지만 해당 결과는 모든 프로젝트의 일반적인 설치 성능을 의미하지 않는다.
이전에 검증한 내용과 현재 의존성이 동일하고 해당 결과를 재사용할 수 있었기 때문에 매우 짧은 시간에 완료된 것이다.
따라서 공식 자료의 평균 14% 개선과 특정 예제의 135ms 결과는 구분해야 한다.
첫 번째는 여러 배포의 평균적인 변화다.
두 번째는 검증 결과를 효과적으로 재사용할 수 있었던 개별 사례다.
또한 두 수치 모두 Vercel의 자체 측정 결과다.
독립적인 제3자가 여러 배포 플랫폼을 같은 조건에서 비교한 Benchmark는 아니다.
설정 변경 필요 여부
이번 기능은 모든 Vercel 요금제에 자동으로 제공된다.
개발자가 별도의 설정을 추가할 필요는 없다.
기존 pnpm 프로젝트에서도 해당 조건이 충족되면 이전 검증 결과를 재사용할 수 있다.
다만 실제 효과는 Lockfile의 변경 여부와 Build Cache 상태에 따라 달라질 수 있다.
예를 들어 새로운 패키지를 설치해 Lockfile이 변경됐다면 이전 검증 결과를 그대로 재사용할 수 없을 수 있다.
따라서 이번 기능이 의존성 변경이 많은 프로젝트의 설치 시간을 항상 크게 줄여주는 것은 아니다.
이번 업데이트는 JavaScript 프로젝트의 배포 과정에서 동일한 의존성 검증 작업을 반복하는 비용을 줄인 빌드 환경 최적화다.
Vercel Sandbox, Scale AI AgentEnv 평가 실행 지원 — 에이전트와 테스트 환경을 각각 격리
- 발표일: 2026-10-26 — 정확한 게시 시각 미확인
- 적용 대상: Vercel Sandbox, Scale AI AgentEnv
- 지원 버전: AgentEnv 0.9.1334 이상
- 원문: https://vercel.com/changelog/run-scale-ais-agentenv-evals-on-vercel-sandbox
Vercel이 Scale AI의 AgentEnv 평가 프레임워크를 Vercel Sandbox에서 실행할 수 있도록 지원한다고 발표했다.
AgentEnv는 AI 에이전트에게 특정 작업을 수행하도록 요청한 뒤 실제 결과를 평가하는 프레임워크다.
예를 들어 AI 코딩 에이전트가 다음 작업을 수행할 수 있다.
- 오류가 있는 코드를 수정한다.
- 테스트를 실행한다.
- 필요한 파일을 변경한다.
- 최종 결과를 제출한다.
하지만 모델이 작업을 완료했다고 답했다는 사실만으로 실제 코드가 올바르게 수정됐다고 판단할 수는 없다.
따라서 별도의 검증 환경에서 실제 결과를 검사할 필요가 있다.
AgentEnv는 이러한 평가를 위한 실행 구조를 제공한다.
에이전트 실행 환경과 테스트 환경 분리
이번 연동의 핵심은 에이전트와 평가 대상 작업을 서로 다른 Sandbox에서 실행한다는 점이다.
예를 들어 에이전트가 특정 코드의 오류를 수정하는 작업을 수행한다고 가정해 보자.
에이전트는 코드 파일을 읽고 수정할 수 있어야 한다.
반면 최종 평가에서는 해당 수정이 실제 요구사항을 충족하는지 확인해야 한다.
이때 에이전트와 평가 환경이 완전히 같은 실행 상태를 공유하면 문제가 발생할 수 있다.
예를 들어 에이전트가 테스트 결과를 변경하거나 평가에 필요한 파일을 직접 수정할 가능성이 있다.
또는 테스트 환경에 남은 이전 실행 상태가 새로운 평가 결과에 영향을 줄 수 있다.
서로 다른 Sandbox를 사용하면 이러한 실행 환경을 분리할 수 있다.
하지만 별도의 Sandbox를 사용했다는 사실만으로 모든 평가 조작이 불가능해지는 것은 아니다.
실제 파일 접근 권한과 평가 데이터의 전달 방식, 외부 API의 인증 정보도 함께 관리해야 한다.
평가 결과와 실제 실행 기록 확인
이번 통합에서는 AgentEnv의 평가 결과를 확인할 수 있다.
대표적으로 다음 정보가 포함된다.
- 작업의 평가 점수
- 에이전트가 수행한 실제 동작
- 실행 기록
따라서 에이전트가 자연어로 작성한 최종 보고서만 확인하는 방식과 다르다.
실제로 어떤 작업이 수행됐고 평가 결과가 무엇인지 별도로 확인할 수 있다.
이는 AI 코딩 에이전트의 기능을 개선하는 과정에서 중요하다.
예를 들어 모델을 변경했을 때 특정 작업을 더 잘 수행하는지 확인할 수 있다.
또는 Prompt를 수정한 이후 기존에 통과하던 작업이 실패하는 회귀 문제가 발생했는지 확인할 수 있다.
하지만 이번 발표에는 새로운 평가 시스템이 기존 방식보다 몇 배 빠르거나 더 정확하다는 정량적인 비교 결과가 포함되지 않았다.
기존 AgentEnv의 평가 작업을 Vercel Sandbox에서 실행할 수 있도록 인프라 지원을 추가한 것이다.
실행 방법과 조건
Vercel은 공식 예제 Repository를 제공한다.
설정이 완료되면 다음 형태의 명령을 사용할 수 있다.
./vercel-eval --scope YOUR_TEAM --project YOUR_PROJECT
여기서 YOUR_TEAM은 Vercel Team의 Slug다.
YOUR_PROJECT는 이미 존재하는 프로젝트의 이름이다.
해당 예제는 기존 Vercel CLI의 로그인 상태를 사용한다.
따라서 평가를 실행하려면 적절한 Vercel 계정과 프로젝트 접근 권한이 필요하다.
또한 실제 Sandbox를 생성하고 실행하는 과정에는 해당 플랫폼의 사용량 및 과금 정책이 적용될 수 있다.
이번 기능은 모든 AI 에이전트가 자동으로 더 높은 평가 점수를 얻도록 하는 것이 아니다.
실제 코드 실행을 포함하는 에이전트 평가를 별도의 격리 환경에서 반복 수행할 수 있도록 연결한 개발 도구 업데이트다.
Vercel Connect CLI, 1,000개 이상 외부 서비스 검색 지원 — 연결 방식과 Webhook 기능별 필터링
- 발표일: 2026-10-26 — 정확한 게시 시각 미확인
- 적용 대상: Vercel CLI, Vercel Connect
- 지원 CLI 버전: 64.1.0 이상
- 주요 명령:
vercel connect discover - 원문: https://vercel.com/changelog/discover-1000-services-on-vercel-connect-from-the-cli
Vercel이 터미널에서 1,000개 이상의 Vercel Connect 서비스를 검색하고 연결 가능한 기능을 확인하는 명령을 추가했다.
Vercel Connect는 애플리케이션이나 AI 에이전트를 외부 서비스에 연결할 수 있도록 하는 기능이다.
예를 들어 다음과 같은 종류의 서비스를 연결할 수 있다.
- 협업 도구
- 데이터베이스
- AI 서비스
- 고객 관리 시스템
- 개발 도구
- 파일 저장 서비스
문제는 실제로 사용할 수 있는 서비스가 많아질수록 원하는 연결 기능을 찾기 어려워진다는 것이다.
예를 들어 AI 에이전트가 고객 관리 시스템에 접근해야 한다고 가정해 보자.
해당 서비스가 Connect에서 제공되는지 확인해야 한다.
또한 MCP 방식으로 연결할 수 있는지, OAuth 인증이 필요한지, Webhook을 지원하는지도 확인해야 할 수 있다.
이번 업데이트에서는 이러한 정보를 Dashboard로 이동하지 않고 CLI에서 검색할 수 있다.
vercel connect discover
새로운 명령은 다음과 같다.
vercel connect discover
검색어를 함께 전달할 수 있다.
예를 들어 다음과 같다.
vercel connect discover crm
이 명령은 고객 관리 시스템과 관련된 서비스를 검색하는 데 사용할 수 있다.
검색에는 다음 정보가 사용된다.
- 서비스 이름
- 별칭
- 키워드
- 설명
따라서 서비스의 정확한 이름을 모르더라도 기능에 관한 단어로 검색할 수 있다.
연결 방식별 필터링
이번 기능에서는 검색 결과를 여러 조건으로 제한할 수 있다.
대표적인 옵션은 다음과 같다.
| 옵션 | 역할 |
|---|---|
--category | 서비스의 카테고리 |
--connection-method | 연결 방식 |
--supports-triggers | Webhook Trigger 지원 여부 |
--format=json | JSON 형태의 결과 반환 |
예를 들어 MCP 연결을 제공하는 서비스만 검색하려면 다음 명령을 사용할 수 있다.
vercel connect discover --connection-method mcp
또는 AI와 데이터 관련 카테고리를 함께 검색할 수도 있다.
vercel connect discover --category ai --category data
사용 가능한 카테고리 목록은 다음 명령으로 확인할 수 있다.
vercel connect categories
이 명령 역시 JSON 출력을 지원한다.
AI 에이전트와 자동화 프로그램의 활용
JSON 출력은 사람이 읽는 CLI 명령 이상의 활용을 가능하게 한다.
예를 들어 AI 코딩 에이전트가 특정 외부 서비스에 연결해야 한다고 가정해 보자.
먼저 Connect에서 해당 서비스를 제공하는지 검색할 수 있다.
검색 결과에서 지원하는 연결 방식을 확인할 수 있다.
이후 사용자가 승인한 범위 안에서 실제 연결을 생성하는 작업을 진행할 수 있다.
이러한 구조는 에이전트가 어떤 외부 서비스를 사용할 수 있는지 사전에 알아야 하는 문제를 줄여준다.
하지만 검색 결과에 나타난다는 사실과 실제 서비스 연결이 완료됐다는 사실은 다르다.
검색은 사용할 수 있는 서비스의 목록과 기능을 조회하는 작업이다.
실제 연결을 생성하려면 별도의 인증과 권한 설정이 필요할 수 있다.
또한 1,000개 이상의 서비스가 검색된다는 사실이 모든 서비스를 동일한 기능과 안정화 수준으로 사용할 수 있다는 의미는 아니다.
예를 들어 일부 서비스는 MCP를 지원하지만 다른 서비스는 OAuth 기반 연결만 지원할 수 있다.
Webhook Trigger 지원 여부도 서비스마다 다르다.
기존 기능과의 관계
이번 발표는 외부 서비스와 통신하는 새로운 프로토콜을 만든 것이 아니다.
Vercel Connect에 등록된 서비스를 검색하는 CLI 기능을 추가한 것이다.
실제 서비스 연결과 인증, API 호출 방식은 각 Connect 서비스가 제공하는 기능에 따라 달라진다.
이번 업데이트는 외부 서비스를 사용하는 개발 도구와 AI 에이전트가 필요한 연결 기능을 터미널에서 발견하고, 검색 결과를 자동화 프로그램에서 처리할 수 있도록 확장한 변경이다.
📚 추천 글
subsets in next/font is a preload list, not a download filter: 3 families, 20 font-face rules, 17 files
- 원제: subsets in next/font is a preload list, not a download filter: 3 families, 20 font-face rules, 17 files
- 저자: Daniel Pertu
- 매체: DEV Community
- 게시일: 2026-10-26
- 원문: https://dev.to/daniel_pertu/subsets-in-nextfont-is-a-preload-list-not-a-download-filter-3-families-20-font-face-rules-17-4l94
실제 Next.js 웹사이트에 적용한 next/font/google 설정이 어떤 Font 파일을 생성하고 브라우저가 어떤 파일을 Preload하는지 공개된 CSS와 네트워크 데이터를 분석한 글이다.
이번 글의 특징은 Next.js의 Font 최적화 기능을 단순히 사용하는 방법을 설명하지 않는다는 점이다.
저자는 자신이 운영하는 Nakodo 웹사이트에 적용한 Font 설정을 기준으로 실제 배포 결과물을 조사했다.
그리고 기존에 올바르다고 생각했던 설정의 의미가 실제 동작과 달랐다는 사실을 발견했다.
특히 subsets가 어떤 파일을 생성할지 결정하는 설정이 아니라 어떤 Font Subset을 미리 로딩할지 결정하는 설정이라는 점이 중요하다.
사용한 세 개의 Font Family
Nakodo에서는 다음 세 가지 글꼴을 사용한다.
| Font | 사용 목적 |
|---|---|
| Besley | 제목과 강조 텍스트 |
| Archivo | 일반적인 UI 텍스트 |
| IBM Plex Mono | 주소, 사용자 이름, 숫자 및 코드 형태의 텍스트 |
Next.js에서는 next/font/google을 이용해 이러한 글꼴을 설정할 수 있다.
이 기능은 빌드 과정에서 Google Fonts의 파일을 가져와 애플리케이션의 자체 정적 파일로 제공할 수 있도록 한다.
따라서 실제 사용자의 브라우저가 글꼴을 가져오기 위해 Google Fonts 서버에 직접 연결할 필요가 줄어든다.
저자는 실제 웹사이트의 HTML에서 외부 Google Fonts 요청 경로가 존재하는지 확인했다.
결과적으로 배포된 페이지는 Font 파일을 자체 도메인에서 제공하고 있었다.
하지만 Font 파일이 몇 개 생성되는지는 예상과 달랐다.
subsets 설정의 실제 의미
저자는 세 Font Family에 다음 Subset을 지정했다.
subsets: ["latin", "latin-ext"]
처음에는 이 설정이 Latin과 Latin Extended에 해당하는 Font 파일만 빌드 결과물에 포함하도록 제한한다고 생각했다.
하지만 실제로는 그렇지 않았다.
Next.js가 생성한 CSS에는 다른 언어의 Font Subset도 선언돼 있었다.
대표적으로 다음과 같은 Subset이 포함됐다.
- Latin
- Latin Extended
- Cyrillic
- Cyrillic Extended
- Vietnamese
이유는 subsets의 역할 때문이다.
subsets는 Preload 대상으로 지정할 Subset을 선택하는 설정이다.
Font의 다른 Subset이 실제 CSS에서 반드시 제거된다는 의미는 아니다.
Next.js는 Font 제공업체가 제공하는 여러 Subset을 @font-face 규칙으로 선언할 수 있다.
각 규칙에는 unicode-range가 포함된다.
unicode-range는 해당 Font 파일이 어떤 문자 범위를 담당하는지 나타낸다.
브라우저는 실제 텍스트에 필요한 문자가 존재할 때 해당 Font 파일을 요청할 수 있다.
따라서 CSS에 Font 파일이 선언돼 있다는 사실과 브라우저가 해당 파일을 실제로 다운로드했다는 사실은 구분해야 한다.
실제 생성된 파일 수
저자는 공개된 CSS를 직접 확인했다.
결과는 다음과 같았다.
| 항목 | 확인된 수 |
|---|---|
@font-face 규칙 | 20개 |
| 실제 WOFF2 파일을 참조하는 규칙 | 17개 |
| Fallback 관련 규칙 | 3개 |
| Preload 대상 Font 파일 | 10개 |
| Preload하지 않는 Font 파일 | 7개 |
17개의 Font 파일 크기를 합하면 389,760바이트였다.
이 가운데 Preload 대상으로 지정된 10개 파일의 크기는 327,700바이트였다.
나머지 7개 파일은 62,060바이트였다.
이 7개 파일에는 Cyrillic과 Vietnamese Subset이 포함됐다.
하지만 웹사이트의 페이지에서는 해당 문자 범위를 사용하지 않았으므로 실제 브라우저 요청이 발생하지 않을 수 있었다.
따라서 다음 두 값을 구분해야 한다.
첫 번째는 배포된 정적 파일에 존재하는 Font의 전체 크기다.
두 번째는 실제 사용자가 해당 페이지에 접근했을 때 다운로드하는 Font의 크기다.
두 값은 동일하지 않을 수 있다.
Preload와 일반적인 Font 로딩의 차이
Preload는 브라우저가 특정 리소스를 가능한 한 이른 시점에 가져오도록 힌트를 제공하는 기능이다.
예를 들어 웹페이지의 주요 제목에서 사용하는 Font를 미리 가져오면 텍스트가 실제 Font로 표시되기까지의 대기 시간을 줄일 수 있다.
하지만 모든 Font를 Preload하는 것이 항상 유리한 것은 아니다.
사용하지 않는 Font를 미리 가져오면 다른 중요한 리소스와 네트워크 대역폭을 경쟁할 수 있기 때문이다.
예를 들어 첫 화면에서 사용하지 않는 Italic Font를 Preload한다면 브라우저는 실제로 필요하지 않은 데이터를 먼저 다운로드할 수 있다.
반대로 실제로 사용하는 Font를 Preload하지 않으면 텍스트 렌더링이 시작된 이후에야 해당 파일을 요청할 수 있다.
따라서 Preload 대상을 결정하려면 어떤 Font가 실제 화면에 표시되는지 확인해야 한다.
이번 글은 이 문제를 실제 배포 데이터로 분석한다.
사용하지 않는 Italic Font 59KB
저자가 발견한 가장 큰 문제는 Besley의 Italic 스타일이었다.
기존 설정에는 다음 값이 포함됐다.
style: ["normal", "italic"]
이 설정으로 Besley의 Normal과 Italic Font가 함께 준비됐다.
그 결과 Besley Italic의 두 파일도 Preload 대상에 포함됐다.
두 파일의 크기는 다음과 같았다.
- Latin Extended Italic: 19,480바이트
- Latin Italic: 39,476바이트
합계는 58,956바이트였다.
하지만 실제 공개 페이지에서는 Besley Italic 글자가 거의 사용되지 않았다.
저자는 다음 페이지를 확인했다.
- 홈페이지
- 가격 페이지
- 사용 방법 페이지
- 소개 페이지
HTML에서 italic 문자열이 발견되기는 했다.
하지만 이는 실제 화면에 표시되는 텍스트가 아니라 React Payload에 포함된 404 페이지 관련 정보였다.
즉 실제 화면에 Italic 글자가 표시되지 않는데도 약 59KB의 Italic Font가 Preload되고 있었다는 것이다.
이러한 문제는 단순히 CSS에서 italic 문자열을 검색하는 것만으로는 발견하기 어려울 수 있다.
문자열이 실제로 렌더링되는지 확인해야 하기 때문이다.
Italic Font만 Preload하지 않는 방법
저자는 한 가지 해결 방법을 제시했다.
Normal 스타일과 Italic 스타일을 별도의 next/font 호출로 분리하는 것이다.
예를 들어 Normal Besley는 기존처럼 Preload한다.
반면 Italic Besley는 별도 설정으로 선언한다.
그리고 다음 값을 지정한다.
preload: false
이렇게 하면 Italic Font의 CSS 선언은 유지하면서 초기 Preload에서는 제외할 수 있다.
하지만 이번 글에서 중요한 점은 저자가 이 변경을 실제로 적용했다고 주장하지 않았다는 사실이다.
저자는 코드가 더 복잡해지는 비용과 다운로드 크기를 줄이는 이점을 비교하고 있었다.
별도 Font 변수와 Utility Class를 관리해야 하기 때문이다.
따라서 약 59KB를 절약할 수 있다는 가능성은 확인했지만 해당 변경을 최종적으로 채택하지는 않았다.
이번 글은 측정 결과와 최종 설계 결정을 구분한다.
Variable Font의 Width Axis
Archivo에는 다음 설정이 포함돼 있다.
axes: ["wdth"]
Variable Font는 하나의 Font 파일 안에서 여러 굵기나 너비 등의 값을 조절할 수 있는 글꼴이다.
일반적으로 Font Weight는 글꼴의 굵기를 의미한다.
반면 Width Axis는 글자의 너비를 조정하는 데 사용한다.
저자는 Archivo의 Width Axis를 이용해 좁은 Label을 구현했다.
예를 들어 작은 테이블의 컬럼 제목이나 Tag에 압축된 글꼴을 사용할 수 있다.
이렇게 하면 별도의 Condensed Font Family를 추가하지 않아도 된다.
저자에 따르면 Width Axis를 추가하면서 Font 파일 크기가 약 30KB 증가했다.
하지만 별도의 네 번째 Font Family를 추가하는 것보다는 현재 프로젝트에 적합한 선택이라고 판단했다.
이 역시 모든 웹사이트에서 동일한 선택이 최적이라는 의미는 아니다.
실제로 얼마나 많은 화면에서 Condensed Style을 사용하는지와 추가 Font의 크기에 따라 결정이 달라질 수 있다.
Fallback Font의 Metrics
웹브라우저가 Web Font를 다운로드하기 전에는 Fallback Font를 사용할 수 있다.
예를 들어 Besley가 아직 다운로드되지 않았다면 Times New Roman을 표시할 수 있다.
문제는 두 Font의 글자 크기와 간격이 다를 수 있다는 점이다.
처음에는 Fallback Font로 표시됐던 문장이 실제 Font로 변경되면서 줄바꿈이 달라질 수 있다.
이는 화면의 레이아웃 변경으로 이어질 수 있다.
Next.js는 이러한 차이를 줄이기 위해 Fallback Font의 Metrics를 조정할 수 있다.
대표적인 CSS 속성은 다음과 같다.
size-adjustascent-overridedescent-overrideline-gap-override
예를 들어 Besley의 Fallback에는 다음 값이 사용됐다.
size-adjust: 123.94%
이는 Fallback Font의 크기를 조정해 실제 Besley와 비슷한 레이아웃을 만들기 위한 설정이다.
하지만 실제 글꼴의 모양까지 동일하게 만드는 것은 아니다.
따라서 모든 시각적인 차이가 사라지는 것은 아니다.
Monospace Fallback의 문제
저자는 IBM Plex Mono의 Fallback에서 또 다른 문제를 발견했다.
IBM Plex Mono는 Monospace Font다.
Monospace Font는 각 문자가 동일한 폭을 차지하는 글꼴이다.
일반적인 코드 편집기나 숫자 테이블에 자주 사용된다.
그런데 Next.js가 생성한 IBM Plex Mono의 Fallback은 Arial을 사용했다.
Arial은 일반적인 비례폭 글꼴이다.
따라서 실제 Mono Font가 다운로드되기 전에는 문자 폭이 서로 다를 수 있다.
저자는 이 Fallback에 다음 값이 적용됐다는 사실을 확인했다.
size-adjust: 134.59%
하지만 size-adjust는 글꼴 크기를 조정하는 기능이지 모든 문자의 Advance Width를 동일하게 만드는 기능은 아니다.
따라서 화면의 높이는 실제 Font와 비슷하더라도 숫자나 코드가 정렬되는 가로 방향의 간격은 달라질 수 있다.
저자는 대안으로 다음 설정을 검토했다.
fallback: ["ui-monospace", "monospace"]
또는 자동 Fallback 조정을 비활성화할 수 있다.
adjustFontFallback: false
하지만 이러한 변경에도 트레이드오프가 있다.
실제 Monospace Font와 더 비슷한 형태를 사용할 수 있지만 레이아웃 Metrics의 차이가 증가할 수 있기 때문이다.
Tailwind CSS와 Font Variable 연결
저자는 next/font에서 생성한 CSS 변수를 Tailwind의 Font 설정과 연결했다.
이를 위해 다음 구조를 사용한다.
@theme inline
예를 들어 Archivo를 일반적인 Sans Font로 사용하도록 매핑할 수 있다.
이렇게 하면 모든 컴포넌트에서 Font Family를 직접 선언할 필요가 없다.
또한 숫자를 표시하는 요소에는 다음 CSS 속성을 적용했다.
font-variant-numeric: tabular-nums
이 설정은 숫자가 일정한 폭을 사용하도록 하는 데 활용할 수 있다.
예를 들어 데이터 테이블에서 숫자가 변경될 때 열의 정렬이 흔들리는 문제를 줄일 수 있다.
다만 실제 효과는 사용 중인 Font가 해당 숫자 형태를 지원하는지에 따라 달라진다.
측정 결과의 의미
이번 글에 포함된 수치는 저자가 실제 공개된 웹사이트의 CSS와 Font 파일을 조사한 결과다.
다른 Next.js 프로젝트를 대상으로 한 독립적인 Benchmark는 아니다.
따라서 모든 next/font 프로젝트가 동일한 수의 파일을 생성하거나 같은 비율의 불필요한 Preload를 수행한다고 판단할 수는 없다.
하지만 중요한 사실은 재현 가능하다.
저자는 공개된 CSS 파일과 실제 Font URL을 통해 결과를 확인했다.
그리고 실제로 사용하지 않는 Italic Font의 크기까지 계산했다.
이번 글은 next/font를 사용하면 Font 최적화가 자동으로 완료된다고 가정하는 대신, 실제 빌드 결과물과 Preload 대상, Fallback Metrics까지 확인해야 한다는 점을 구체적인 수치로 보여준다.
PDF.js Deep Dive 3: getViewport & render
- 저자: StreamPDF
- 매체: DEV Community / StreamPDF
- 게시일: 2026-10-26
- 분석 대상: PDF.js 6.3.289
- 원문: https://dev.to/streampdf/pdfjs-deep-dive-3-getviewport-render-2nm5
PDF.js의 실제 소스코드를 분석하면서 PDF 문서의 좌표를 Canvas 좌표로 변환하는 과정과 Web Worker에서 생성한 렌더링 명령이 브라우저 화면에 적용되는 과정을 설명한 글이다.
PDF.js는 Mozilla가 개발하는 JavaScript 기반 PDF 렌더링 라이브러리다.
웹 애플리케이션에서 PDF를 표시하거나 특정 페이지를 이미지로 변환하는 등의 기능을 구현할 때 사용할 수 있다.
일반적인 사용 방법은 비교적 간단하다.
PDF 문서를 로딩한 뒤 특정 페이지를 가져온다.
해당 페이지의 Viewport를 생성한다.
이후 Canvas에 페이지를 렌더링한다.
하지만 실제 내부에서는 훨씬 많은 작업이 발생한다.
PDF 파일의 내용을 분석해야 한다.
Font와 이미지를 처리해야 한다.
좌표계를 변환해야 한다.
그리고 최종적으로 Canvas 2D API를 통해 화면에 그래픽을 그려야 한다.
이번 글은 이러한 과정 가운데 getViewport()와 render()의 역할을 실제 소스코드 기준으로 분석한다.
getViewport()의 역할
PDF.js에서는 다음 메서드를 사용할 수 있다.
page.getViewport()
이 메서드는 PDF 페이지를 어떤 크기와 방향으로 표시할지 결정하기 위한 Viewport를 생성한다.
주요 인자는 다음과 같다.
scalerotationoffsetXoffsetYdontFlip
scale은 페이지를 얼마나 확대하거나 축소할지 결정한다.
rotation은 페이지의 회전 각도를 나타낸다.
offsetX와 offsetY는 화면에 배치할 위치를 조정한다.
dontFlip은 좌표계의 Y축 반전 여부와 관련된다.
반환된 Viewport에는 실제 렌더링 크기뿐 아니라 좌표 변환에 필요한 정보도 포함된다.
대표적인 값은 다음과 같다.
widthheighttransform
따라서 Viewport는 단순한 화면 크기 객체가 아니다.
PDF 좌표를 실제 화면의 Canvas 좌표로 변환하기 위한 계산 결과를 가진 객체다.
PDF 좌표계와 Canvas 좌표계의 차이
PDF와 Canvas는 일반적으로 서로 다른 좌표계를 사용한다.
PDF에서는 페이지의 왼쪽 아래를 기준으로 좌표를 표현하는 경우가 많다.
반면 Canvas에서는 일반적으로 왼쪽 위가 원점이다.
따라서 같은 좌표값을 그대로 사용하면 서로 다른 위치에 요소가 그려질 수 있다.
예를 들어 PDF에서 페이지 아래쪽에 존재하는 텍스트가 Canvas에서는 위쪽에 표시될 수 있다.
이를 올바르게 변환하려면 Y축 방향을 조정해야 한다.
또한 페이지를 회전해서 표시하는 경우에는 X축과 Y축의 관계도 달라질 수 있다.
예를 들어 페이지를 90도 회전하면 원래 가로 방향이었던 값이 세로 방향의 계산에 영향을 줄 수 있다.
PDF.js의 PageViewport는 이러한 문제를 해결하기 위해 Affine Transform Matrix를 생성한다.
Affine Transform Matrix
Affine Transform은 좌표에 다음과 같은 변환을 적용할 수 있는 수학적 구조다.
- 이동
- 확대 및 축소
- 회전
- 반전
Canvas에서는 일반적으로 여섯 개의 값으로 2D 변환을 표현할 수 있다.
다음과 같은 형태다.
[a, b, c, d, e, f]
이 여섯 값은 좌표의 변환 관계를 나타낸다.
PDF.js는 Viewport를 생성할 때 다음 정보를 계산에 반영한다.
- PDF 페이지의 원래 ViewBox
- 페이지의 중앙 위치
- 회전 각도
- Scale
- PDF의 User Unit
- 화면의 Offset
- Y축 반전 여부
이러한 계산 결과를 이용하면 PDF 내부 좌표를 Canvas의 좌표로 변환할 수 있다.
예를 들어 PDF 문서에서 텍스트가 위치한 좌표를 화면의 특정 픽셀 위치로 변환할 수 있다.
이는 단순한 렌더링 외에도 활용할 수 있다.
PDF 위에 주석을 표시하거나 클릭한 위치에 대응하는 PDF 좌표를 찾는 경우가 대표적이다.
90도 회전과 크기 계산
PDF.js의 Viewport는 페이지가 90도 또는 270도 회전한 경우에도 크기를 계산한다.
예를 들어 원래 페이지의 너비가 600이고 높이가 800이라고 가정해 보자.
페이지를 90도 회전하면 화면에서 차지하는 가로와 세로의 관계가 바뀐다.
Viewport는 이러한 관계를 계산한다.
따라서 개발자가 단순히 원래 페이지 크기에 Scale만 곱하는 방식보다 정확한 결과를 얻을 수 있다.
다만 PDF.js는 모든 회전 각도를 동일하게 처리하는 것이 아니다.
분석한 구현에서는 기본적으로 90도 단위의 회전을 처리한다.
유효하지 않은 각도가 전달되면 오류를 발생시킬 수 있다.
render()의 역할
Viewport가 준비되면 실제 페이지를 그리는 작업을 시작할 수 있다.
대표적인 API는 다음과 같다.
page.render()
이 메서드는 Canvas와 Viewport를 이용해 PDF 페이지를 렌더링한다.
하지만 호출 직후 모든 PDF 내용을 한 번에 Canvas에 그리는 것은 아니다.
실제 구현은 여러 단계로 구성된다.
가장 중요한 부분은 PDF 페이지를 해석하는 작업과 Canvas에 그리는 작업이 분리돼 있다는 사실이다.
PDF.js는 Web Worker를 이용해 PDF의 복잡한 내용을 처리할 수 있다.
Web Worker는 브라우저의 메인 스레드와 분리된 실행 환경이다.
이를 이용하면 PDF 문서의 일부 파싱 및 분석 작업을 메인 스레드에서 분리할 수 있다.
하지만 실제 Canvas에 그래픽을 그리는 작업은 렌더링 구조에 따라 메인 스레드에서 수행될 수 있다.
따라서 모든 PDF 관련 작업이 Worker에서 이루어지는 것은 아니다.
Operator List
PDF.js는 PDF 문서의 내용을 분석해 렌더링 명령의 목록을 생성한다.
이를 Operator List라고 한다.
예를 들어 PDF에는 다음과 같은 종류의 그래픽 작업이 포함될 수 있다.
- 특정 위치로 이동
- 선 그리기
- 사각형 채우기
- 텍스트 표시
- 이미지 배치
- 색상 변경
- 그래픽 상태 저장 및 복원
이러한 내용을 브라우저의 Canvas API로 처리하려면 PDF 내부 표현을 렌더링에 사용할 수 있는 명령으로 변환해야 한다.
Worker는 PDF 페이지를 분석해 이러한 Operator List를 생성한다.
이후 메인 스레드에서는 전달받은 명령을 실제 Canvas에 적용한다.
이 구조는 PDF의 복잡한 데이터 파싱과 화면 렌더링을 분리하는 데 도움이 된다.
GetOperatorList 메시지
글에서는 Worker와 메인 스레드 사이의 실제 메시지 흐름을 분석한다.
핵심 메시지는 다음과 같다.
GetOperatorList
메인 스레드는 PDF 페이지를 렌더링하기 위해 Worker에 Operator List를 요청한다.
Worker는 해당 메시지를 받으면 PDF 문서에서 필요한 페이지를 찾는다.
이후 페이지의 Content Stream과 Resource를 분석한다.
대표적인 내부 메서드는 다음과 같다.
Page.getOperatorList()
이 과정에서는 PartialEvaluator가 PDF의 내용을 해석한다.
그리고 렌더링에 사용할 Operator List를 생성한다.
중요한 점은 모든 Operator List가 완성될 때까지 메인 스레드가 반드시 기다릴 필요는 없다는 사실이다.
PDF.js는 Operator List의 일부를 Chunk 단위로 전달할 수 있다.
따라서 Worker가 명령을 생성하는 과정과 메인 스레드가 명령을 처리하는 과정이 서로 연결된다.
StartRenderPage 메시지
Worker는 페이지 렌더링을 시작할 준비가 되면 다음 메시지를 전달한다.
StartRenderPage
메인 스레드에서는 해당 메시지를 받아 렌더링 준비 상태를 변경한다.
이 과정은 내부 Promise와 연결된다.
대표적인 상태는 다음과 같다.
displayReadyCapability
렌더링에 필요한 초기 정보가 준비되면 해당 Promise가 완료된다.
메인 스레드는 이 정보를 바탕으로 실제 그래픽 처리를 시작할 수 있다.
하지만 이 시점에 PDF 페이지 전체의 렌더링이 완료된 것은 아니다.
PDF.js는 이후에도 필요한 Operator List를 계속 받아 처리할 수 있다.
따라서 렌더링을 시작할 준비가 완료된 상태와 실제 렌더링이 끝난 상태를 구분해야 한다.
InternalRenderTask
PDF.js는 내부적으로 InternalRenderTask를 생성한다.
이 객체는 실제 Canvas 렌더링을 관리한다.
대표적으로 다음 정보를 가진다.
- Canvas
- Canvas Context
- Viewport
- Transform
- Operator List
- 이미지 및 Font 관련 객체
- 렌더링 상태
- 실행 완료 및 오류 처리 정보
렌더링이 시작되면 CanvasGraphics를 생성한다.
CanvasGraphics는 Operator List에 포함된 그래픽 명령을 실제 Canvas 작업으로 처리하는 역할을 한다.
예를 들어 PDF에 포함된 텍스트나 이미지를 화면에 그리는 작업이 여기에서 수행될 수 있다.
렌더링 명령은 executeOperatorList()를 통해 처리된다.
따라서 전체 구조를 단순화하면 다음과 같다.
PDF 파일 → Worker에서 PDF 분석 → Operator List 생성 → 메인 스레드로 전달 → CanvasGraphics 실행 → Canvas에 화면 표시
하나의 Canvas를 동시에 여러 작업에서 사용하면 안 된다
이번 글에서 실제 프론트엔드 개발에 직접적으로 연결되는 부분은 Canvas의 사용 제한이다.
PDF.js의 InternalRenderTask는 이미 다른 렌더링 작업에서 사용 중인 Canvas를 확인한다.
같은 Canvas에 두 개의 렌더링 작업이 동시에 실행되면 오류를 발생시킬 수 있다.
오류 메시지의 핵심 내용은 다음과 같다.
Cannot use the same canvas during multiple render() operations.
예를 들어 React 컴포넌트에서 PDF 페이지를 렌더링한다고 가정해 보자.
사용자가 페이지 크기를 변경했다.
새로운 Scale로 다시 렌더링을 시작한다.
하지만 이전 렌더링 작업이 아직 종료되지 않았다.
이때 같은 Canvas를 다시 사용하면 문제가 발생할 수 있다.
따라서 새로운 렌더링을 시작하기 전에 이전 작업이 완료됐는지 확인하거나 기존 작업을 취소해야 한다.
이러한 문제는 React의 useEffect와 PDF.js의 비동기 렌더링을 연결할 때 발생할 수 있다.
특히 페이지 번호와 Scale이 빠르게 변경되는 PDF Viewer에서 중요하다.
Print 렌더링과 메모리 정리
PDF.js는 화면에 PDF를 표시하는 작업과 인쇄용 렌더링을 구분한다.
인쇄 작업에서는 여러 페이지를 높은 해상도로 처리할 수 있다.
따라서 메모리 사용량이 증가할 가능성이 있다.
분석한 구현에서는 인쇄 렌더링이 완료된 이후 관련 리소스를 더 적극적으로 정리하도록 처리한다.
예를 들어 intentPrint 상태를 기준으로 정리 작업을 예약할 수 있다.
하지만 단순히 페이지 렌더링이 완료됐다는 사실만으로 모든 PDF 관련 객체가 즉시 메모리에서 제거된다고 볼 수는 없다.
실제 정리 시점은 다른 렌더링 작업과 객체의 사용 상태에 영향을 받는다.
따라서 PDF Viewer에서 여러 페이지를 동시에 렌더링한다면 Canvas의 크기와 RenderTask, 페이지 캐시 등을 함께 관리해야 한다.
구현의 한계
이번 글은 PDF.js 6.3.289의 실제 소스코드를 기반으로 한다.
따라서 다른 PDF.js 버전에서는 일부 내부 메서드와 실행 흐름이 달라질 수 있다.
또한 PDF.js의 공개 API와 내부 구현은 구분해야 한다.
예를 들어 getViewport()와 render()는 일반적인 개발자가 사용할 수 있는 API다.
반면 _pumpOperatorList()나 InternalRenderTask는 라이브러리의 내부 구현이다.
이러한 내부 구현을 애플리케이션에서 직접 호출하도록 설계하는 것은 권장되지 않을 수 있다.
내부 구현은 이후 버전에서 변경될 수 있기 때문이다.
이번 글은 렌더링 속도를 정량적으로 비교한 Benchmark도 아니다.
대신 실제 소스코드를 따라가며 PDF 파싱과 화면 렌더링을 어떻게 분리하고, Worker와 메인 스레드가 어떤 메시지로 협력하는지를 설명한다.
특히 JavaScript 애플리케이션에서 Web Worker와 Canvas, 비동기 작업, 메모리 정리를 함께 이해하는 데 유용한 기술 자료다.
Your React API Layer Is a Mess. Let's Fix It with TanStack Query and tanstack-fetch
- 저자: Mohammad Garmabi
- 매체: DEV Community
- 게시일: 2026-10-26
- 원문: https://dev.to/mwmdgmb/your-react-api-layer-is-a-mess-lets-fix-it-with-tanstack-query-and-tanstack-fetch-409n
React 애플리케이션에서 API 통신 코드를 컴포넌트마다 직접 작성하는 방식에서 벗어나 HTTP Client와 서버 상태 관리, UI 표현을 서로 다른 계층으로 분리하는 방법을 설명한 글이다.
저자는 Next.js App Router 애플리케이션을 예제로 사용한다.
그리고 TanStack Query와 tanstack-fetch를 연결한다.
이번 글은 단순히 API 요청을 보내는 방법만 설명하지 않는다.
다음 문제를 함께 다룬다.
- 중복된 HTTP 요청 설정
- 인증 Header 관리
- HTTP 상태 코드별 오류 처리
- 요청 취소
- TanStack Query의 Cache
- Mutation 이후 상태 갱신
- Server Component에서의 데이터 조회
- SSR 데이터의 Hydration
핵심은 HTTP 통신과 서버 상태 관리는 서로 다른 책임이라는 사실이다.
처음에는 단순한 fetch()로 충분하다
작은 React 애플리케이션에서는 브라우저의 fetch()를 직접 호출할 수 있다.
예를 들어 사용자 목록을 가져오는 함수를 생각해 보자.
/api/users에 요청한다.- HTTP 응답을 확인한다.
- JSON을 읽는다.
- 결과를 화면에 표시한다.
이 구조는 단순하고 이해하기 쉽다.
하지만 애플리케이션이 성장하면 여러 요구사항이 추가된다.
예를 들어 모든 API 요청에 인증 정보를 포함해야 할 수 있다.
HTTP 401 응답이 발생하면 로그인 화면으로 이동해야 할 수 있다.
HTTP 403 응답은 접근 권한이 부족하다는 메시지로 처리해야 할 수 있다.
또한 같은 데이터를 여러 컴포넌트에서 사용할 수 있다.
각 컴포넌트가 개별적으로 API를 호출하면 중복 요청이 발생할 수 있다.
이러한 문제를 해결하기 위해 저자는 세 가지 역할을 구분한다.
세 가지 책임
| 계층 | 담당하는 역할 |
|---|---|
| HTTP Client | 요청 URL, Header, 인증, HTTP 오류 처리 |
| TanStack Query | 서버 데이터의 Cache, 갱신, 재시도, 상태 관리 |
| React Component | 로딩·오류·성공 상태의 화면 표시 |
이러한 분리는 중요하다.
예를 들어 HTTP Client는 서버가 반환한 JSON을 파싱하고 오류를 전달한다.
하지만 해당 데이터를 60초 동안 캐싱해야 하는지는 TanStack Query의 설정이 결정할 수 있다.
반면 사용자가 로딩 중에 Spinner를 볼지 Skeleton을 볼지는 React Component가 결정한다.
이렇게 하면 한 계층의 변경이 다른 계층에 불필요하게 영향을 주는 문제를 줄일 수 있다.
tanstack-fetch로 HTTP Client 생성
저자는 tanstack-fetch의 createFetch()를 이용해 공통 HTTP Client를 만든다.
이 Client에는 기본 API URL을 설정할 수 있다.
예를 들어 다음 환경 변수를 사용할 수 있다.
NEXT_PUBLIC_API_URL
이후 실제 API 요청에서는 공통 Client를 사용한다.
중요한 특징은 요청 결과로 HTTP Response 객체가 아니라 파싱된 데이터를 직접 반환할 수 있다는 점이다.
예를 들어 일반적인 Axios 코드에서는 다음과 같은 접근이 필요할 수 있다.
response.data
반면 예제의 tanstack-fetch는 데이터를 직접 반환하도록 구성한다.
이는 TanStack Query가 기대하는 Query Function의 반환 형태와 잘 맞는다.
TanStack Query는 일반적으로 다음 두 결과를 구분한다.
- Promise가 성공적으로 완료돼 데이터를 반환
- Promise가 실패해 오류를 발생
따라서 HTTP Client가 이러한 형태로 동작하면 React Component에서 HTTP Response 객체를 직접 다룰 필요가 줄어든다.
하지만 tanstack-fetch를 사용해야만 이러한 구조를 구현할 수 있는 것은 아니다.
일반적인 fetch()를 Wrapper 함수로 감싸도 비슷한 책임 분리가 가능하다.
저자는 자신의 라이브러리를 이용해 해당 구조를 구체적으로 설명한다.
Query Options를 컴포넌트 밖으로 분리
TanStack Query에서는 queryOptions()를 이용해 공통 조회 설정을 만들 수 있다.
예를 들어 사용자 목록을 조회하는 Query가 있다고 가정해 보자.
다음 정보를 정의할 수 있다.
- Query Key
- Query Function
- Stale Time
Query Key는 특정 서버 데이터를 식별한다.
예를 들어 다음과 같다.
["users"]
TanStack Query는 이 Key를 이용해 관련 데이터를 캐싱하고 조회한다.
Query Function은 실제 데이터를 가져오는 작업이다.
Stale Time은 데이터를 일정 시간 동안 Fresh 상태로 취급하기 위한 설정이다.
예를 들어 다음 값을 사용할 수 있다.
staleTime: 60_000
이는 가져온 데이터를 60초 동안 Fresh 상태로 간주한다는 의미다.
하지만 60초가 지나면 데이터가 즉시 삭제되는 것은 아니다.
Fresh 상태에서 Stale 상태로 변경되는 것이다.
이후 TanStack Query의 설정과 사용자 동작에 따라 다시 데이터를 가져올 수 있다.
저자는 이러한 Query Options를 React Component 안에 직접 작성하지 않고 별도의 모듈로 분리했다.
이렇게 하면 여러 컴포넌트와 서버 측 Prefetch 로직에서 동일한 Query 정의를 재사용할 수 있다.
AbortSignal 전달
이번 글은 요청 취소도 다룬다.
TanStack Query는 Query Function에 AbortSignal을 전달할 수 있다.
AbortSignal은 실행 중인 비동기 작업을 취소하도록 신호를 전달하는 Web API다.
예를 들어 사용자가 검색어를 빠르게 변경한다고 가정해 보자.
첫 번째 검색어로 요청을 보냈다.
그 직후 사용자가 새로운 검색어를 입력했다.
기존 검색 요청의 결과가 더 이상 필요하지 않을 수 있다.
이러한 경우 해당 요청을 취소하도록 구성할 수 있다.
저자는 TanStack Query가 제공하는 signal을 HTTP Client에 그대로 전달한다.
이렇게 하면 요청 취소 기능을 HTTP Client와 연결할 수 있다.
다만 AbortSignal이 전달됐다고 해서 서버에서 이미 시작한 모든 작업이 자동으로 취소되는 것은 아니다.
브라우저에서 네트워크 요청을 취소하더라도 서버는 이미 요청을 처리하고 있을 수 있다.
따라서 클라이언트 요청의 취소와 실제 서버 작업의 취소는 구분해야 한다.
HTTP 401과 403의 차이
저자는 HTTP 오류를 처리할 때 특히 401과 403을 구분해야 한다고 설명한다.
| 상태 코드 | 일반적인 의미 |
|---|---|
| 400 | 잘못된 요청 |
| 401 | 인증 필요 또는 인증 실패 |
| 403 | 접근 권한 부족 |
| 404 | 리소스를 찾을 수 없음 |
| 429 | 요청량 제한 |
| 500 | 서버 내부 오류 |
예를 들어 로그인하지 않은 사용자가 인증이 필요한 API를 호출하면 401 응답이 발생할 수 있다.
이 경우 로그인 화면으로 이동하거나 인증을 다시 시도하는 방식이 적합할 수 있다.
반면 로그인한 사용자가 권한이 없는 관리자 API를 호출하면 403 응답이 발생할 수 있다.
이 경우 사용자를 무조건 로그아웃시키는 것은 적절하지 않을 수 있다.
사용자의 인증 상태는 정상적이지만 해당 리소스에 접근할 권한이 없기 때문이다.
저자는 이러한 차이를 HTTP Client의 공통 오류 처리 계층에서 구분한다.
하지만 실제 애플리케이션에서는 API가 어떤 인증 체계를 사용하는지에 따라 처리 방법이 달라질 수 있다.
Server Component에서의 API 호출
Next.js App Router에서는 서버에서 데이터를 가져올 수 있다.
예를 들어 Server Component가 사용자 목록을 조회할 수 있다.
문제는 클라이언트에서 사용하는 API Client와 서버에서 사용하는 API Client의 실행 환경이 다르다는 점이다.
브라우저에서는 다음과 같은 공개 환경 변수를 사용할 수 있다.
NEXT_PUBLIC_API_URL
하지만 서버에서는 외부에 공개하지 않아야 하는 설정도 사용할 수 있다.
예를 들어 다음과 같은 환경 변수다.
API_URL
또한 서버에서는 사용자의 Cookie를 읽어 외부 API로 전달해야 할 수 있다.
저자는 이를 위해 별도의 Server API Client를 생성했다.
해당 파일에는 다음 Import를 사용한다.
import "server-only"
이는 서버 전용 모듈이 클라이언트 코드에 잘못 포함되는 문제를 방지하는 데 도움을 준다.
Cookie Forwarding
Next.js Server Component에서 사용자의 인증 상태를 바탕으로 외부 API를 호출할 수 있다.
예를 들어 사용자가 로그인한 상태로 /users 페이지를 열었다고 가정해 보자.
Next.js 서버는 사용자가 전달한 Cookie를 읽을 수 있다.
이후 인증이 필요한 외부 API에 관련 정보를 전달할 수 있다.
저자는 next/headers의 cookies()를 사용한다.
그리고 tanstack-fetch의 SSR 관련 기능을 이용해 필요한 Cookie를 전달한다.
이러한 과정은 편리하지만 보안상 주의가 필요하다.
사용자가 전달한 모든 Header를 외부 API에 그대로 전달하는 것은 적절하지 않을 수 있다.
특히 Cookie에는 인증 정보가 포함될 수 있다.
따라서 어떤 외부 API에 어떤 Cookie를 전달할지 명확하게 정의해야 한다.
또한 서버 전용 Secret을 NEXT_PUBLIC_ 환경 변수에 저장해서는 안 된다.
해당 Prefix를 가진 환경 변수는 클라이언트 코드에서 사용할 수 있도록 포함될 수 있기 때문이다.
SSR Prefetch와 Hydration
저자는 서버에서 TanStack Query의 Prefetch를 수행하는 방식도 설명한다.
서버는 먼저 데이터를 가져온다.
이후 해당 데이터를 Query Client의 Cache에 저장한다.
그리고 다음 함수를 이용해 캐시 상태를 직렬화할 수 있다.
dehydrate()
클라이언트에서는 HydrationBoundary를 이용해 서버에서 가져온 데이터를 TanStack Query의 상태로 복원할 수 있다.
이러한 구조에서는 사용자가 페이지를 처음 열었을 때 서버에서 준비한 데이터를 사용할 수 있다.
그리고 이후 클라이언트에서는 같은 Query Key를 기반으로 캐시를 관리할 수 있다.
하지만 서버와 클라이언트에서 사용하는 Query Key가 서로 다르면 같은 데이터를 별개의 Query로 취급할 수 있다.
따라서 공통 Query Options를 재사용하는 설계가 중요하다.
요청별 Query Client
이번 글에서 중요한 제한사항은 서버 측 Query Client의 수명이다.
예를 들어 서버가 다음과 같은 데이터를 조회한다고 가정해 보자.
- 사용자 A의 주문 목록
- 사용자 B의 주문 목록
만약 서버의 전역 Query Client가 사용자별 데이터를 구분하지 않고 캐싱한다면 잘못된 데이터를 반환할 위험이 생길 수 있다.
저자는 이를 방지하기 위해 서버의 요청마다 새로운 Query Client를 생성하도록 했다.
이렇게 하면 서로 다른 사용자의 서버 측 Query Cache가 불필요하게 공유되는 문제를 줄일 수 있다.
하지만 요청별 Query Client를 사용했다는 사실만으로 모든 권한 문제가 해결되는 것은 아니다.
실제 API 서버에서는 사용자가 해당 데이터를 조회할 권한을 가지고 있는지 검증해야 한다.
또한 클라이언트에서 사용하는 Query Key에도 필요한 식별 정보를 올바르게 포함해야 한다.
설계상의 트레이드오프
이번 글은 tanstack-fetch를 이용한 예제다.
따라서 모든 React 프로젝트에 해당 라이브러리를 도입해야 한다는 의미는 아니다.
예를 들어 API 요청이 적은 작은 프로젝트에서는 기본 fetch()만으로도 충분할 수 있다.
또한 Next.js Server Component에서는 기본 fetch()를 이용해 직접 데이터를 가져오는 방식이 더 간단할 수 있다.
반면 다음 요구사항이 증가한다면 공통 API 계층이 도움이 될 수 있다.
- 여러 API에 반복적으로 인증 정보 전달
- 통일된 HTTP 오류 처리
- 여러 컴포넌트의 Query 설정 공유
- 요청 취소
- SSR과 Client의 데이터 조회 구조 통합
다만 추상화 계층이 추가되면 새로운 라이브러리의 API와 버전 호환성을 관리해야 한다.
따라서 단순히 코드가 짧아진다는 이유만으로 새로운 HTTP Client를 도입하는 것이 항상 유리하지는 않다.
이번 글은 성능 개선을 정량적으로 비교한 Benchmark를 제공하지 않는다.
대신 React 애플리케이션의 서버 상태와 HTTP 통신, UI 표현을 어떤 경계로 분리해야 중복 코드를 줄이고 오류 처리를 일관되게 관리할 수 있는지 구체적인 Next.js 예제로 설명한다.
How do coding agents make TDD more practical?
- 저자: Chinwike Maduabuchi
- 매체: LogRocket
- 게시일: 2026-10-26
- 원문: https://blog.logrocket.com/agentic-tdd-coding-agents/
AI 코딩 에이전트가 테스트를 먼저 작성하고 실제 코드 구현과 검증을 반복하도록 구성한 뒤, 별도의 에이전트와 Mutation Testing을 이용해 테스트 자체의 품질을 검증하는 방법을 설명한 글이다.
이번 글은 단순히 AI에게 테스트 코드를 작성해 달라고 요청하는 방법을 소개하지 않는다.
저자는 Next.js 애플리케이션을 이용해 실제 사용자 흐름과 서버의 보안 규칙을 테스트한다.
그리고 테스트가 모두 통과한 이후에도 해당 테스트가 실제로 버그를 발견할 수 있는지 별도로 확인한다.
특히 테스트 통과율과 실제 테스트 품질을 구분해야 한다는 점을 구체적인 실험으로 보여준다.
Test-Driven Development
TDD(Test-Driven Development)는 실제 구현 코드를 작성하기 전에 테스트를 먼저 작성하는 개발 방법이다.
일반적으로 다음 세 단계를 반복한다.
| 단계 | 작업 |
|---|---|
| Red | 실패하는 테스트 작성 |
| Green | 테스트를 통과할 최소한의 코드 작성 |
| Refactor | 동작을 유지하면서 코드 구조 개선 |
첫 번째 단계에서는 테스트가 실제로 실패해야 한다.
예를 들어 사용자의 장바구니에 상품을 추가하는 기능을 개발한다고 가정해 보자.
아직 장바구니 기능이 존재하지 않는다면 해당 기능을 검증하는 테스트는 실패해야 한다.
이후 최소한의 코드를 작성해 테스트를 통과시킨다.
그리고 필요한 경우 코드 구조를 개선한다.
중요한 점은 테스트가 통과했다는 사실이 기능이 완성됐다는 보장은 아니라는 것이다.
예를 들어 장바구니가 비어 있을 때 총금액이 0이라는 테스트 하나만 존재한다고 가정해 보자.
구현 코드에서 언제나 0을 반환해도 해당 테스트는 통과한다.
하지만 실제 장바구니 계산 기능이 정상적으로 구현된 것은 아니다.
따라서 새로운 테스트를 추가해 더 많은 상황을 검증해야 한다.
AI 에이전트와 TDD의 역할 분리
저자는 AI 에이전트가 TDD의 반복 작업을 줄일 수 있다고 설명한다.
예를 들어 에이전트는 다음 작업을 수행할 수 있다.
- 테스트 코드 작성
- 테스트 실행
- 오류 메시지 분석
- 구현 코드 수정
- 테스트 재실행
- Lint와 타입 검사
- 코드 구조 개선
하지만 어떤 동작을 검증해야 하는지는 개발자가 먼저 결정해야 한다.
저자는 이를 위해 Test List를 작성한다.
Test List는 구현해야 할 기능과 검증해야 하는 상황을 정리한 목록이다.
예를 들어 장바구니 기능에는 다음 조건이 존재할 수 있다.
- 사용자가 자신의 상품을 제거할 수 있다.
- 다른 사용자의 상품은 제거할 수 없다.
- 재고가 없는 상품은 구매할 수 없다.
- 남은 재고보다 많은 수량을 주문할 수 없다.
이러한 요구사항을 먼저 정의한 뒤 에이전트가 각 항목을 테스트와 구현으로 연결하도록 한다.
이는 에이전트가 코드를 생성하면서 검증 기준까지 임의로 결정하는 문제를 줄이기 위한 것이다.
실제 테스트 프로젝트
저자는 Next.js 16 기반의 작은 온라인 쇼핑몰을 만들었다.
기술 구성은 다음과 같다.
| 영역 | 사용 기술 |
|---|---|
| 프론트엔드 | Next.js 16 |
| 데이터베이스 | PostgreSQL |
| 인증 | Better Auth |
| 단위 및 통합 테스트 | Vitest |
| React 컴포넌트 테스트 | Testing Library |
| 브라우저 테스트 | Playwright |
| Mutation Testing | StrykerJS |
테스트 프로젝트에는 의도적으로 보안 문제가 포함돼 있었다.
대표적인 문제가 다른 사용자의 장바구니 항목을 삭제할 수 있는 취약점이었다.
이는 BOLA(Broken Object Level Authorization)의 사례다.
BOLA는 사용자가 특정 리소스의 ID를 알고 있다는 이유만으로 해당 리소스에 접근하거나 변경할 수 있게 되는 문제다.
예를 들어 사용자 A가 자신의 장바구니 상품을 삭제한다고 가정해 보자.
서버는 삭제 대상의 ID를 전달받는다.
하지만 해당 상품이 실제로 사용자 A의 장바구니에 속하는지 확인하지 않는다면 사용자 B의 상품까지 삭제할 수 있을지 모른다.
이는 프론트엔드에서 삭제 버튼을 숨기는 것으로 해결되지 않는다.
사용자가 HTTP 요청을 직접 구성할 수 있기 때문이다.
따라서 서버에서 리소스의 소유권을 검증해야 한다.
BOLA를 발견한 테스트
저자는 다음 두 테스트를 작성하도록 했다.
첫 번째는 사용자가 자신의 장바구니 상품을 삭제할 수 있는지 확인하는 테스트다.
두 번째는 다른 사용자의 장바구니 상품을 삭제할 수 없는지 확인하는 테스트다.
에이전트는 테스트를 실행했다.
그리고 실제로 다른 사용자의 상품을 삭제할 수 있다는 사실을 확인했다.
이후 삭제 Query에 사용자 식별자 조건을 추가했다.
핵심 조건은 다음과 같다.
where: { id: itemId, userId }
이렇게 하면 단순히 상품 ID가 일치하는지만 확인하지 않는다.
삭제할 상품이 현재 사용자에게 속하는지도 함께 확인한다.
다만 실제 ORM과 데이터베이스의 Query 구조는 프로젝트마다 다를 수 있다.
중요한 것은 특정 코드 한 줄이 아니라 리소스 변경 시 소유권과 접근 권한을 서버에서 검증해야 한다는 원칙이다.
테스트가 처음부터 통과하면 어떻게 할까
TDD에서는 처음 작성한 테스트가 실제로 실패하는지 확인하는 과정이 중요하다.
하지만 에이전트가 테스트를 작성했는데 처음부터 통과할 수 있다.
예를 들어 이미 비슷한 기능이 다른 테스트를 위해 구현돼 있었을 수 있다.
저자의 실험에서도 이러한 상황이 발생했다.
에이전트는 단순히 테스트가 통과했으므로 작업을 완료했다고 판단하지 않았다.
대신 실제 구현을 일시적으로 변경해 테스트가 실패하는지 확인했다.
이후 원래 구현을 복구했다.
이렇게 하면 테스트가 실제로 해당 동작을 검사하고 있다는 사실을 확인할 수 있다.
예를 들어 인증 이후 Redirect 기능을 검사하는 테스트를 생각해 보자.
테스트가 통과했다고 하더라도 실제 Redirect 로직을 제거했을 때 여전히 통과한다면 해당 테스트는 기대한 기능을 제대로 검증하지 못하는 것이다.
저자는 이러한 상황을 False Red 또는 False Green과 관련된 문제로 설명한다.
핵심은 테스트가 실제로 검증 대상의 동작에 민감하게 반응하는지 확인하는 것이다.
Mutation Testing
이번 글에서 가장 중요한 부분은 Mutation Testing이다.
Mutation Testing은 실제 구현 코드에 작은 버그를 의도적으로 삽입한 뒤 기존 테스트가 해당 버그를 발견하는지 확인하는 방법이다.
예를 들어 다음과 같은 변경을 수행할 수 있다.
>를>=로 변경- 특정 함수 호출 제거
- 조건문 반전
- 반환값 변경
- Query의 조건 제거
이러한 변경을 Mutation이라고 한다.
의도적으로 잘못된 코드를 만들었으므로 정상적인 테스트라면 해당 변경을 발견해야 한다.
예를 들어 재고가 3개인 상품을 3개까지 장바구니에 추가할 수 있다고 가정해 보자.
테스트에서는 재고가 3개일 때 수량 3개가 허용되는지 확인해야 한다.
하지만 기존 테스트가 재고보다 큰 수량만 검사한다면 경계 조건을 놓칠 수 있다.
이때 비교 연산자를 >에서 >=로 변경해도 테스트가 통과할 수 있다.
Mutation Testing은 이러한 문제를 발견한다.
별도 감사 에이전트
저자는 구현 에이전트와 별도로 테스트를 검토하는 에이전트를 사용했다.
구현 에이전트는 기능을 개발하고 테스트를 통과시키는 역할을 담당한다.
감사 에이전트는 이미 작성된 테스트가 실제 버그를 발견할 수 있는지 검사한다.
저자는 감사 에이전트에게 다음 작업을 요청했다.
- 변경된 파일을 확인한다.
- Mutation Testing을 실행한다.
- 테스트를 통과하는 잘못된 변경을 찾는다.
- 부족한 검증 항목을 보고한다.
- 기존 테스트는 직접 수정하지 않는다.
이러한 구조는 코드 생성과 테스트 품질 검토의 책임을 분리한다.
다만 두 개의 AI 에이전트를 사용했다는 사실만으로 독립적인 검증이 완성되는 것은 아니다.
두 에이전트가 같은 잘못된 가정을 공유할 가능성도 존재한다.
따라서 실제 Mutation Testing 도구의 실행 결과와 사람의 검토가 함께 필요하다.
테스트가 놓친 세 가지 문제
저자의 Mutation Testing에서는 여러 문제가 발견됐다.
대표적인 사례는 다음과 같다.
첫 번째는 장바구니에서 상품 하나만 삭제하는 상황이다.
기존 테스트에서는 장바구니에 상품이 하나만 존재했다.
따라서 삭제 Query에서 상품 ID 조건을 제거해도 테스트가 통과할 수 있었다.
장바구니에 하나의 상품만 존재하기 때문에 한 개만 삭제한 것과 전체를 삭제한 것의 결과가 같았기 때문이다.
이를 해결하려면 장바구니에 여러 상품이 존재하는 상황을 검증해야 한다.
두 번째는 서버 API 호출의 누락이다.
UI에서는 Optimistic Update를 사용하고 있었다.
Optimistic Update는 서버 응답을 기다리지 않고 먼저 화면을 변경하는 방식이다.
따라서 실제 서버에 삭제 요청을 보내지 않더라도 화면에서는 상품이 삭제된 것처럼 보일 수 있었다.
기존 테스트는 화면에서 상품이 사라졌는지만 확인했다.
따라서 실제 API 호출이 제거돼도 테스트가 통과했다.
이를 해결하려면 페이지를 새로고침한 이후에도 삭제 결과가 유지되는지 확인하는 등의 검증이 필요하다.
세 번째는 재고 경계 조건이다.
재고가 3개인 상품을 3개 구매할 수 있는지 검사하지 않았다.
따라서 >를 >=로 변경해도 일부 테스트가 통과했다.
저자는 이러한 문제를 발견한 뒤 추가 테스트를 작성했다.
Mutation Score
저자는 새로 작성한 코드와 기존 코드의 Mutation Testing 결과도 비교했다.
결과는 다음과 같았다.
| 코드 범위 | Mutation Testing 결과 |
|---|---|
| 이번 TDD 과정에서 추가한 코드 | 100% |
| 기존 코드 일부 | 약 36~45% |
새롭게 작성한 코드에서는 실험한 Mutation을 모두 테스트가 발견했다.
반면 기존 코드에서는 많은 Mutation이 테스트를 통과했다.
이는 기존 코드의 테스트가 실제 동작을 충분히 검증하지 못하고 있었다는 신호다.
하지만 Mutation Score가 100%라는 사실이 코드에 버그가 전혀 존재하지 않는다는 의미는 아니다.
Mutation Testing 도구가 실제로 생성한 변경을 얼마나 발견했는지 나타내는 수치다.
도구가 생성하지 못한 종류의 버그나 외부 시스템의 문제까지 모두 검증하는 것은 아니다.
또한 이번 결과는 저자가 만든 작은 테스트 프로젝트의 결과다.
다른 프로젝트에서도 동일한 점수를 얻을 수 있다고 보장하지 않는다.
최종 테스트 구성
저자는 TDD와 감사 과정을 완료한 뒤 다음 테스트가 통과했다고 보고했다.
- Vitest 테스트 30개
- Playwright 테스트 9개
또한 pnpm guard를 실행해 프로젝트의 Lint와 구조적인 검사를 수행했다.
pnpm guard는 프로젝트에서 정의한 검증 명령이다.
일반적인 pnpm에 기본으로 존재하는 범용 보안 명령은 아니다.
저자는 해당 명령을 통해 타입 검사와 코드 품질 관련 규칙을 실행하도록 구성했다.
AGENTS.md와 코드 품질 규칙
저자는 AI 에이전트가 테스트를 통과하기 위해 기존 규칙을 약화시키는 문제도 다룬다.
예를 들어 함수가 너무 길어서 Lint 오류가 발생했다고 가정해 보자.
에이전트는 코드를 여러 함수로 분리할 수 있다.
하지만 더 쉬운 방법으로 Lint의 최대 함수 길이 제한을 증가시킬 수도 있다.
이렇게 하면 테스트와 Lint는 통과하지만 원래 코드 품질 규칙을 지키지 못한다.
저자는 이를 방지하기 위해 AGENTS.md에 규칙을 작성했다.
대표적으로 다음과 같은 내용이다.
- 테스트를 통과하기 위해 Assertion을 제거하지 않는다.
- Lint 설정을 임의로 완화하지 않는다.
- 필요 없는
any타입을 추가하지 않는다. - 검증 오류를 무시하는 주석을 추가하지 않는다.
- 규칙을 변경해야 한다면 이유를 설명한다.
하지만 이러한 지침을 작성했다는 사실만으로 모든 위반을 방지할 수 있는 것은 아니다.
따라서 실제 CI에서 타입 검사와 Lint, 의존성 규칙을 실행하도록 구성해야 한다.
이번 실험의 한계
저자는 AI 에이전트를 이용한 TDD가 반복적인 테스트 작성과 오류 수정 작업을 줄일 수 있다고 설명한다.
하지만 다른 개발 방법과 동일한 조건에서 실행 시간을 비교한 정량적인 Benchmark는 제공하지 않았다.
따라서 Agentic TDD가 일반적인 TDD보다 특정 배수만큼 빠르다고 단정할 수 없다.
또한 실험은 하나의 작은 Next.js 쇼핑몰을 대상으로 했다.
실제 대규모 시스템에서는 동시성 문제와 외부 API, 배포 환경, 데이터베이스 트랜잭션 등 더 복잡한 조건이 존재할 수 있다.
그럼에도 이번 글은 다음 세 가지를 구체적으로 구분한다.
첫 번째는 테스트가 존재하는가다.
두 번째는 테스트가 실제로 실행됐는가다.
세 번째는 테스트가 잘못된 구현을 발견할 수 있는가다.
이 세 가지는 서로 다른 문제다.
이번 글은 AI 코딩 에이전트가 테스트를 작성했다는 사실만으로 결과를 신뢰하는 대신, Mutation Testing과 별도의 검증 절차를 통해 테스트 자체의 품질을 확인하는 방법을 보여주는 실제 구현 사례다.