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

기준 시점: 2026-11-05 06:00 KST
조사 범위: 전일 2026-11-04 06:00 ~ 당일 2026-11-05 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
React, Next.js, Vercel, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro, Remix의 공식 발표와 릴리스 채널을 확인했다.
이번 기간에는 중요한 프론트엔드 프레임워크의 새로운 정식 버전이나 안정화된 렌더링 API 발표를 확인하지 못했다.
Next.js 16.5 계열의 Canary 업데이트도 검토했지만, 이전 브리핑에서 다룬 실험적 Navigation API와 React Runtime 변경을 반복하지 않았다.
Vite와 주요 프론트엔드 개발 도구의 공식 릴리스 기록에서도 이번 조사 기간에 해당하는 중요한 신규 정식 기능은 확인하지 못했다.
다만 프론트엔드 프로젝트의 실행과 배포에 영향을 줄 수 있는 새로운 발표는 존재했다.
Vercel은 Workflow SDK에서 동일한 작업이 중복 실행되지 않도록 하는 기능을 공개했다. 또한 Cloudflare는 AI Gateway에서 애플리케이션을 다시 배포하지 않고 AI 모델의 연결 대상을 변경할 수 있는 기능을 추가했다.
두 발표 모두 프론트엔드 프레임워크 자체의 변경은 아니므로 백엔드·인프라 섹션에서 다룬다.
이번 추천 글에서는 Next.js로 실제 서비스를 개발하면서 AI의 역할을 조정한 사례와 CSS 프레임워크의 유지보수 종료에 대응하는 마이그레이션 도구의 설계를 소개한다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome·Chromium, Firefox·Mozilla, WebKit·Safari의 공식 발표와 JavaScript, TypeScript, CSS, 웹 성능 및 접근성 관련 정보를 확인했다.
이번 24시간에 해당하는 중요한 웹 표준의 신규 채택이나 주요 브라우저 API의 정식 출시를 확인하지 못했다.
최근 공개된 Chrome 158 Beta와 Firefox 159 정식 릴리스는 이전 브리핑에서 이미 다뤘으므로 반복하지 않았다.
개발 빌드의 생성과 규격에 관한 논의도 새로운 안정 기능의 출시와 구분했다.
이번 추천 글에서는 기존 브라우저 기술을 실제 애플리케이션에 적용하면서 발견한 문제를 살펴본다.
특히 PDF.js로 추출한 날짜에 JavaScript의 Date를 적용할 때 발생할 수 있는 시간대 문제와, 브라우저의 Web Speech API에서 음성 재생 순서를 제어하는 문제를 다룬다.
3️⃣ 백엔드·인프라
Vercel Workflow SDK 5.3.1, 중복 작업 시작 방지 — Workflow 생성과 Hook 등록을 하나의 원자적 작업으로 처리
- 발표일: 2026-11-04 — 정확한 게시 시각 미확인
- 발표 주체: Vercel
- 버전: Workflow SDK 5.3.1
- 안정화 단계: 정식 SDK 업데이트. 신규 Hook 옵션은 Experimental API 명칭 사용
- 적용 대상: Workflow SDK, 서버리스 작업 실행, 이벤트 기반 비동기 처리
- 원문: https://vercel.com/changelog/atomic-start-hooks-in-the-workflow-sdk
Vercel이 Workflow를 시작하는 작업과 중복 실행을 방지하기 위한 Hook 등록을 하나의 원자적인 작업으로 처리하도록 Workflow SDK를 확장했다.
이번 변경은 일반적인 웹 애플리케이션에서도 발생할 수 있는 중복 요청 문제와 관련된다.
예를 들어 쇼핑몰에서 주문이 생성됐다고 가정해 보자.
서버는 주문을 처리하기 위한 Workflow를 시작한다.
Workflow는 다음 작업을 수행할 수 있다.
- 주문 정보 확인
- 결제 상태 확인
- 재고 예약
- 배송 작업 생성
- 사용자에게 결과 알림
문제는 동일한 주문에 대한 요청이 여러 번 들어올 수 있다는 점이다.
사용자가 버튼을 두 번 누를 수 있다.
브라우저가 네트워크 오류 이후 요청을 재시도할 수도 있다.
또한 외부 Webhook 제공업체가 같은 이벤트를 중복 전달할 수 있다.
이러한 상황에서 모든 요청이 새로운 Workflow를 생성한다면 같은 주문을 여러 번 처리할 위험이 있다.
기존 방식의 Race Condition
기존 구현에서는 먼저 해당 작업이 이미 존재하는지 확인한 다음 새로운 Workflow를 시작할 수 있었다.
대표적으로 다음 두 API를 조합했다.
getHookByToken()start()
예를 들어 주문 ID가 123이라고 가정해 보자.
첫 번째 요청은 order:123이라는 Token으로 이미 생성된 Hook이 있는지 확인한다.
존재하지 않으면 새로운 Workflow를 시작한다.
하지만 동시에 두 요청이 도착하면 다음 상황이 발생할 수 있다.
- 요청 A가 기존 Hook을 조회한다.
- 아직 Hook이 없어 조회 결과가 비어 있다.
- 요청 B도 기존 Hook을 조회한다.
- 요청 B 역시 Hook이 없다고 판단한다.
- 요청 A가 새로운 Workflow를 시작한다.
- 요청 B도 새로운 Workflow를 시작한다.
이렇게 하면 같은 주문에 대한 Workflow가 두 개 생성될 수 있다.
이러한 문제를 TOCTOU(Time-of-check to Time-of-use) Race Condition이라고 한다.
어떤 상태를 확인한 시점과 실제 변경 작업을 수행하는 시점 사이에 다른 작업이 개입할 수 있기 때문이다.
새로운 Atomic Start
이번 업데이트에서는 start()를 호출할 때 Hook Token을 함께 전달할 수 있다.
대표적인 설정은 다음과 같다.
experimental_hook: { token }
예를 들어 주문 ID를 이용해 다음 Token을 생성할 수 있다.
order:123
그리고 해당 Token을 새로운 Workflow의 시작 옵션에 포함한다.
Workflow SDK는 다음 두 작업을 원자적으로 처리한다.
- 새로운 Workflow 실행을 등록한다.
- 해당 Token을 새로운 실행에 연결한다.
따라서 같은 Token을 사용하는 두 요청이 동시에 도착하더라도 하나의 요청만 새로운 실행을 생성할 수 있다.
중복 요청의 처리
이미 사용 중인 Token으로 start()를 다시 호출하면 새로운 Workflow를 생성하지 않는다.
대신 다음 오류를 반환한다.
HookConflictError
이 오류에는 기존 실행을 식별하는 정보가 포함된다.
대표적인 속성은 다음과 같다.
conflictingRunId
따라서 애플리케이션은 오류를 일반적인 서버 장애로 취급하지 않고 이미 존재하는 작업을 반환할 수 있다.
예를 들어 다음과 같은 응답을 구성할 수 있다.
| 상황 | 응답 |
|---|---|
| 새로운 Workflow 생성 성공 | started: true |
| 동일한 Token의 Workflow가 이미 존재 | started: false |
| 새로운 실행 식별자 | runId |
| 중복 요청에서 기존 실행 식별자 | conflictingRunId 또는 기존 Hook의 runId |
이러한 방식은 클라이언트가 요청을 재시도하더라도 동일한 작업을 중복으로 생성하지 않도록 하는 데 도움이 된다.
중복 요청의 데이터를 기존 Workflow로 전달
이번 업데이트는 중복 실행을 차단하는 데서 끝나지 않는다.
이미 존재하는 Workflow에 추가 데이터를 전달할 수 있다.
대표적인 API는 다음과 같다.
resumeHook()
예를 들어 이미 주문 처리 Workflow가 시작됐다고 가정해 보자.
그런데 사용자가 같은 주문에 대한 추가 메모를 전달했다.
새로운 Workflow를 생성할 필요는 없다.
대신 해당 메모를 기존 Workflow에 전달하면 된다.
이때 다음 형태의 호출을 사용할 수 있다.
resumeHook(token, payload)
Workflow SDK는 해당 Token을 소유한 기존 실행을 찾는다.
그리고 전달한 데이터를 해당 실행의 Hook으로 보낸다.
Workflow가 아직 시작되지 않았어도 데이터 전달 가능
기존 구조에서는 Workflow 내부에서 Hook을 생성하기 전에 이벤트가 도착하면 처리 순서가 문제가 될 수 있었다.
하지만 새로운 Atomic Start에서는 Workflow 실행을 등록하는 순간 Hook도 함께 등록된다.
따라서 실제 Workflow 코드가 실행되기 전에도 resumeHook()을 호출할 수 있다.
이때 전달한 Payload는 Buffer에 저장된다.
이후 Workflow가 실행되면 저장된 Payload를 순서대로 전달할 수 있다.
Workflow 내부에서는 다음 API를 사용할 수 있다.
createHook({ token })
이미 시작 단계에서 등록한 Token을 전달하면 기존 Hook에 연결된다.
새로운 Hook을 중복 생성하지 않는다.
이러한 구조는 외부 이벤트가 실제 작업 실행보다 먼저 도착하는 상황에서 유용하다.
Idempotency Key로만 사용 가능
이번 기능에서 Hook을 반드시 이벤트 수신 용도로 사용할 필요는 없다.
Token을 중복 실행 방지에만 사용할 수도 있다.
예를 들어 주문 처리 Workflow가 사용자로부터 추가 데이터를 받지 않는다고 가정해 보자.
이 경우 Workflow 내부에서 createHook()을 호출하지 않아도 된다.
시작할 때 지정한 Token이 Idempotency Key 역할을 수행한다.
따라서 동일한 Token으로 새로운 실행을 만들지 못하도록 제한할 수 있다.
experimental_minRetention
Workflow가 완료된 이후에도 중복 요청이 도착할 수 있다.
예를 들어 주문 처리가 완료된 뒤 외부 서비스가 이전 Webhook을 다시 전달할 수 있다.
이러한 상황에서는 완료된 Workflow의 Token을 일정 시간 유지해야 한다.
이번 SDK에서는 다음 설정을 제공한다.
experimental_minRetention
이 옵션은 실행이 완료된 이후에도 해당 Token을 사용할 수 없도록 유지하는 최소 기간을 제어한다.
기간은 Workflow를 시작한 시점을 기준으로 계산된다.
따라서 실제 서비스의 중복 이벤트 발생 특성에 맞춰 설정할 수 있다.
하지만 Token의 보존 기간이 종료된 이후에는 같은 Token을 사용하는 새로운 실행이 가능해질 수 있다.
영구적인 중복 방지가 필요하다면 실제 데이터베이스의 고유 제약이나 비즈니스 상태 검증을 함께 고려해야 한다.
정확히 한 번 실행과 외부 작업의 차이
이번 기능은 하나의 Token에 대해 새로운 Workflow 실행이 중복 생성되는 문제를 해결한다.
하지만 Workflow 내부의 모든 외부 작업이 정확히 한 번만 실행된다는 의미는 아니다.
예를 들어 Workflow가 외부 결제 API를 호출했다고 가정해 보자.
결제는 성공했다.
하지만 Workflow가 성공 결과를 기록하기 전에 실행 환경이 종료됐다.
이후 해당 작업을 재시도하면 외부 결제가 다시 실행될 가능성이 있다.
따라서 외부 API 호출에는 별도의 Idempotency Key가 필요할 수 있다.
Workflow 시작 단계의 중복 방지와 실제 비즈니스 작업의 중복 방지는 구분해야 한다.
적용 범위와 한계
새로운 기능을 사용하려면 다음 버전이 필요하다.
workflow@5.3.1
이번 발표에는 중복 요청의 최대 처리량이나 Atomic Start 도입 전후의 지연 시간에 관한 Benchmark가 포함되지 않았다.
따라서 이번 변경으로 모든 Workflow 실행이 빨라졌다고 판단할 수 없다.
이번 업데이트는 서버리스 환경에서 중복 요청과 이벤트를 처리할 때 발생하는 Race Condition을 줄이고, 기존 실행에 안전하게 추가 메시지를 전달할 수 있도록 만든 Workflow 실행 모델의 개선이다.
Cloudflare AI Gateway, Model Alias 지원 — 여러 AI 모델을 하나의 이름으로 호출하고 장애 시 자동 전환
- 발표일: 2026-11-04 — 정확한 게시 시각 미확인
- 발표 주체: Cloudflare
- 안정화 단계: 정식 기능
- 적용 대상: Cloudflare AI Gateway
- 주요 기능: Model Alias, 순차적 Fallback, Smart Routing
- 원문: https://developers.cloudflare.com/changelog/post/2026-11-04-model-aliases/
- 공식 문서: https://developers.cloudflare.com/ai-gateway/features/routing/model-aliases/
Cloudflare가 여러 AI 모델을 하나의 논리적인 이름으로 묶고 애플리케이션에서 해당 이름만 호출하도록 하는 Model Alias 기능을 추가했다.
이번 기능은 AI 모델 자체의 성능을 개선하는 업데이트가 아니다.
애플리케이션이 어떤 모델을 호출할지 결정하는 방식을 변경한 것이다.
예를 들어 웹 애플리케이션에서 고객의 문의에 답변하는 AI 기능을 제공한다고 가정해 보자.
기존에는 백엔드 코드에 특정 AI 모델의 이름을 직접 작성할 수 있었다.
하지만 해당 모델에 장애가 발생할 수 있다.
가격이 변경될 수도 있다.
또는 더 적합한 새로운 모델이 출시될 수 있다.
이때 모델의 이름이 코드에 직접 포함돼 있다면 모델을 변경하기 위해 애플리케이션을 다시 배포해야 할 수 있다.
새로운 Model Alias는 이러한 의존성을 줄인다.
Model Alias의 구조
Cloudflare AI Gateway에서는 다음과 같은 Alias를 생성할 수 있다.
custom/support-fast
이 Alias에는 여러 모델을 등록할 수 있다.
예를 들어 다음과 같은 Pool을 구성할 수 있다.
- 기본 응답 모델 A
- Fallback 모델 B
- 추가 Fallback 모델 C
애플리케이션은 실제 모델 이름을 사용할 필요가 없다.
대신 다음 값만 전달한다.
model: "custom/support-fast"
Cloudflare AI Gateway는 Alias의 설정에 따라 실제 모델을 선택한다.
따라서 애플리케이션과 모델 제공업체 사이에 별도의 라우팅 계층을 두는 구조다.
순차적 Fallback
첫 번째 라우팅 방식은 순차적인 Fallback이다.
예를 들어 AI 모델 A를 먼저 호출한다고 가정해 보자.
모델 A의 요청이 실패하면 다음 모델 B를 시도한다.
모델 B도 실패하면 설정된 다음 모델을 시도할 수 있다.
이러한 방식은 특정 모델 제공업체에 장애가 발생했을 때 다른 모델을 사용할 수 있도록 한다.
하지만 Fallback이 존재한다고 해서 모든 실패가 자동으로 정상적인 답변으로 복구되는 것은 아니다.
예를 들어 이미 사용자에게 스트리밍 응답의 일부를 전달한 이후 오류가 발생했다면 일반적인 요청 실패와 다르게 처리해야 할 수 있다.
또한 모델마다 다음 특성이 다를 수 있다.
- 출력 형식
- Tool Calling 지원 범위
- Context Window
- 응답 품질
- 응답 시간
- 비용
따라서 단순히 모델을 대체할 수 있다는 사실만으로 모든 모델이 동일한 애플리케이션 계약을 충족한다고 보장할 수 없다.
Smart Routing
두 번째 방식은 Smart Routing이다.
이 방식에서는 AI Gateway가 요청마다 사용할 모델을 선택한다.
따라서 항상 첫 번째 모델을 호출한 뒤 실패했을 때만 다음 모델을 사용하는 구조와 다르다.
예를 들어 하나의 Alias에 여러 모델을 등록한 뒤 Routing 정책에 따라 실제 모델을 선택할 수 있다.
하지만 이번 발표에는 Smart Routing을 사용했을 때 비용이나 품질이 얼마나 개선되는지에 관한 정량적인 Benchmark가 포함되지 않았다.
또한 선택 알고리즘의 모든 내부 판단 기준이 공개된 것은 아니다.
따라서 특정 요청이 반드시 특정 모델로 전달된다고 가정해서는 안 된다.
REST API에서 호출
Cloudflare AI Gateway의 REST API에서는 다음 Endpoint를 사용한다.
/accounts/{account_id}/ai/run
요청 본문에는 Alias를 모델 이름으로 전달한다.
대표적인 값은 다음과 같다.
"model": "custom/support-fast"
실제 Gateway를 지정하려면 다음 Header를 사용한다.
cf-aig-gateway-id
여기에는 Model Alias를 생성한 Gateway의 ID를 전달한다.
Alias는 개별 Gateway에 속한다.
따라서 다른 Gateway에 같은 이름의 Alias가 존재하지 않는다면 동일한 이름으로 요청하더라도 정상적으로 처리되지 않을 수 있다.
배포 없이 모델 변경
이번 기능의 중요한 특성은 모델 구성을 애플리케이션 코드와 분리한다는 점이다.
예를 들어 운영 중인 AI 기능의 모델 A가 느려졌다고 가정해 보자.
관리자는 AI Gateway의 Routing 설정에서 모델의 우선순위를 변경할 수 있다.
애플리케이션에서는 여전히 다음 Alias를 사용한다.
custom/support-fast
따라서 실제 모델 목록을 변경하기 위해 애플리케이션을 다시 배포할 필요가 줄어든다.
이는 서버리스 및 프론트엔드 애플리케이션에서 환경 변수나 모델 이름을 직접 수정하는 방식과 다르다.
장애 복구와 비용의 관계
Fallback에는 추가 비용이 발생할 가능성이 있다.
예를 들어 모델 A가 요청을 일부 처리한 이후 실패했다고 가정해 보자.
그다음 모델 B를 호출하면 추가적인 모델 요청이 발생한다.
이 경우 모델 제공업체의 과금 정책에 따라 실제 사용 비용이 증가할 수 있다.
또한 여러 모델을 순차적으로 시도하면 최종 응답까지 걸리는 시간이 증가할 수 있다.
따라서 Fallback은 서비스의 가용성을 높이는 수단이지만 모든 상황에서 응답 시간을 줄이는 기능은 아니다.
이번 발표는 AI 모델의 실제 선택과 장애 대응 정책을 애플리케이션 배포와 분리하고, 하나의 Alias로 여러 모델을 관리하도록 만든 AI 인프라 기능이다.
Cloudflare Private Load Balancer, Session Affinity 지원 — 사용자별 요청을 동일한 내부 서버로 전달
- 발표일: 2026-11-04 — 정확한 게시 시각 미확인
- 발표 주체: Cloudflare
- 안정화 단계: 정식 기능
- 적용 대상: Cloudflare One Private Load Balancer
- 연결 방식: Cloudflare Tunnel, Mesh, WAN
- 주요 기능: Hash Steering, Session Affinity
- 원문: https://developers.cloudflare.com/changelog/post/2026-11-04-private-lb-session-affinity/
Cloudflare가 Private Load Balancer에서 동일한 사용자의 요청을 가능한 한 동일한 Endpoint로 전달하는 Session Affinity 기능을 추가했다.
Load Balancer는 여러 서버에 요청을 분배하는 역할을 한다.
예를 들어 사내 시스템이 세 개의 서버에서 실행되고 있다고 가정해 보자.
사용자는 하나의 도메인이나 IP 주소로 서비스에 접근한다.
Load Balancer는 요청을 세 서버 가운데 하나로 전달한다.
이러한 구조는 서버의 부하를 분산하고 장애에 대응하는 데 도움이 된다.
하지만 모든 애플리케이션이 서버 사이에서 상태를 완전히 공유하는 것은 아니다.
서버 메모리에 Session을 저장하는 경우
예를 들어 사용자가 사내 애플리케이션에 로그인했다고 가정해 보자.
서버 A는 해당 사용자의 Session 상태를 메모리에 저장한다.
이후 사용자의 다음 요청이 서버 B로 전달된다.
하지만 서버 B에는 해당 Session이 존재하지 않을 수 있다.
그 결과 사용자가 다시 로그인해야 하는 상황이 발생할 수 있다.
일반적으로는 Redis나 데이터베이스 같은 공유 저장소에 Session을 저장하는 방법을 사용할 수 있다.
하지만 기존 시스템이나 특정 내부 애플리케이션에서는 서버별로 상태를 유지하는 구조가 존재할 수 있다.
Session Affinity는 이러한 환경에서 동일한 사용자의 요청을 같은 서버로 전달하도록 지원한다.
Hash Steering
첫 번째 방식은 Hash 기반 Endpoint Steering이다.
Dashboard에서 Pool의 Endpoint Steering을 다음 값으로 설정한다.
Hash
이 방식에서는 사용자별 정보를 기반으로 Endpoint를 선택한다.
따라서 같은 사용자가 다시 요청하면 같은 Endpoint로 전달될 가능성이 높아진다.
이러한 구조는 단순한 Round-robin 방식과 다르다.
Round-robin은 요청을 여러 서버에 순서대로 분배할 수 있다.
반면 Hash Steering은 사용자와 Endpoint 사이의 관계를 유지하는 데 초점을 맞춘다.
Session Affinity
두 번째 방식은 API를 이용하는 것이다.
다음 값을 설정할 수 있다.
session_affinity: ip_cookie
이 설정을 사용하면 사용자가 선택된 Endpoint에 계속 연결되도록 구성할 수 있다.
다만 Session Affinity는 무조건 영구적으로 유지되는 연결이 아니다.
공식 문서는 다음 조건을 명시한다.
- Endpoint가 정상 상태여야 한다.
- Load Balancer의 Pool 구성이 변경되지 않아야 한다.
Endpoint가 비정상 상태가 되면 다른 Endpoint로 요청을 전달할 수 있다.
또한 Pool의 구성이 변경되면 기존 사용자와 Endpoint 사이의 매핑도 달라질 수 있다.
따라서 Session Affinity만으로 서버 메모리에 저장한 중요한 데이터의 안전성을 보장할 수는 없다.
실제로 보존해야 하는 Session 데이터는 별도의 지속 저장소에 저장하는 편이 적합할 수 있다.
Cloudflare One Client와 IP 변경
이번 기능에서 중요한 특징은 사용자를 식별하는 방법이다.
Cloudflare One Client에서 발생한 요청은 기기를 기준으로 사용자를 식별한다.
따라서 사용자의 IP 주소가 변경되더라도 같은 Endpoint를 유지할 수 있다.
예를 들어 사용자가 회사 Wi-Fi에서 모바일 핫스팟으로 연결을 변경했다고 가정해 보자.
외부 IP 주소는 달라질 수 있다.
하지만 동일한 Cloudflare One Client의 기기라면 기존 Endpoint에 계속 연결될 수 있다.
반면 Cloudflare WAN 등 다른 방식으로 연결된 트래픽에서는 Source IP 주소를 기준으로 분배할 수 있다.
따라서 실제 Session Affinity의 유지 조건은 연결 방식에 따라 달라진다.
Endpoint의 식별 방식
Cloudflare는 Endpoint를 다음 두 정보의 조합으로 식별한다.
- IP 주소
- Virtual Network
예를 들어 서로 다른 Virtual Network 안에 같은 사설 IP 주소가 존재할 수 있다.
이때 IP 주소만으로 Endpoint를 식별하면 서로 다른 서버가 같은 대상으로 취급될 수 있다.
따라서 Virtual Network 정보도 함께 사용한다.
또한 이번 Affinity는 실제 Endpoint에 적용되는 것이지 특정 cloudflared 또는 Mesh Replica에 고정되는 것은 아니다.
따라서 연결을 전달하는 중간 Replica가 변경되더라도 실제 대상 서버를 유지할 수 있다.
적용 범위와 한계
이번 기능은 다음 연결에서 사용할 수 있다.
- Cloudflare Tunnel
- Cloudflare Mesh
- Cloudflare WAN
공식 발표에는 Session Affinity를 활성화한 이후 응답 시간이나 서버의 처리량이 얼마나 변경되는지에 관한 Benchmark가 포함되지 않았다.
또한 Session Affinity는 부하 분산의 균형에 영향을 줄 수 있다.
예를 들어 특정 Endpoint에 많은 사용자가 고정되면 일부 서버에 부하가 집중될 수 있기 때문이다.
이번 업데이트는 서버별로 Session을 유지하는 내부 애플리케이션에서 사용자와 Endpoint의 연결 관계를 유지하도록 Private Load Balancer의 분배 정책을 확장한 기능이다.
Amazon RDS for Oracle, 리전 간 Snapshot 복사 시 Option Group 동시 복사 지원
- 발표일: 2026-11-04 — 정확한 게시 시각 미확인
- 발표 주체: AWS
- 안정화 단계: 정식 기능
- 적용 대상: Amazon RDS for Oracle
- 주요 API:
CopyDBSnapshot - 관련 파라미터:
CopyOptionGroup - 지원 리전: AWS 상용 리전 및 GovCloud(US)
- 원문: https://aws.amazon.com/about-aws/whats-new/2026/11/amazon-rds-oracle-cross-region-snapshot-copy-option-group/
AWS가 RDS for Oracle의 Snapshot을 다른 리전으로 복사하면서 해당 Snapshot과 연결된 Option Group도 함께 복사할 수 있도록 기능을 확장했다.
RDS는 관계형 데이터베이스를 관리형 서비스로 제공한다.
사용자는 데이터베이스 서버의 일부 운영 작업을 AWS에 맡길 수 있다.
하지만 데이터베이스의 설정과 확장 기능은 엔진과 서비스에 따라 별도로 관리해야 한다.
Oracle에서는 이러한 설정을 관리하기 위해 Option Group을 사용할 수 있다.
Option Group의 역할
Option Group은 RDS for Oracle에서 특정 데이터베이스 기능을 활성화하고 관리하는 데 사용된다.
예를 들어 데이터베이스에 필요한 추가 기능이나 설정을 Option Group으로 관리할 수 있다.
문제는 Option Group이 AWS 리전별 리소스라는 점이다.
예를 들어 서울 리전에서 사용하는 Option Group이 있다고 가정해 보자.
해당 Option Group은 서울 리전의 리소스다.
따라서 다른 리전에 데이터베이스를 복구할 때 동일한 설정이 자동으로 존재한다고 가정할 수 없다.
기존 Snapshot 복사 과정
예를 들어 서울 리전의 데이터베이스 Snapshot을 도쿄 리전으로 복사한다고 가정해 보자.
기존에는 다음 작업이 필요할 수 있었다.
- 원본 데이터베이스 Snapshot 생성
- 다른 리전으로 Snapshot 복사
- 대상 리전에 필요한 Option Group 생성
- 원본 Option Group의 설정 재구성
- 해당 Option Group을 이용해 데이터베이스 복원
이러한 과정에서는 Snapshot의 데이터와 데이터베이스 설정이 별도로 관리됐다.
특히 여러 리전에서 동일한 설정을 유지해야 하는 시스템에서는 관리 작업이 증가할 수 있었다.
새로운 CopyOptionGroup
이번 업데이트에서는 Snapshot을 복사할 때 다음 파라미터를 사용할 수 있다.
CopyOptionGroup: true
이 값은 CopyDBSnapshot API에서 사용한다.
또한 CLI 명령에서도 동일한 기능을 사용할 수 있다.
이렇게 하면 Snapshot과 연결된 Option Group을 대상 리전에 함께 복사할 수 있다.
따라서 대상 리전에서 Option Group을 수동으로 다시 생성하는 작업을 줄일 수 있다.
Cross-region과 Cross-account
이번 변경은 리전 간 복사를 지원한다.
또한 계정 간 Snapshot 복사에도 사용할 수 있다.
예를 들어 운영 계정과 재해 복구 계정을 분리한 상황을 생각할 수 있다.
운영 계정에서 Snapshot을 생성한다.
이후 다른 계정과 리전으로 복사한다.
이 과정에서 필요한 Option Group도 함께 복사할 수 있다.
하지만 Snapshot을 복사했다는 사실만으로 데이터베이스의 실제 복구와 서비스 전환이 자동으로 완료되는 것은 아니다.
실제로 데이터베이스를 복원하고 애플리케이션의 연결 정보를 변경하는 작업은 별도로 필요할 수 있다.
적용 범위와 한계
이번 기능은 Amazon RDS for Oracle에 관한 것이다.
다른 데이터베이스 엔진의 Snapshot 복사에서도 동일한 방식으로 Option Group을 처리한다고 가정해서는 안 된다.
또한 Option Group을 함께 복사하는 것이므로 대상 리전에서 실제 데이터베이스를 실행하기 위한 다른 설정과 리소스는 별도로 확인해야 한다.
이번 발표에는 Snapshot 복사 시간이 얼마나 감소하는지에 관한 정량적인 Benchmark가 포함되지 않았다.
리전 또는 계정 간 데이터베이스 복구 과정에서 Snapshot의 데이터와 필요한 Option Group을 하나의 복사 작업으로 관리할 수 있도록 한 운영 기능 개선이다.
4️⃣ 개발 도구 및 보안
GitHub CodeQL 2.27.4 공개 — Java·Kotlin 보안 분석 오탐 감소와 CodeQL Pack의 심볼릭 링크 검사 강화
- 발표일: 2026-11-04 — 정확한 게시 시각 미확인
- 버전: CodeQL 2.27.4
- 안정화 단계: 정식 릴리스
- 적용 대상: GitHub Code Scanning, CodeQL CLI, Custom Query
- 변경 유형: 보안 분석 정확도 개선, 메타데이터 수정, CLI 보안 검사
- 원문: https://github.blog/changelog/2026-11-04-codeql-2-27-4-improves-accuracy-and-query-metadata/
GitHub가 CodeQL 2.27.4를 공개하면서 여러 언어에서 발생하는 잘못된 보안 경고를 줄이고 CodeQL Pack을 배포하는 과정의 파일 검사를 강화했다.
이번 릴리스는 전날 공개된 2.27.3과 별개의 업데이트다.
2.27.3에서는 JSON Data Extension과 AI Threat Model 지원 등이 추가됐다.
이번 2.27.4는 주로 기존 보안 분석의 정확성과 CLI의 동작을 개선한다.
CodeQL의 데이터 흐름 분석
CodeQL은 소스코드의 구조와 데이터 흐름을 분석한다.
예를 들어 사용자가 입력한 문자열이 로그에 전달되는 상황을 생각할 수 있다.
외부 사용자는 개행 문자나 특수문자를 포함한 값을 전달할 수 있다.
이러한 값이 검증 없이 로그에 포함되면 로그의 구조를 변경하거나 실제 기록을 혼동하게 만들 가능성이 있다.
이를 Log Injection이라고 한다.
CodeQL은 외부 입력이 어떤 함수를 거쳐 로그에 전달되는지 분석할 수 있다.
하지만 실제 애플리케이션에서 적절한 Sanitizer를 사용하고 있어도 CodeQL이 해당 함수를 인식하지 못하면 잘못된 경고를 발생시킬 수 있다.
이번 업데이트는 이러한 상황을 개선한다.
Java·Kotlin의 Log Injection 분석
이번 변경에서는 다음 Query가 더 많은 Sanitizer를 인식하도록 개선됐다.
java/log-injection
새롭게 인식하는 범위에는 다음 라이브러리가 포함된다.
- JDK
- Apache Commons Lang
- Apache Commons Text
- Guava
- OWASP Java Encoder
- Spring Framework
예를 들어 외부 입력에 포함된 위험한 문자를 적절하게 처리한 뒤 로그에 기록한다고 가정해 보자.
기존에는 CodeQL이 해당 처리를 충분히 인식하지 못할 수 있었다.
새로운 버전에서는 관련 Sanitizer와 Guard를 더 정확하게 이해할 수 있다.
따라서 실제로 보호된 코드에서 불필요한 경고가 발생하는 상황을 줄일 수 있다.
하지만 Sanitizer를 사용했다는 사실만으로 모든 종류의 Injection이 방지된다는 의미는 아니다.
실제 보호 범위는 사용하는 함수와 출력 대상의 특성에 따라 달라진다.
Path Injection 분석
Path Injection은 외부 입력을 파일 경로에 사용할 때 발생할 수 있는 문제다.
예를 들어 사용자가 다음 값을 전달했다고 가정해 보자.
../../secret.txt
서버가 해당 값을 검증 없이 파일 경로에 사용한다면 의도하지 않은 파일에 접근할 가능성이 있다.
CodeQL은 이러한 입력이 파일 접근 함수에 전달되는 경로를 분석한다.
하지만 실제 애플리케이션에서는 문자열을 검사하거나 위험한 부분을 제거할 수 있다.
이번 버전에서는 다음 처리를 더 정확하게 인식한다.
- Substring 기반 Guard
- 문자열 치환
- 문자열 일부 제거
- Apache Commons Lang 및 Guava의 관련 함수
이렇게 하면 실제로 입력을 제한하는 코드를 잘못된 취약점으로 보고하는 상황을 줄일 수 있다.
다만 문자열에서 ../만 제거하는 방식이 모든 Path Traversal 공격을 방지한다는 의미는 아니다.
운영체제의 경로 정규화와 인코딩, 심볼릭 링크 등의 영향을 함께 고려해야 한다.
Rust의 오탐 감소
Rust에서는 다음 Query의 False Positive를 줄였다.
rust/uncontrolled-allocation-sizerust/log-injectionrust/cleartext-logging
첫 번째 Query는 외부 입력에 따라 지나치게 많은 메모리를 할당할 수 있는 상황과 관련된다.
두 번째는 로그의 내용이 조작되는 문제다.
세 번째는 민감한 정보가 일반 텍스트로 기록되는 문제다.
이번 수정은 해당 Query들이 정상적인 코드를 잘못 보고하는 일부 상황을 줄이려는 목적이다.
공식 발표에는 False Positive가 몇 퍼센트 감소했는지에 관한 정량적인 수치는 포함되지 않았다.
C와 C++ 분석
C와 C++에서도 일부 수정이 이루어졌다.
cpp/suspicious-pointer-scaling은 std::byte*를 이용한 의도적인 Pointer Arithmetic을 잘못 보고하지 않도록 변경됐다.
또한 다음 Query는 [[maybe_unused]]가 지정된 변수를 불필요한 변수로 보고하지 않도록 수정됐다.
cpp/unused-local-variablecpp/unused-static-variable
[[maybe_unused]]는 변수가 특정 구성에서 사용되지 않을 수 있다는 사실을 컴파일러와 분석 도구에 알려주는 Attribute다.
따라서 해당 Attribute가 존재하는 변수를 일률적으로 사용하지 않는 변수라고 판단하는 것은 적절하지 않을 수 있다.
JavaScript·TypeScript의 CWE 메타데이터 수정
이번 변경에서 웹 개발과 관련된 부분은 CWE 메타데이터다.
CodeQL은 다음 JavaScript·TypeScript Query에서 기존 CWE-730 태그를 제거했다.
js/redosjs/polynomial-redosjs/regex-injectionjs/regex-injection-more-sourcesjs/server-crashjs/loop-bound-injection
CWE는 소프트웨어의 보안 약점을 분류하기 위한 체계다.
Query에 잘못된 CWE 태그가 연결돼 있으면 실제 분석 결과의 분류가 부정확해질 수 있다.
이번 변경은 해당 메타데이터를 수정한 것이다.
따라서 ReDoS나 Regex Injection 취약점이 모두 해결됐다는 의미는 아니다.
또한 해당 Query가 완전히 제거됐다는 의미도 아니다.
잘못 연결된 CWE 태그를 수정한 것이다.
CodeQL Pack의 심볼릭 링크 검사
이번 릴리스에서는 CodeQL Pack을 Bundle로 만들거나 게시할 때 심볼릭 링크를 검사한다.
심볼릭 링크는 파일시스템의 다른 경로를 가리키는 링크다.
예를 들어 다음 구조를 생각할 수 있다.
- CodeQL Pack Root:
/project/codeql-pack - 링크 파일:
/project/codeql-pack/config - 실제 대상:
/home/user/private-config
이 경우 Pack 안의 파일처럼 보이지만 실제 내용은 Pack Root 밖에 존재한다.
Bundle 생성 도구가 이러한 링크를 그대로 따라가면 의도하지 않은 파일이 포함될 위험이 있다.
새로운 CodeQL CLI는 실제 경로를 해석한 뒤 해당 파일이 Pack Root 안에 존재하는지 확인한다.
Root 밖에 있는 파일은 Pack에 포함하지 않는다.
그리고 경고를 표시한다.
이는 CodeQL Pack을 배포하는 과정에서 의도하지 않은 파일이 포함되는 위험을 줄이기 위한 기능이다.
CLI의 표준 출력 처리
이번 버전에서는 Java Virtual Machine의 경고와 초기화 오류를 표준 오류 출력으로 전달하도록 변경했다.
대표적인 출력은 다음과 같다.
- stdout: 실제 분석 결과
- stderr: 경고와 오류 메시지
이러한 구분은 자동화 도구에서 중요하다.
예를 들어 CodeQL CLI가 JSON 결과를 stdout에 반환한다고 가정해 보자.
그런데 같은 출력에 JVM의 경고 메시지가 섞이면 JSON Parser가 실패할 수 있다.
새로운 방식에서는 경고를 stderr로 분리해 기계가 읽는 결과를 안정적으로 처리할 수 있도록 한다.
적용 범위
GitHub는 github.com의 Code Scanning 사용자에게 새로운 CodeQL 버전을 자동으로 배포한다.
GitHub Enterprise Server에서는 3.24 버전에 포함될 예정이다.
이전 GHES 버전에서는 필요한 경우 CodeQL을 직접 업데이트해야 할 수 있다.
이번 릴리스에는 실제 취약점 탐지율이나 전체 분석 시간이 얼마나 개선됐는지에 관한 정량적인 Benchmark가 포함되지 않았다.
기존 보안 Query의 정확성과 결과 메타데이터를 개선하고 CodeQL Pack 배포 및 CLI 출력의 안정성을 높인 정식 업데이트다.
Cloudflare, 개인용 AI 에이전트별 사이트 접근 정책과 트래픽 분석 지원 — 검색·학습 크롤러와 별도로 제어
- 발표일: 2026-11-04 — 정확한 게시 시각 미확인
- 발표 주체: Cloudflare
- 안정화 단계: 정식 접근 정책 및 Analytics 기능. 일부 계정 분석 기능은 Early Access
- 적용 대상: Cloudflare Security Analytics, AI Crawler and Agent Access Policies
- 원문: https://developers.cloudflare.com/changelog/post/2026-11-04-personal-agent-policies/
- 상세 기술 설명: https://blog.cloudflare.com/personal-agents-visibility-control
Cloudflare가 개인 사용자의 요청에 따라 웹사이트를 탐색하는 AI 에이전트의 접근을 개별적으로 허용하거나 차단할 수 있도록 정책 기능을 확장했다.
이번 업데이트는 일반적인 AI 학습 크롤러와 개인용 AI 에이전트를 구분한다는 점에서 의미가 있다.
예를 들어 사용자가 AI 비서에게 다음과 같이 요청했다고 가정해 보자.
이 사이트에서 가장 적합한 제품을 찾아줘.
AI 에이전트는 사용자를 대신해 사이트에 접근할 수 있다.
상품 정보를 가져올 수 있다.
필요하다면 다른 페이지도 탐색할 수 있다.
이러한 요청은 자동화된 트래픽이지만 일반적인 대규모 학습 데이터 수집과 목적이 다르다.
따라서 웹사이트 운영자는 두 종류의 트래픽에 서로 다른 정책을 적용하고 싶을 수 있다.
기존 AI Bot 정책
Cloudflare는 기존에 AI Bot을 다음 목적에 따라 구분해 관리하는 정책을 제공하고 있었다.
- Search
- Training
- Agent
예를 들어 AI 모델 학습을 위한 대규모 크롤링은 제한할 수 있다.
반면 사용자의 요청에 따라 특정 페이지를 가져오는 에이전트에는 접근을 허용할 수 있다.
하지만 기존에는 Agent 정책 안에서 개별 에이전트를 세밀하게 구분하는 기능이 제한적이었다.
이번 업데이트에서는 지원하는 개인용 AI 에이전트마다 다른 정책을 지정할 수 있다.
세 가지 접근 정책
새롭게 제공하는 주요 정책은 다음과 같다.
| 정책 | 동작 |
|---|---|
| Allow (do not block) | 해당 AI 에이전트를 차단하지 않음 |
| Block on pages with ads | 광고가 있는 페이지에서 차단 |
| Block | 해당 AI 에이전트 차단 |
예를 들어 온라인 매장을 운영한다고 가정해 보자.
운영자는 일부 개인용 AI 에이전트가 상품을 비교하는 작업을 허용하고 싶을 수 있다.
반면 다른 종류의 자동화된 접근은 제한하고 싶을 수 있다.
이 경우 Agent 전체의 기본 정책을 설정한 뒤 특정 에이전트에 별도의 정책을 적용할 수 있다.
또한 광고 수익을 기반으로 운영하는 콘텐츠 사이트에서는 광고가 있는 페이지에만 차단 정책을 적용하는 방식도 선택할 수 있다.
기본 정책과 개별 정책
Cloudflare는 전체 Agent에 적용하는 기본 정책을 유지한다.
그리고 지원하는 개별 개인용 AI 에이전트에 다른 정책을 지정할 수 있다.
예를 들어 다음과 같은 구성이 가능하다.
- 전체 Agent 기본값: 광고가 있는 페이지에서 차단
- 특정 개인용 AI 에이전트: 허용
- 다른 특정 AI 에이전트: 차단
따라서 모든 Agent를 한꺼번에 허용하거나 차단할 필요가 없다.
또한 기존 Search와 Training 정책은 이번 변경으로 자동 수정되지 않는다.
이러한 정책은 서로 다른 목적의 트래픽을 관리하기 때문이다.
Dashboard 설정
기존 설정의 이름도 변경됐다.
이전 이름은 다음과 같았다.
Configure AI bot policies
새로운 이름은 다음과 같다.
AI Crawler and Agent access policies
실제 Dashboard에서는 다음 경로로 이동한다.
Security > Settings > AI Crawler and Agent access policies
이 화면에서 전체 정책과 개별 에이전트 정책을 관리할 수 있다.
Security Analytics의 새로운 분류
이번 업데이트에서는 Analytics도 확장됐다.
Security Analytics의 Bot Analysis에는 다음 새로운 카테고리가 추가됐다.
Personal Assistants
이 카테고리를 이용하면 개인용 AI 에이전트의 트래픽을 별도로 확인할 수 있다.
또한 Top Statistics에는 개인용 AI 에이전트를 표시하는 Widget이 추가됐다.
대표적으로 다음 정보를 확인할 수 있다.
- 어떤 에이전트가 접근했는지
- 각 에이전트의 요청 수
- 접근한 URL 경로
- HTTP 상태 코드
- 요청이 발생한 네트워크
예를 들어 특정 에이전트가 상품 페이지에 주로 접근하는지 확인할 수 있다.
또는 로그인 페이지나 Checkout Endpoint에 자주 접근하는지도 확인할 수 있다.
하지만 특정 에이전트의 요청 수가 많다는 사실만으로 해당 에이전트가 실제 사용자에게 도움이 됐다고 판단할 수는 없다.
실제로 사용자가 구매를 완료했는지 또는 답변에 해당 콘텐츠가 사용됐는지는 별도의 데이터가 필요하다.
계정 단위 분석
Cloudflare의 Account Abuse Protection과 연결하면 더 세밀한 정보를 확인할 수 있다.
해당 기능에서는 어떤 사용자의 계정에 어떤 개인용 AI 에이전트가 접근했는지 분석할 수 있다.
예를 들어 사용자의 로그인 기록에 새로운 AI 에이전트가 나타났다고 가정해 보자.
보안 담당자는 해당 에이전트가 이전부터 사용하던 에이전트인지 확인할 수 있다.
또한 특정 사용자 계정에서만 의심스러운 접근이 발견됐다면 해당 사용자의 Hashed User ID를 기준으로 보안 정책을 적용할 수 있다.
다만 Account Abuse Protection의 관련 기능은 Early Access 상태다.
모든 Cloudflare 사용자가 동일하게 사용할 수 있다고 가정해서는 안 된다.
에이전트 식별과 사용자 권한은 다르다
이번 발표에서 중요한 제한사항은 다음과 같다.
AI 에이전트가 어떤 제품인지 식별했다는 사실만으로 해당 에이전트가 어떤 사용자를 대신하는지 또는 무엇을 하도록 승인받았는지 알 수 있는 것은 아니다.
예를 들어 사용자의 요청으로 상품 정보를 가져오는 AI 에이전트가 있다고 가정해 보자.
사이트는 해당 에이전트를 식별할 수 있다.
하지만 실제 사용자가 로그인한 고객인지, 특정 유료 구독을 가지고 있는지, 에이전트에게 결제를 승인했는지는 별도의 문제다.
Cloudflare도 이러한 사용자 Context를 안전하게 전달하는 방법은 앞으로 해결해야 할 과제라고 설명한다.
따라서 이번 기능은 AI 에이전트의 신원을 이용한 트래픽 분류와 접근 제어에 관한 것이다.
사용자 인증이나 결제 권한의 검증을 대체하지 않는다.
적용 범위와 한계
이번 정책은 Cloudflare가 식별하고 지원하는 개인용 AI 에이전트를 대상으로 한다.
알려지지 않은 Bot이나 일반적인 브라우저처럼 동작하는 자동화 요청까지 모두 동일하게 식별할 수 있다고 보장하지 않는다.
또한 접근 정책을 변경하더라도 실제 애플리케이션의 인증 및 인가 정책은 유지해야 한다.
이번 발표에는 에이전트 탐지 정확도나 차단율에 관한 독립적인 Benchmark가 포함되지 않았다.
AI 학습 크롤러와 실제 사용자의 요청을 수행하는 개인용 AI 에이전트를 구분하고, 에이전트별로 사이트 접근 정책을 설정할 수 있도록 만든 보안 및 트래픽 관리 기능이다.
Cloudflare Browser Isolation, 읽기 전용 키보드 모드 Beta 공개 — 입력은 차단하고 검색·복사·확대는 유지
- 발표일: 2026-11-04 — 정확한 게시 시각 미확인
- 발표 주체: Cloudflare
- 안정화 단계: Beta
- 적용 대상: Cloudflare Browser Isolation, Cloudflare One
- 신규 설정:
enable_read_only_actions - 원문: https://developers.cloudflare.com/changelog/post/2026-11-04-granular-keyboard-controls/
Cloudflare가 Browser Isolation 환경에서 사용자의 일반적인 데이터 입력은 차단하면서 브라우저의 읽기 전용 키보드 동작은 허용할 수 있도록 새로운 정책을 추가했다.
Browser Isolation은 웹페이지의 실행을 격리된 환경에서 처리할 수 있도록 하는 보안 기능이다.
예를 들어 직원이 외부 웹사이트에 접근해야 하지만 해당 사이트가 기업의 내부 데이터에 접근하거나 특정 작업을 수행하는 상황은 제한하고 싶을 수 있다.
이때 브라우저에서 허용하는 입력과 출력의 범위를 관리할 수 있다.
기존 키보드 정책의 한계
기존에는 키보드 입력에 두 가지 선택지가 존재했다.
- 모든 입력 허용
- 모든 입력 차단
하지만 모든 키보드 입력을 차단하면 실제 데이터 입력뿐 아니라 일반적인 탐색 동작도 불가능해질 수 있다.
예를 들어 사용자는 다음 작업을 수행할 수 없을 수 있다.
- 페이지 안의 단어 검색
- 전체 텍스트 선택
- 복사
- 인쇄
- 화면 확대 및 축소
이러한 작업은 실제로 Form에 데이터를 입력하는 작업과 성격이 다르다.
따라서 웹페이지를 읽기만 해야 하는 사용자에게 모든 키보드 입력을 차단하는 것은 지나치게 강한 제한이 될 수 있다.
Allow Read-only Actions
이번 업데이트에서는 새로운 모드를 제공한다.
Allow read-only actions
이 모드를 사용하면 다음 동작을 허용한다.
| 동작 | 지원 여부 |
|---|---|
| 페이지 안의 검색 | 허용 |
| 전체 선택 | 허용 |
| 복사 | 허용 가능 |
| 인쇄 | 허용 가능 |
| 확대 및 축소 | 허용 |
| 일반적인 데이터 입력 | 차단 |
다만 복사와 인쇄는 별도의 정책에도 영향을 받는다.
예를 들어 키보드 정책에서 Read-only Actions를 허용했다고 가정해 보자.
하지만 복사 정책에서는 복사를 차단했다.
이 경우 페이지 검색과 확대는 사용할 수 있어도 실제 복사는 제한된다.
따라서 이번 설정은 기존 보안 정책을 덮어쓰는 것이 아니라 독립적인 제어 조건으로 동작한다.
API 설정
API에서는 다음 값으로 설정한다.
keyboard: "enable_read_only_actions"
해당 값은 다음 객체에 포함한다.
biso_admin_controls
또한 다음 버전을 사용해야 한다.
version: "v2"
따라서 기존 API 설정을 사용하는 프로젝트에서는 새로운 정책 형식을 확인해야 한다.
실제 적용의 한계
이번 기능은 키보드 입력에 관한 제어다.
웹 애플리케이션의 모든 데이터 변경 작업을 자동으로 차단하는 범용 읽기 전용 모드는 아니다.
예를 들어 웹페이지에 마우스로 클릭할 수 있는 버튼이 존재한다고 가정해 보자.
실제 접근 정책과 다른 입력 제한이 어떻게 구성됐는지에 따라 해당 버튼의 사용 가능 여부가 달라질 수 있다.
따라서 데이터 변경 자체를 막으려면 애플리케이션의 서버 측 접근 권한도 검증해야 한다.
또한 이번 기능은 Beta 상태다.
따라서 지원 범위와 세부 동작이 이후 변경될 수 있다.
이번 발표에는 사용자 작업 시간이나 보안 탐지 정확도에 관한 정량적인 Benchmark가 포함되지 않았다.
브라우저에서 콘텐츠를 읽는 데 필요한 동작을 유지하면서 키보드를 이용한 데이터 입력을 제한하도록 보안 정책을 세분화한 기능이다.
📚 추천 글
娘とのクイズ遊びをアプリにしたら、日本語の奥深さを体感した話
- 원제: 娘とのクイズ遊びをアプリにしたら、日本語の奥深さを体感した話
- 저자: tomi
- 매체: Zenn
- 게시일: 2026-11-04 — 정확한 게시 시각 미확인
- 기술: Next.js 14, React, TypeScript, Tailwind CSS 4, Web Speech API, Claude, Firebase
- 원문: https://zenn.dev/tomipetit/articles/272c4d70a2bf67
실제 가족과 함께 사용할 목적으로 아이가 정답을 맞히는 게임이 아니라 직접 퀴즈를 만들도록 돕는 Next.js 애플리케이션을 개발한 과정을 설명한 글이다.
저자는 5월에 프로젝트를 시작했다.
10월 말까지 개발한 기록을 다시 살펴보면서 어떤 설계가 성공했고 어떤 설계가 기대한 대로 동작하지 않았는지 정리했다.
실제 개발 기록은 다음과 같다.
- 전체 Git Commit: 307개
- Merge Commit 제외: 191개
- Pull Request: 114개
이번 글의 특징은 AI 모델과 프론트엔드 아키텍처의 역할을 명확하게 구분한 과정이다.
처음에는 AI가 완성된 퀴즈 문장을 생성하도록 만들었다.
하지만 실제로 아이와 함께 사용해 보니 퀴즈를 만드는 가장 재미있는 과정은 단어를 고르고 직접 순서를 바꾸는 작업이었다.
또한 단어의 순서를 변경할 때마다 AI 모델의 응답을 기다려야 하는 문제도 있었다.
따라서 저자는 AI가 필요한 데이터를 생성하고, 실제 문장을 조립하는 작업은 프론트엔드의 규칙으로 처리하도록 설계를 변경했다.
실제 사용자 흐름
애플리케이션은 다음 네 단계로 구성된다.
- 정답 선택
- 정답의 특징을 설명하는 힌트 선택
- 힌트의 순서 변경
- 완성된 퀴즈를 읽고 공유
예를 들어 정답을 사과로 선택했다고 가정해 보자.
AI는 다음과 같은 힌트를 제안할 수 있다.
- 빨갛다
- 둥글다
- 달다
사용자는 필요한 힌트를 선택한다.
그리고 순서를 변경할 수 있다.
앱은 현재 순서에 맞는 퀴즈 문장을 생성한다.
여기서 중요한 부분은 문장을 조립하는 과정이다.
AI가 매번 문장을 생성하던 초기 구조
초기 구현에서는 사용자가 힌트의 순서를 변경하면 AI가 새로운 문장을 생성하도록 설계했다.
하지만 이 방식에서는 두 가지 문제가 발생했다.
첫 번째는 응답 시간이다.
사용자가 카드를 한 번 옮길 때마다 모델을 호출해야 한다면 인터랙션이 느려진다.
두 번째는 사용자 경험이다.
아이에게 퀴즈 문장을 완성하는 과정 자체가 중요한 놀이였기 때문이다.
AI가 모든 문장을 완성해 주면 아이가 직접 문장을 조합하는 과정이 줄어든다.
저자는 이러한 문제를 해결하기 위해 일반 모드의 문장 생성을 로컬 규칙으로 이전했다.
이후 사용자가 힌트의 순서를 변경하면 프론트엔드에서 즉시 미리보기를 갱신한다.
따라서 매번 외부 AI 모델의 응답을 기다릴 필요가 없다.
AI와 규칙 기반 로직의 책임 분리
변경 이후 AI는 다음 작업에 사용한다.
- 정답 후보 생성
- 힌트 후보 생성
- 사용자가 입력한 단어의 품사 분류
- 정답의 다른 표현이나 대체 정답 탐색
반면 실제 문장을 조립하는 작업은 로컬 규칙이 처리한다.
이러한 구조에서는 외부 모델의 응답 시간이 사용자 인터랙션에 직접적인 영향을 주는 상황을 줄일 수 있다.
또한 문장의 구조를 보다 결정적으로 관리할 수 있다.
하지만 AI가 생성한 힌트의 품사를 잘못 판단한다면 실제 문장도 부자연스럽게 생성될 수 있다.
따라서 AI가 생성한 데이터를 그대로 사용하지 않고 추가적인 규칙과 검증을 적용해야 한다.
일본어 품사와 동사 활용 문제
이번 글에서 가장 많은 시행착오가 발생한 부분은 일본어의 문장 구조였다.
예를 들어 다음 단어를 생각할 수 있다.
あかい
이 단어는 형용사다.
문장 중간에서는 다음 형태로 변경할 수 있다.
あかくて
하지만 모든 단어를 같은 규칙으로 변경할 수는 없다.
예를 들어 다음 단어가 존재한다.
きれい
마지막 글자가 い이지만 일반적인 い형용사와 같은 방식으로 활용하지 않는다.
또한 동사도 서로 다른 활용 규칙을 가진다.
예를 들어 다음과 같다.
およぐ→およいではしる→はしって
단순히 마지막 글자를 제거하고 일정한 접미사를 붙이는 방식으로는 이러한 표현을 올바르게 생성하기 어렵다.
저자는 이를 해결하기 위해 각 힌트에 추가 정보를 저장했다.
HintCard의 데이터 모델
저자는 하나의 힌트를 HintCard로 표현했다.
주요 속성은 다음과 같다.
| 속성 | 역할 |
|---|---|
id | 힌트 식별자 |
displayText | 화면에 표시할 단어 |
kind | 단어의 종류 |
teForm | 동사의 て형 |
placeParticle | 장소와 동사를 연결할 때 사용하는 조사 |
mergeWithNext | 다음 카드와 연결할지 여부 |
category | 색상·형태·크기 등 의미 분류 |
confirmed | 사용자가 확정한 힌트인지 여부 |
이 가운데 가장 중요한 값은 kind다.
저자는 총 아홉 종류의 품사 및 표현 범주를 정의했다.
예를 들어 다음과 같다.
- 일반적인 형용사
- な형용사
- 명사
- 장소 명사
- 생물 명사
- 시간 관련 명사
- 부사
각 종류마다 문장 중간과 끝에서 사용할 표현 규칙을 정의했다.
따라서 사용자가 카드의 순서를 변경하면 해당 규칙을 적용해 문장을 다시 만들 수 있다.
AI가 판단한 카드 연결이 실패한 이유
저자는 처음에 두 단어를 연결해야 하는지도 AI가 결정하도록 했다.
예를 들어 다음 두 단어가 있다고 가정해 보자.
- 바다
- 수영하다
두 단어는 하나의 표현으로 연결할 수 있다.
하지만 AI가 연결 여부를 잘못 판단하면 일부 카드가 문장에서 사라질 수 있었다.
따라서 저자는 연결을 실제 사용자가 결정하도록 변경했다.
사용자는 두 카드 사이에 표시된 버튼을 눌러 연결할 수 있다.
다만 모든 카드 조합에 버튼이 표시되는 것은 아니다.
실제로 연결 가능한 조합인지는 다음 규칙으로 검사한다.
canMerge
즉 사용자는 어떤 단어를 연결할지 결정한다.
애플리케이션은 해당 연결이 문법적으로 허용되는지 검증한다.
이러한 방식은 AI의 판단과 사용자의 선택, 애플리케이션의 규칙을 분리한다.
어린이용 UI
저자는 실제 사용자가 3~8세 정도의 아이라는 점을 고려했다.
따라서 다음 원칙을 적용했다.
- 텍스트 입력 최소화
- 터치와 드래그 중심의 UI
- 한글에 해당하는 일본어 히라가나 중심 표시
- 어린이가 누르기 쉬운 큰 버튼
- 음성 읽기 기능
- 연령별 난이도
하지만 모든 아이가 드래그 앤 드롭을 쉽게 사용할 수 있는 것은 아니었다.
따라서 카드 정렬을 생략하는 간단 모드도 추가했다.
간단 모드에서는 AI가 문장을 직접 생성할 수 있도록 했다.
이는 일반 모드와 다른 설계다.
저자는 사용자 연령과 실제 놀이 목적에 따라 AI의 역할을 다르게 선택했다.
Web Speech API의 브라우저별 차이
저자는 음성 읽기 기능에 Web Speech API를 사용했다.
외부 TTS 라이브러리를 추가하지 않고 브라우저의 기본 기능을 활용했다.
하지만 실제 모바일 기기에서 문제가 발생했다.
대표적인 사례는 다음과 같다.
첫 번째는 iOS Safari의 onend 이벤트다.
음성 읽기가 완료됐더라도 종료 이벤트가 정상적으로 전달되지 않는 경우가 있었다.
저자는 해당 이벤트에만 의존하지 않고 시간 기반 처리를 추가했다.
두 번째는 연속적인 음성 읽기다.
퀴즈를 시작하기 전에 짧은 안내 문구를 읽어 준다.
그다음 실제 퀴즈 문장을 읽는다.
하지만 iOS에서는 두 문장을 연속해서 실행하면 정상적으로 읽히지 않는 경우가 있었다.
따라서 첫 번째 읽기가 끝난 뒤 다음 읽기를 시작하는 Queue 구조로 변경했다.
세 번째는 Chrome의 cancel()이다.
기존 음성을 취소한 직후 새로운 음성을 시작하면 소리가 나지 않는 경우가 있었다.
저자는 취소 이후 짧은 간격을 두고 다음 음성을 실행하도록 했다.
이러한 문제는 실제 기기에서 확인하지 않으면 발견하기 어려울 수 있다.
배포 과정의 문제
프로젝트는 Firebase App Hosting에 배포했다.
처음에는 Next.js의 기능을 이용해 OGP 이미지를 동적으로 생성하려고 했다.
하지만 Firebase App Hosting에서 이미지 URL이 기대한 방식으로 처리되지 않는 문제가 발생했다.
저자는 이를 해결하기 위해 정적 PNG 이미지를 사용하도록 변경했다.
동적 생성이 기술적으로 가능하더라도 실제 배포 환경에서 정상적으로 동작하지 않는다면 단순한 방식을 선택할 수 있다는 사례다.
테스트와 한계
저자는 다음 도구를 사용했다.
- Vitest
- React Testing Library
- GitHub Actions
PR마다 테스트를 실행하도록 구성했다.
또한 AI 모델을 교체할 수 있도록 여러 Provider를 지원했다.
하지만 이번 글에는 AI 모델별 응답 정확도나 사용자별 평균 대기 시간에 관한 정량적인 Benchmark가 포함되지 않았다.
따라서 특정 모델이 일본어 힌트를 가장 정확하게 생성한다고 단정할 수 없다.
이번 글은 실제 사용자의 행동을 관찰한 뒤 AI에게 맡길 작업과 프론트엔드에서 결정적으로 처리할 작업의 경계를 다시 설정한 개발 사례다.
Pico CSS is archived. I built the exit plan.
- 원제: Pico CSS is archived. I built the exit plan.
- 저자: TECHDEV
- 매체: DEV Community
- 게시일: 2026-11-03
- 기술: CSS, TypeScript, CLI, Codemod, HTML Parser, CI
- 원문: https://dev.to/techdevfun/pico-css-is-archived-i-built-the-exit-plan-c9h
Pico CSS의 공식 Repository가 보관 처리된 이후 기존 웹사이트에서 어떤 Pico CSS 기능을 사용하고 있는지 분석하고 다른 CSS 프레임워크로 이전하기 위한 도구를 설계한 과정을 설명한 글이다.
Pico CSS는 적은 양의 Class와 설정으로 기본적인 HTML 요소의 스타일을 제공하는 CSS 프레임워크다.
저자에 따르면 공식 Repository는 2026년 10월 4일 보관 처리됐다.
마지막 버전은 2.1.1이다.
따라서 이후 새로운 기능이나 버그 수정이 공식 유지보수자로부터 계속 제공된다고 기대하기 어렵다.
하지만 사용 중인 라이브러리의 유지보수가 종료됐다는 사실만으로 모든 프로젝트를 즉시 다른 프레임워크로 이전해야 하는 것은 아니다.
실제 위험은 프로젝트에서 어떤 기능을 사용하는지에 따라 달라질 수 있다.
저자는 이를 조사하기 위해 picoexit이라는 도구를 만들었다.
먼저 실제 사용량을 측정
저자는 마이그레이션을 시작하기 전에 현재 프로젝트가 Pico CSS를 얼마나 사용하고 있는지 조사해야 한다고 설명한다.
대표적인 명령은 다음과 같다.
picoexit audit
이 도구는 프로젝트의 다음 파일을 검사한다.
- HTML
- CSS
- JavaScript
그리고 다음 정보를 수집한다.
- Pico CSS를 가져오는 방식
- 사용 중인 버전
- 실제 사용한 Selector
- Selector가 사용된 파일과 줄 번호
- 컴포넌트별 사용 횟수
- 유지보수 종료 이후의 위험이 큰 패턴
예를 들어 저자의 샘플 프로젝트에서는 다음 결과가 나타났다.
- 서로 다른 Pico Selector: 45개
- 전체 사용 횟수: 85회
이러한 정보를 이용하면 전체 CSS 프레임워크를 사용하는 것처럼 보이는 프로젝트에서도 실제로 필요한 기능의 범위를 확인할 수 있다.
:has()에 대한 의존성
저자가 특히 주목한 기능은 다음 CSS 선택자다.
:has()
이 선택자는 특정 하위 요소나 관계를 만족하는 요소를 선택하는 데 사용할 수 있다.
예를 들어 Form 안의 특정 입력이 Focus 상태일 때 부모 요소의 스타일을 변경하는 데 활용할 수 있다.
저자는 Pico CSS 2에서 :has()가 23번 사용된다는 사실을 확인했다.
주로 다음 영역에서 사용됐다.
- Form의 Focus 처리
- Dropdown
- 상태에 따른 시각적 표현
이러한 기능은 단순한 색상 장식이 아니다.
실제 UI의 상호작용과 연결될 수 있다.
따라서 유지보수가 종료된 프레임워크를 그대로 사용할 경우 이후 브라우저의 동작 변화나 새로운 호환성 문제에 직접 대응해야 할 수 있다.
다만 :has()가 현재 모든 브라우저에서 문제가 있는 기능이라는 의미는 아니다.
저자는 유지보수가 종료된 프레임워크에서 복잡한 브라우저 동작에 의존하는 부분을 우선 확인해야 한다는 관점으로 분석했다.
세 가지 이전 전략
저자는 모든 프로젝트에 동일한 마이그레이션을 적용하지 않았다.
세 가지 선택지를 제공한다.
| 전략 | 내용 |
|---|---|
| 기존 버전 고정 | Pico 2.1.1을 자체 프로젝트에 보관하고 유지 |
| 다른 프레임워크로 이전 | Tailwind CSS 4, Bulma 1, Open Props 또는 일반 CSS 사용 |
| 커뮤니티 Fork 검토 | 이후 유지보수가 이루어지는 별도 프로젝트로 이전 |
첫 번째 방식은 실제 사용하는 기능이 단순한 장식에 가까운 경우 선택할 수 있다.
예를 들어 버튼의 기본 Padding과 색상 정도만 사용하는 프로젝트라면 전체 UI를 다시 작성하는 비용이 더 클 수 있다.
이 경우 기존 Pico CSS를 Vendoring하고 필요한 수정만 직접 적용하는 방식이 가능하다.
두 번째 방식은 다른 CSS 프레임워크로 이전하는 것이다.
하지만 같은 HTML 구조가 모든 프레임워크에서 동일하게 동작하는 것은 아니다.
세 번째 방식은 커뮤니티에서 관리하는 Fork를 이용하는 것이다.
다만 새롭게 생성된 Fork의 유지보수가 계속될지는 별도로 확인해야 한다.
자동 변환이 가능한 부분과 불가능한 부분
저자는 Codemod를 구현했다.
Codemod는 기존 코드를 일정한 규칙에 따라 자동으로 변경하는 도구다.
예를 들어 Pico CSS의 Grid Class를 Tailwind CSS의 Class로 변경할 수 있다.
하지만 실제 Grid의 열 개수를 결정하려면 하위 요소의 구조를 확인해야 한다.
저자는 단순한 문자열 치환 대신 Tokenizer를 이용해 실제 자식 요소의 개수를 계산한다.
예를 들어 자식 요소가 세 개라면 다음과 같은 Class를 생성할 수 있다.
grid gap-4 md:grid-cols-3
하지만 모든 Pico 컴포넌트가 같은 방식으로 변환되는 것은 아니다.
대표적으로 다음 기능은 수동 작업이 필요할 수 있다.
- Dropdown
- Dialog
- Tooltip
- Switch
- Theme 전환
예를 들어 Pico CSS에서는 HTML의 <details> 요소를 이용해 Dropdown을 구현할 수 있다.
반면 다른 프레임워크에서는 별도의 JavaScript와 DOM 구조가 필요할 수 있다.
이 경우 CSS Class만 변경해서는 같은 동작을 구현할 수 없다.
따라서 저자는 변환 규칙이 검증되지 않은 요소를 자동으로 변경하지 않는다.
대신 수동으로 검토해야 하는 항목으로 보고한다.
Dry Run
마이그레이션 도구는 기본적으로 실제 파일을 수정하지 않는다.
대표적인 옵션은 다음과 같다.
--dry-run
이 모드에서는 변경 전후의 Diff를 표시한다.
개발자는 어떤 HTML과 CSS가 변경되는지 확인할 수 있다.
실제로 변경하려면 다음 옵션을 사용해야 한다.
--apply
이렇게 하면 자동 변환 결과를 검토한 뒤 적용할 수 있다.
특히 여러 파일에 영향을 주는 마이그레이션에서는 변경 내용을 먼저 확인하는 과정이 중요하다.
CI에서 위험 패턴 검사
저자는 Audit 결과를 CI에 연결할 수 있도록 했다.
위험한 Selector가 발견되면 프로세스가 종료 코드 1을 반환한다.
따라서 CI에서 해당 검사를 실행하면 문제가 발견된 PR을 실패 처리할 수 있다.
하지만 위험 Selector가 존재한다는 사실만으로 실제 사용자에게 오류가 발생한다는 의미는 아니다.
검사는 마이그레이션 과정에서 우선적으로 검토해야 할 코드의 위치를 알려주는 역할을 한다.
실제 문제의 심각도는 UI의 사용 방식에 따라 달라질 수 있다.
구현의 한계
저자는 도구가 모든 Pico CSS 코드를 자동으로 변환한다고 주장하지 않는다.
현재 확인한 매핑은 약 60개다.
또한 자동 변환이 어려운 기능은 수동 검토가 필요하다고 명시한다.
따라서 다른 프레임워크로 변경한 이후 실제 UI의 모습과 상호작용을 별도로 테스트해야 한다.
예를 들어 다음 검증이 필요할 수 있다.
- Form의 Focus 상태
- 키보드 접근성
- Dropdown의 열림과 닫힘
- Dark Mode
- 화면 크기에 따른 Layout 변화
이번 글에는 마이그레이션 전후의 실제 웹 성능 비교가 포함되지 않았다.
또한 특정 프레임워크로 이전하면 모든 프로젝트의 Bundle 크기가 감소한다고 보장하지 않는다.
유지보수가 종료된 CSS 프레임워크를 즉시 교체하기보다 실제 사용처와 위험 범위를 분석하고, 자동 변환의 신뢰할 수 있는 범위와 수동 작업을 명확하게 구분한 개발 도구 설계 사례다.
How to Build an MCP Server in NestJS: Streamable HTTP, Guards and DI (Tested)
- 원제: How to Build an MCP Server in NestJS: Streamable HTTP, Guards and DI (Tested)
- 저자: Waseem Anjum
- 매체: fabwaseem.com
- 게시일: 2026-11-04
- 기술: NestJS 12, TypeScript 6, Express 5, MCP SDK 2, Zod 4
- 원문: https://fabwaseem.com/blog/nestjs-mcp-server
NestJS 애플리케이션 안에서 기존 Service와 Dependency Injection, Guard를 그대로 사용하면서 MCP 서버를 구현하는 방법을 실제 코드와 HTTP 테스트로 검증한 글이다.
MCP(Model Context Protocol)는 AI 에이전트가 외부 도구와 데이터를 사용할 수 있도록 하는 프로토콜이다.
예를 들어 다음과 같은 Tool을 제공할 수 있다.
- 고객 정보 조회
- 주문 검색
- 문서 검색
- 새로운 작업 생성
하지만 MCP 서버를 별도의 애플리케이션으로 만들면 기존 백엔드의 인증과 비즈니스 로직을 중복으로 구현해야 할 수 있다.
저자는 이러한 문제를 해결하기 위해 기존 NestJS 애플리케이션에 MCP Endpoint를 추가했다.
실제 테스트 환경
저자가 사용한 환경은 다음과 같다.
| 구성요소 | 버전 |
|---|---|
| Node.js | 24.22.1 |
| NestJS | 12.3.2 |
| Express | 5.3.0 |
| MCP Server SDK | 2.4.0 |
| MCP Node Adapter | 2.1.2 |
| Zod | 4.6.7 |
| TypeScript | 6.0.3 |
저자는 MCP의 공식 TypeScript SDK를 직접 연결했다.
별도의 NestJS 전용 MCP Module에 의존하지 않았다.
대신 기존 NestJS의 다음 기능을 사용했다.
- Provider
- Dependency Injection
- Guard
- Controller
- Module
기존 Service를 MCP Tool에서도 사용
저자는 간단한 메모 관리 API를 구현했다.
실제 비즈니스 로직은 다음 Service에 존재한다.
NotesService
이 Service는 다음 기능을 제공한다.
- 메모 검색
- 메모 생성
일반적인 REST API에서도 해당 Service를 사용할 수 있다.
MCP Tool에서도 동일한 Service를 사용할 수 있다.
따라서 메모 검색 로직을 REST Controller와 MCP Tool에 각각 중복으로 작성할 필요가 없다.
이러한 구조는 MCP가 애플리케이션의 새로운 비즈니스 계층이 아니라 기존 기능을 노출하는 추가 인터페이스 역할을 하도록 만든다.
createMcpHandler
저자는 MCP Handler를 다음 함수로 생성했다.
createMcpHandler()
해당 Handler는 실제 Tool을 등록한다.
그리고 MCP Client의 요청을 처리한다.
하지만 NestJS는 Express의 Request와 Response 객체를 사용한다.
따라서 Fetch API 형태의 Handler를 그대로 사용할 수 없다.
저자는 다음 Adapter를 사용했다.
toNodeHandler
이 Adapter는 MCP Handler를 Node.js의 Request와 Response 형태로 연결한다.
NestJS Controller
실제 Endpoint는 다음 경로를 사용한다.
POST /mcp
Controller에서는 다음 Decorator를 사용한다.
@Post("mcp")@Req()@Res()
MCP SDK가 직접 HTTP 응답을 작성하도록 구성했다.
따라서 NestJS의 일반적인 Response Serialization을 다시 거치지 않도록 했다.
하지만 이 구조에는 실제 구현에서 발견한 문제가 존재했다.
첫 번째 문제: req.body 누락
NestJS는 기본적으로 JSON Request Body를 파싱한다.
즉 Controller가 실행되기 전에 실제 Request Stream의 내용이 읽힐 수 있다.
그런데 저자는 처음에 파싱된 Body를 MCP Handler에 전달하지 않았다.
그 결과 MCP SDK가 이미 소비된 Request Stream을 다시 읽으려고 했다.
실제 요청에는 데이터가 존재했지만 SDK에서는 비어 있는 입력처럼 처리됐다.
결과는 다음 오류였다.
-32700 Parse error
저자는 이를 해결하기 위해 req.body를 명시적으로 전달했다.
이 사례는 서로 다른 Framework와 SDK가 같은 HTTP Request를 처리할 때 어느 계층에서 Request Body를 소비하는지 확인해야 한다는 점을 보여준다.
두 번째 문제: Request Body 크기 제한
NestJS의 기본 JSON Parser는 약 100KB의 Body 제한을 사용한다.
반면 저자가 사용한 MCP SDK의 기본 요청 크기 제한은 4MiB였다.
따라서 큰 MCP Tool 요청을 보내면 실제 SDK의 검증 로직까지 도달하지 못할 수 있었다.
예를 들어 200KB의 데이터를 전달했다고 가정해 보자.
Express가 먼저 다음 오류를 반환할 수 있다.
413 Request Entity Too Large
이 경우 MCP Client는 JSON-RPC 오류가 아니라 일반적인 HTTP 오류를 받는다.
저자는 NestJS의 Body Parser 제한을 4MB로 변경했다.
다음 설정을 사용했다.
app.useBodyParser("json", { limit: "4mb" })
하지만 실제 서비스를 운영할 때 모든 요청을 4MB까지 허용해야 한다는 의미는 아니다.
요청 크기가 커지면 메모리 사용량과 서비스 남용 위험도 증가할 수 있다.
따라서 실제 Tool의 입력 크기에 맞는 제한을 선택해야 한다.
Guard에서 인증과 Origin 검사
저자는 다음 두 정보를 확인하는 Guard를 만들었다.
- Bearer Token
- Origin Header
Bearer Token이 존재하지 않으면 HTTP 401을 반환한다.
허용하지 않은 Origin에서 요청하면 HTTP 403을 반환한다.
실제 인증에 성공하면 사용자와 Scope 정보를 Request에 연결한다.
대표적인 정보는 다음과 같다.
- Client ID
- 사용자 식별자
- 허용된 Scope
MCP Handler는 이러한 정보를 받아 실제 Tool 목록을 구성한다.
따라서 어떤 사용자가 요청했는지에 따라 다른 Tool을 제공할 수 있다.
Scope별 Tool 목록
저자는 두 종류의 Token을 만들었다.
첫 번째는 읽기 전용 Token이다.
두 번째는 쓰기 권한을 가진 Token이다.
두 Token이 볼 수 있는 Tool은 다음과 같다.
| Token | 사용 가능한 Tool |
|---|---|
| 읽기 전용 | search_notes |
| 쓰기 권한 | search_notes, create_note |
읽기 전용 Token으로 접속하면 create_note 자체가 Tool 목록에 나타나지 않는다.
이는 Tool을 등록한 뒤 실행 단계에서 거부하는 방식과 다르다.
현재 사용자의 권한으로 호출할 수 있는 Tool만 실제로 등록하는 방식이다.
이렇게 하면 AI 모델이 사용할 수 없는 Tool을 선택하는 상황도 줄일 수 있다.
하지만 Tool을 목록에서 숨기는 것만으로 권한 검증이 완성되는 것은 아니다.
실제 데이터를 변경하는 Service에서도 사용자와 데이터의 소유권을 확인해야 한다.
JSON 응답과 SSE의 차이
저자는 MCP의 두 프로토콜 버전을 테스트했다.
- 2026-07-28
- 2025-11-25
새로운 프로토콜을 사용하는 클라이언트에서는 JSON 응답을 받았다.
반면 이전 프로토콜의 클라이언트에서는 SSE 응답이 사용됐다.
저자는 MCP Handler의 다음 옵션을 사용했다.
responseMode: "json"
하지만 이 옵션을 설정했다고 해서 이전 프로토콜의 모든 응답이 JSON으로 변경되는 것은 아니었다.
이전 프로토콜을 위한 호환성 경로에서는 SSE를 계속 사용할 수 있었다.
따라서 Reverse Proxy가 SSE 응답을 차단하거나 Buffering하면 이전 클라이언트에서 문제가 발생할 수 있다.
숨겨진 Tool과 잘못된 입력의 오류 차이
저자는 두 오류 상황도 구분했다.
첫 번째는 읽기 전용 Token으로 create_note를 호출하는 상황이다.
이 경우 해당 Tool은 사용자의 Tool 목록에 존재하지 않는다.
클라이언트는 Tool을 찾을 수 없다는 오류를 반환한다.
두 번째는 쓰기 권한이 있지만 잘못된 입력을 전달하는 상황이다.
예를 들어 빈 제목으로 메모를 생성하려고 한다.
이 경우 실제 Tool은 존재한다.
하지만 Zod Validation에 실패한다.
MCP SDK는 Tool Result에 다음 정보를 포함할 수 있다.
isError: true
따라서 다음 두 상태는 다르다.
- Tool을 호출할 권한이 없음
- Tool은 존재하지만 입력이 올바르지 않음
이러한 차이는 실제 AI 에이전트의 오류 처리에도 영향을 준다.
모델은 입력 오류라면 올바른 인자로 다시 호출할 수 있다.
반면 권한이 없는 Tool은 동일한 방식으로 재시도해도 사용할 수 없다.
실제 HTTP 성능 측정
저자는 MCP 서버를 로컬에서 실행했다.
그리고 동일한 Client에서 search_notes를 순차적으로 호출했다.
측정 조건은 다음과 같다.
- Warm-up: 50회
- 실제 호출: 500회
- 프로토콜별 3회 실행
- 동일한 로컬 머신 사용
결과는 다음과 같았다.
| 프로토콜 | P50 | P95 |
|---|---|---|
| 2026-07-28 | 1.36~1.69ms | 1.94~2.73ms |
| 2025-11-25 | 1.39~1.71ms | 2.05~2.97ms |
이번 실험에서는 두 프로토콜의 응답 시간이 비슷했다.
하지만 이 수치는 로컬에서 실행한 작은 메모 관리 Tool을 대상으로 한다.
실제 데이터베이스와 외부 API를 사용하는 경우에는 다른 작업의 지연 시간이 더 큰 영향을 줄 수 있다.
또한 여러 사용자가 동시에 요청하는 환경의 최대 처리량을 측정한 것은 아니다.
독립적인 검증
저자는 다음 방법을 사용해 구현을 확인했다.
curl- MCP Inspector CLI
- 공식 MCP SDK Client
- E2E 테스트 8개
테스트에서는 다음 내용을 확인했다.
- 인증되지 않은 요청 거부
- 허용하지 않은 Origin 거부
- Scope별 Tool 목록 구분
- 사용자별 데이터 분리
- 실제 Tool 호출 결과
- 기존 및 새로운 MCP 프로토콜과의 호환성
이번 글은 기존 NestJS 애플리케이션의 Service와 인증 체계를 재사용해 MCP 서버를 구성하면서 실제 HTTP 프로토콜의 처리 차이와 구현상의 오류를 검증한 자료다.
ISUCON2026 で 94 位だったので、人間なしのハーネスを作って感想戦に挑んだ
- 원제: ISUCON2026 で 94 位だったので、人間なしのハーネスを作って感想戦に挑んだ
- 저자: おかざき
- 매체: Zenn
- 게시일: 2026-11-04 — 정확한 게시 시각 미확인
- 기술: 웹 백엔드 성능 최적화, 부하 테스트, Claude Code, Codex, Agent Harness
- 원문: https://zenn.dev/marie673/articles/isucon2026-full-harness
일본의 웹 애플리케이션 성능 최적화 대회인 ISUCON에 참가한 개발자가 실제 대회에서 사용한 다중 AI 에이전트 구조를 다시 설계하고, 사람의 중간 지시 없이 성능을 개선할 수 있도록 반복 실험한 기록이다.
ISUCON에서는 주어진 웹 애플리케이션의 동작을 유지하면서 처리량과 응답 성능을 개선해야 한다.
일반적으로 다음 작업이 포함된다.
- 서버의 성능 분석
- 데이터베이스 Query 최적화
- 불필요한 작업 제거
- Cache 전략 변경
- 서버 설정 변경
- 부하 테스트
- 실제 성능 점수 확인
이번 글의 특징은 AI 에이전트가 코드를 얼마나 많이 변경했는지보다 어떤 근거로 성능 병목을 선택하고 실제로 개선 효과를 검증하는지에 초점을 맞춘다는 점이다.
실제 대회의 결과
저자가 참여한 팀의 결과는 다음과 같다.
| 항목 | 결과 |
|---|---|
| 참가 팀 | 856개 |
| 최종 순위 | 94위 |
| 최종 점수 | 145,523점 |
| 대회 중 최고 점수 | 150,508점 |
| 병합한 PR | 658개 |
| 동시 실행한 Codex Agent | 최대 20개 |
팀은 많은 AI 에이전트를 병렬로 실행했다.
Issue를 생성하고 작업을 분배했다.
각 에이전트가 코드를 변경했다.
이후 테스트와 배포를 수행했다.
한 시간에 111개의 PR을 병합한 시간대도 존재했다.
하지만 실제 점수는 기대한 만큼 증가하지 않았다.
즉 코드 변경 속도와 실제 성능 개선 사이에 큰 차이가 존재했다.
변경을 빠르게 만드는 시스템과 무엇을 변경할지 결정하는 시스템
저자는 대회 이후 실행 기록을 분석했다.
문제는 단순히 에이전트의 수가 부족했다는 것이 아니었다.
실제로 많은 코드가 변경되고 있었다.
하지만 각 에이전트는 애플리케이션의 전체 구조와 실제 성능 점수의 계산 방식을 충분히 이해하지 못했다.
예를 들어 다음 정보를 모두 이해하는 에이전트가 존재하지 않았다.
- 실제 사용자가 어떤 순서로 요청을 보내는가?
- 특정 시간대에 트래픽이 집중되는가?
- 어떤 작업이 실제 점수에 영향을 주는가?
- CPU와 메모리는 어떤 시점에 가장 많이 사용되는가?
- 현재 병목을 제거하면 다음 병목은 무엇이 되는가?
따라서 개별적인 최적화 작업이 서로 연결되지 않았다.
저자는 새로운 Harness를 만들면서 이러한 문제를 해결하려고 했다.
다중 에이전트에서 하나의 Main Agent로 변경
기존 구조에서는 여러 에이전트가 서로 다른 역할을 담당했다.
예를 들어 다음 역할이 존재했다.
- 분석 담당
- 코드 수정 담당
- 배포 담당
- Benchmark 담당
- 결과 검토 담당
이러한 구조는 작업을 병렬로 수행할 수 있다는 장점이 있다.
하지만 각 에이전트가 서로 다른 정보를 가지고 판단할 수 있다.
또한 실제 성능을 개선하기 위한 의사결정이 여러 실행 사이에서 분산될 수 있다.
저자는 이를 해결하기 위해 하나의 Main Agent를 사용하는 방향으로 변경했다.
Main Agent는 다음 작업을 모두 관리한다.
- 애플리케이션의 요구사항 분석
- 실제 사용자 요청 흐름 분석
- 병목 선정
- 코드 변경
- 배포
- Benchmark
- 결과를 바탕으로 다음 변경 결정
이러한 구조는 병목을 선택한 이유와 실제 결과를 하나의 실행 흐름에서 연결하는 데 목적이 있다.
초기 구현에 대한 과도한 의존
저자는 ISUCON9의 과거 문제를 사용해 새로운 Harness를 테스트했다.
초기 결과는 124,567점이었다.
이후 실행 기록을 분석했다.
놀라운 부분은 총 3,275번의 작업 가운데 애플리케이션의 요구사항 문서를 읽은 횟수가 세 번뿐이었다는 사실이다.
에이전트는 이미 존재하는 코드의 구조를 기준으로 최적화하려고 했다.
하지만 초기 구현 자체가 실제 부하 패턴에 적합하지 않을 수 있었다.
저자는 이를 해결하기 위해 요구사항을 기준으로 애플리케이션의 구조를 다시 설계하도록 했다.
그 결과 약 15분 안에 새로운 구현이 처음 Benchmark를 통과했다.
최종 점수는 471,143점까지 증가했다.
하지만 이 결과를 완전히 자율적인 에이전트의 성능이라고 해석해서는 안 된다.
실험 도중 사람이 판단과 방향을 수정했기 때문이다.
성능 병목을 잘못 판단한 사례
다음 실험에서는 ISUCON14를 사용했다.
처음에는 Benchmark 서버의 메모리 부족 현상이 발생했다.
에이전트는 이를 Benchmark 환경의 제한으로 판단했다.
따라서 요청의 처리 속도를 의도적으로 낮추는 방식에 많은 시간을 사용했다.
하지만 실제 원인은 애플리케이션에 있었다.
일부 사용자의 연결이 정상적으로 처리되지 않고 대기 상태로 남았다.
그 결과 Benchmark 서버의 메모리가 계속 증가했다.
즉 환경이 부족한 것이 아니라 애플리케이션의 실행 방식이 불필요한 자원 사용을 만들고 있었던 것이다.
저자는 이 문제를 해결하기 위해 다음 원칙을 추가했다.
외부 시스템이나 Benchmark 환경이 한계에 도달한 것처럼 보여도 먼저 애플리케이션이 해당 부하를 얼마나 만들고 있는지 확인한다.
이러한 원칙은 실제 운영 서비스에서도 적용할 수 있다.
예를 들어 데이터베이스의 CPU가 높다고 가정해 보자.
이를 해결하기 위해 더 큰 인스턴스로 변경할 수 있다.
하지만 실제로는 애플리케이션이 불필요한 Query를 반복해서 실행하고 있을 수 있다.
이 경우 서버를 확장하기 전에 Query의 발생 원인을 분석해야 할 수 있다.
애플리케이션을 처음부터 다시 설계한 결과
저자는 실제 요구사항과 부하 패턴을 바탕으로 애플리케이션을 다시 설계하도록 했다.
이후 ISUCON14의 점수는 654,641점까지 증가했다.
하지만 다음 문제가 남았다.
에이전트는 일정 점수에 도달한 이후 추가적인 최적화를 적극적으로 수행하지 않았다.
저자는 원인을 조사했다.
에이전트에게 목표 점수를 제공한 것이 오히려 제한으로 작용하고 있었다.
예를 들어 60만 점을 목표로 제시하면 에이전트는 해당 점수에 도달한 이후 추가 작업을 중단할 수 있었다.
하지만 실제로는 더 큰 병목이 존재할 수 있다.
따라서 저자는 구체적인 목표 점수보다 실제로 어떤 병목이 남아 있는지 계속 확인하는 과정을 강조하도록 변경했다.
지침을 계속 추가하는 방식의 실패
초기 Harness에서는 문제가 발생할 때마다 새로운 지침과 검사를 추가했다.
예를 들어 다음과 같은 규칙이다.
- 일정 시간마다 진행 상황 보고
- 일정 점수 아래에서는 특정 최적화 수행
- 일정 시간이 지나면 다시 Benchmark
- 코드 변경 이전에 정해진 질문에 답변
하지만 규칙이 증가하면서 다른 문제가 발생했다.
실제 판단에 필요한 정보가 여러 파일에 분산됐다.
에이전트는 필요한 지침을 찾아 읽지 않았다.
또는 실제 상황보다 문서에 정의된 절차를 우선했다.
저자는 이러한 문제를 해결하기 위해 문서를 줄였다.
대표적인 변경은 다음과 같다.
- Main Agent Skill: 150줄에서 65줄로 축소
- 반복적인 감사 Hook 제거
- 특정 문제에만 적용되는 예제 제거
- 여러 설계 문서를 세 개로 통합
- 판단 원칙을 여덟 개로 정리
즉 새로운 규칙을 계속 추가하기보다 중요한 원칙을 짧고 명확하게 제공하는 방향으로 변경했다.
성능 개선의 판단 구조
저자는 새로운 Harness에서 다음 세 단계를 명시하도록 했다.
무엇을 관찰했는가 → 어떻게 해석했는가 → 무엇을 변경할 것인가
예를 들어 다음 상황을 생각할 수 있다.
첫 번째 관찰은 특정 API의 P95 응답 시간이 매우 높다는 사실이다.
두 번째 해석은 해당 API가 특정 SQL Query를 반복해서 실행하기 때문이라는 것이다.
세 번째 결정은 Query의 실행 횟수를 줄이는 것이다.
이후 실제 변경을 적용했다면 다음 두 결과를 확인해야 한다.
- 원래 병목이 실제로 감소했는가?
- 새로운 병목이 어디에 나타났는가?
이러한 방식은 단순히 성능 점수가 증가했는지 확인하는 것보다 더 구체적이다.
점수가 증가했더라도 원래 의도한 개선 때문인지 다른 환경 변화 때문인지 구분해야 하기 때문이다.
새로운 Harness의 실제 결과
저자는 여러 과거 문제와 실제 대회 환경을 이용해 반복 실험했다.
실제 대회 문제를 다시 실행한 실험에서는 다음 결과가 나타났다.
| 실험 | 점수 |
|---|---|
| 실제 대회 결과 | 145,523 |
| 대회 이후 첫 번째 실험 | 78,780 |
| 두 번째 실험 | 263,447 |
| 세 번째 실험 | 575,333 |
세 번째 실험에서는 사람이 성능 개선 방향을 직접 지시하지 않은 상태로 약 1시간 30분 만에 51만 점에 도달했다.
최고 점수는 575,333점이었다.
하지만 이러한 결과를 AI 에이전트의 일반적인 성능 향상 비율로 사용해서는 안 된다.
실험마다 Harness와 환경, 실제 코드가 달랐기 때문이다.
또한 일부 실험에는 사람이 직접 개입했다.
동일한 조건에서 무작위로 배정한 A/B 실험은 아니었다.
공개 코드의 제한
저자는 세 번째 실험의 Repository를 아직 공개하지 않았다.
관련 문제 코드의 라이선스가 MIT인지 확인하지 못했기 때문이다.
따라서 해당 구현을 직접 재현하려면 공개된 자료에 한계가 존재한다.
저자는 라이선스가 확인된 이후 Repository를 공개할 계획이라고 설명했다.
이번 글에서 실제로 확인할 수 있는 것은 개발자가 공개한 실행 기록과 성능 점수, Harness의 설계 변화다.
결과의 해석
이번 글은 AI 코딩 에이전트의 수를 늘리면 성능 최적화가 자동으로 개선된다는 근거를 제공하지 않는다.
오히려 매우 많은 PR을 병합했음에도 실제 성능 점수가 기대한 만큼 증가하지 않았던 사례를 보여준다.
또한 Main Agent 하나로 변경한 것만으로 모든 문제가 해결된 것도 아니다.
실제로는 다음 작업이 중요했다.
- 사용자의 행동 분석
- 실제 부하 패턴 확인
- 병목의 원인 분석
- 기존 구조를 유지할지 다시 설계할지 판단
- 변경 이후 실제 측정
이번 글은 AI 코딩 에이전트를 이용한 성능 최적화에서 코드 작성 속도보다 실제 시스템을 이해하고 검증하는 의사결정 과정이 중요하다는 점을 여러 Benchmark와 실패 사례로 설명한다.
請求書の「発行日より30日以内」「Net 30」を支払期限の日付に直す時に決めた 4 つのこと(pdf.js・外部送信ゼロ)
- 원제: 請求書の「発行日より30日以内」「Net 30」を支払期限の日付に直す時に決めた 4 つのこと(pdf.js・外部送信ゼロ)
- 저자: SoraProject
- 매체: Zenn
- 게시일: 2026-10-31
- 기술: JavaScript, PDF.js, Chrome Extension, 날짜 계산, 정규식, CSV
- 원문: https://zenn.dev/sora_proj/articles/2fb2c5f78e0846
브라우저에서 PDF 청구서를 분석하는 도구를 개발하면서 지급 기한이 실제 날짜가 아니라 상대적인 기간으로 작성된 경우 이를 어떻게 처리해야 하는지 설명한 기술 글이다.
예를 들어 청구서에는 다음과 같은 내용이 존재할 수 있다.
- 2026년 11월 30일까지 지급
- 발행일로부터 30일 이내
- Net 30
- 청구일 이후 30일
첫 번째는 실제 지급 날짜가 명시돼 있다.
하지만 나머지는 다른 날짜를 기준으로 계산해야 한다.
이번 글은 이러한 차이를 JavaScript로 처리하면서 발생한 실제 오류와 설계 선택을 설명한다.
실제 프로젝트의 구조
저자는 브라우저에서 실행하는 청구서 분석 도구를 개발했다.
사용자는 PDF 파일을 선택한다.
애플리케이션은 PDF.js를 이용해 텍스트를 추출한다.
그리고 다음 정보를 찾는다.
- 청구서 발행일
- 지급 기한
- 청구 금액
- 관련 거래처 정보
추출한 결과는 표로 표시한다.
이후 CSV 파일로 저장할 수도 있다.
중요한 특징은 실제 PDF 파일을 외부 서버에 업로드하지 않는다는 점이다.
텍스트 추출과 날짜 계산은 브라우저 안에서 수행한다.
실제 발견한 문제
저자는 가상의 청구서 13개로 테스트하고 있었다.
그중 다섯 개 청구서는 지급 기한을 찾지 못했다.
대표적으로 다음 두 종류가 존재했다.
첫 번째는 일본어로 작성한 상대 기한이다.
発行日より30日以内
두 번째는 영어로 작성한 상대 기한이다.
Net 30
기존 파서는 실제 날짜 형태만 찾았다.
따라서 이러한 표현에서는 지급일을 계산하지 못했다.
저자는 이를 해결하기 위해 새로운 함수 두 개를 추가했다.
전체 코드는 약 15줄이었다.
하지만 중요한 부분은 코드의 길이가 아니라 어떤 경우에만 자동 계산을 허용할지 결정한 것이다.
첫 번째 결정: 명확한 일수가 있을 때만 계산
저자는 상대 기한 가운데 실제 일수가 명시된 표현만 처리했다.
대표적인 표현은 다음과 같다.
- 발행일로부터 30일 이내
- 청구일로부터 30일 이내
- Net 30
- Within 30 days
반면 다음과 같은 표현은 자동 계산하지 않았다.
월말 마감, 익월 말 지급
이유는 실제 기준일이 불명확할 수 있기 때문이다.
예를 들어 거래처마다 월말 마감의 기준이 다를 수 있다.
또한 청구서 본문에 실제 지급일을 계산하기 위한 충분한 정보가 존재하지 않을 수 있다.
이러한 상황에서 임의로 날짜를 생성하면 잘못된 지급일을 정상적인 결과처럼 표시할 수 있다.
저자는 확신할 수 없는 날짜를 잘못 채우는 것보다 빈 값으로 남겨 두고 사용자가 직접 확인하도록 하는 편이 안전하다고 판단했다.
정규식 기반 일수 추출
저자는 다음 함수를 만들었다.
pickDueTerm()
이 함수는 청구서의 텍스트에서 지급 기한을 나타내는 일수를 추출한다.
예를 들어 다음 표현을 검사한다.
Net 30
결과는 숫자 30이다.
일본어와 영어에 서로 다른 정규식을 사용했다.
또한 전각 숫자와 반각 숫자를 통일하는 전처리를 수행했다.
이렇게 하면 다음처럼 표기 방식이 다른 숫자도 처리할 수 있다.
3030
하지만 정규식이 실제 청구서의 모든 표현을 이해할 수 있는 것은 아니다.
예를 들어 계약에 따라 달라지는 지급 조건이나 특수한 약어는 별도로 처리해야 할 수 있다.
두 번째 결정: 실제 날짜가 있다면 우선 사용
청구서에는 다음처럼 두 표현이 함께 존재할 수 있다.
지급 기한: 2026년 11월 30일
발행일로부터 30일 이내
이 경우 저자는 실제 지급 날짜를 우선한다.
즉 먼저 명시적인 날짜를 찾는다.
그다음 명시적인 날짜가 없을 때만 상대 기한을 계산한다.
이러한 순서가 중요한 이유는 발행일을 잘못 추출할 가능성 때문이다.
예를 들어 실제 지급일은 올바르게 추출했지만 발행일을 잘못 읽었다고 가정해 보자.
상대 기한을 먼저 계산하면 잘못된 지급일이 생성될 수 있다.
따라서 이미 명시된 지급 날짜가 존재한다면 해당 값을 우선 사용한다.
발행일이 없다면 계산하지 않음
상대 기한을 계산하려면 기준 날짜가 필요하다.
예를 들어 Net 30이라는 표현만 존재한다고 가정해 보자.
하지만 청구서의 발행일을 찾지 못했다.
이 경우 30일을 더할 기준이 없다.
저자는 다른 날짜를 임의로 선택하지 않는다.
대신 지급 기한을 빈 값으로 유지한다.
이러한 방식은 실제 데이터가 불완전할 때 자동으로 추측한 값을 생성하지 않도록 한다.
세 번째 결정: UTC 기준으로 날짜 계산
이번 글에서 JavaScript와 직접 관련된 부분은 날짜 계산이다.
저자는 청구서의 날짜를 다음 형식으로 저장했다.
2026-10-05
문제는 JavaScript의 Date가 시간대와 연결된다는 점이다.
예를 들어 다음 표현을 생각할 수 있다.
new Date("2026-10-05")
이 값은 UTC 기준의 특정 시각으로 해석될 수 있다.
하지만 이후 로컬 시간대의 날짜를 가져오면 실제 표시 날짜가 달라질 수 있다.
특히 UTC보다 느린 시간대에서는 전날의 날짜로 표시될 수 있다.
저자는 지급 기한 계산에 실제 시각의 개념이 필요하지 않다고 판단했다.
청구서에는 날짜만 존재하기 때문이다.
따라서 다음 API를 사용했다.
Date.UTC()
실제 연도와 월, 일을 분리한다.
이후 UTC 기준의 날짜를 생성한다.
그리고 일수를 밀리초로 변환해 더한다.
마지막으로 ISO 문자열의 앞부분을 가져와 날짜만 반환한다.
대표적으로 다음과 같은 계산이다.
2026-10-05 + 30일 = 2026-11-04
이렇게 하면 실행하는 사용자의 로컬 시간대에 따라 날짜가 달라지는 문제를 줄일 수 있다.
다만 이러한 방식은 지급일과 같은 날짜 단위 계산에 적합하다.
실제 시각이나 지역 시간대의 자정, 일광절약시간을 다뤄야 한다면 다른 접근이 필요할 수 있다.
네 번째 결정: 지급 기간도 별도 데이터로 저장
저자는 실제 지급 날짜만 저장하지 않았다.
다음 값도 함께 관리했다.
term
이 값은 발행일부터 지급일까지 며칠인지 나타낸다.
예를 들어 다음 결과가 존재할 수 있다.
| 발행일 | 지급일 | Term |
|---|---|---|
| 2026-10-05 | 2026-11-04 | 30 |
| 2026-10-05 | 2026-11-19 | 45 |
| 2026-10-05 | 2026-12-04 | 60 |
이렇게 하면 실제 청구서마다 지급 조건이 어떻게 다른지 확인할 수 있다.
또한 상대 기한에서 계산한 날짜가 올바른지 검증하기도 쉽다.
예를 들어 테스트에서 지급일만 확인했다고 가정해 보자.
우연히 비슷한 날짜가 반환되더라도 실제로 30일을 더한 결과인지 확인하기 어렵다.
하지만 term도 함께 검사하면 날짜 차이가 올바른지 확인할 수 있다.
실제 테스트 결과
저자는 가상의 청구서 13개를 변경 전후의 Parser에서 실행했다.
결과는 다음과 같았다.
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| 전체 테스트 청구서 | 13개 | 13개 |
| 지급 기한을 찾지 못한 청구서 | 5개 | 3개 |
| 상대 기한 계산으로 추가 처리 | 0개 | 2개 |
새롭게 처리한 두 청구서는 다음 표현을 사용했다.
- 발행일로부터 30일 이내
- Net 30
두 청구서 모두 다음 결과를 반환했다.
- 발행일: 2026-10-05
- 지급일: 2026-11-04
- Term: 30
나머지 세 청구서는 자동 계산하지 않았다.
실제 기준일이나 계산 규칙이 충분히 명확하지 않았기 때문이다.
월별 미지급 금액에 대한 영향
이번 변경에는 다른 데이터 처리 기능에도 영향을 주는 부분이 있었다.
예를 들어 10월 5일 발행된 청구서가 있다고 가정해 보자.
하지만 지급 기한은 11월 4일이다.
기존에는 지급일을 찾지 못해 월별 미지급 금액 계산에서 제외되거나 별도로 표시됐을 수 있다.
새로운 Parser에서는 실제 지급 기한을 계산한다.
그 결과 해당 청구서는 11월의 미지급 금액에 포함될 수 있다.
저자는 이를 오류가 아니라 실제 지급 조건을 반영한 결과로 판단했다.
하지만 월별 합계를 어떤 기준으로 계산해야 하는지는 별도의 비즈니스 규칙이다.
발행월 기준의 합계와 지급월 기준의 합계는 서로 다르기 때문이다.
따라서 날짜 추출과 실제 회계 집계 규칙을 구분해야 한다.
구현의 한계
이번 글의 테스트 데이터는 가상의 청구서 13개다.
실제 여러 기업의 청구서 수천 개를 이용해 검증한 결과는 아니다.
따라서 실제 청구서의 날짜 인식 정확도가 특정 비율로 개선됐다고 단정할 수 없다.
또한 정규식 기반의 접근은 지원하지 않는 표현에서 실패할 수 있다.
저자는 이러한 상황에서 날짜를 추측하지 않고 비워 두는 방식을 선택했다.
이번 글은 브라우저 안에서 PDF 텍스트를 처리할 때 날짜 계산의 정확성과 잘못된 자동 추론의 위험을 어떻게 관리할 수 있는지를 작고 명확한 JavaScript 함수와 실제 테스트 결과로 설명한다.