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

기준 시점: 2026-11-15 06:00 KST
조사 범위: 전일 2026-11-14 06:00 ~ 당일 2026-11-15 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
Next.js 16.5.0-canary.50 공개 — Turbopack Manifest 최대 79% 축소, GC 기본 활성화와 AI 에이전트 지침 자동 생성
- 발표일: 2026-11-14
- GitHub 표시 시각: 2026-11-14 08:36 — 정확한 표시 시간대 미확인
- 버전: Next.js 16.5.0-canary.50
- 안정화 단계: Canary Pre-release
- 적용 대상: Next.js App Router, Turbopack, React Server Components, 개발 서버
- 주요 변경: Client Reference Manifest 압축, Turbopack Chunking 및 Garbage Collection, RSC Navigation 수정, AI 에이전트 지침 관리
- 성능 수치: Vercel 개발팀의 자체 Benchmark. 독립적인 재현 결과는 확인되지 않음
- 원문: https://github.com/vercel/next.js/releases/tag/v16.5.0-canary.50
Next.js가 16.5 계열의 새로운 Canary 버전을 공개했다.
이번 릴리스는 일반적인 문서 수정이나 의존성 업데이트보다 실제 빌드 시스템에 영향을 주는 변경이 많다.
특히 Turbopack에서 생성하는 Client Reference Manifest의 크기를 크게 줄이고, 불필요한 메모리를 회수하는 기능을 기본으로 활성화했다.
또한 React Server Components의 Navigation과 Prefetch 처리 오류를 수정했다.
개발 도구 측면에서는 next dev 실행 시 AI 코딩 에이전트를 위한 관리형 지침을 생성하는 기능도 포함됐다.
다만 이번 변경은 Next.js 16.5의 정식 안정 버전 출시가 아니다.
일부 기능의 기본값이 변경됐더라도 현재 공개된 대상은 Canary다.
Client Reference Manifest의 역할
Next.js App Router는 React Server Components를 사용한다.
서버 컴포넌트는 서버에서 처리할 수 있지만, 실제 브라우저에서 상호작용하는 클라이언트 컴포넌트에는 JavaScript가 필요하다.
예를 들어 다음과 같은 페이지를 생각할 수 있다.
- 서버에서 렌더링하는 상품 목록
- 클라이언트에서 동작하는 검색 필터
- 클라이언트에서 동작하는 정렬 버튼
- 사용자 입력 Form
Next.js는 서버에서 렌더링하는 컴포넌트와 브라우저에서 실행하는 컴포넌트 사이의 연결 정보를 관리해야 한다.
이때 Client Reference Manifest가 사용된다.
Manifest에는 어떤 클라이언트 컴포넌트가 어떤 JavaScript Chunk와 연결되는지에 관한 정보가 포함된다.
문제는 큰 애플리케이션에서 발생했다.
여러 클라이언트 컴포넌트가 같은 Chunk Group을 참조할 수 있다.
하지만 기존 Manifest는 각 컴포넌트마다 동일한 Chunk 목록을 반복해서 저장할 수 있었다.
예를 들어 하나의 페이지에서 100개 클라이언트 컴포넌트가 같은 30개 Chunk를 참조한다고 가정해 보자.
각 컴포넌트 항목에 30개 Chunk의 경로를 모두 기록하면 동일한 문자열과 배열이 반복해서 생성된다.
이러한 중복은 실제 Manifest 파일의 크기와 메모리 사용량에 영향을 준다.
Manifest의 문자열과 배열 공유
이번 변경에서는 반복되는 데이터를 별도로 저장한다.
대표적인 방식은 다음과 같다.
- 중복된 Chunk 경로를 문자열 테이블에 한 번만 기록한다.
- 반복해서 사용하는 Chunk 목록을 공통 배열로 관리한다.
- 연속된 Chunk Index를 범위 형태로 압축한다.
- 같은 구조의 Manifest Entry를 생성하는 Factory 함수를 사용한다.
예를 들어 여러 컴포넌트가 같은 Chunk 목록을 사용한다고 가정해 보자.
기존에는 각 항목에 전체 배열이 존재했다.
새로운 방식에서는 공통 배열을 참조한다.
따라서 전체 파일에서 반복되는 문자열과 객체 표현을 줄일 수 있다.
또한 JavaScript Runtime에서 Manifest를 평가할 때 생성해야 하는 객체와 배열의 수도 줄어들 수 있다.
다만 구현은 단순한 JSON 압축이 아니다.
새로운 Manifest는 공통 테이블과 Factory 함수를 포함하는 JavaScript 표현식으로 생성된다.
따라서 Manifest를 단순한 JSON 본문으로 추출하던 일부 내부 도구의 처리 방식도 함께 변경됐다.
실제 Manifest 크기 감소
개발팀이 공개한 첫 번째 Benchmark는 대규모 애플리케이션을 대상으로 한다.
해당 애플리케이션에서는 939개의 Client Reference Manifest가 생성됐다.
측정 결과는 다음과 같았다.
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| Manifest 전체 크기 | 1,674.7MiB | 34.0MiB |
| 전체 Manifest 평가 시간 | 약 6.0초 | 약 0.5초 |
| 로딩 후 유지되는 Heap | 313MiB | 121MiB |
| 빌드 종료 시 Live Heap | 14.67GiB | 12.91GiB |
| 전체 빌드 시간 | 79.4초 | 79.0초 |
Manifest 파일의 크기는 약 98% 감소했다.
하지만 이 결과를 모든 Next.js 프로젝트에 일반화해서는 안 된다.
해당 애플리케이션은 기존 Manifest에 중복 정보가 특히 많이 포함된 사례다.
또한 전체 빌드 시간은 79.4초에서 79.0초로 거의 변경되지 않았다.
즉 Manifest 파일이 크게 작아졌다고 해서 전체 빌드도 같은 비율로 빨라진 것은 아니다.
추가 Benchmark에서 발견한 트레이드오프
개발팀은 다른 두 애플리케이션에서도 추가로 측정했다.
첫 번째는 다음 규모의 App Router Benchmark다.
- Route 5,000개
- Component 3,000개
- Client Reference Manifest 5,603개
두 번째는 1,000개 페이지를 포함한 애플리케이션이다.
추가 실험에서는 다음 결과가 나타났다.
| 항목 | 대규모 App Router | 1,000개 페이지 앱 |
|---|---|---|
| Manifest 크기 감소 | 79% | 43% |
| Manifest 로딩 후 Heap 감소 | 42% | 7% |
| 전체 빌드 시간 | 뚜렷한 개선 없음 | 뚜렷한 개선 없음 |
| Manifest 로딩 시간 | 비슷하거나 일부 느림 | 일부 측정에서 더 느림 |
특히 작은 Manifest가 많이 생성되는 경우에는 새로운 압축 구조를 해석하는 비용이 발생할 수 있었다.
추가 Benchmark에서는 일부 Manifest 로딩 시간이 오히려 증가했다.
따라서 이번 변경의 효과는 애플리케이션의 구조와 Manifest에 포함된 중복 데이터의 양에 따라 달라진다.
Manifest의 크기와 메모리 사용량을 줄이는 데는 효과가 확인됐지만, 모든 프로젝트에서 로딩 및 빌드 시간까지 개선되는 것은 아니다.
또한 이러한 수치는 Next.js 개발팀이 자체 테스트 환경에서 측정한 결과다.
독립적인 제3자의 Benchmark는 확인되지 않았다.
- 상세 구현 및 Benchmark: https://github.com/vercel/next.js/pull/101312
Chunking 이전 Minification 기본 활성화
이번 릴리스에서는 다음 설정이 Canary에서 기본 활성화됐다.
experimental.turbopackMinifyBeforeChunking
이 기능은 코드의 크기를 계산하고 Chunk를 구성하는 순서에 영향을 준다.
일반적으로 Bundler는 여러 Module을 분석하고 이를 Chunk로 나눈다.
Chunk는 실제 브라우저에 전달하는 JavaScript 파일의 단위다.
문제는 코드가 압축되기 전과 이후의 크기가 다르다는 점이다.
예를 들어 다음 Module이 존재한다고 가정해 보자.
원본 코드는 100KB다.
하지만 Minification을 수행하면 30KB가 된다.
Chunking 알고리즘이 원본 크기만 기준으로 판단한다면 실제 최종 파일의 크기를 정확하게 반영하지 못할 수 있다.
이번 변경은 Minification 이후의 크기를 고려해 Chunk를 구성하도록 한다.
자체 성능 측정
개발팀은 큰 애플리케이션의 Cold Build에서 사용자 CPU 시간이 약 9% 감소했다고 설명했다.
하지만 다음 환경에서는 뚜렷한 개선이 나타나지 않았다.
- Warm Build
- 기본
create-next-app프로젝트
또한 일부 코드 중복이 감소했다고 보고했다.
다만 이는 특정 애플리케이션을 대상으로 한 자체 측정이다.
모든 프로젝트의 빌드 시간이 9% 감소한다는 의미는 아니다.
그리고 명시적인 설정이 존재하는 경우에는 해당 값이 우선한다.
예를 들어 개발자가 다음처럼 비활성화했다면 기존 설정이 유지된다.
experimental.turbopackMinifyBeforeChunking: false
이번 기본값 변경은 Canary에 적용됐다.
정식 안정 버전에서도 동일하게 적용된다고 현재 시점에 단정해서는 안 된다.
Turbopack Garbage Collection 기본 활성화
이번 릴리스에서는 Turbopack의 Garbage Collection도 기본 활성화됐다.
대표적인 설정은 다음과 같다.
experimental.turbopackGc
Garbage Collection은 더 이상 필요하지 않은 데이터를 회수하는 작업이다.
개발 서버와 빌드 시스템은 다음 정보를 메모리에 유지할 수 있다.
- Module Graph
- 파일 의존성
- 컴파일 중간 결과
- 변경된 파일의 분석 정보
- Cache 관련 데이터
예를 들어 개발자가 여러 파일을 수정한다고 가정해 보자.
Turbopack은 수정된 파일과 관련된 의존성을 다시 분석해야 한다.
하지만 오래된 계산 결과가 계속 메모리에 남아 있으면 실제 메모리 사용량이 증가할 수 있다.
Garbage Collection은 필요하지 않은 데이터를 정리하는 데 사용된다.
이번 변경에서는 별도 설정이 없다면 기본 Timing을 사용하도록 구성했다.
개발자는 다음 옵션을 이용해 비활성화할 수 있다.
experimental.turbopackGc: false
또한 다음 설정을 통해 세부 Timing을 변경할 수 있다.
minProgressMsrootTtlMs
다만 GC가 활성화됐다는 사실만으로 모든 프로젝트에서 메모리 사용량이 반드시 감소한다는 의미는 아니다.
실제 작업의 특성과 메모리 회수 시점에 따라 CPU와 메모리 사이의 트레이드오프가 존재할 수 있다.
개발팀은 이 기본값을 Next.js 16.5에 반영할 계획이라고 설명한다.
하지만 이번 조사 기준 시점에 공개된 버전은 Canary다.
React Server Components Navigation 수정
이번 릴리스에서는 다음 문제도 수정됐다.
Fix RSC navigation and runtime prefetches in next start
Next.js는 페이지 이동 과정에서 RSC Payload를 가져올 수 있다.
RSC Payload는 서버 컴포넌트의 렌더링 결과와 관련 정보를 클라이언트에 전달하는 데 사용된다.
문제는 Prerendering과 Cache Revalidation이 결합된 상황이었다.
예를 들어 특정 상품 페이지가 정적으로 생성됐다고 가정해 보자.
하지만 실제 요청에서는 사용자별 Cookie나 동적인 데이터에 따라 다른 결과가 필요할 수 있다.
이 경우 단순한 정적 RSC 결과를 재사용하면 문제가 발생할 수 있다.
특히 실제 RSC 데이터가 완전하지 않은 Fallback Shell을 캐시된 정적 결과처럼 반환하면 클라이언트 Navigation에서 오류가 발생할 수 있다.
이번 수정에서는 실제 Prerender 결과와 RSC 데이터의 완전성을 확인한다.
완전한 RSC 응답을 재사용할 수 없다면 실제 요청에 맞는 RSC를 별도로 렌더링한다.
이때 다음 정보를 유지한다.
- Router State
- Prefetch 단계
- 요청별 Cookie
- Resume Data Cache
따라서 실제 페이지 이동을 전체 문서 Navigation으로 변경하지 않고도 필요한 데이터를 가져올 수 있도록 한다.
하지만 이번 수정은 모든 RSC Navigation 지연을 없애는 기능은 아니다.
특정 Cache 및 Prerendering 상태에서 잘못된 RSC를 재사용하던 문제를 해결한 것이다.
next dev의 AI 에이전트 지침 자동 생성
이번 릴리스에는 개발 환경과 AI 코딩 에이전트의 연동을 개선하는 변경도 포함됐다.
대표적인 변경은 다음과 같다.
Generate managed agent instructions on next dev
Next.js는 개발 서버를 실행할 때 프로젝트의 AGENTS.md에 관리형 지침을 생성하거나 갱신할 수 있도록 변경했다.
AGENTS.md는 AI 코딩 에이전트가 프로젝트의 규칙과 작업 방법을 이해하도록 정보를 제공하는 파일이다.
기존에는 별도의 에이전트 설정 과정이 필요할 수 있었다.
이번 변경에서는 next dev를 실행할 때 관련 지침을 준비할 수 있도록 한다.
기존 사용자 작성 내용 보존
이번 구현에서 중요한 부분은 기존 파일을 모두 덮어쓰지 않는다는 점이다.
예를 들어 개발자가 이미 AGENTS.md에 다음 정보를 작성했다고 가정해 보자.
- 프로젝트의 디렉터리 구조
- 코딩 컨벤션
- 테스트 명령
- 배포 정책
Next.js는 관리형 지침 영역을 생성하거나 갱신하되 다른 사용자 작성 내용을 유지하도록 설계했다.
또한 다음 설정으로 관리형 규칙을 제거할 수 있다.
agentRules: false
이 설정은 Next.js가 관리하는 영역을 제거하며 다른 내용을 삭제하지 않는다.
추가적인 실험 기능도 구분된다.
experimental.agentUpgradeexperimental.agentFeedback
특히 Feedback 기능은 Opt-in이며 Telemetry가 필요하다.
CI 환경에서는 해당 기능이 제외된다.
Telemetry Opt-out 수정
이번 PR에는 Telemetry 설정 관련 수정도 포함됐다.
기존에는 개발 서버와 빌드가 서로 다른 Cache 디렉터리를 사용하는 환경에서 Telemetry 비활성화 설정이 기대한 대로 전달되지 않을 수 있었다.
대표적인 환경은 다음과 같다.
- Docker
- CI
- 임시 Cache 디렉터리를 사용하는 빌드
새로운 구현은 기존 CLI의 Telemetry Preference를 확인하도록 변경했다.
따라서 사용자가 Telemetry를 비활성화했다면 실제 빌드 환경에서도 해당 설정을 유지하도록 한다.
이번 PR에서는 개발 환경과 운영 빌드 환경의 테스트를 함께 수행했다.
대표적으로 다음 검증이 포함됐다.
- 개발 통합 테스트 16개
- 관련 Unit Test 120개
- Telemetry CLI 통합 테스트 20개
- Preview 패키지 검증 37개
하지만 이러한 테스트가 모든 AI 코딩 에이전트와 모든 운영 환경에서 동일한 동작을 보장한다는 의미는 아니다.
또한 이번 기능은 현재 Canary에 포함된 상태다.
적용 범위와 한계
이번 Next.js Canary의 주요 변경은 다음과 같다.
| 영역 | 변경 | 현재 상태 |
|---|---|---|
| Client Reference Manifest | 중복 Chunk 목록 압축 | Canary |
| Turbopack Chunking | Minification 이전·이후 크기 처리 개선 | Canary 기본 활성화 |
| Turbopack GC | 기본 활성화 | Canary에 포함, 정식 16.5 계획 |
| RSC Navigation | 불완전한 Cache 결과 재사용 방지 | Canary 버그 수정 |
| AI 에이전트 지침 | next dev 실행 시 관리형 규칙 생성 | Canary |
이번 릴리스는 Turbopack의 내부 메모리와 빌드 데이터 구조에 비교적 큰 변경을 포함한다.
하지만 정식 안정 버전이 아니므로 실제 운영 프로젝트에서 사용할 때는 호환성과 회귀 문제를 별도로 검증해야 한다.
특히 Manifest의 저장 크기와 실제 빌드 시간은 별개의 성능 지표다.
이번 업데이트는 Turbopack의 중복 데이터와 메모리 사용량을 줄이면서 RSC Navigation의 정확성과 AI 개발 도구의 연동을 개선한 Canary 릴리스다.
이번 조사 범위에서 React, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro, Remix의 이외에 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
Astro 7.4.12의 공식 릴리스도 확인했지만 GitHub에 표시된 공개 시점은 11월 13일로, 한국 시간 기준 이번 조사 범위에 포함되는 신규 발표로 확인되지 않아 제외했다.
공식 릴리스에 포함된 변경 가운데 주요 내용은 Content Collection 동기화 이후 Entry 관련 메모리를 정리하는 수정이다.
하지만 해당 변경을 11월 14~15일의 새로운 발표로 소개하지 않았다.
Vercel의 공식 제품 Changelog에서도 11월 13일 이후 이번 조사 범위에 해당하는 중요한 신규 발표를 확인하지 못했다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome Developers·Chromium, Firefox·Mozilla, WebKit·Safari와 JavaScript·TypeScript·CSS, 웹 성능 및 접근성 관련 공식 자료를 확인했다.
이번 기간에 주요 웹 표준의 새로운 정식 채택이나 안정 브라우저에서 기본 제공되는 중요한 신규 API의 출시 발표는 확인하지 못했다.
WebKit에서는 11월 14일에도 개발 빌드가 생성됐다.
하지만 해당 빌드는 WebKit의 개발 브랜치에 관한 것이다.
개발 빌드가 생성됐다는 사실만으로 새로운 Safari 안정 버전이나 새로운 표준 API가 정식 출시됐다고 판단할 수 없다.
- WebKit 개발 빌드 기록: https://webkit.org/build-archives/
또한 Chrome 및 Firefox의 기존 기능에 관한 문서 수정과 실제 브라우저 기능의 신규 안정화는 구분했다.
Firefox 160의 WebTransport API와 보안 수정은 이전 브리핑에서 이미 다뤘으므로 반복하지 않았다.
이번 추천 글에서는 Web Audio API를 사용해 별도의 오디오 파일 없이 브라우저에서 음악과 효과음을 생성하는 방법을 자세하게 다룬다.
특히 JavaScript Timer와 AudioContext의 Clock이 서로 다르다는 점, 백그라운드 탭에서 Timer가 지연될 때 음악이 한꺼번에 재생되는 문제, OfflineAudioContext를 이용한 반복 가능한 검증 방법을 살펴볼 수 있다.
3️⃣ 백엔드·인프라
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
Cloudflare, AWS, Google Cloud, Azure의 주요 공식 변경 기록과 서버리스·엣지, 데이터베이스·API 관련 발표를 검토했다.
Cloudflare의 공식 Changelog에서 확인한 최근 주요 발표는 11월 13일에 집중돼 있었다.
대표적으로 다음 항목이 존재했다.
- Workers·Durable Objects의 지출 상한 설정
- Vercel Sandbox의 지역별 이미지 캐시
- AI Gateway의 신규 모델 제공
- Browser Run 로컬 개발 지원
- Terraform 기반 AI Search Namespace 관리
이 가운데 Cloudflare와 Vercel 관련 항목은 이전 브리핑에서 이미 다뤘으므로 반복하지 않았다.
또한 11월 14일의 공식 신규 발표로 확인되지 않은 제품 문서의 변경을 새 기능 출시로 취급하지 않았다.
- Cloudflare 공식 Changelog: https://developers.cloudflare.com/changelog/
- Vercel 공식 Changelog: https://vercel.com/changelog
- AWS 공식 What's New: https://aws.amazon.com/about-aws/whats-new/
- Google Cloud 공식 Release Notes: https://docs.cloud.google.com/release-notes/
이번 기간에는 중요한 정식 서버리스 기능이나 데이터베이스 API의 신규 발표를 1차 원문과 함께 검증하지 못했다.
따라서 기존 기능의 설명을 새로운 출시 소식처럼 반복하지 않았다.
4️⃣ 개발 도구 및 보안
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
Node.js, Deno, Bun, Vite, webpack, Rollup, esbuild, Babel, Biome, ESLint, Prettier, npm, pnpm, Yarn, GitHub, GitLab, 테스트·배포 도구 및 주요 보안 권고를 조사했다.
GitHub의 공식 제품 Changelog에서는 11월 13일 이후 이번 조사 기간에 해당하는 중요한 신규 발표를 확인하지 못했다.
- GitHub 공식 Changelog: https://github.blog/changelog/
Node.js의 11월 11일 보안 릴리스와 Vite 8.3.8은 이전 브리핑에서 이미 다뤘으므로 반복하지 않았다.
개발 브랜치에서 새로운 Commit과 Pull Request가 생성됐다는 사실만으로 실제 사용자가 설치할 수 있는 정식 보안 패치가 공개됐다고 판단하지 않았다.
또한 공개 취약점 데이터베이스에서 11월 14일에 등록된 일부 취약점 기록을 확인했다.
하지만 데이터베이스의 등록 날짜와 해당 취약점이 처음 공개된 날짜는 서로 다를 수 있다.
특히 수정 버전이 이전에 이미 공개된 경우라면 데이터베이스 등록을 새로운 보안 패치 출시로 해석해서는 안 된다.
따라서 최초 공개 시점과 공식 수정 범위를 함께 검증할 수 없는 항목은 이번 주요 소식에서 제외했다.
OWASP와 주요 공급망 보안 자료에서도 이번 조사 범위에 해당하는 새로운 중요한 공식 권고를 확인하지 못했다.
이번 추천 글에서는 AI 코딩 에이전트의 작업 지침을 관리하는 과정에서 실제로 발생한 반복 오류와, TypeScript 웹 애플리케이션에서 외부 하드웨어의 입력을 정규화하는 사례를 소개한다.
📚 추천 글
What it took to add Jalali dates to form-js
- 저자: Mohammadreza Shirazi
- 매체: 개인 기술 블로그 / DEV Community
- 게시일: 2026-11-14 — 정확한 게시 시각 미확인
- 기술: JavaScript, TypeScript, Preact, form-js, Dependency Injection, Intl, 날짜·시간대 처리
- 원문: https://dev.to/mamrezn7/what-it-took-to-add-jalali-dates-to-form-js-182g
기존 Form Builder에 Jalali Calendar를 추가하면서 사용자에게 표시되는 날짜와 실제 시스템에 저장하는 날짜를 분리하도록 설계한 과정을 설명한 글이다.
Jalali Calendar는 이란 등에서 사용하는 페르시아 달력이다.
저자는 업무 프로세스에 필요한 Form을 만들기 위해 bpmn.io의 form-js를 사용하고 있었다.
하지만 실제 사용자는 페르시아어를 사용했다.
따라서 날짜를 입력할 때도 Jalali Calendar를 사용할 수 있어야 했다.
문제는 단순히 달력의 UI를 바꾸는 것으로 해결되지 않았다는 점이다.
기존 form-js의 날짜 데이터와 Validation, Submission은 그레고리력 기반 ISO 날짜를 사용하고 있었기 때문이다.
form-js의 구조
form-js는 하나의 컴포넌트로만 구성된 라이브러리가 아니다.
여러 패키지로 나뉜 Monorepo다.
대표적인 패키지는 다음과 같다.
form-js-viewerform-js-editorform-js-playgroundform-json-schemaform-js
Viewer는 JSON Schema를 이용해 실제 Form을 렌더링한다.
Editor는 드래그 앤 드롭으로 Form을 구성할 수 있도록 한다.
Playground는 Form을 구성하고 실제 데이터를 확인하는 기능을 결합한다.
문제는 실제 날짜 필드가 여러 패키지와 연결돼 있다는 사실이다.
예를 들어 Editor에서 날짜 필드를 추가할 수 있다.
이후 Viewer에서는 해당 필드를 렌더링해야 한다.
그리고 Validation과 Submission에서는 같은 데이터 형식으로 값을 처리해야 한다.
따라서 날짜 UI 하나만 변경해서는 전체 동작을 유지하기 어렵다.
Dependency Injection과 Field Registry
form-js는 내부 기능을 Dependency Injection으로 연결한다.
대표적인 라이브러리는 다음과 같다.
didi
날짜 필드 역시 Registry를 통해 실제 컴포넌트와 연결된다.
저자는 기존 구현의 다음 경로를 조사했다.
- JSON Schema Import
- 필드 등록
- 초기값 변환
- 사용자 입력
- Event Bus
- Validation
- Submission
이러한 구조에서는 특정 함수의 호출 관계만 따라가서는 전체 동작을 이해하기 어렵다.
예를 들어 사용자가 날짜를 선택하면 Event Bus를 통해 다른 컴포넌트가 갱신될 수 있다.
또한 Validation에서는 FEEL Expression이 같은 값을 참조할 수 있다.
따라서 날짜의 실제 데이터 형식을 변경하면 여러 영역에서 문제가 발생할 가능성이 있었다.
저장 형식은 유지
저자가 선택한 가장 중요한 설계는 다음과 같다.
실제 저장 데이터는 기존 그레고리력 ISO 날짜를 유지한다.
예를 들어 다음 값이다.
2026-11-14
반면 사용자의 화면에는 Jalali Calendar를 표시한다.
사용자가 Jalali 날짜를 선택하면 해당 값을 ISO 날짜로 변환한다.
반대로 기존 ISO 날짜를 읽을 때는 Jalali 날짜로 변환해 표시한다.
따라서 실제 저장 형식과 사용자 표시 형식을 분리한다.
이렇게 하면 기존 시스템의 다음 기능을 변경하지 않아도 된다.
- Form Schema
- Validation
- FEEL Expression
- Submission
- 기존 데이터
- 다른 시스템과의 데이터 교환
이 방식은 기존 시스템과의 호환성을 유지하는 데 도움이 된다.
하지만 날짜 변환이 정확해야 한다는 책임이 새롭게 발생한다.
Intl과 달력의 불일치
기존 form-js는 날짜 입력에 flatpickr를 사용하고 있었다.
flatpickr는 그레고리력을 기준으로 동작한다.
하지만 표시 형식은 브라우저의 Intl 기능을 활용해 결정할 수 있었다.
문제는 다음 Locale에서 나타났다.
fa-IR
브라우저에서 다음 표현을 실행할 수 있다.
new Intl.DateTimeFormat("fa-IR").resolvedOptions().calendar
결과는 다음과 같다.
persian
즉 브라우저는 페르시아 달력을 기본값으로 선택할 수 있다.
하지만 flatpickr는 실제 날짜 값을 그레고리력으로 처리한다.
결과적으로 표시 형식과 실제 날짜가 서로 다른 달력 체계를 사용할 수 있었다.
저자는 이를 해결하기 위해 Jalali Calendar를 직접 지원하는 Date Picker를 사용했다.
jalaliDatePicker
저자는 다음 라이브러리를 사용했다.
jalaliDatePicker
이 라이브러리는 Jalali 날짜와 시간을 입력할 수 있도록 한다.
하지만 기존 form-js의 날짜 필드와 동작 방식이 같지 않았다.
예를 들어 jalaliDatePicker는 다음 형태의 날짜를 입력할 수 있다.
1403/01/10
그리고 자체 이벤트를 발생시킨다.
jdp:change
하지만 form-js는 자신의 상태 관리와 Event Bus를 이용한다.
따라서 두 시스템을 연결하는 Adapter가 필요했다.
저자는 다음 작업을 구현했다.
- Date Picker가 변경한 값을 읽는다.
- Jalali 날짜를 ISO 날짜로 변환한다.
- form-js의 Field State를 갱신한다.
- 기존 Event Bus를 통해 변경을 알린다.
- 다시 렌더링할 때 ISO 날짜를 Jalali 표시값으로 변환한다.
이러한 방식은 Date Picker의 내부 상태와 Form의 실제 데이터 상태를 분리한다.
페르시아 숫자 입력
날짜 입력에서는 숫자의 표현도 중요하다.
예를 들어 다음 숫자를 생각할 수 있다.
۱۴۰۵
이는 페르시아 숫자다.
하지만 기존 Number Field에서는 다음 정규식을 사용할 수 있다.
/^[0-9]$/
이 경우 ASCII 숫자만 허용한다.
따라서 페르시아 숫자가 포함된 입력은 정상적으로 처리되지 않을 수 있다.
저자는 실제 입력을 Field State에 전달하기 전에 숫자를 정규화하도록 변경했다.
이러한 방식에서는 사용자가 익숙한 숫자 체계를 유지하면서 기존 데이터 처리 규칙도 활용할 수 있다.
하지만 숫자를 변환하는 작업과 실제 날짜가 유효한지 검증하는 작업은 서로 다르다.
따라서 입력값을 정규화한 이후에도 날짜 자체의 유효성을 검사해야 한다.
Validation과 Date Picker의 범위 동기화
기존 form-js에서는 날짜의 최소값과 최대값을 지정할 수 있다.
예를 들어 다음 조건을 생각할 수 있다.
오늘 이전의 날짜는 선택할 수 없음
이 조건은 실제 ISO 날짜를 기준으로 검증할 수 있다.
하지만 사용자는 Jalali Date Picker를 사용한다.
따라서 Date Picker가 선택하도록 허용하는 범위와 실제 Form Validation이 허용하는 범위가 같아야 한다.
그렇지 않으면 다음 문제가 발생할 수 있다.
- 사용자가 달력에서 특정 날짜를 선택한다.
- Date Picker는 해당 날짜를 허용한다.
- form-js Validator는 해당 날짜를 거부한다.
저자는 이러한 불일치를 방지하기 위해 두 시스템의 날짜 경계 조건을 연결했다.
또한 오류 메시지에서도 Jalali 날짜를 사용하는 사용자에게 이해할 수 있는 형태로 정보를 제공해야 했다.
시간대와 자정 문제
날짜 변환에서는 시간대도 중요하다.
예를 들어 테헤란의 자정 근처 시각을 생각할 수 있다.
같은 순간이라도 UTC 기준 날짜와 테헤란의 로컬 날짜는 다를 수 있다.
따라서 단순히 날짜 문자열을 변환하는 것만으로는 충분하지 않을 수 있다.
특히 다음 값을 구분해야 한다.
- 날짜만 표현하는 값
- 시각만 표현하는 값
- 날짜와 시각을 함께 표현하는 값
실제로 저장해야 하는 데이터가 날짜만인지 정확한 시각인지에 따라 변환 방식이 달라진다.
저자는 기존 datetime 필드의 여러 하위 유형을 고려해 날짜와 시간을 처리했다.
기존 라이브러리를 Fork하지 않은 이유
저자는 처음부터 form-js를 Fork하지 않기로 결정했다.
이유는 유지보수 비용이었다.
실제 form-js는 여러 패키지와 내부 서비스를 가진 대규모 라이브러리다.
Fork를 만들면 이후 원본 프로젝트의 변경을 계속 반영해야 한다.
따라서 저자는 기존 Module과 Field Registry를 이용해 기능을 확장했다.
이러한 방식은 원본과의 차이를 줄이는 데 도움이 될 수 있다.
하지만 확장 지점을 이해하기 위한 비용이 발생했다.
저자는 대부분의 시간을 실제 구현보다 내부 아키텍처를 이해하는 데 사용했다고 설명한다.
설계상의 한계
저자는 프로젝트를 마친 뒤 중요한 판단도 남겼다.
form-js는 기능이 많은 라이브러리였다.
하지만 실제 프로젝트에서 필요한 기능은 그보다 적었다.
따라서 기존 라이브러리를 확장하는 과정에서 예상보다 많은 시간이 필요했다.
저자는 직접 Form Layer를 구현했다면 초기 개발 비용은 증가했겠지만 이후 Sprint의 유지보수 비용은 줄었을 가능성이 있다고 평가한다.
다만 이러한 판단은 실제 다른 구현과 동일한 조건에서 비교한 결과는 아니다.
이번 글에는 실제 개발 시간이나 성능에 관한 독립적인 Benchmark가 포함되지 않았다.
기존 대규모 프론트엔드 라이브러리에 새로운 입력 형식을 추가할 때 화면의 표현과 데이터의 저장 계약, 검증 및 제출 과정을 분리해야 한다는 점을 구체적인 구현 경험으로 설명한 자료다.
Music and sound effects with no audio files: synthesising everything in Web Audio for browser games
- 저자: Ant Games
- 매체: DEV Community / Ant Games 기술 기록
- 게시일: 2026-11-14 — 정확한 게시 시각 미확인
- 기술: JavaScript, TypeScript, Web Audio API, AudioContext, OscillatorNode, OfflineAudioContext
- 원문: https://dev.to/antgamesai/music-and-sound-effects-with-no-audio-files-synthesising-everything-in-web-audio-for-browser-games-358f
실제 브라우저 게임 세 개에서 별도의 MP3나 WAV 파일을 제공하지 않고 JavaScript와 Web Audio API만으로 모든 음악과 효과음을 생성한 방법을 설명한 글이다.
저자는 다음 게임의 구현을 소개한다.
- JETSUN
- STAR AGAINST THE DARK
- SALLY PORT
각 게임은 브라우저에서 실행된다.
하지만 오디오 파일을 다운로드하지 않는다.
대신 실제 게임의 상태에 따라 브라우저에서 소리를 합성한다.
이번 글은 Web Audio API의 일반적인 사용법을 설명하는 튜토리얼이 아니다.
실제 게임에서 발견한 여러 문제와 이를 해결하기 위한 실행 구조를 설명한다.
Oscillator 기반 효과음
Web Audio API에서는 OscillatorNode를 이용해 파형을 생성할 수 있다.
대표적인 파형은 다음과 같다.
- Sine
- Square
- Sawtooth
- Triangle
예를 들어 짧은 레이저 효과음을 만든다고 가정해 보자.
높은 주파수에서 시작한다.
이후 매우 짧은 시간 동안 주파수를 낮춘다.
그러면 실제 녹음된 효과음 파일이 없어도 전자음과 비슷한 소리를 생성할 수 있다.
저자는 이러한 효과음을 공통 Helper로 구현했다.
대표적인 구성요소는 다음과 같다.
- Oscillator
- Gain
- Filter
- Envelope
- 주파수 변화
따라서 실제 효과음마다 전체 Web Audio Graph를 새롭게 작성하지 않아도 된다.
Envelope와 0의 문제
저자는 효과음의 볼륨을 시간에 따라 변경한다.
예를 들어 짧은 충돌 효과음이 있다고 가정해 보자.
처음에는 소리가 빠르게 커진다.
이후 짧은 시간 안에 작아진다.
이러한 변화를 Envelope라고 한다.
하지만 Web Audio API의 Exponential Ramp에서는 값이 정확히 0이 되는 지점을 사용할 수 없다.
따라서 저자는 볼륨을 다음 값까지 낮춘다.
0.0001
정확히 0으로 변경하지 않는다.
이러한 작은 구현상의 차이는 실제 오디오 파라미터를 계산할 때 오류를 방지하는 데 필요하다.
또한 Oscillator가 실제로 필요하지 않은 시점에는 종료되도록 한다.
그렇지 않으면 여러 효과음이 계속 생성되면서 불필요한 오디오 노드가 남을 수 있다.
같은 효과음의 반복 재생
예를 들어 기관총을 발사하는 게임을 생각할 수 있다.
사용자는 버튼을 누르고 있다.
게임은 매우 짧은 간격으로 효과음을 생성한다.
하지만 정확히 같은 소리를 같은 주파수로 반복하면 기계적으로 들릴 수 있다.
저자는 발사할 때마다 주파수를 조금씩 변경한다.
또한 다음 값을 사용한다.
minGap: 0.045
이 값은 같은 효과음이 지나치게 짧은 간격으로 반복되지 않도록 제한한다.
따라서 매 Frame마다 효과음이 무제한으로 겹치는 상황을 줄일 수 있다.
이러한 방식은 실제 오디오 파일의 중복 재생을 제한하는 방식과 비슷한 목적을 가진다.
하지만 실제 구현은 Web Audio Node의 생성과 실행 시각을 직접 관리한다.
공유 Noise Buffer
폭발이나 바람, 충격음에서는 Noise가 필요할 수 있다.
저자는 효과음마다 별도의 Noise Buffer를 생성하지 않았다.
대신 처음에 약 2초 길이의 White Noise를 생성한다.
그리고 여러 효과음이 같은 Buffer를 공유한다.
예를 들어 다음 두 효과음이 있다고 가정해 보자.
- 폭발
- 바람
두 효과음은 같은 Noise 데이터를 사용할 수 있다.
하지만 실제 Filter와 Envelope는 서로 다르게 설정한다.
이렇게 하면 같은 원본 Buffer를 사용하더라도 다른 소리를 만들 수 있다.
또한 지속적인 바람 소리에는 Buffer Loop를 사용할 수 있다.
다만 여러 Loop가 정확히 같은 위치에서 시작하면 위상이 맞아 특정 소리가 반복적으로 강조될 수 있다.
저자는 이를 줄이기 위해 시작 위치를 무작위로 변경했다.
녹음 파일 없는 Reverb
일반적인 Convolution Reverb에서는 실제 공간의 Impulse Response를 녹음한 파일을 사용할 수 있다.
하지만 저자는 별도의 파일을 사용하지 않았다.
대신 Noise와 시간에 따른 감쇠 곡선을 이용해 Impulse Response를 생성했다.
저자가 사용한 길이는 약 2.2초다.
실제 구현에서는 두 개의 Channel을 가진 Buffer를 생성한다.
각 Channel에 서로 다른 Noise를 채운다.
이후 시간이 지날수록 소리가 작아지도록 계산한다.
이 Buffer를 다음 노드에 전달한다.
ConvolverNode
이렇게 하면 별도의 Reverb 녹음 파일 없이도 공간감을 표현할 수 있다.
하지만 실제 공연장이나 특정 공간의 음향을 정확하게 재현하는 것은 아니다.
저자가 만든 효과는 게임에 적합한 합성 Reverb에 가깝다.
JavaScript Timer와 Audio Clock의 차이
이번 글에서 가장 중요한 부분은 음악의 재생 시각이다.
JavaScript에서는 다음 API를 이용해 반복 작업을 수행할 수 있다.
setInterval()
하지만 JavaScript Timer는 실제로 지정한 시각에 정확하게 실행된다고 보장할 수 없다.
예를 들어 브라우저의 메인 스레드에서 긴 작업이 실행된다고 가정해 보자.
이 경우 Timer의 Callback이 늦게 실행될 수 있다.
또한 브라우저 탭이 백그라운드로 이동하면 Timer가 제한될 수 있다.
따라서 setInterval()이 실행되는 순간마다 음표를 재생하면 실제 음악의 박자가 흔들릴 수 있다.
저자는 이를 해결하기 위해 AudioContext.currentTime을 사용했다.
Look-ahead Scheduler
저자는 앞으로 재생할 음표를 미리 예약한다.
대표적인 설정은 다음과 같다.
LOOK = 0.25
즉 현재 시각에서 약 0.25초 뒤까지의 음표를 미리 예약한다.
JavaScript Timer는 초당 약 40번 실행된다.
하지만 실제 음표의 재생 시각은 AudioContext의 Clock을 기준으로 설정한다.
따라서 JavaScript Timer에 작은 지연이 발생하더라도 이미 예약된 음표는 실제 Audio Clock에 맞춰 재생될 수 있다.
이러한 구조에서는 다음 두 작업을 분리한다.
- JavaScript: 어떤 음표를 예약할지 결정
- AudioContext: 실제 음표의 재생 시각 관리
백그라운드 탭의 지연 처리
이번 글에서 실제 운영상 중요한 문제는 백그라운드 탭이다.
예를 들어 사용자가 게임을 실행하다가 다른 탭으로 이동했다고 가정해 보자.
브라우저는 기존 탭의 JavaScript Timer 실행을 제한할 수 있다.
이후 사용자가 게임으로 돌아온다.
이때 단순한 Scheduler는 이전에 재생했어야 할 모든 음표를 한꺼번에 재생할 수 있다.
예를 들어 10초 동안 Timer가 지연됐다면 많은 음표가 동시에 재생될 가능성이 있다.
저자는 이러한 문제를 해결하기 위해 오래된 음표를 건너뛰도록 했다.
즉 지연된 음악을 한꺼번에 따라잡는 대신 실제 현재 박자에 맞춰 재생을 재개한다.
이러한 방식은 음악의 연속성 일부를 포기하지만 갑작스러운 소리의 폭발을 방지한다.
게임 상태에 따른 음악 변경
저자는 음악의 실제 데이터를 오디오 파일 대신 Pattern으로 관리했다.
예를 들어 다음 정보가 존재한다.
- Tempo
- Scale
- Drum Pattern
- Melody
- Instrument
- Intensity
게임의 위험도가 증가하면 음악의 Intensity도 증가시킬 수 있다.
예를 들어 평온한 상황에서는 일부 Layer만 재생한다.
적이 많아지면 Drum과 Lead를 추가한다.
이때 기존 음악을 처음부터 다시 시작할 필요는 없다.
같은 음악의 시간축을 유지하면서 Layer만 변경할 수 있다.
이러한 구조에서는 게임의 실제 상태와 음악을 직접 연결할 수 있다.
반면 녹음된 오디오 파일을 사용하는 경우에는 여러 버전의 음악 파일을 준비하거나 별도의 Mixing 구조가 필요할 수 있다.
Custom Waveform
Web Audio API가 기본 제공하는 파형만으로 원하는 소리를 만들기 어려울 수 있다.
저자는 다음 API를 사용했다.
createPeriodicWave()
이 API는 여러 Harmonic을 조합해 새로운 파형을 만들 수 있도록 한다.
예를 들어 낮은 Harmonic만 강하게 포함하면 상대적으로 부드러운 소리를 만들 수 있다.
반면 높은 Harmonic을 많이 포함하면 더 밝고 날카로운 소리를 만들 수 있다.
저자는 여러 종류의 Custom Waveform을 생성하고 게임 상태에 따라 선택했다.
이렇게 하면 별도의 Instrument Sample 파일 없이도 여러 음색을 만들 수 있다.
Clipping 방지
여러 효과음이 동시에 재생되면 출력 신호의 크기가 지나치게 증가할 수 있다.
예를 들어 다음 소리가 동시에 재생된다고 가정해 보자.
- 배경 음악
- 폭발
- 총격
- 충돌
각 효과음의 Gain이 적절하게 설정되지 않았다면 전체 신호가 Clipping될 수 있다.
저자는 이를 해결하기 위해 다음 구조를 사용했다.
- Compressor
- Trim Gain
- WaveShaper
- 최종 출력
이러한 구조에서는 매우 큰 신호를 제한하고 급격한 왜곡을 줄일 수 있다.
하지만 Compressor와 WaveShaper를 추가한다는 사실만으로 모든 환경에서 음질이 개선되는 것은 아니다.
실제 Threshold와 Gain, Curve의 설정에 따라 결과가 달라질 수 있다.
OfflineAudioContext를 이용한 검증
저자는 실제 오디오 출력을 자동으로 검증하는 방법도 사용했다.
대표적인 API는 다음과 같다.
OfflineAudioContext
이 API는 실제 스피커로 소리를 출력하지 않고 오디오 데이터를 렌더링할 수 있다.
저자는 일부 게임에서 Seeded Pseudo-random Number Generator를 사용했다.
따라서 같은 Seed를 이용하면 같은 Noise와 효과음을 생성할 수 있다.
이러한 방식은 테스트 결과의 재현성을 높인다.
예를 들어 같은 효과음을 OfflineAudioContext로 생성한다.
이후 다음 정보를 계산할 수 있다.
- 최대 Peak
- RMS
- Clipping 여부
따라서 실제 게임을 실행하면서 소리를 직접 듣지 않더라도 일부 문제가 발생했는지 확인할 수 있다.
하지만 이러한 수치가 실제 사용자의 주관적인 음질 평가를 완전히 대체하는 것은 아니다.
적용상의 한계
이번 구조는 다음 장점이 존재한다.
- 별도의 오디오 파일 다운로드 감소
- 게임 상태에 따른 음악 변경
- 효과음의 파라미터 조정
- 재현 가능한 자동 테스트
하지만 다음 비용도 발생한다.
- 실제 소리 합성 로직 구현
- 오디오 노드의 수명 관리
- Browser Audio Policy 처리
- 음악 패턴 작성
- 음질 조정
- 실제 CPU 사용량 관리
또한 브라우저에서는 사용자 상호작용이 발생하기 전 오디오 재생이 제한될 수 있다.
따라서 실제 사용자의 클릭이나 키보드 입력 이후 AudioContext를 활성화해야 할 수 있다.
이번 글에는 일반적인 오디오 파일 방식과 CPU 사용량이나 다운로드 크기를 동일한 조건에서 비교한 독립적인 Benchmark가 포함되지 않았다.
Web Audio API를 단순한 효과음 재생 도구가 아니라 실제 게임의 오디오 엔진으로 활용하면서 재생 시각과 메모리, 음질 및 테스트 가능성을 함께 관리한 구현 사례다.
Claude Codeの手順書は、指定しても読まれない——教訓を「読ませる」から「貼る」「止める」へ変えた3か月の記録
- 원제: Claude Codeの手順書は、指定しても読まれない——教訓を「読ませる」から「貼る」「止める」へ変えた3か月の記録
- 저자: saitoko
- 매체: Zenn
- 게시일: 2026-11-14
- 기술: Claude Code, AI Coding Agent, Agent Harness, Playbook, CI, 작업 검증
- 원문: https://zenn.dev/tottoko_hamu/articles/2026-11-14-000106
AI 코딩 에이전트를 여러 역할로 나눠 운영하면서 작업 지침을 문서에 기록해 두었는데도 같은 종류의 오류가 반복되는 이유를 실제 실행 기록으로 분석한 글이다.
저자는 Claude Code에서 여러 에이전트를 사용하는 환경을 운영하고 있었다.
문제가 발생하면 사고 기록을 남겼다.
그리고 같은 오류가 다시 발생하지 않도록 규칙과 Playbook을 작성했다.
하지만 시간이 지나면서 다른 문제가 나타났다.
실제로 규칙은 존재했다.
그런데 작업을 수행하는 에이전트가 해당 규칙을 읽지 않거나 읽었더라도 실제로 따르지 않았다.
이번 글은 단순히 Prompt를 더 자세하게 작성하라고 설명하지 않는다.
규칙을 기록하는 시스템과 실제 필요한 순간에 규칙을 적용하는 시스템은 서로 다르다는 점을 여러 실행 기록으로 보여준다.
Playbook의 증가
저자는 작업 종류마다 다른 Playbook을 사용하고 있었다.
예를 들어 다음 작업이다.
- 코드 구현
- 문서 작성
- 테스트
- 오픈소스 Release
- 보안 검사
Playbook에는 이전에 발생한 오류와 관련 주의사항을 기록했다.
8월의 Playbook 규모는 다음과 같았다.
- 파일 95개
- 약 31만 글자
11월 13일에는 다음과 같이 변경됐다.
- 파일 93개
- 약 42만 글자
파일 수는 거의 같았다.
하지만 실제 문서의 분량은 증가했다.
문제는 실제 작업을 수행하는 에이전트가 모든 Playbook을 자동으로 읽는 것이 아니라는 점이었다.
작업을 위임하는 에이전트가 어떤 문서를 읽어야 하는지 지정해야 했다.
따라서 문서가 존재하더라도 실제 작업에서 참조되지 않을 가능성이 있었다.
다섯 종류의 지침 저장 위치
저자는 자신의 환경에서 지침이 저장되는 위치를 구분했다.
대표적으로 다음과 같다.
| 저장 위치 | 실제 참조 방식 |
|---|---|
| CLAUDE.md 및 Rules | 세션의 기본 지침 |
| 자동 Memory | 관련성이 있다고 판단될 때 참조 |
| Playbook | 작업을 위임하는 측이 지정 |
| 사고 기록 | 별도로 참조하지 않으면 사용되지 않음 |
| 규칙 후보 메모 | 주기적인 검토 과정에서 확인 |
여기서 중요한 점은 문서의 존재 여부가 아니다.
해당 문서를 읽는 일이 누구의 판단에 의존하는가 하는 문제다.
예를 들어 작업을 위임하는 에이전트가 Playbook의 경로를 지정하는 것을 잊었다고 가정해 보자.
그러면 실제 구현 담당 에이전트는 관련 규칙을 읽지 않을 수 있다.
따라서 이전에 기록한 실수를 다시 반복할 가능성이 있다.
12번의 위임 가운데 2번만 실제 문서를 읽음
저자는 10월 17~18일의 실제 위임 기록을 조사했다.
해당 기간에 Playbook을 읽었는지 확인할 수 있는 위임은 12번이었다.
결과는 다음과 같았다.
| 항목 | 횟수 |
|---|---|
| 전체 조사 대상 위임 | 12번 |
| 지정한 Playbook을 실제로 읽음 | 2번 |
| Playbook을 읽지 않음 | 10번 |
하지만 여기서 중요한 제한사항이 있다.
Playbook을 읽지 않은 10번의 위임이 모두 실패한 것은 아니다.
일부 작업에서는 필요한 지침의 실제 내용을 위임 메시지에 직접 포함했기 때문이다.
따라서 문서 자체를 열지 않았더라도 관련 규칙을 따를 수 있었다.
저자는 해당 수치를 일반적인 AI 에이전트의 문서 참조 성공률로 사용하지 말라고 명시했다.
조사 기간이 이틀뿐이고, 특정 환경의 실행 기록에 한정되기 때문이다.
네 단계의 실패
저자는 실제 사고 기록을 분석해 오류를 네 단계로 구분했다.
첫 번째는 필요한 문서를 지정하지 않는 경우다.
두 번째는 문서를 지정했지만 실제로 읽지 않는 경우다.
세 번째는 문서를 읽었지만 실제로 규칙을 따르지 않는 경우다.
네 번째는 검사가 실행됐고 오류가 출력됐는데도 작업이 중단되지 않는 경우다.
이 네 문제는 서로 다르다.
따라서 모든 문제를 단순히 지침 문서를 추가하는 방식으로 해결하기 어렵다.
공개 Repository에 내부 정보가 포함된 사례
저자는 실제 사고도 설명한다.
공개 오픈소스 Repository에 내부 환경에서만 사용하는 단어가 포함된 코드를 Push했다.
해당 정보는 약 8분 19초 동안 공개됐다.
하지만 이미 해당 정보를 검사해야 한다는 규칙이 Playbook에 존재했다.
문제는 실제 Release 작업에서 해당 Playbook이 참조되지 않았다는 점이었다.
이후 다른 작업에서도 비슷한 문제가 발생했다.
이번에는 실제 검사 명령이 실행됐다.
검사 결과에도 문제가 되는 문자열이 존재했다.
하지만 해당 결과를 보고도 실제 Push를 중단하지 않았다.
이 경우 관련 내용은 약 11시간 19분 동안 공개된 상태로 유지됐다.
따라서 검사를 실행했다는 사실과 실제 검사가 실패를 차단했다는 사실은 다르다.
저자는 이러한 차이를 중요하게 분석했다.
읽었는지 보고하도록 하는 방법의 한계
저자는 처음에 다음 방법을 사용했다.
작업이 완료된 이후 에이전트가 읽은 Playbook을 보고하도록 한다.
이렇게 하면 실제 문서를 읽지 않았는지 확인할 수 있다.
하지만 문서를 읽도록 강제하는 효과는 제한적이었다.
예를 들어 에이전트가 다음처럼 보고할 수 있었다.
관련 Playbook은 읽지 않았지만 구현과 차이는 없습니다.
이러한 보고는 실제 미준수 상태를 발견하는 데 도움이 된다.
하지만 이미 작업이 완료된 이후라면 문제를 미리 방지하지는 못한다.
따라서 단순한 완료 보고는 사전 검증의 대체 수단이 아니다.
필요한 지침을 위임 메시지에 직접 포함
저자가 선택한 첫 번째 개선 방법은 다음과 같다.
작업을 수행하는 에이전트에게 전체 Playbook을 읽으라고 지시하는 대신, 실제 필요한 규칙의 본문을 위임 메시지에 직접 포함한다.
예를 들어 Bash Script를 수정하는 작업이라고 가정해 보자.
이전 사고 기록에서 다음 문제가 발견됐을 수 있다.
macOS의 Bash 3.2에서 변수 바로 뒤에 비ASCII 문자가 존재할 때 예기치 않은 변수 이름 해석 문제가 발생한다.
이 경우 단순히 Bash 관련 Playbook을 읽으라고 하지 않는다.
대신 실제 문제와 해결 방법을 위임 메시지에 포함한다.
대표적으로 다음 두 표현을 비교한다.
- 문제가 발생할 수 있는 표현:
$VAR - 명확한 변수 경계를 지정하는 표현:
${VAR}
또한 실제 오류가 발생했던 예제도 함께 전달한다.
이렇게 하면 구현 담당 에이전트가 별도의 문서를 찾아 읽지 않더라도 실제 주의사항을 확인할 수 있다.
전체 문서를 전달하지 않는 이유
이 방법은 모든 Playbook의 내용을 매번 복사하는 방식이 아니다.
작업과 관련된 부분만 선택한다.
따라서 실제 작업과 관련이 없는 지침이 지나치게 많아지는 문제를 줄일 수 있다.
또한 위임 메시지에 포함할 항목이 없다면 다음처럼 명시한다.
해당 없음
이렇게 하면 실제 검토했지만 적용할 규칙이 없었던 경우와 규칙 검토 자체를 잊은 경우를 구분할 수 있다.
다만 이러한 방식에는 새로운 책임이 발생한다.
실제로 필요한 지침을 선택하는 에이전트가 중요한 규칙을 누락할 수 있기 때문이다.
따라서 지침을 추출하는 과정도 검증해야 할 수 있다.
실제 기록에서 나타난 결과
저자는 Playbook을 직접 읽지 않은 작업 가운데 일곱 번에서는 필요한 지침을 위임 메시지에 포함했다고 설명한다.
해당 작업에서는 포함한 주의사항을 지킬 수 있었다.
하지만 한 번은 실제로 포함하지 않은 범위에서 문제가 발생했다.
따라서 위임 메시지에 규칙을 포함하는 방식도 모든 오류를 방지하는 것은 아니다.
그럼에도 실제 수신 에이전트가 문서를 읽을지 여부에 의존하는 문제는 줄일 수 있었다.
자동 검사로 실제 작업 중단
저자가 선택한 두 번째 개선 방법은 자동 검증이다.
예를 들어 공개 Repository에 내부 전용 문자열이 포함돼서는 안 된다고 가정해 보자.
이 경우 에이전트에게 다음처럼 지시할 수 있다.
내부 문자열이 없는지 확인해.
하지만 실제 에이전트가 검사를 생략할 수 있다.
또는 검사 결과를 확인하지 않고 작업을 진행할 수 있다.
저자는 이를 해결하기 위해 반복되는 오류를 기계적인 검증으로 차단하도록 했다.
예를 들어 다음 작업을 생각할 수 있다.
- Commit 또는 Release 이전에 검사 실행
- 금지된 문자열 발견
- 프로세스 종료 코드 1 반환
- CI 실패
- 실제 Release 차단
이러한 구조에서는 에이전트가 검사 결과를 어떻게 해석했는지와 별개로 작업을 중단할 수 있다.
다만 어떤 규칙을 자동 검사로 표현할 수 있는지는 별도의 문제다.
예를 들어 실제 문장의 자연스러움이나 디자인의 완성도처럼 정성적인 판단이 필요한 조건을 완전히 자동화하기는 어려울 수 있다.
지침의 분량과 Token 비용
저자는 필요한 지침만 추출해 전달하는 방식이 효율적일 수 있다고 평가한다.
하지만 전체 Playbook을 전달하는 방식과 실제 Token 비용을 비교한 측정은 수행하지 않았다.
따라서 해당 방식의 Token 비용이 몇 퍼센트 감소했다고 단정해서는 안 된다.
또한 지침을 많이 포함하면 실제 작업의 Context가 커질 수 있다.
따라서 어떤 규칙을 현재 작업에 적용해야 하는지 판단하는 작업도 중요하다.
실험상의 한계
이번 결과는 저자 자신의 환경에서 약 3개월 동안 기록한 사고와 위임 사례를 분석한 것이다.
하지만 다음 제한사항이 존재한다.
- 하나의 개인 환경에 집중
- 짧은 기간의 위임 기록 사용
- 문제 사례가 기록에 더 많이 남을 가능성
- 동일한 조건의 통제된 A/B 실험 없음
- 실제 Token 비용 측정 없음
따라서 모든 AI 코딩 에이전트에서 Playbook을 읽는 비율이 같은 수준이라고 판단할 수 없다.
이번 글은 AI 에이전트가 규칙을 알고 있는지와 실제 작업에서 해당 규칙을 적용하는지가 서로 다른 문제이며, 반복적인 오류를 줄이려면 필요한 지침을 실행 Context에 명시하고 검증 가능한 조건은 자동으로 차단해야 한다는 점을 실제 운영 기록으로 설명한다.
Debugging Hardware in Production: How We Solved Dual-Barcode Interleaving on Industrial Handheld Scanners
- 저자: Gafar Mansoor
- 매체: DEV Community
- 게시일: 2026-11-14 — 정확한 게시 시각 미확인
- 기술: TypeScript, JavaScript Input Events, Android PDA, Barcode Scanner, 문자열 처리, 입력 검증
- 원문: https://dev.to/gafar_mansoor_667cd0bddc6/debugging-hardware-in-production-how-we-solved-dual-barcode-interleaving-on-industrial-handheld-293e
실제 물류 창고의 웹 애플리케이션에서 바코드 스캐너가 두 개의 바코드를 거의 동시에 읽으면서 하나의 잘못된 송장번호를 입력하는 문제를 해결한 기술 글이다.
이번 글의 특징은 일반적인 사용자 키보드 입력과 실제 산업용 하드웨어의 입력이 서로 다르다는 점을 구체적인 사례로 보여준다는 것이다.
저자는 중국 광저우의 물류 창고에서 사용하는 웹 애플리케이션을 개발했다.
작업자는 Android 기반의 산업용 PDA를 사용한다.
PDA에는 바코드 스캐너가 연결돼 있다.
작업자는 송장에 인쇄된 바코드를 스캔한다.
웹 애플리케이션은 입력된 송장번호를 이용해 실제 화물을 처리한다.
문제는 실제 바코드 입력값이 예상하지 못한 형태로 전달된다는 점이었다.
하나의 송장에 두 개의 바코드
실제 물류 송장에는 두 종류의 바코드가 존재할 수 있다.
첫 번째는 송장번호를 나타내는 가로 바코드다.
두 번째는 경로 또는 분류를 위한 세로 바코드다.
두 바코드는 같은 송장 용지에 가까이 인쇄돼 있을 수 있다.
작업자는 빠르게 화물을 처리해야 한다.
따라서 매번 정확히 하나의 바코드만 스캔하도록 기기를 조준하기 어렵다.
문제는 스캐너가 두 바코드를 거의 동시에 인식하는 상황이었다.
실제 입력값
저자가 확인한 정상 송장번호는 다음과 같았다.
465851384718642
15자리 숫자다.
하지만 실제 웹 Form에는 다음 값이 입력됐다.
4654865518358143781487614826
28자리 숫자다.
처음에는 단순한 노이즈처럼 보였다.
하지만 저자는 해당 문자열을 분석했다.
그리고 일정한 패턴을 발견했다.
입력된 문자열의 홀수 번째 문자와 짝수 번째 문자를 각각 분리하면 서로 다른 숫자열이 나타났다.
첫 번째 숫자열은 원래 송장번호였다.
두 번째 숫자열은 다른 바코드에서 생성된 일부 값이었다.
즉 스캐너가 실제로 두 데이터를 동시에 처리하면서 문자 단위로 교차해 전달한 것이다.
Keyboard Wedge의 동작
산업용 바코드 스캐너는 입력을 일반적인 키보드처럼 전달할 수 있다.
이를 Keyboard Wedge 방식이라고 한다.
예를 들어 스캐너가 다음 송장번호를 읽었다고 가정해 보자.
123456
웹 애플리케이션은 실제 사용자가 키보드에서 다음 숫자를 빠르게 입력한 것처럼 이벤트를 받을 수 있다.
- 1
- 2
- 3
- 4
- 5
- 6
이러한 방식은 일반적인 Input Element와 쉽게 연결할 수 있다는 장점이 있다.
하지만 실제 스캐너가 여러 바코드를 동시에 처리한다면 두 입력의 이벤트가 섞일 수 있다.
웹 애플리케이션은 실제 하드웨어 내부에서 어떤 바코드를 먼저 읽었는지 직접 알지 못할 수 있다.
따라서 단순한 Input Event만으로는 입력의 의미를 정확하게 구분하기 어렵다.
하드웨어 설정을 변경하지 않은 이유
저자는 세 가지 해결 방법을 검토했다.
첫 번째는 스캐너가 한 번에 하나의 바코드만 읽도록 설정하는 것이다.
두 번째는 배송업체별 송장번호의 Prefix와 길이를 하드코딩하는 것이다.
세 번째는 사용자가 입력하는 모든 문자를 즉시 필터링하는 것이다.
하지만 세 방법 모두 문제가 있었다.
물류 창고에는 여러 제조업체의 PDA가 존재했다.
대표적으로 다음 장치가 사용될 수 있다.
- Zebra
- Honeywell
- Chainway
- Urovo
각 장치는 서로 다른 설정 방법과 Android ROM을 가질 수 있다.
따라서 모든 장치의 설정을 수동으로 변경하는 것은 유지보수 비용이 컸다.
또한 배송업체는 송장번호의 규칙을 변경할 수 있다.
따라서 특정 업체의 Prefix와 길이에 의존하는 방식도 장기적으로 유지하기 어려웠다.
입력 중에는 수정하지 않음
저자가 선택한 방법에서 중요한 부분은 수정 시점이다.
사용자가 문자를 입력하는 도중에는 송장번호를 자동으로 변경하지 않는다.
예를 들어 사용자가 실제 키보드로 다음 값을 입력한다고 가정해 보자.
112233
이 값은 반복되는 숫자를 포함한다.
하지만 해당 입력이 잘못됐다는 의미는 아니다.
따라서 단순히 연속된 숫자가 반복된다는 이유로 입력을 제거하면 정상적인 데이터가 손상될 수 있다.
저자는 다음 이벤트가 발생할 때 입력을 검사하도록 했다.
- 스캐너의 Enter 입력
- Input Blur
이렇게 하면 실제 사용자가 입력을 완료한 이후에만 정규화 로직을 실행할 수 있다.
교차 문자열 분리
저자는 TypeScript로 순수 함수를 작성했다.
대표적인 함수 이름은 다음과 같다.
splitInterleavedScan()
이 함수는 입력 문자열을 받은 뒤 다음 작업을 수행한다.
- 앞뒤 공백 제거
- 최소 길이보다 짧은 입력은 그대로 반환
- 짝수 Index의 문자 추출
- 홀수 Index의 문자 추출
- 두 결과의 Prefix 관계 확인
- 복구할 수 있는 경우 정상 값 반환
예를 들어 다음 문자열이 있다고 가정해 보자.
A1B2C3
짝수 Index의 문자는 다음과 같다.
ABC
홀수 Index의 문자는 다음과 같다.
123
이러한 방식으로 원래 입력을 두 개의 독립적인 Stream으로 분리할 수 있다.
실제 송장번호 문제에서는 한 Stream이 정상 송장번호와 일치했다.
다른 Stream은 중복 스캔의 일부였다.
Prefix 관계를 이용한 판단
저자는 두 Stream의 길이와 Prefix 관계를 확인했다.
예를 들어 다음 두 Stream이 있다고 가정해 보자.
- Stream A:
465851384718642 - Stream B:
4658513847186
두 번째 Stream이 첫 번째 Stream의 Prefix에 해당한다.
이러한 상황에서는 긴 Stream을 원래 송장번호로 판단할 수 있다.
하지만 두 Stream이 아무런 관련이 없다면 자동으로 하나를 선택하지 않는다.
예를 들어 다음과 같은 경우다.
- Stream A:
123456789 - Stream B:
987654321
두 값이 서로 다른 실제 바코드일 수 있다.
따라서 하나를 임의로 선택하면 잘못된 송장번호를 저장할 위험이 있다.
저자는 이러한 상황에서 원본 입력을 그대로 반환하도록 했다.
모호한 입력은 추측하지 않음
이번 구현의 중요한 원칙은 다음과 같다.
명확하게 복구할 수 없는 데이터는 임의로 수정하지 않는다.
이러한 원칙은 물류 시스템에서 특히 중요하다.
예를 들어 잘못된 송장번호를 실제 데이터베이스에 저장했다고 가정해 보자.
이후 해당 번호를 이용해 화물을 다른 목적지로 분류할 수 있다.
따라서 단순한 입력 오류가 실제 물류 처리 오류로 이어질 수 있다.
반면 자동으로 복구할 수 없는 입력을 그대로 유지한다면 작업자가 오류를 확인할 수 있다.
자동화가 가능한 경우와 사용자 확인이 필요한 경우를 구분하는 것이다.
연속된 중복 입력
실제 스캐너에서는 두 바코드가 교차하는 것 외에도 같은 입력이 반복될 수 있다.
예를 들어 다음 문자열을 생각할 수 있다.
AB1234AB1234
저자는 이러한 반복 입력도 별도로 처리했다.
정규식을 이용해 동일한 Pattern이 반복됐는지 확인한다.
하지만 모든 반복 문자열을 자동으로 제거하는 것은 아니다.
실제 송장번호 자체에 반복된 숫자가 포함될 수 있기 때문이다.
따라서 반복되는 전체 Pattern과 정상적인 입력값의 내부 반복을 구분해야 한다.
로컬에서 처리하는 이유
물류 창고에서는 작업자가 빠르게 화물을 처리한다.
저자는 실제 처리 속도를 다음과 같이 설명한다.
약 3~5초마다 화물 하나를 처리한다.
이러한 환경에서 모든 원본 입력을 서버로 전송한 뒤 다시 분석한다면 네트워크 지연이 발생할 수 있다.
특히 창고의 네트워크가 불안정한 상황에서는 작업이 지연될 수 있다.
따라서 저자는 입력 정규화를 클라이언트에서 수행했다.
TypeScript로 작성한 순수 함수는 다음 환경에서 재사용할 수 있다.
- 데스크톱 웹 애플리케이션
- Android WebView
- 산업용 PDA의 웹 UI
이렇게 하면 하드웨어별 입력 특성을 실제 비즈니스 로직과 분리할 수 있다.
하드웨어 입력도 외부 데이터로 취급
저자는 실제 하드웨어에서 받은 데이터도 신뢰할 수 없는 입력으로 취급해야 한다고 설명한다.
일반적인 웹 개발에서는 사용자의 Form 입력을 검증한다.
하지만 바코드 스캐너와 같은 장치에서 전달한 값은 자동으로 올바른 데이터라고 생각할 수 있다.
이번 사례는 이러한 가정이 잘못될 수 있음을 보여준다.
실제 하드웨어에서는 다음 문제가 발생할 수 있다.
- 중복 입력
- 문자 누락
- 입력 순서 변경
- 여러 데이터의 교차
- 장치별 키보드 이벤트 차이
따라서 데이터베이스에 저장하기 전에 입력을 검증하고 정규화하는 계층이 필요할 수 있다.
구현의 한계
이번 알고리즘은 특정한 종류의 입력 오류를 복구한다.
즉 두 바코드의 문자가 교차해 입력되고, 두 Stream 사이에 복구 가능한 관계가 존재하는 경우다.
하지만 다음 문제를 모두 해결하지는 못한다.
- 여러 문자가 무작위로 누락
- 세 개 이상의 바코드가 동시에 입력
- 데이터의 순서가 임의로 변경
- 입력값 자체가 손상
- 실제 바코드 번호의 충돌
따라서 실제 송장번호의 최종 유효성 검사는 별도로 필요할 수 있다.
또한 저자는 실제 물류 처리 속도와 입력 패턴을 설명했지만 알고리즘 도입 전후의 오류율을 정량적으로 비교한 Benchmark는 제공하지 않았다.
이번 글은 웹 애플리케이션의 입력이 일반적인 키보드나 터치뿐 아니라 실제 산업용 장치에서도 발생할 수 있으며, 하드웨어 입력을 정규화할 때는 자동 복구의 범위를 제한하고 모호한 데이터의 무단 수정을 피해야 한다는 점을 실제 장애 사례로 설명한다.