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

기준 시점: 2026-10-30 06:00 KST
조사 범위: 전일 2026-10-29 06:00 ~ 당일 2026-10-30 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
Vercel Flags, Cold Start 초기화 약 7배 가속 — 네트워크 요청 없이 Feature Flag 평가 및 Function 실행 시간 감소
- 발표일: 2026-10-29 — 정확한 게시 시각 미확인
- 발표 주체: Vercel
- 적용 대상: Vercel Flags SDK, Vercel Functions
- 필요 버전:
@flags-sdk/vercel1.5.3 이상 - 안정화 단계: 정식 SDK 업데이트
- 발표 성능: Cold Start 초기화 약 7배 가속, Flag 조회 P99 1ms 미만
- 측정 주체: Vercel 자체 측정
- 원문: https://vercel.com/changelog/vercel-flags-starts-7x-faster-and-no-longer-extends-function-duration
Vercel이 Feature Flag SDK의 초기화 과정에서 발생하던 네트워크 요청을 제거하고, 사용량 보고 작업이 서버리스 Function의 실행 시간을 증가시키지 않도록 변경했다.
이번 업데이트는 새로운 Feature Flag 평가 기능을 추가하는 것이 아니다.
기존에 제공하던 Feature Flag를 더 빠르고 효율적으로 평가할 수 있도록 SDK와 Vercel Runtime의 연결 방식을 개선한 것이다.
Feature Flag는 애플리케이션의 특정 기능을 코드 배포와 별개로 활성화하거나 비활성화하는 데 사용된다.
예를 들어 React와 Next.js로 개발한 서비스에서 새로운 결제 화면을 출시한다고 가정해 보자.
개발자는 새로운 결제 화면의 코드를 운영 환경에 배포할 수 있다.
하지만 모든 사용자에게 즉시 새로운 화면을 제공하지는 않을 수 있다.
처음에는 내부 직원에게만 제공한다.
이후 일부 고객에게 공개한다.
문제가 없으면 전체 사용자에게 확대한다.
이러한 방식은 새로운 기능을 점진적으로 배포하는 데 도움이 된다.
문제는 Feature Flag 평가 자체에도 비용이 발생할 수 있다는 점이다.
특히 서버리스 환경에서는 새로운 Function 인스턴스가 시작될 때 필요한 설정을 로딩해야 한다.
기존 초기화 과정의 네트워크 대기
기존 Vercel Flags SDK는 초기화 과정에서 Flag 설정의 최신 상태를 확인하기 위해 네트워크 요청을 수행할 수 있었다.
예를 들어 사용자가 Next.js 페이지에 접근했다고 가정해 보자.
서버는 해당 사용자에게 새로운 화면을 제공할지 결정해야 한다.
이를 위해 Feature Flag를 평가한다.
하지만 새로운 Function 인스턴스가 시작된 상태라면 먼저 Flag 설정을 준비해야 한다.
이 과정에서 네트워크 요청이 발생하면 실제 애플리케이션 코드가 실행되기 전에 대기 시간이 추가될 수 있다.
특히 Cold Start에서 영향을 받을 수 있다.
Cold Start는 서버리스 실행 환경이 새롭게 시작되는 상황을 의미한다.
이미 초기화된 Function이 요청을 처리하는 Warm Invocation과는 다르다.
이번 업데이트는 이러한 초기화 과정의 불필요한 대기를 줄인다.
배포에 포함된 Datafile 활용
새로운 SDK는 배포 결과물에 포함된 Flag Datafile을 초기화 과정에서 바로 사용한다.
Datafile은 Feature Flag의 평가에 필요한 설정 정보를 포함하는 파일이다.
이전에는 SDK가 처음 Flag를 평가하는 시점에 필요한 데이터를 준비할 수 있었다.
새로운 방식에서는 SDK Client가 생성되는 즉시 Datafile을 로딩하고 파싱한다.
따라서 첫 번째 Flag 평가가 실행되기 전에 필요한 데이터가 준비된다.
여기서 중요한 점은 단순히 파일을 조금 더 빨리 읽도록 변경한 것이 아니라는 사실이다.
초기 Flag 평가를 위해 별도의 네트워크 응답을 기다리는 과정을 제거했다.
일반적으로 네트워크 요청에는 다음과 같은 비용이 발생할 수 있다.
- 연결 설정
- 요청 전송
- 서버 응답 대기
- 응답 데이터 수신
- 데이터 파싱
배포 결과물에 이미 포함된 Datafile을 사용하면 이러한 작업의 일부를 생략할 수 있다.
최신 설정 확인 방식 변경
Feature Flag는 배포 이후에도 변경될 수 있다.
예를 들어 운영 중인 서비스에서 새로운 기능에 오류가 발견됐다고 가정해 보자.
개발자는 코드를 다시 배포하지 않고 Feature Flag를 비활성화하고 싶을 수 있다.
따라서 배포에 포함된 Datafile만 영구적으로 사용해서는 안 된다.
Vercel은 이를 해결하기 위해 Function Invocation에 현재 Datafile의 Revision 정보를 포함하도록 변경했다.
Revision은 특정 설정 버전을 식별하는 값이다.
SDK는 배포에 포함된 Datafile과 현재 Revision을 비교할 수 있다.
현재 Datafile이 최신 상태라면 별도의 네트워크 요청 없이 계속 사용할 수 있다.
반면 변경된 Revision이 확인되면 필요한 설정을 갱신할 수 있다.
이렇게 하면 일반적인 요청에서는 불필요한 네트워크 작업을 줄이면서도 Feature Flag의 변경 사항을 반영할 수 있다.
다만 Revision을 이용한다는 사실만으로 모든 설정 변경이 정확히 같은 밀리초에 모든 Function에 적용된다고 보장되는 것은 아니다.
실제 반영 시점은 실행 환경과 데이터 갱신 과정에 영향을 받을 수 있다.
Cold Start 성능 측정
Vercel이 공개한 내부 측정 결과는 다음과 같다.
| 측정 항목 | 변경 전 | 변경 후 |
|---|---|---|
| Cold Start SDK 초기화 P75 | 70ms 초과 | 10ms |
| 초기화 속도 | 기준 | 약 7배 |
| Flag 조회 P99 | 별도 수치 미공개 | 1ms 미만 |
P75는 측정한 실행 시간의 75번째 백분위수다.
즉 전체 측정값 가운데 약 75%가 해당 시간 이하에 위치한다는 의미다.
P99는 99번째 백분위수다.
상대적으로 느린 요청 일부의 성능을 확인하는 데 사용할 수 있다.
이번 결과에서 Cold Start 초기화 시간은 70ms를 초과하던 수준에서 약 10ms로 감소했다.
또한 초기화가 완료된 이후의 Flag 조회 시간은 Cold Invocation과 Warm Invocation을 포함해 P99 기준 1ms 미만이라고 발표했다.
하지만 이 수치는 Vercel의 자체 내부 테스트 결과다.
독립적인 제3자가 동일한 조건으로 여러 Feature Flag 플랫폼을 비교한 Benchmark는 아니다.
또한 전체 Next.js 페이지의 TTFB가 7배 개선됐다는 의미도 아니다.
이번 측정의 대상은 Feature Flag SDK 초기화 시간이다.
실제 페이지의 응답 시간에는 데이터베이스 조회와 React 서버 렌더링, 외부 API 호출 등 다른 작업도 영향을 준다.
따라서 전체 애플리케이션에서 관찰되는 성능 개선은 Flag 평가가 실제 요청 처리 시간에서 얼마나 큰 비중을 차지했는지에 따라 달라진다.
waitUntil 사용 방식 변경
두 번째 중요한 변경은 Feature Flag 사용량을 보고하는 작업이 Function의 실행 시간을 더 이상 늘리지 않는다는 점이다.
기존 SDK는 사용량 보고에 waitUntil을 사용했다.
waitUntil은 HTTP 응답을 반환한 이후에도 특정 비동기 작업을 계속 실행하도록 하는 데 사용할 수 있는 기능이다.
예를 들어 사용자의 요청을 처리한 뒤 분석 이벤트를 전송하는 상황에서 활용할 수 있다.
하지만 서버리스 Function에서는 응답을 보냈더라도 waitUntil에 등록한 작업이 완료될 때까지 실행 환경이 유지될 수 있다.
따라서 사용자에게 응답이 이미 전달됐는데도 Function의 실제 실행 시간이 증가할 수 있었다.
이번 SDK에서는 사용량 이벤트를 Vercel Runtime에 직접 전달하도록 변경했다.
그 결과 사용량 보고가 Function의 실행 시간을 추가로 연장하지 않는다.
이는 사용자 응답 시간과 서버리스 실행 비용을 구분하는 데 중요한 변화다.
사용자가 빠르게 응답을 받았더라도 백그라운드 작업 때문에 실행 비용이 증가할 수 있기 때문이다.
다만 이번 발표에는 사용량 보고 방식 변경으로 실제 Function 비용이 몇 퍼센트 감소했는지에 관한 수치가 포함되지 않았다.
적용 조건
새로운 동작을 사용하려면 다음 버전이 필요하다.
@flags-sdk/vercel@1.5.3
또는 그 이후 버전이다.
기존 프로젝트에서는 패키지를 업데이트하고 다시 배포해야 한다.
패키지를 설치하기만 하고 기존 배포를 그대로 사용한다면 새 SDK 코드가 운영 Function에 반영되지 않을 수 있다.
이번 기능은 Vercel Runtime과 연결되는 최적화이므로 다른 서버리스 플랫폼에서 동일한 결과가 발생한다고 단정해서는 안 된다.
또한 Feature Flag는 기능 노출 여부를 제어하는 수단이다.
실제 API의 접근 권한을 검증하는 서버 측 인증과 인가를 대체하지 않는다.
이번 업데이트는 Feature Flag의 초기화 비용과 사용량 보고 비용을 줄이기 위해 배포 시점의 설정 데이터와 Runtime 정보를 활용하도록 실행 구조를 변경한 것이다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome Developers·Chromium, Mozilla·Firefox, WebKit·Safari의 공식 발표와 JavaScript·TypeScript·CSS, 웹 성능 및 접근성 관련 공개 정보를 검토했다.
이번 기간에 새롭게 정식 출시된 주요 웹 표준이나 브라우저 API의 안정화 발표는 확인하지 못했다.
10월 28일 공개된 Chrome 158 Beta는 이전 브리핑에서 이미 다뤘으므로 반복하지 않았다.
Firefox 159의 정식 출시와 관련 보안 수정 역시 이전 브리핑에 포함된 내용이다.
WebMCP는 여전히 개발 중인 기술이다. W3C가 Polyfill 프로젝트를 지원한다는 사실과 WebMCP가 모든 브라우저에서 정식 표준 기능으로 제공된다는 사실은 구분해야 한다.
이번 추천 글에서는 새로운 브라우저 API 발표 대신 이미 존재하는 웹 기술을 실제 프로젝트에서 잘못 사용해 발생한 문제를 다룬다.
특히 서버 렌더링된 HTML을 JavaScript 애니메이션이 실행될 때까지 숨기는 방식이 실제 모바일 브라우저에서 콘텐츠를 보이지 않게 만든 사례를 소개한다.
3️⃣ 백엔드·인프라
Cloudflare Agents SDK 0.32.0, Agent Skills를 Workspace 파일로 관리 — 배포 없이 새로운 Skill 추가 및 에이전트의 자체 Skill 생성 지원
- 발표일: 2026-10-29 — 정확한 게시 시각 미확인
- 필요 버전: Agents SDK 0.32.0 이상, Think 0.23.0 이상
- 적용 대상: Cloudflare Agents SDK, Think, Pi Durable, AI SDK Harness
- 변경 유형: 신규 Skill 관리 구조 및 Breaking Change
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-29-agent-skills-from-files/
- 공식 문서: https://developers.cloudflare.com/agents/tools/skills/
Cloudflare가 AI 에이전트의 Skill을 실제 파일로 관리하고, 에이전트가 실행 중에도 새로운 Skill을 발견하고 사용할 수 있도록 Agents SDK의 구조를 변경했다.
이번 업데이트는 기존에 공개된 Computer Workspace의 실행 기능을 확장하는 발표다.
특히 Skill을 코드에 미리 등록하거나 빌드 과정에서 컴파일해야 했던 구조에서 벗어났다는 점이 중요하다.
AI 에이전트는 사용자의 요청을 처리하기 위해 다양한 도구와 지침을 사용할 수 있다.
예를 들어 다음과 같은 작업이다.
- 코드 리뷰
- 릴리스 노트 작성
- 데이터 분석
- 테스트 실행
- 기술 문서 생성
이러한 작업을 모두 하나의 긴 System Prompt에 작성할 수도 있다.
하지만 에이전트가 처리할 수 있는 작업이 많아질수록 Prompt가 복잡해진다.
또한 대부분의 요청에서는 전체 지침이 필요하지 않을 수 있다.
예를 들어 사용자가 릴리스 노트를 작성해 달라고 요청했다면 데이터베이스 마이그레이션에 관한 상세 지침까지 전달할 필요는 없다.
Agent Skills는 이러한 문제를 해결하기 위해 필요한 작업에 해당하는 지침과 파일만 선택적으로 로딩하는 구조를 제공한다.
SKILL.md 기반 파일 구조
새로운 방식에서는 하나의 Skill을 디렉터리로 표현한다.
각 디렉터리에는 다음 파일을 포함할 수 있다.
SKILL.md
이 파일에는 Skill의 이름과 설명, 작업 지침 등이 포함된다.
관련 참고 자료와 실행 스크립트를 같은 디렉터리에 배치할 수도 있다.
Cloudflare Computer Workspace에서는 다음 경로를 사용한다.
/workspace/.agents/skills/
예를 들어 release-notes라는 Skill을 만든다고 가정해 보자.
해당 디렉터리 안에 SKILL.md를 작성한다.
필요하다면 참고 문서와 스크립트도 추가한다.
이후 에이전트는 해당 Skill을 발견하고 사용할 수 있다.
중요한 점은 새 Skill을 추가할 때마다 Worker를 다시 빌드하거나 배포할 필요가 없다는 사실이다.
Workspace에 파일을 작성하면 다음 에이전트 Turn부터 사용할 수 있다.
이는 애플리케이션의 코드 배포와 AI 에이전트의 작업 지침 변경을 분리한 것이다.
세 가지 Skill Source
새로운 구조에서는 Skill을 세 종류의 Source에서 가져올 수 있다.
| Source | 특징 |
|---|---|
| Computer Workspace | 실행 환경의 파일로 Skill 관리 |
| R2 Bucket | Object Storage에서 Skill 로딩 |
| Code | 애플리케이션 코드에 Skill 정의 |
Computer Workspace를 사용하면 실행 중인 에이전트가 파일을 생성하거나 수정할 수 있다.
R2를 사용하면 여러 에이전트가 공통으로 사용하는 Skill을 별도로 보관할 수 있다.
Code 기반 Skill은 기존 애플리케이션 코드와 함께 관리할 수 있다.
Cloudflare는 Think에서 여러 Source를 동시에 사용할 수 있도록 한다.
예를 들어 공통 Skill은 R2에 저장하고 에이전트가 작업 중에 생성한 Skill은 Workspace에 저장할 수 있다.
또한 Think는 각 Turn이 시작되기 전에 Skill Source의 목록을 다시 확인한다.
따라서 대화 도중 새로운 Skill이 추가되더라도 이후 Turn에서 발견할 수 있다.
에이전트가 직접 Skill 생성
이번 변경의 특징은 에이전트 자신이 새로운 Skill을 만들 수 있다는 점이다.
예를 들어 에이전트가 어떤 작업을 반복해서 수행한다고 가정해 보자.
매번 같은 파일을 읽고 동일한 절차를 따라야 한다면 해당 절차를 Skill로 정리할 수 있다.
에이전트는 Workspace에 새로운 디렉터리를 생성한다.
그리고 해당 디렉터리에 SKILL.md를 작성한다.
필요한 경우 스크립트도 추가한다.
다음 Turn부터 에이전트는 해당 Skill을 사용할 수 있다.
이러한 구조는 사용자가 직접 모든 Skill을 미리 등록해야 하는 부담을 줄일 수 있다.
하지만 에이전트가 Skill을 생성할 수 있다는 사실에는 보안상 중요한 고려사항이 있다.
Skill에는 실제로 실행할 수 있는 지침과 스크립트가 포함될 수 있기 때문이다.
예를 들어 외부 문서의 악의적인 지시를 에이전트가 신뢰한다면 잘못된 Skill을 저장할 가능성이 있다.
따라서 실제 시스템에서는 Skill을 작성할 수 있는 권한과 실행할 수 있는 권한을 구분하는 것이 중요하다.
특히 외부 API 호출이나 파일 변경을 수행하는 스크립트라면 별도의 검증과 실행 환경 제한이 필요할 수 있다.
공통 Skill 도구
Cloudflare는 다음 도구를 제공한다.
| 도구 | 역할 |
|---|---|
activate_skill | Skill의 지침과 포함된 리소스 목록 로딩 |
read_skill_resource | Skill에 포함된 개별 파일 읽기 |
run_skill_script | Skill의 스크립트 실행 |
여기서 run_skill_script는 실험적인 Script Runner를 설정한 경우에만 추가된다.
따라서 Skill을 등록했다고 해서 모든 환경에서 스크립트 실행 도구가 자동으로 제공되는 것은 아니다.
또한 Cloudflare는 같은 Skill 도구를 여러 Harness에서 사용할 수 있도록 한다.
지원 대상에는 다음이 포함된다.
- Think
- Pi Durable
- AI SDK
따라서 Skill을 사용하는 인터페이스와 실제 AI 모델의 실행 루프를 분리할 수 있다.
Skill 실행 방식
Workspace에 존재하는 Skill Script는 Workspace의 exec 도구를 통해 실행할 수 있다.
예를 들어 Skill에 데이터 처리 스크립트가 포함돼 있다고 가정해 보자.
에이전트는 Skill을 활성화한다.
그리고 필요한 스크립트의 위치를 확인한다.
이후 Workspace에서 스크립트를 실행할 수 있다.
하지만 실행 가능한 Script의 종류와 권한은 실제 Workspace Backend의 지원 범위에 영향을 받는다.
모든 Skill이 임의의 시스템 명령을 무제한으로 실행할 수 있는 것은 아니다.
또한 R2에 저장된 Skill을 사용하는 경우에는 해당 리소스를 실제 실행 환경에서 어떻게 가져오고 사용할지 고려해야 한다.
표준 Skill 형식과 검증
Cloudflare는 공개된 Agent Skills Specification을 사용한다.
SKILL.md는 해당 규격의 구조를 기준으로 검증된다.
하지만 이번 구현에서는 중요한 차이가 존재한다.
규격에 완전히 일치하지 않는 Skill도 경고를 기록한 뒤 로딩할 수 있다.
즉 검증 실패가 반드시 Skill 로딩 실패를 의미하지 않는다.
Cloudflare는 Skill 작성 전에 다음 함수를 이용해 검증할 수 있도록 한다.
parseSkill()
이를 통해 Skill이 기대한 구조를 충족하는지 확인할 수 있다.
하지만 형식이 올바르다는 사실만으로 Skill에 포함된 실제 지침이나 스크립트가 안전하다는 의미는 아니다.
형식 검증과 보안 검증은 서로 다른 작업이다.
Prompt Cache 유지
이번 업데이트에는 Prompt Cache 관련 개선도 포함됐다.
일반적으로 AI 에이전트의 System Prompt에는 사용할 수 있는 도구와 Skill 목록이 포함될 수 있다.
문제는 Skill 목록이 변경될 때 발생한다.
예를 들어 새로운 Skill이 추가됐다고 가정해 보자.
기존 구현에서 System Prompt 전체를 다시 생성하면 이전 Prompt Cache를 재사용하기 어려워질 수 있다.
새로운 Think에서는 Skill 목록이 변경되더라도 전체 System Prompt를 다시 구성하지 않는다.
대신 새로운 Skill Catalog를 Reminder 형태로 전달한다.
이렇게 하면 기존 System Prompt의 공통 부분을 유지할 수 있다.
또한 사용자 정의 getSystemPrompt()의 결과가 다른 Context Block보다 위에 배치되도록 변경했다.
이는 Prompt의 구성 순서와 Cache 재사용에 영향을 줄 수 있다.
하지만 이번 발표에는 Token 비용이나 Cache Hit Rate가 실제로 얼마나 개선됐는지에 관한 정량적인 수치가 포함되지 않았다.
따라서 모든 에이전트에서 특정 비율의 비용 절감이 발생한다고 단정할 수 없다.
기존 API의 Breaking Change
이번 업데이트에서 가장 중요한 호환성 변경은 기존 Skill API의 일부가 제거됐다는 점이다.
대표적으로 다음 기능이 제거됐다.
agents:skillsVite Importagents/skills/compileSkillRegistry- Think의
skillWorkspace
또한 runner()의 Import 경로도 변경됐다.
새로운 경로는 다음과 같다.
agents/skills/runner
따라서 기존 agents/skills API를 직접 사용하는 프로젝트에서는 Import와 실행 구성을 수정해야 할 수 있다.
Cloudflare가 요구하는 최소 버전은 다음과 같다.
- Agents SDK 0.32.0
- Think 0.23.0
새로운 Workspace Skill 구조로 이전하려면 두 패키지의 버전과 실제 Import 경로를 함께 확인해야 한다.
이번 발표는 AI 에이전트의 능력을 애플리케이션 코드에 고정하지 않고, 실행 중인 Workspace의 파일과 Object Storage를 통해 확장할 수 있도록 Skill 관리 구조를 변경한 업데이트다.
Vercel IP Geolocation 정확도 개선 — 거의 모든 요청에 도시 정보, 약 90%에 우편번호 정보 제공
- 발표일: 2026-10-29 — 정확한 게시 시각 미확인
- 적용 대상: Vercel Functions, Routing Middleware
- 안정화 단계: 정식 플랫폼 업데이트
- 발표 수치: 거의 모든 요청에 도시 수준 정보, 트래픽의 약 90%에 우편번호 정보
- 측정 주체: Vercel 자체 집계
- 원문: https://vercel.com/changelog/improved-ip-geolocation-data
Vercel이 IP 주소를 기반으로 방문자의 위치를 추정하는 Geolocation 데이터의 정확도와 세부 수준을 개선했다.
이번 업데이트는 새로운 API를 추가하는 것이 아니다.
기존 Geolocation API가 반환하는 데이터의 품질을 개선한 것이다.
웹 애플리케이션에서는 방문자의 국가와 지역에 따라 다른 콘텐츠를 제공하는 경우가 있다.
예를 들어 다음과 같은 상황이다.
- 국가별 언어 선택
- 지역별 서비스 안내
- 배송 가능 지역 표시
- 특정 국가의 접근 제한
- 국가별 개인정보 처리 안내
이러한 기능에서는 사용자의 현재 위치나 거주 국가를 판단해야 할 수 있다.
하지만 브라우저의 위치 권한을 요청하는 방식이 항상 적절한 것은 아니다.
예를 들어 국가 수준의 언어를 선택하기 위해 사용자의 GPS 위치까지 요청할 필요는 없을 수 있다.
따라서 서버에서는 요청의 IP 주소를 이용해 대략적인 위치를 추정하는 경우가 있다.
이번 업데이트는 이러한 IP Geolocation의 데이터 품질을 개선한다.
IP Registry 정보만으로 위치를 판단하는 문제
IP 주소에는 등록된 조직과 관련된 정보가 존재한다.
예를 들어 특정 IP 대역이 어느 국가의 기업에 할당됐는지 확인할 수 있다.
하지만 IP 주소가 등록된 국가와 실제 네트워크 트래픽이 처리되는 국가가 항상 일치하는 것은 아니다.
예를 들어 한 기업이 미국에서 IP 대역을 등록했다고 가정해 보자.
해당 기업은 실제 서버를 다른 국가에서 운영할 수 있다.
또는 특정 IP 주소를 다른 지역의 데이터센터에서 사용할 수 있다.
이러한 상황에서 Registry 정보만 사용하면 실제 네트워크 위치를 잘못 판단할 수 있다.
Vercel은 이번 업데이트에서 등록 정보뿐 아니라 실제로 관찰한 네트워크 동작을 이용해 위치 데이터를 개선했다고 설명한다.
따라서 IP 주소가 다른 지역에서 실제로 사용되는 경우에도 해당 정보를 더 정확하게 반영하려고 한다.
도시 및 우편번호 데이터 확대
Vercel이 공개한 결과는 다음과 같다.
| 정보 | 제공 범위 |
|---|---|
| 도시 수준 위치 | 거의 모든 요청 |
| 우편번호 | 전체 트래픽의 약 90% |
이전보다 더 많은 요청에서 세부 위치 정보를 얻을 수 있다.
하지만 이 수치는 실제 방문자가 물리적으로 존재하는 위치를 100% 정확하게 파악한다는 의미는 아니다.
예를 들어 VPN을 사용하는 사용자는 VPN 서버의 위치로 판별될 수 있다.
또한 모바일 네트워크나 기업 네트워크에서는 실제 사용자의 위치와 인터넷 트래픽이 외부로 나가는 위치가 다를 수 있다.
따라서 우편번호가 반환됐다는 사실만으로 해당 사용자의 실제 주소를 알 수 있다고 판단해서는 안 된다.
기존 API 유지
이번 업데이트에서는 기존 Geolocation API의 형태를 변경하지 않았다.
따라서 기존 프로젝트의 코드를 수정할 필요가 없다.
관련 API는 다음과 같다.
geolocation()
이 함수는 @vercel/functions 패키지에서 사용할 수 있다.
또한 Vercel이 제공하는 Geolocation 관련 Request Header도 기존과 동일하게 유지된다.
이러한 정보는 다음 실행 환경에서 사용할 수 있다.
- Vercel Functions
- Routing Middleware
따라서 기존에 국가 정보를 이용해 Route를 분기하던 애플리케이션도 같은 API를 사용할 수 있다.
하지만 반환되는 실제 값은 이전과 달라질 수 있다.
기존 사용자의 국가 판별 결과가 변경될 수 있다
이번 업데이트에서 중요한 호환성 문제는 API의 구조가 같더라도 실제 데이터가 변경될 수 있다는 사실이다.
예를 들어 기존에는 특정 IP 주소가 미국으로 판별됐다고 가정해 보자.
하지만 해당 IP 대역의 트래픽이 실제로는 유럽의 데이터센터에서 처리되고 있었다.
새로운 Geolocation 데이터에서는 해당 요청이 유럽 국가로 판별될 수 있다.
이 경우 국가별 Route나 접근 제한 정책의 결과가 달라질 수 있다.
예를 들어 다음과 같은 로직이 존재할 수 있다.
country === "US"
이 조건을 이용해 특정 화면을 표시하거나 접근을 제한한다고 가정해 보자.
Geolocation 데이터가 변경되면 동일한 IP 주소를 사용하는 요청이라도 다른 결과가 발생할 수 있다.
따라서 데이터 제공업체의 정확도 개선이 실제 서비스에서는 동작 변경으로 나타날 수 있다는 점을 고려해야 한다.
접근 제어와 규정 준수의 한계
Vercel은 Geolocation을 접근 제어와 지역별 서비스 제공에 사용하는 프로젝트에서는 변경된 결과를 확인하도록 안내한다.
하지만 IP Geolocation을 실제 사용자의 신원이나 법적 거주 국가를 증명하는 수단으로 사용해서는 안 된다.
예를 들어 사용자가 VPN을 통해 접속하면 실제 국가와 다른 결과가 나타날 수 있다.
또한 사용자가 실제로 어느 국가에 거주하는지는 IP 주소만으로 확정하기 어렵다.
따라서 법적 요구사항이나 중요한 접근 제한이 존재한다면 사용자가 명시적으로 제공한 정보와 실제 인증 및 검증 절차를 함께 고려해야 한다.
이번 발표에는 기존 데이터와 새로운 데이터의 실제 위치 정확도를 독립적으로 비교한 Benchmark가 포함되지 않았다.
도시 및 우편번호 제공 범위는 Vercel이 자체적으로 발표한 수치다.
이번 업데이트는 기존 API의 호환성을 유지하면서 IP 위치 데이터의 해상도와 갱신 주기를 개선한 플랫폼 변경이다.
Vercel AI SDK, fx Harness 실행 구조 변경 — 모델 호출은 Host에서, 파일·Shell 작업은 Sandbox에서 처리
- 발표일: 2026-10-29 — 정확한 게시 시각 미확인
- 적용 대상: Vercel AI SDK, HarnessAgent, fx Adapter
- 관련 패키지:
@ai-sdk/harness-fx - 변경 유형: Harness 실행 아키텍처 변경
- 원문: https://vercel.com/changelog/fx-harness-adapter-now-separates-host-and-sandbox
Vercel이 AI SDK의 fx Harness Adapter를 새로운 실행 구조로 변경했다.
이번 업데이트는 단순히 모델 목록을 추가한 것이 아니다.
AI 에이전트의 실행 루프와 실제 코드 실행 환경을 분리하도록 Adapter의 구조를 다시 작성한 것이다.
AI 코딩 에이전트는 여러 종류의 작업을 수행한다.
예를 들어 사용자가 React 컴포넌트를 수정해 달라고 요청했다고 가정해 보자.
에이전트는 프로젝트 파일을 읽는다.
수정할 코드를 생성한다.
실제 파일을 변경한다.
이후 테스트를 실행한다.
이러한 작업에는 두 종류의 실행 환경이 필요할 수 있다.
첫 번째는 AI 모델과 통신하는 실행 환경이다.
두 번째는 실제 파일과 Shell 명령을 처리하는 실행 환경이다.
이번 업데이트는 이 두 실행 환경을 분리한다.
기존 구조
기존 fx Adapter에서는 Harness를 Sandbox 안에서 실행하는 구성이 사용됐다.
이 경우 Sandbox에 fx를 설치해야 했다.
또한 Sandbox와 외부 애플리케이션이 통신하기 위한 Bridge가 필요할 수 있었다.
대표적으로 다음 작업이 필요했다.
- Sandbox에 fx 설치
- Sandbox Bridge 실행
- 통신에 필요한 Port 노출
- Model Request 및 실행 결과 전달
이러한 구성은 실제 에이전트의 작업과 이를 관리하는 애플리케이션 사이에 추가적인 실행 계층을 만든다.
특히 Sandbox 안에서 모델 API를 직접 호출한다면 해당 환경에 인증 정보를 제공해야 할 수 있다.
새로운 libfx 기반 구조
새로운 Adapter는 다음 라이브러리를 사용한다.
libfx
이는 fx의 TypeScript 라이브러리다.
이번 변경 이후 실행 위치는 다음과 같이 나뉜다.
| 작업 | 실행 위치 |
|---|---|
| Harness의 실행 루프 | Host Application |
| AI 모델 요청 | Host Application |
| 파일시스템 작업 | Sandbox |
| Shell 명령 실행 | Sandbox |
Host는 실제 애플리케이션이 실행되는 환경이다.
예를 들어 AI 코딩 에이전트를 관리하는 서버가 Host에 해당할 수 있다.
반면 Sandbox는 신뢰할 수 없는 코드를 실행하거나 실제 프로젝트 파일을 수정하는 격리된 환경이다.
이렇게 하면 모델 요청에 필요한 인증 정보는 Host에 유지할 수 있다.
Sandbox에는 실제로 필요한 파일과 명령 실행 권한만 제공할 수 있다.
Sandbox Bridge 제거
이번 구조에서는 Sandbox 안에 fx를 별도로 설치할 필요가 없다.
또한 기존 Sandbox Bridge와 관련 Port 설정도 필요하지 않다.
따라서 초기 실행 환경을 구성하는 단계가 줄어든다.
예를 들어 기존에는 에이전트를 실행하기 전에 Sandbox 내부의 서버가 준비됐는지 확인해야 했을 수 있다.
새로운 방식에서는 Host의 Harness가 Sandbox의 파일 및 Shell 도구를 직접 사용할 수 있다.
Vercel은 이러한 구조가 응답 시간을 개선한다고 설명한다.
하지만 이번 발표에는 정확히 몇 밀리초 또는 몇 퍼센트 빨라졌는지에 관한 정량적인 Benchmark가 포함되지 않았다.
따라서 모든 작업에서 일정한 비율의 속도 개선이 발생한다고 단정할 수 없다.
모델 인증 정보의 격리
이번 변경에서 중요한 보안상 차이는 모델 인증 정보가 Sandbox 밖에 유지된다는 사실이다.
예를 들어 에이전트가 생성한 코드를 Sandbox에서 실행한다고 가정해 보자.
해당 코드는 신뢰할 수 없는 입력을 포함할 수 있다.
만약 Sandbox 내부에 모델 API Key가 존재한다면 잘못된 코드가 해당 정보를 읽으려고 시도할 가능성이 있다.
새로운 구조에서는 모델 호출을 Host에서 처리한다.
따라서 Sandbox의 파일 및 Shell 명령 실행 환경에 모델 API Key를 직접 제공할 필요가 줄어든다.
하지만 Sandbox가 어떤 외부 API에 접근할 수 있는지는 별도의 문제다.
실제 프로젝트의 환경 변수나 인증 정보를 Sandbox에 전달한다면 해당 정보는 여전히 보호해야 한다.
이번 구조가 모든 종류의 비밀 정보 노출을 자동으로 방지한다는 의미는 아니다.
Built-in Tool Filtering
이번 Adapter에서는 사용할 수 있는 기본 도구를 선택하는 기능도 추가됐다.
예를 들어 에이전트에게 파일 읽기 권한은 제공하지만 특정 Shell 명령은 허용하지 않도록 구성할 수 있다.
이러한 방식은 에이전트가 실제 작업에 필요한 도구만 사용하도록 제한하는 데 도움이 된다.
하지만 Tool Filtering만으로 외부 코드 실행의 모든 위험을 방지할 수는 없다.
실제 Sandbox의 파일 접근 범위와 네트워크 정책, 실행 권한도 함께 제한해야 한다.
Mid-turn Steering
새로운 Adapter는 실행 중인 Turn에 추가 지시를 전달하는 기능도 지원한다.
이를 Mid-turn Steering이라고 한다.
예를 들어 에이전트가 여러 파일을 수정하는 작업을 수행하고 있다고 가정해 보자.
사용자가 작업 중간에 특정 파일은 수정하지 말라고 요청할 수 있다.
또는 새로운 요구사항을 전달할 수 있다.
Mid-turn Steering을 사용하면 이러한 지시를 현재 실행 중인 작업 흐름에 전달할 수 있다.
하지만 이미 완료된 외부 작업을 자동으로 되돌리는 기능은 아니다.
예를 들어 이미 파일을 수정하거나 외부 API를 호출했다면 별도의 복구 작업이 필요할 수 있다.
구조화된 응답과 이미지 결과
이번 업데이트에는 다음 기능도 포함됐다.
- Structured Output
- Image Tool Result
- Session 저장 및 재개
Structured Output은 모델이 지정된 Schema에 맞는 데이터를 반환하도록 요청하는 기능이다.
Image Tool Result는 도구가 생성한 이미지를 모델에 다시 전달하는 기능이다.
이때 다음 함수를 사용할 수 있다.
toModelOutput()
또한 실행 중인 Session을 저장했다가 다른 Host Process에서 다시 사용할 수 있다.
이는 Host Process의 수명과 에이전트 Session을 분리하는 데 도움이 된다.
다만 Session을 저장할 수 있다는 사실만으로 외부 API 호출의 중복 실행이나 모든 Sandbox 상태의 복구까지 자동으로 보장되는 것은 아니다.
이번 업데이트는 모델 실행과 신뢰할 수 없는 코드 실행을 분리하고, Harness의 관리 기능을 Host에 집중하도록 만든 아키텍처 변경이다.
Vercel Connect, Resend·Telegram·Twilio·Notion·X Webhook Trigger 지원 — 인증 검증과 이벤트 전달을 플랫폼에서 처리
- 발표일: 2026-10-29 — 정확한 게시 시각 미확인
- 적용 대상: Vercel Connect
- 신규 지원 서비스: Resend, X, Twilio, Notion, Telegram
- 원문: https://vercel.com/changelog/vercel-connect-adds-webhook-triggers-for-more-providers
Vercel이 Connect에서 외부 서비스의 Webhook 이벤트를 받아 Vercel 프로젝트의 Endpoint로 전달하는 기능을 확장했다.
이번 업데이트는 외부 서비스를 단순히 API로 호출하는 기능과 다르다.
외부 서비스에서 특정 사건이 발생했을 때 애플리케이션이 이를 자동으로 전달받는 기능이다.
예를 들어 Telegram Bot을 운영한다고 가정해 보자.
사용자가 Bot에 새로운 메시지를 전송한다.
이때 애플리케이션은 해당 메시지가 발생했다는 사실을 알아야 한다.
이를 구현하는 방법 가운데 하나는 Polling이다.
Polling은 일정한 간격으로 새로운 메시지가 있는지 확인하는 방식이다.
하지만 새로운 메시지가 없어도 반복적인 API 요청이 발생할 수 있다.
Webhook은 반대 방향으로 동작한다.
외부 서비스에서 이벤트가 발생하면 해당 서비스가 등록된 URL로 HTTP 요청을 전송한다.
따라서 애플리케이션은 실제 이벤트가 발생한 시점에 관련 작업을 수행할 수 있다.
새롭게 지원하는 서비스
이번 업데이트에서 추가된 서비스는 다음과 같다.
| 서비스 | 이벤트 예시 |
|---|---|
| Resend | 이메일 수신 |
| Telegram | Bot 메시지 수신 |
| Twilio | 통신 관련 Webhook 이벤트 |
| Notion | 연결된 리소스의 변경 이벤트 |
| X | 지원되는 계정 및 서비스 이벤트 |
기존에는 다음 서비스의 Webhook Trigger를 지원하고 있었다.
- Slack
- Discord
- Microsoft Teams
- GitHub
- Linear
이번 업데이트로 Connect에서 사용할 수 있는 이벤트 기반 연결 범위가 확대됐다.
다만 각 서비스에서 제공하는 모든 종류의 이벤트가 동일하게 지원된다는 의미는 아니다.
실제로 사용할 수 있는 이벤트는 제공업체와 연결 방식에 따라 달라질 수 있다.
Connect의 실행 흐름
새로운 구조에서는 Connect가 외부 서비스와 애플리케이션 사이의 Webhook 전달 계층 역할을 한다.
예를 들어 Telegram 메시지를 처리하는 흐름은 다음과 같다.
- 사용자가 Telegram Bot에 메시지를 전송한다.
- Telegram이 Webhook 이벤트를 전송한다.
- Vercel Connect가 이벤트를 수신한다.
- Connect가 요청을 검증한다.
- 설정된 Vercel 프로젝트의 Endpoint로 전달한다.
- 애플리케이션이 메시지를 처리한다.
이러한 방식에서는 애플리케이션이 각 외부 제공업체의 Webhook 수신 주소를 직접 관리하는 부담을 줄일 수 있다.
또한 Connect는 제공업체가 요구하는 일부 검증 절차도 처리한다.
Webhook 검증
Webhook은 일반적인 HTTP 요청이므로 보안 검증이 필요하다.
예를 들어 공격자가 실제 Telegram이나 Twilio에서 발생한 이벤트처럼 보이는 요청을 임의로 생성할 수 있다.
따라서 애플리케이션은 해당 요청이 실제 제공업체에서 발생했는지 확인해야 한다.
Vercel Connect는 외부 제공업체에서 전달하는 요청의 검증을 처리한다.
또한 다음과 같은 제공업체별 절차도 관리할 수 있다.
- Heartbeat
- Verification Challenge
Heartbeat는 연결이나 서비스의 상태를 확인하기 위해 사용하는 요청이다.
Verification Challenge는 Webhook Endpoint를 등록할 때 해당 주소를 실제로 관리하고 있는지 확인하기 위한 절차다.
이러한 과정은 제공업체마다 구현 방식이 다를 수 있다.
Connect는 이러한 차이를 관리형 기능으로 제공한다.
다만 Connect가 요청을 검증한다고 해서 실제 Payload의 비즈니스 의미까지 모두 검증하는 것은 아니다.
예를 들어 Telegram 메시지에 포함된 사용자의 명령을 실행할지 결정하는 것은 애플리케이션의 책임이다.
서비스별 설정 차이
이번 업데이트에서는 서비스별 Webhook 등록 방식이 다르다.
Resend와 Telegram은 Connect가 Webhook을 자동으로 등록할 수 있다.
반면 다음 서비스는 제공업체의 설정 화면에서 추가 작업이 필요하다.
- Twilio
- Notion
- X
이 경우 Connect에서 제공하는 Webhook URL을 확인한다.
이후 해당 서비스의 설정 화면에서 URL을 등록한다.
따라서 Connect를 생성했다는 사실만으로 모든 외부 서비스의 이벤트 전달이 자동으로 시작되는 것은 아니다.
제공업체별 필요한 설정을 완료해야 한다.
Trigger Destination
Connect는 수신한 이벤트를 실제 애플리케이션으로 전달하기 위한 Destination을 제공한다.
개발자는 다음 정보를 지정할 수 있다.
- 대상 Vercel 프로젝트
- 이벤트를 전달할 Endpoint
예를 들어 동일한 외부 서비스의 이벤트를 특정 API Route로 전달하도록 구성할 수 있다.
이후 애플리케이션은 이벤트의 종류와 실제 데이터를 확인해 필요한 작업을 수행한다.
Live Observability
이번 기능은 Connect의 Observability와도 연결된다.
관리자는 다음 화면에서 실시간 이벤트를 확인할 수 있다.
Connect > Observability > Live
여기에서는 Connect에 전달된 Webhook 이벤트를 확인할 수 있다.
이는 Webhook이 발생하지 않는 문제와 애플리케이션이 이벤트를 처리하지 못하는 문제를 구분하는 데 도움이 된다.
예를 들어 실제 이벤트가 Connect에 도착했다면 외부 제공업체와 Connect 사이의 연결은 일부 정상적으로 동작하고 있다고 판단할 수 있다.
하지만 해당 이벤트가 실제 애플리케이션에서 성공적으로 처리됐는지는 별도로 확인해야 한다.
또한 Webhook 시스템에서는 중복 이벤트가 전달될 가능성이 존재한다.
따라서 주문 생성이나 결제 처리처럼 외부 상태를 변경하는 작업에서는 Idempotency를 고려해야 한다.
이번 발표는 외부 서비스의 이벤트 수신과 검증, 애플리케이션 전달 기능을 Vercel Connect에 통합해 이벤트 기반 웹서비스 개발을 단순화한 업데이트다.
4️⃣ 개발 도구 및 보안
AWS, Brotli 취약점의 Lambda·Amplify·App Runner·CodeBuild 영향 범위 공개 — 관리형 런타임도 업데이트 단계별로 확인 필요
- 발표일: 2026-10-29 07:00 KST
- 공식 게시 시각: 2026-10-28 15:00 PDT
- 보안 공지: 2026-155-AWS
- 관련 취약점: CVE-2026-111497
- 영향받는 환경: AWS Lambda의 일부 Runtime, Amplify Hosting, App Runner, CodeBuild 등
- 원문: https://aws.amazon.com/security/security-bulletins/2026-155-aws/
AWS가 Brotli Decoder의 원격 코드 실행 및 메모리 노출 취약점에 대해 AWS 서비스별 영향 범위와 패치 배포 현황을 공개했다.
이번 발표는 새로운 CVE가 발견됐다는 소식이 아니다.
CVE-2026-111497 자체는 10월 21일 Node.js 보안 릴리스와 함께 공개됐으며 이전 브리핑에서 다뤘다.
이번 AWS 공지는 해당 취약점이 AWS의 관리형 Runtime과 개발 인프라에 어떤 영향을 주는지 설명한 새로운 운영 정보다.
특히 AWS Lambda 서비스 자체는 영향을 받지 않더라도 Lambda에서 실행하는 특정 언어 Runtime은 영향을 받을 수 있다는 점이 중요하다.
Lambda 플랫폼과 Runtime의 구분
AWS Lambda는 사용자가 직접 서버를 관리하지 않고 코드를 실행할 수 있도록 하는 서비스다.
하지만 실제 코드가 실행되는 언어 Runtime은 별도로 존재한다.
예를 들어 Lambda에서 다음 Runtime을 사용할 수 있다.
- Node.js
- Python
- .NET
- Ruby
AWS는 이번 보안 공지에서 다음 두 사실을 구분했다.
첫 번째는 AWS Lambda의 관리형 서비스 자체가 해당 취약점의 영향 대상이 아니라는 점이다.
두 번째는 Lambda에서 제공하는 Node.js 및 .NET Runtime이 영향을 받을 수 있다는 점이다.
즉 관리형 서버리스 서비스를 사용한다는 사실만으로 그 안에서 실행되는 모든 언어 Runtime이 자동으로 안전하다고 판단해서는 안 된다.
Lambda Runtime별 영향 범위
AWS가 공개한 영향 범위는 다음과 같다.
| Runtime | 영향 여부 및 상태 |
|---|---|
| Node.js 22 | 영향받음. 패치 배포 진행 |
| Node.js 24 | 영향받음. 패치 배포 진행 |
| Node.js 26 | 영향받음. 패치 배포 진행 |
| .NET 8 | 영향받음 |
| .NET 9 Container | 영향받음 |
| .NET 10 | 영향받음 |
| Python | 기본 Runtime에 Brotli 미포함 |
| Ruby | 기본 Runtime에 Brotli 미포함 |
| OS-only provided.al2023 | 기본 Runtime에 Brotli 미포함 |
하지만 Python과 Ruby Runtime에 Brotli가 포함되지 않는다는 사실이 해당 언어로 작성한 모든 Lambda Function이 안전하다는 의미는 아니다.
사용자가 직접 Brotli 라이브러리를 포함했을 수 있기 때문이다.
예를 들어 Python 패키지에 Brotli Decoder가 포함돼 있다면 해당 패키지의 버전을 별도로 확인해야 한다.
또한 Lambda Layer에 취약한 라이브러리가 포함돼 있을 가능성도 고려해야 한다.
Node.js Runtime 패치 배포 일정
AWS는 Node.js Runtime의 패치를 두 단계로 배포한다.
첫 번째 단계는 Function을 새롭게 생성하거나 업데이트할 때 수정된 Runtime을 사용하는 방식이다.
두 번째 단계는 자동 Runtime Update를 사용하는 기존 Function에 패치를 적용하는 방식이다.
공식 공지에서 제시한 일정은 다음과 같다.
| 단계 | 목표 날짜 |
|---|---|
| Node.js ZIP Package 1단계 | 2026-10-28 |
| Node.js ZIP Package 2단계 | 2026-10-30 |
| .NET ZIP Package 1단계 | 2026-10-29 |
| .NET ZIP Package 2단계 | 2026-10-31 |
다만 이 날짜는 배포 목표다.
AWS는 리전별로 순차 배포한다고 설명한다.
따라서 특정 리전에서 패치가 정확히 같은 시점에 적용된다고 보장하지 않는다.
Node.js 24의 별도 처리
이번 공지에는 중요한 예외가 존재한다.
AWS는 Node.js 24.22.1에 알려진 Regression이 있다고 설명했다.
따라서 Lambda의 Node.js 24 Runtime에서는 모든 환경을 24.22.1로 직접 변경하는 대신 기존 안정 버전에 필요한 보안 수정만 선택적으로 반영하고 있다.
이 때문에 실제 Lambda Runtime의 Node.js 버전 문자열과 보안 패치 여부가 단순하게 일치하지 않을 수 있다.
특히 AWS는 다음 사실을 명시했다.
Node.js 24 Lambda Runtime의 process.version만으로 이번 보안 패치 적용 여부를 판단할 수 없다.
이는 관리형 Runtime의 보안 수정이 기존 버전에 Backport될 수 있기 때문이다.
Backport는 새로운 버전의 수정 사항을 기존 버전의 코드에 맞게 다시 적용하는 방식이다.
따라서 실제 패치 상태는 AWS가 제공하는 Runtime Patch 정보를 확인해야 한다.
Lambda 패치 버전 확인
AWS는 Lambda 로그의 다음 항목을 확인하도록 안내한다.
INIT_START
해당 로그에는 실제 Runtime Patch 버전이 포함될 수 있다.
공식 공지는 다음 기준을 제시했다.
| Runtime | 수정 기준 |
|---|---|
| Node.js 22 | nodejs:22.mainline.v157 이상 |
| Node.js 24 | nodejs:24.mainline.v114 이상 |
따라서 실제 Lambda Function이 어떤 Runtime을 사용하는지 확인할 때는 단순히 Node.js 메이저 버전만 보는 것보다 Runtime Patch 버전을 확인하는 것이 적절하다.
또한 이러한 로그는 새로운 실행 환경이 시작될 때 생성될 수 있다.
이미 실행 중인 Warm Function에서는 즉시 새로운 INIT_START 로그가 나타나지 않을 수 있다.
Container Image 기반 Lambda
Lambda를 Container Image로 배포하는 경우에는 별도의 작업이 필요하다.
AWS는 수정된 Node.js Base Image를 제공하고 있다.
하지만 기존 Container Image가 자동으로 새로운 Base Image를 사용하는 것은 아니다.
필요한 작업은 다음과 같다.
- 수정된 Base Image 확인
- Container Image 재빌드
- 새로운 Image 배포
- 실제 실행 Runtime 확인
기존 이미지를 다시 실행하는 것만으로는 수정된 라이브러리가 반영되지 않을 수 있다.
특히 Image Digest를 고정한 프로젝트에서는 새로운 Image를 명시적으로 선택해야 한다.
Amplify Hosting
AWS Amplify Hosting의 SSR 애플리케이션도 영향을 받을 수 있다.
공식 공지는 Node.js 22와 24를 사용하는 SSR 환경에서 관리형 Runtime 업데이트를 적용한다고 설명한다.
해당 환경에서는 고객의 별도 조치가 필요하지 않다.
하지만 Node.js 22보다 오래된 Runtime을 사용하는 경우에는 지원되는 버전으로 변경할 것을 권장한다.
따라서 기존에 Node.js 18이나 20을 사용하는 프로젝트는 단순히 관리형 플랫폼을 사용한다는 이유로 패치를 기대해서는 안 된다.
App Runner
AWS App Runner에서는 수정된 Node.js 관리형 Runtime이 10월 26일 모든 AWS 리전에 배포됐다.
하지만 실제 서비스가 해당 Runtime을 사용하려면 배포가 필요할 수 있다.
AWS는 10월 26일 이후 다시 배포하지 않은 서비스는 한 번 재배포하도록 안내한다.
또한 특정 Runtime 버전을 명시적으로 고정한 경우에는 설정을 변경해야 한다.
이 경우 단순히 다시 배포하는 것만으로는 수정된 Runtime으로 이동하지 않을 수 있다.
CodeBuild
AWS CodeBuild의 관리형 Image도 업데이트 대상이다.
AWS는 10월 30일까지 수정된 Image의 배포를 완료하는 것을 목표로 한다.
다음 버전이 포함될 예정이다.
- Node.js 22.23.4
- Node.js 24.22.1
- Node.js 26.12.1
또한 Ubuntu와 Amazon Linux의 Brotli 라이브러리 패치도 포함된다.
하지만 기존 Image의 특정 버전이나 Digest를 고정했다면 새로운 Image를 직접 선택해야 한다.
Custom Image를 사용하는 경우에는 수정된 Base Image를 바탕으로 다시 빌드해야 한다.
또한 AWS는 새로운 관리형 Image에서 지원이 종료된 Node.js 18과 20을 제거한다고 설명했다.
따라서 Buildspec에서 오래된 Node.js 버전을 명시적으로 사용하는 프로젝트는 수정이 필요할 수 있다.
Elastic Beanstalk
Elastic Beanstalk에서는 다음 플랫폼 버전이 공개됐다.
6.11.12
이 버전에는 다음 Node.js가 포함된다.
- 24.22.1
- 22.23.4
관리형 Platform Update를 사용하는 환경은 다음 유지보수 시점에 수정 버전을 적용할 수 있다.
반면 자동 업데이트가 활성화되지 않은 환경은 직접 플랫폼을 업데이트해야 한다.
또한 지원이 종료된 Node.js 18과 20을 사용하는 환경에서는 지원되는 플랫폼으로 이전해야 한다.
자체 운영 EC2·ECS·EKS
자체적으로 관리하는 Node.js 환경은 AWS가 자동으로 모두 업데이트하지 않는다.
예를 들어 다음과 같은 환경이다.
- EC2에서 직접 실행하는 Node.js 서버
- ECS의 사용자 정의 Container Image
- EKS에서 실행하는 Node.js 서비스
이러한 환경에서는 애플리케이션과 Base Image, 포함된 라이브러리의 버전을 운영자가 직접 관리해야 한다.
따라서 AWS의 관리형 Runtime 패치 공지만으로 해당 환경의 취약점이 수정됐다고 볼 수 없다.
AWS WAF의 임시 대응
AWS는 즉시 패치할 수 없는 환경을 위해 WAF 기반 임시 대응 방법도 제공한다.
핵심 조건은 다음 Header다.
Content-Encoding: br
이는 HTTP Request Body가 Brotli로 인코딩됐다는 의미다.
AWS는 해당 요청을 검사하는 WAF 규칙을 예제로 제공한다.
하지만 처음부터 Block하지 않도록 안내한다.
먼저 Count 모드로 실행한다.
그리고 실제로 어떤 요청이 조건에 일치하는지 확인한다.
정상적인 트래픽에 영향을 주지 않는다고 판단한 뒤 Block으로 변경할 수 있다.
여기에는 두 가지 제한사항이 있다.
첫 번째는 정상적인 Brotli Request Body도 차단될 수 있다는 점이다.
두 번째는 WAF가 일반적인 인바운드 요청을 검사한다는 점이다.
따라서 외부 API의 응답이나 파일에서 가져온 Brotli 데이터를 애플리케이션이 직접 압축 해제하는 경우에는 동일한 방식으로 보호하지 못할 수 있다.
이번 AWS 공지는 같은 보안 취약점이라도 관리형 서비스와 언어 Runtime, Container Image, 자체 운영 인프라에 따라 실제 패치 책임과 적용 방법이 다르다는 점을 명확하게 보여준다.
Vercel Firewall, AI 크롤러와 AI 비서의 요청을 별도로 제어 — 학습용 수집과 사용자 질문 기반 접근 구분
- 발표일: 2026-10-29 — 정확한 게시 시각 미확인
- 적용 대상: Vercel Firewall
- 기능: AI Bots Managed Ruleset
- 지원 동작: Log, Block
- 원문: https://vercel.com/changelog/block-ai-crawlers-and-assistants-independently-with-managed-rules
Vercel이 AI 크롤러와 AI 비서의 웹사이트 접근을 서로 다른 정책으로 관리할 수 있도록 Firewall의 AI Bot 규칙을 확장했다.
이번 기능은 전날 공개된 AI Traffic Insights와 관련이 있지만 목적이 다르다.
AI Traffic Insights는 실제 AI Bot의 접근을 분석하는 Observability 기능이다.
반면 이번 업데이트는 AI Bot의 요청을 허용하거나 차단하는 Firewall 정책에 관한 것이다.
AI Crawler와 AI Assistant의 차이
Vercel은 AI Bot을 크게 두 종류로 구분한다.
첫 번째는 AI Crawler다.
Crawler는 웹사이트의 콘텐츠를 수집하는 자동화 프로그램이다.
대표적으로 검색 색인이나 모델 학습에 사용할 데이터를 수집할 수 있다.
두 번째는 AI Assistant다.
Assistant는 사용자의 질문이나 검색 요청에 응답하기 위해 특정 페이지를 가져올 수 있다.
예를 들어 사용자가 AI 비서에게 특정 기술 문서의 내용을 설명해 달라고 요청했다고 가정해 보자.
해당 AI 비서는 답변에 필요한 최신 정보를 얻기 위해 문서 페이지에 접근할 수 있다.
두 Bot 모두 자동화된 HTTP 요청을 발생시키지만 목적은 다르다.
따라서 웹사이트 운영자는 서로 다른 접근 정책을 적용하고 싶을 수 있다.
서로 다른 접근 정책
예를 들어 기술 문서 사이트를 운영한다고 가정해 보자.
운영자는 대규모 학습 데이터 수집을 제한하고 싶을 수 있다.
하지만 사용자의 요청에 따라 문서를 가져오는 AI 비서에는 접근을 허용하고 싶을 수 있다.
새로운 AI Bots Managed Ruleset에서는 다음과 같은 구성이 가능하다.
| Bot 종류 | 정책 예시 |
|---|---|
| AI Crawler | Block |
| AI Assistant | Log |
이렇게 하면 AI Crawler의 요청은 차단할 수 있다.
반면 AI Assistant의 요청은 기록하면서 계속 허용할 수 있다.
두 종류를 모두 차단하거나 두 종류를 모두 기록하는 구성도 가능하다.
Dashboard 설정
Vercel Dashboard에서는 다음 경로에서 설정할 수 있다.
Security > Firewall > Rules
이후 Bot Management에서 AI Bots를 활성화한다.
그리고 AI Crawler와 AI Assistant에 각각 다른 동작을 지정할 수 있다.
설정을 변경한 뒤에는 실제로 Publish해야 적용된다.
따라서 Dashboard에서 설정 화면만 수정했다고 해서 정책이 즉시 변경된다고 가정해서는 안 된다.
CLI 지원
Vercel CLI에서도 같은 정책을 설정할 수 있다.
예를 들어 AI Crawler를 차단하려면 다음 명령을 사용할 수 있다.
vercel firewall rules edit ai-bots --category ai-crawlers --action deny --yes
AI Assistant의 접근을 기록하려면 다음 명령을 사용할 수 있다.
vercel firewall rules edit ai-bots --category ai-assistants --action log --yes
이러한 방식은 여러 프로젝트의 Firewall 정책을 자동화하는 데 활용할 수 있다.
하지만 --yes 옵션은 대화형 확인을 생략할 수 있으므로 실제 운영 프로젝트에서 사용할 때는 적용 대상과 정책을 확인해야 한다.
Bot 분류의 한계
Vercel은 새로운 AI Bot이 확인되면 Managed Ruleset을 갱신한다고 설명한다.
하지만 모든 AI 관련 요청을 정확하게 분류할 수 있다는 보장은 아니다.
예를 들어 알려지지 않은 Bot이 일반적인 브라우저처럼 요청할 수 있다.
또한 하나의 AI 서비스가 여러 종류의 요청을 수행할 수 있다.
따라서 Bot을 Crawler와 Assistant로 구분하는 방식은 Vercel이 식별할 수 있는 Bot과 요청의 특성에 의존한다.
또한 실제 요청을 차단했다고 해서 해당 콘텐츠가 이미 다른 곳에 저장돼 있는 상황까지 되돌릴 수 있는 것은 아니다.
이번 기능은 웹사이트에 접근하는 AI Bot의 목적에 따라 서로 다른 네트워크 접근 정책을 적용할 수 있도록 한 Firewall 업데이트다.
이번 조사 범위에서 이외에 포함할 만한 주요 신규 개발 도구 및 보안 발표는 확인하지 못했습니다.
GitHub의 주요 신규 발표는 10월 28일 GitHub Universe 2026과 함께 집중적으로 공개됐다.
전날 브리핑에서 다룬 Agentic Workflows의 정식 출시, Dependabot Reachability Analysis, GitHub Actions Workflow Lockfile, AI Code Scanning 등의 발표는 반복하지 않았다.
Node.js와 Vite, webpack, Rollup, esbuild, Babel, Biome, ESLint, Prettier, npm, pnpm, Yarn의 공식 릴리스 채널에서도 이번 조사 범위에 해당하는 새로운 주요 정식 변경은 확인하지 못했다.
Cloudflare Tunnel의 cloudflared 2026.10.6 릴리스도 확인했지만, 공개된 릴리스 정보만으로 중요한 기능 변경이나 보안 수정의 구체적인 내용을 검증할 수 없어 별도의 주요 소식으로 포함하지 않았다.
📚 추천 글
My portfolio was blank on every iPhone and BrowserStack said it was fine
- 저자: Kunj Hirapara
- 매체: DEV Community
- 게시일: 2026-10-28
- 원문: https://dev.to/kunjhirapara/my-portfolio-was-blank-on-every-iphone-and-browserstack-said-it-was-fine-1jip
실제 Next.js 포트폴리오 사이트에서 데스크톱과 Android에서는 정상적으로 보이던 페이지가 iPhone에서는 거의 빈 화면으로 나타난 문제를 조사하고 해결한 개발 기록이다.
이번 글의 특징은 문제가 발생한 원인이 Safari의 특정 버그나 React의 Hydration 실패가 아니었다는 점이다.
실제로 HTML은 정상적으로 렌더링돼 있었다.
하지만 개발자가 작성한 애니메이션 코드가 콘텐츠를 숨기고 있었다.
저자는 이를 발견하기까지 약 일주일 동안 실제 iPhone과 테스트 서비스를 반복해서 비교했다.
실제로 발생한 문제
저자는 자신의 포트폴리오 사이트를 배포했다.
데스크톱에서는 정상적으로 보였다.
Android에서도 문제없이 표시됐다.
하지만 실제 iPhone에서는 페이지의 일부만 보였다.
대표적으로 다음 요소는 정상적으로 표시됐다.
- Header
- Hero의 배경 Grid
반면 실제 텍스트와 주요 콘텐츠는 보이지 않았다.
저자는 처음에 Safari의 문제라고 생각했다.
하지만 iPhone에서 Chrome으로 열어도 결과는 같았다.
이 결과는 문제가 Chrome이라는 브라우저 앱 자체와 관련되지 않을 수 있다는 점을 보여줬다.
저자가 테스트한 iPhone 환경에서는 Safari와 Chrome 모두 WebKit 기반으로 페이지를 렌더링하고 있었다.
따라서 같은 엔진을 사용하는 브라우저 두 개에서 테스트했다고 해서 완전히 독립적인 브라우저 엔진을 검증한 것은 아니다.
BrowserStack에서는 정상적으로 보였다
저자는 BrowserStack에서도 문제를 확인하려고 했다.
하지만 BrowserStack의 테스트 환경에서는 페이지가 정상적으로 표시됐다.
반면 실제 iPhone에서는 계속 문제가 발생했다.
이러한 차이는 테스트 환경의 네트워크 속도와 실제 기기의 상태, 페이지가 초기화되는 시점 등에 영향을 받을 수 있다.
하지만 저자는 BrowserStack과 실제 iPhone 사이의 차이가 발생한 정확한 이유를 확인하지 못했다고 명시했다.
따라서 테스트 서비스가 특정 원인 때문에 문제를 놓쳤다고 단정하지 않는다.
중요한 사실은 실제 사용자 기기에서 문제가 재현됐다는 점이다.
저자는 이후 iPhone에서 JavaScript Console을 확인할 수 있는 도구를 사용했다.
그리고 실제 원인을 발견했다.
콘텐츠는 DOM에 존재했지만 보이지 않았다
문제의 핵심은 다음 CSS 속성이었다.
opacity: 0
실제 콘텐츠는 DOM에 존재했다.
HTML도 정상적으로 로딩됐다.
하지만 화면에는 표시되지 않았다.
왜냐하면 애니메이션 코드가 해당 콘텐츠를 처음부터 숨김 상태로 만들었기 때문이다.
저자는 두 가지 방식을 사용하고 있었다.
첫 번째는 Motion의 Scroll Reveal이다.
두 번째는 GSAP을 이용한 Hero Animation이다.
두 방식 모두 JavaScript가 실행되기 전에 콘텐츠를 숨기는 구조였다.
Motion의 initial="hidden"
Scroll Reveal 컴포넌트에서는 다음 설정을 사용했다.
initial="hidden"
이 값은 애니메이션이 시작되기 전의 상태를 나타낸다.
저자의 Hidden Variant는 다음 속성을 포함했다.
opacity: 0y: 24
이후 요소가 화면에 나타나면 Visible 상태로 변경하도록 구성했다.
대표적인 속성은 다음과 같다.
whileInView="show"
문제는 JavaScript가 실행되기 전이다.
서버에서 생성된 HTML에는 이미 숨김 상태가 반영될 수 있었다.
따라서 브라우저가 HTML을 정상적으로 가져왔더라도 콘텐츠는 보이지 않았다.
이후 실제 콘텐츠를 표시하려면 다음 과정이 필요했다.
- JavaScript Bundle 다운로드
- JavaScript 파싱
- React Hydration
- Motion 초기화
- Intersection Observer 연결
- 애니메이션 실행
이 과정 가운데 일부가 지연되면 사용자는 빈 화면을 오래 보게 된다.
특히 JavaScript가 실행되지 않으면 콘텐츠가 계속 숨겨질 수 있다.
이 구조에서는 콘텐츠를 표시하기 위해 JavaScript의 성공적인 실행이 필수 조건이 된다.
하지만 포트폴리오 사이트의 본문 텍스트는 애니메이션이 없어도 읽을 수 있어야 한다.
GSAP의 숨김 상태
Hero Animation에서도 비슷한 문제가 존재했다.
저자는 애니메이션이 시작되기 전에 요소가 최종 위치에서 잠깐 표시되는 것을 방지하고 싶었다.
이를 위해 다음 CSS를 사용했다.
.hero-reveal { opacity: 0; }
이후 GSAP Timeline이 실행되면 해당 상태를 변경했다.
하지만 GSAP이 실행되지 않으면 콘텐츠를 표시할 다른 경로가 존재하지 않았다.
즉 CSS는 기본적으로 콘텐츠를 숨겼다.
JavaScript만 콘텐츠를 다시 표시할 수 있었다.
저자는 이를 장식적인 애니메이션이 실제 콘텐츠의 접근 가능성을 결정하는 구조였다고 설명한다.
prefers-reduced-motion의 첫 번째 실패
저자는 처음에 사용자의 움직임 감소 설정을 이용해 문제를 해결하려고 했다.
대표적인 Media Query는 다음과 같다.
prefers-reduced-motion
이 설정은 사용자가 운영체제에서 움직임을 줄이도록 설정했는지 확인하는 데 사용할 수 있다.
하지만 저자의 첫 번째 구현은 JavaScript를 통해 해당 설정을 읽었다.
따라서 JavaScript가 실행되기 전에는 여전히 콘텐츠가 숨겨져 있었다.
움직임 감소 설정이 활성화돼 있더라도 실제 코드는 JavaScript가 로딩된 이후에야 동작했다.
즉 근본적인 문제가 해결되지 않았다.
저자는 이 방식이 단순히 콘텐츠를 표시하는 조건만 변경한 것이라고 판단했다.
Hero Animation을 CSS로 이전
저자는 Hero Animation에서 GSAP을 제거했다.
대신 기본 CSS Animation을 사용했다.
애니메이션에는 다음 두 속성을 사용했다.
opacitytransform
예를 들어 콘텐츠가 아래에서 위로 조금 이동하면서 나타나는 효과를 구현할 수 있다.
애니메이션의 실행 시간은 약 0.6초로 설정했다.
그리고 여러 요소에 서로 다른 Animation Delay를 적용했다.
이렇게 하면 배지와 제목, 설명, 버튼 등이 순차적으로 나타나는 효과를 만들 수 있다.
중요한 설정은 다음과 같다.
animation-fill-mode: both
both는 Animation이 시작되기 전과 종료된 이후의 상태를 처리하는 데 사용된다.
이렇게 하면 Delay가 존재하는 동안에도 애니메이션의 초기 상태를 적용할 수 있다.
따라서 별도의 영구적인 숨김 CSS를 작성할 필요가 줄어든다.
기본 상태는 보이도록 변경
저자는 CSS Animation을 다음 Media Query 안에 배치했다.
@media (prefers-reduced-motion: no-preference)
이 설정은 사용자가 움직임 감소를 요청하지 않은 경우에만 Animation을 적용한다.
핵심은 기본 CSS에 별도의 opacity: 0을 작성하지 않는다는 점이다.
따라서 다음 두 상황에서는 콘텐츠가 그대로 보인다.
- JavaScript가 실행되지 않음
- 사용자가 Reduced Motion을 요청함
반면 일반적인 환경에서는 CSS Animation이 적용된다.
이러한 구조는 콘텐츠를 먼저 정상적으로 제공하고, 애니메이션을 추가적인 기능으로 제공하는 방식이다.
이는 Progressive Enhancement와 관련된다.
Progressive Enhancement는 기본 콘텐츠와 기능을 먼저 제공하고, 브라우저가 지원하는 추가 기능을 점진적으로 제공하는 설계 방식이다.
Scroll Reveal은 다른 방식으로 처리
Scroll Reveal은 스크롤 위치에 따라 동작하므로 저자는 JavaScript를 완전히 제거하지 않았다.
대신 초기 상태를 변경했다.
기존에는 다음 구조였다.
서버에서 Hidden 상태 렌더링 → JavaScript 실행 → Visible 상태 변경
새로운 구조는 다음과 같다.
서버에서 Visible 상태 렌더링 → Hydration 완료 → 필요한 요소에만 Hidden 상태 적용
이를 위해 다음 상태를 사용했다.
hydrated
초기값은 false다.
React의 useEffect가 실행되면 true로 변경한다.
이후 useInView 결과를 확인한다.
컴포넌트가 아직 Hydration되지 않았다면 Visible 상태를 유지한다.
컴포넌트가 화면 안에 있다면 Visible 상태를 유지한다.
반면 Hydration이 완료됐고 화면 밖에 있는 요소에만 Hidden 상태를 적용한다.
이렇게 하면 JavaScript가 아직 실행되지 않은 상태에서는 콘텐츠가 정상적으로 표시된다.
또한 이미 화면에 표시된 콘텐츠가 Hydration 직후 다시 사라지는 문제도 줄일 수 있다.
initial={false}
저자는 Motion 컴포넌트에 다음 설정도 추가했다.
initial={false}
이렇게 하면 Motion이 자체적인 초기 Hidden 상태를 적용하지 않도록 구성할 수 있다.
따라서 서버에서 표시한 콘텐츠와 클라이언트에서 Hydration하는 콘텐츠의 초기 상태가 더 일관되게 유지된다.
하지만 부모 컴포넌트의 Hidden Variant가 남아 있다면 문제가 계속 발생할 수 있다.
저자는 실제로 이 문제도 발견했다.
자식 컴포넌트의 초기 상태를 Visible로 변경했지만 부모 컨테이너가 Hidden 상태를 가지고 있었다.
따라서 자식 콘텐츠 역시 보이지 않을 수 있었다.
이를 해결하기 위해 애니메이션을 관리하던 부모를 일반적인 div로 변경했다.
그리고 각 자식에 개별적인 Animation Delay를 전달하도록 했다.
불필요한 Scroll Library 제거
저자는 이번 문제를 해결하면서 다른 성능 관련 코드도 정리했다.
대표적으로 다음 변경을 수행했다.
- Lenis를 Lazy Loading으로 변경
- 터치 기기에서는 Lenis 비활성화
- 일부 Scroll Effect를 CSS
position: sticky로 교체 - Timeline Progress는 작은 Scroll Handler로 처리
- SVG가 많은 Marquee는 Idle 이후 로딩
특히 Lenis는 Smooth Scrolling을 구현하기 위한 라이브러리다.
하지만 사용자가 실제로 스크롤하는 기기와 브라우저에 따라 기본 스크롤 동작이 충분할 수 있다.
저자는 터치 기기에서는 브라우저의 기본 스크롤을 사용하도록 변경했다.
이렇게 하면 불필요한 Animation Frame 작업을 줄일 수 있다.
다만 이번 글에는 실제 CPU 사용량이나 LCP, INP가 얼마나 개선됐는지에 관한 정량적인 Benchmark는 포함되지 않았다.
따라서 성능이 특정 비율로 개선됐다고 단정할 수 없다.
실제 문제를 발견하는 방법
저자는 다음 검사를 소개한다.
첫 번째는 JavaScript를 비활성화한 상태에서 페이지를 다시 로딩하는 것이다.
이 상태에서도 실제 콘텐츠가 읽혀야 한다.
두 번째는 브라우저의 네트워크 속도를 제한한 상태에서 초기 렌더링을 확인하는 것이다.
완전히 로딩된 화면이 아니라 처음 1~2초 동안 무엇이 표시되는지 확인해야 한다.
세 번째는 서버가 반환한 HTML을 직접 확인하는 것이다.
예를 들어 curl을 이용해 HTML을 가져올 수 있다.
그리고 실제 콘텐츠에 opacity: 0과 같은 숨김 속성이 포함돼 있는지 확인할 수 있다.
네 번째는 실제 모바일 기기에서 테스트하는 것이다.
특히 사용자의 네트워크 환경과 비슷한 조건에서 확인해야 한다.
이번 글은 React의 Hydration과 JavaScript Animation을 사용하는 과정에서 실제 콘텐츠의 표시 여부를 JavaScript의 성공적인 실행에 의존하게 만든 문제를 보여준다.
애니메이션을 추가적인 시각 효과로 취급하고, 실제 콘텐츠는 기본적으로 읽을 수 있도록 설계해야 한다는 점을 구체적인 오류와 수정 과정으로 설명한다.
Next.js 16, one year on: what broke when I upgraded a real site
- 저자: Kunj Hirapara
- 매체: DEV Community
- 게시일: 2026-10-28
- 원문: https://dev.to/kunjhirapara/nextjs-16-one-year-on-what-broke-when-i-upgraded-a-real-site-428a
실제 운영 중인 Next.js 애플리케이션을 Next.js 15에서 16으로 업데이트하면서 발생한 두 가지 Production Build 오류와 여러 API 변경을 분석한 글이다.
이번 글은 단순한 Next.js 마이그레이션 체크리스트가 아니다.
저자는 PostgreSQL과 TypeORM, 관리자 로그인, Docker 기반 배포가 포함된 실제 서비스를 운영하고 있었다.
개발 환경에서는 정상적으로 동작했지만 Production Build에서는 오류가 발생했다.
그 원인을 분석하면서 번들러와 모듈의 정체성, 동적 Import, Output Tracing 사이의 관계를 설명한다.
실제 애플리케이션 구조
저자가 운영한 서비스는 다음 기술을 사용한다.
- Next.js App Router
- PostgreSQL
- TypeORM
- Credentials 기반 관리자 로그인
- Docker
- VPS
이 애플리케이션은 일반적인 정적 사이트보다 복잡하다.
사용자에게 공개하는 페이지뿐 아니라 관리자 인증과 데이터베이스 접근 기능도 존재한다.
따라서 단순히 홈페이지가 정상적으로 표시되는지만 확인해서는 전체 서비스가 올바르게 동작하는지 판단하기 어렵다.
Turbopack이 기본 Production Bundler로 변경
Next.js 16에서는 Production Build에서 Turbopack을 기본으로 사용한다.
기존 Next.js 15의 Production Build는 webpack을 사용하던 구성이 일반적이었다.
저자는 이 차이 때문에 예상하지 못한 문제가 발생했다고 설명한다.
Turbopack은 Route별로 모듈을 별도로 번들링할 수 있다.
문제는 서로 다른 Route에서 가져온 같은 클래스가 JavaScript 실행 환경에서는 서로 다른 객체로 취급될 수 있다는 점이다.
TypeORM의 Entity Class 정체성 문제
저자의 프로젝트에서는 데이터베이스 연결을 globalThis에 저장하고 있었다.
이렇게 하면 같은 서버 프로세스에서 데이터베이스 연결을 반복해서 생성하는 문제를 줄일 수 있다.
하지만 TypeORM은 Entity Class를 식별할 때 클래스의 정체성을 사용한다.
예를 들어 다음 클래스를 생각할 수 있다.
Admin
저자는 관리자 정보를 조회할 때 다음과 같은 코드를 사용했다.
dataSource.getRepository(Admin)
문제는 Turbopack이 서로 다른 Route에 같은 Entity Class의 별도 복사본을 포함할 수 있다는 점이었다.
예를 들어 공개 페이지가 먼저 데이터베이스 연결을 생성했다고 가정해 보자.
이때 TypeORM은 공개 페이지 Bundle에 포함된 Admin Class를 등록할 수 있다.
이후 관리자 로그인 Route가 실행된다.
하지만 해당 Route에서 사용하는 Admin Class는 다른 Bundle에 포함된 별도의 객체일 수 있다.
TypeORM은 두 객체가 같은 클래스를 나타낸다는 사실을 자동으로 인식하지 못할 수 있다.
그 결과 다음 오류가 발생했다.
No metadata for "Admin" was found
즉 소스코드에는 동일한 클래스가 존재하지만 실행 환경에서는 서로 다른 클래스 객체로 취급된 것이다.
Entity 이름으로 Repository 조회
저자는 이 문제를 해결하기 위해 Class Reference 대신 Entity 이름을 사용했다.
기존 방식은 다음과 같다.
dataSource.getRepository(Admin)
변경한 방식은 다음과 같다.
dataSource.getRepository<Admin>("Admin")
이렇게 하면 Route마다 다른 Class 객체를 사용하더라도 Entity 이름을 기준으로 Repository를 찾을 수 있다.
하지만 이 변경이 모든 TypeORM 프로젝트에 반드시 필요한 것은 아니다.
실제 문제가 발생하는지는 데이터베이스 연결과 Entity 등록 방식, 번들러가 모듈을 구성하는 방식에 따라 달라진다.
중요한 것은 같은 소스코드에서 Import한 Class라도 서로 다른 Bundle의 실행 문맥에서는 객체 정체성이 달라질 수 있다는 사실이다.
특히 모듈 수준의 Singleton과 Class Reference에 의존하는 애플리케이션에서 관련 문제가 발생할 수 있다.
개발 서버에서는 발견하지 못한 이유
저자의 개발 환경에서는 관리자 로그인이 정상적으로 동작했다.
하지만 Production Build에서는 오류가 발생했다.
개발 서버와 Production Bundler가 모듈을 구성하는 방식이 달랐기 때문이다.
따라서 next dev에서 정상적으로 동작한다는 사실만으로 next build 이후에도 동일한 동작이 보장된다고 볼 수 없다.
특히 다음 기능을 별도로 확인할 필요가 있다는 점을 보여준다.
- 관리자 로그인
- 인증이 필요한 페이지
- 데이터베이스 연결
- Route 간 공유 상태
- 서버 전용 모듈
Docker Standalone Build의 동적 의존성 문제
두 번째 문제는 TypeORM 업데이트와 함께 발생했다.
저자는 Next.js의 다음 설정을 사용하고 있었다.
output: "standalone"
Standalone Build는 실제 서버 실행에 필요한 파일을 추적해 별도의 출력 디렉터리로 복사하는 방식이다.
대표적인 출력 경로는 다음과 같다.
.next/standalone
이러한 구조는 Docker Image에 불필요한 파일을 포함하지 않도록 하는 데 도움이 된다.
하지만 필요한 의존성을 정확하게 추적해야 한다.
문제는 TypeORM 1.x의 데이터베이스 Driver 로딩 방식이었다.
TypeORM은 다음과 같은 동적 Require를 사용했다.
require(name)
여기서 name은 실행 시점에 결정되는 값이다.
따라서 정적 분석 도구가 실제로 어떤 패키지를 가져와야 하는지 파악하기 어려울 수 있다.
저자의 프로젝트에서는 PostgreSQL Driver인 pg가 Standalone Build에서 누락됐다.
그 결과 데이터베이스를 사용하는 Route에서 다음 오류가 발생했다.
DriverPackageNotInstalledError
Driver를 명시적으로 전달
저자는 이 문제를 해결하기 위해 PostgreSQL Driver를 명시적으로 Import했다.
import pg from "pg"
이후 TypeORM의 DataSource 설정에 Driver를 직접 전달했다.
이렇게 하면 Output Tracing이 실제 의존성을 확인할 수 있다.
따라서 Standalone Build에 필요한 Driver가 포함될 수 있다.
이번 문제는 Next.js 16 자체의 버그라기보다 TypeORM의 동적 모듈 로딩 방식과 Next.js의 파일 추적 방식이 결합하면서 발생한 호환성 문제였다.
저자는 두 라이브러리를 같은 시점에 업데이트했지만 서로 다른 Commit으로 변경해 원인을 추적할 수 있었다고 설명한다.
실제 Docker Image의 Smoke Test
저자는 CI에서 실제 Docker Image를 생성한 뒤 실행하는 Smoke Test를 사용하고 있었다.
Smoke Test는 핵심 기능이 정상적으로 실행되는지 간단하게 확인하는 테스트다.
예를 들어 다음 작업을 수행할 수 있다.
- Production Build 생성
- Docker Image 생성
- 실제 Container 실행
- 핵심 Route에 요청
- 정상 응답 확인
이번 TypeORM Driver 누락 문제도 이 테스트에서 발견됐다.
따라서 잘못된 Image가 실제 사용자에게 배포되는 것을 방지할 수 있었다.
이는 개발 환경에서 테스트가 통과했다는 사실과 실제 배포 결과물이 정상적으로 실행된다는 사실을 구분해야 한다는 점을 보여준다.
Middleware에서 Proxy로 변경
Next.js 16에서는 기존 middleware.ts가 proxy.ts로 변경됐다.
Export하는 함수의 이름도 이에 맞게 변경할 수 있다.
하지만 단순한 파일 이름 변경만은 아니다.
저자는 Proxy가 Node.js Runtime에서 실행된다는 점을 강조한다.
기존에 Edge Runtime의 동작을 전제로 Middleware를 구성했다면 해당 차이를 확인해야 한다.
저자의 프로젝트에서는 관리자 페이지의 접근을 제한하는 데 Proxy를 사용했다.
하지만 Proxy만을 유일한 보안 경계로 사용하지는 않았다.
각 관리자 페이지와 Server Action에서도 실제 Session을 다시 확인했다.
이렇게 하면 Proxy 설정이 변경되거나 특정 경로에서 예상하지 못한 동작이 발생하더라도 중요한 작업을 처리하는 서버에서 권한을 검증할 수 있다.
next lint 제거
Next.js 16에서는 기존 next lint 명령도 제거됐다.
개발자는 ESLint CLI를 직접 사용할 수 있다.
저자의 프로젝트에서는 Flat Config를 사용했다.
관련 파일은 다음과 같다.
eslint.config.mjs
그런데 기존 Next.js에서 자동으로 처리하던 Lint 대상과 새로운 ESLint 설정이 검사하는 대상이 달라졌다.
그 결과 이전에는 검사하지 않던 도구 디렉터리까지 Lint 대상에 포함됐다.
저자는 필요한 Ignore 설정을 추가했다.
이러한 문제는 새로운 Lint 버전을 설치했다는 사실보다 실제로 어떤 파일을 검사하는지가 중요하다는 점을 보여준다.
React Compiler
Next.js 16에서는 React Compiler의 지원도 안정화됐다.
하지만 저자는 이를 활성화하지 않았다.
React Compiler는 컴포넌트와 값을 자동으로 Memoization해 불필요한 계산과 렌더링을 줄이려는 기능이다.
하지만 Compiler를 활성화하면 빌드 과정에 추가적인 작업이 발생할 수 있다.
저자는 현재 프로젝트에서 기존 구현이 충분히 정상적으로 동작한다고 판단했다.
따라서 React Compiler를 즉시 활성화하지 않고 이후 별도로 검증하기로 결정했다.
이는 새로운 기능이 안정화됐다는 사실과 실제 프로젝트에서 즉시 활성화해야 한다는 판단을 구분한 것이다.
적용 범위와 한계
이번 글은 하나의 실제 Next.js 프로젝트에 대한 개발 기록이다.
따라서 모든 Next.js 16 프로젝트에서 TypeORM 오류나 Standalone Driver 누락 문제가 발생한다는 의미는 아니다.
실제 문제는 다음 조건에 영향을 받는다.
- TypeORM 사용 여부
- Entity 등록 방식
- 데이터베이스 연결의 공유 구조
- 동적 Require 사용
- Standalone Build 사용 여부
- Production Bundler 구성
저자는 성능 개선을 정량적으로 비교하지 않았다.
핵심은 마이그레이션 전후의 처리 속도보다 실제 운영 환경에서 발견한 호환성 문제와 해결 방법이다.
이번 글은 React와 Next.js 애플리케이션을 업데이트할 때 개발 서버의 동작뿐 아니라 실제 Production Bundle과 Docker Image의 동작까지 확인해야 한다는 점을 구체적인 오류 사례로 보여준다.
How I built TestMQTT: a browser MQTT client that can still reach TCP brokers
- 저자: TestMQTT 개발자
- 매체: DEV Community
- 게시일: 2026-10-29
- 원문: https://dev.to/testmqtt/how-i-built-testmqtt-a-browser-mqtt-client-that-can-still-reach-tcp-brokers-acd
브라우저에서 WebSocket을 지원하는 MQTT Broker뿐 아니라 일반 TCP 및 TLS 기반 MQTT Broker에도 연결할 수 있는 웹 애플리케이션을 구현한 과정을 설명한 글이다.
이번 글의 특징은 브라우저의 네트워크 API가 가진 제한을 해결하기 위해 서버 측 Bridge를 구성하면서 보안과 운영상의 트레이드오프를 함께 설명한다는 점이다.
저자가 개발한 TestMQTT는 브라우저에서 MQTT 연결을 검사하기 위한 도구다.
사용자는 별도의 데스크톱 프로그램을 설치하지 않고 MQTT Broker에 연결할 수 있다.
이후 Topic을 구독하고 메시지를 확인하거나 발행할 수 있다.
하지만 일반적인 MQTT Broker는 브라우저에서 직접 연결할 수 없는 TCP Port를 사용할 수 있다.
이 문제가 이번 구현의 출발점이다.
브라우저는 일반 TCP Socket을 직접 열 수 없다
MQTT는 경량 메시지 전달 프로토콜이다.
IoT 기기와 센서, 실시간 데이터 전달 등에 사용된다.
일반적인 MQTT Broker는 다음 Port를 사용할 수 있다.
| Port | 전송 방식 |
|---|---|
| 1883 | MQTT over TCP |
| 8883 | MQTT over TLS |
| 8083 또는 80 | MQTT over WebSocket |
| 8084 또는 443 | MQTT over Secure WebSocket |
브라우저에서는 WebSocket API를 사용할 수 있다.
따라서 WebSocket 기반 MQTT Broker에는 직접 연결할 수 있다.
하지만 일반적인 웹페이지의 JavaScript는 임의의 TCP Socket을 직접 열 수 없다.
예를 들어 브라우저에서 다음 Broker에 직접 TCP 연결을 생성하는 방식은 사용할 수 없다.
mqtt://broker.example.com:1883
따라서 일반 TCP Broker에 연결하려면 별도의 서버 측 구성요소가 필요하다.
저자는 이를 두 가지 경로로 분리했다.
WebSocket 경로
첫 번째는 Broker가 WebSocket을 지원하는 경우다.
이때는 브라우저가 MQTT.js를 이용해 직접 연결한다.
지원하는 URL Scheme은 다음과 같다.
ws://wss://
MQTT.js는 MQTT 프로토콜을 처리하기 위한 JavaScript 라이브러리다.
저자는 이 라이브러리를 Dynamic Import로 로딩했다.
대표적인 형태는 다음과 같다.
import("mqtt")
이렇게 하면 모든 페이지에서 MQTT.js를 초기 JavaScript Bundle에 포함할 필요가 없다.
예를 들어 마케팅 페이지나 사용 방법을 설명하는 페이지에서는 MQTT 연결 기능을 사용하지 않을 수 있다.
따라서 실제 MQTT Client가 필요한 화면에서만 라이브러리를 로딩할 수 있다.
이러한 방식은 Route별 Code Splitting과 연결된다.
TCP Broker를 위한 두 번째 경로
두 번째는 일반 TCP 또는 TLS 기반 Broker에 연결하는 경우다.
지원하는 URL Scheme은 다음과 같다.
mqtt://mqtts://
브라우저는 해당 Broker에 직접 연결하지 못한다.
따라서 서버 측 Bridge를 사용한다.
전체 구조는 다음과 같다.
브라우저 → HTTPS → TestMQTT Bridge → TCP 또는 TLS → MQTT Broker
이때 중요한 선택지가 존재한다.
Bridge가 단순히 TCP 데이터를 전달하는 Byte Relay가 될 수도 있다.
또는 Bridge가 실제 MQTT Client를 실행할 수도 있다.
저자는 두 방법을 비교했다.
첫 번째 구현 후보: Byte Relay
가장 단순한 방식은 WebSocket 서버를 생성하는 것이다.
브라우저는 해당 WebSocket에 연결한다.
서버는 실제 TCP Broker에 연결한다.
이후 두 연결 사이에서 데이터를 그대로 전달한다.
예를 들어 다음 흐름이다.
브라우저 WebSocket → Bridge → Broker TCP
이 구조는 구현이 비교적 간단하다.
저자는 약 30줄의 코드로 기본적인 연결을 구성할 수 있다고 설명한다.
하지만 공개 서비스에서는 심각한 문제가 발생할 수 있다.
서버가 임의의 TCP 연결을 전달하는 Open Relay로 악용될 가능성이 있기 때문이다.
예를 들어 사용자가 임의의 Host와 Port를 전달할 수 있다고 가정해 보자.
Bridge가 해당 주소에 무조건 연결하면 외부 사용자가 서버를 이용해 여러 네트워크 대상으로 요청을 전송하려고 시도할 수 있다.
또한 단순 Byte Relay는 실제 MQTT 메시지를 해석하지 않는다.
따라서 다음 정책을 적용하기 어려울 수 있다.
- MQTT 프로토콜만 허용
- Session별 요청 제한
- 메시지 크기 제한
- 연결 시간 제한
- 잘못된 프로토콜 데이터 차단
저자는 이러한 이유로 단순한 Byte Relay를 최종 구조로 선택하지 않았다.
서버 자체를 MQTT Client로 사용
최종 구현에서는 Bridge 서버가 실제 MQTT Session을 생성하고 관리한다.
브라우저는 MQTT 프로토콜의 원본 Byte를 그대로 전달하지 않는다.
대신 HTTP API로 필요한 작업을 요청한다.
대표적인 API는 다음과 같다.
| 작업 | HTTP API |
|---|---|
| 새로운 Session 생성 | POST /api/bridge |
| 이벤트 수신 | GET /api/bridge/{id} |
| Topic 구독 | POST /api/bridge/{id} |
| 메시지 발행 | POST /api/bridge/{id} |
| 연결 종료 | POST /api/bridge/{id} |
이 구조에서는 서버가 실제 MQTT Client 역할을 수행한다.
따라서 브라우저는 HTTP 요청을 이용해 Broker를 제어할 수 있다.
예를 들어 Topic을 구독하려면 Session ID와 Topic 정보를 전달한다.
서버는 해당 Session의 MQTT Client에서 구독 작업을 수행한다.
이후 Broker에서 메시지가 도착하면 브라우저에 이벤트를 전달한다.
SSE를 이용한 이벤트 전달
서버에서 브라우저로 전달하는 이벤트에는 SSE를 사용한다.
SSE(Server-Sent Events)는 서버에서 클라이언트로 이벤트를 지속적으로 전달하는 기술이다.
브라우저에서는 다음 API를 사용할 수 있다.
EventSource
저자는 SSE가 일반적인 HTTP 기반이라는 점을 활용했다.
일부 서버와 Proxy 환경에서는 WebSocket Upgrade를 처리하기 위해 별도의 설정이 필요할 수 있다.
반면 SSE는 일반적인 HTTP Response Stream을 이용하므로 기존 HTTP 인프라와 연결하기 쉬운 경우가 있다.
또한 EventSource는 연결이 끊어졌을 때 자동으로 재연결을 시도하는 기능을 제공한다.
하지만 SSE는 기본적으로 서버에서 클라이언트로 전달하는 단방향 통신이다.
따라서 브라우저에서 서버로 전달하는 작업에는 별도의 HTTP POST 요청을 사용한다.
이렇게 해서 다음 두 방향을 분리했다.
- 서버 → 브라우저: SSE
- 브라우저 → 서버: REST API
두 실행 경로의 인터페이스 통일
이번 글에서 프론트엔드 아키텍처와 직접 관련되는 부분은 WebSocket 기반 Client와 HTTP Bridge 기반 Client가 동일한 인터페이스를 제공하도록 설계했다는 점이다.
예를 들어 다음 메서드를 제공한다.
onsubscribeAsyncpublishAsyncend
브라우저가 WebSocket Broker에 연결한 경우에는 MQTT.js가 실제 작업을 수행한다.
반면 TCP Broker에 연결한 경우에는 BridgeClient가 HTTP 요청을 수행한다.
하지만 UI 컴포넌트는 두 방식을 구분할 필요가 없다.
예를 들어 Topic Tree 컴포넌트는 현재 연결 방식이 WebSocket인지 HTTP Bridge인지 알 필요가 없다.
단순히 새로운 MQTT 메시지가 도착했다는 이벤트를 받으면 된다.
이러한 구조는 Adapter Pattern과 연결된다.
서로 다른 내부 구현을 공통 인터페이스로 감싸면 상위 UI에서 실행 방식의 차이를 처리할 필요가 줄어든다.
Binary Payload 처리
MQTT 메시지는 일반적인 텍스트만 포함하지 않는다.
임의의 Byte 데이터를 포함할 수 있다.
하지만 HTTP Bridge의 제어 요청은 JSON을 사용한다.
JSON은 일반적인 바이너리 데이터를 그대로 표현하기 어렵다.
따라서 저자는 Binary Payload를 Base64로 인코딩해 전달했다.
이렇게 하면 Byte 데이터를 문자열로 표현할 수 있다.
서버는 해당 문자열을 다시 Byte 데이터로 변환해 MQTT Broker에 전달할 수 있다.
다만 Base64는 암호화가 아니다.
따라서 Payload를 숨기거나 보호하는 기능으로 취급해서는 안 된다.
또한 Base64는 원본 데이터보다 전송 크기가 증가할 수 있다.
Payload Inspector
저자는 MQTT 메시지의 내용을 검사하는 기능도 구현했다.
입력된 Payload를 다음 순서로 분석한다.
- UTF-8로 해석 가능한지 확인
- 숫자로 표현할 수 있는지 확인
- JSON 형식인지 확인
- 일반 텍스트인지 확인
- 어떤 형식에도 해당하지 않으면 Binary로 표시
예를 들어 센서에서 다음 메시지가 도착했다고 가정해 보자.
24.5
Inspector는 이를 숫자로 표시할 수 있다.
반면 JSON 형식의 메시지가 도착하면 객체의 필드를 탐색할 수 있도록 표현한다.
일반적인 문자열이라면 Text로 표시한다.
UTF-8로 해석할 수 없는 데이터는 Hex 형식으로 확인할 수 있다.
하지만 이러한 형식 판별은 입력 데이터의 실제 의미를 완벽하게 이해하는 것은 아니다.
예를 들어 숫자로만 이루어진 문자열이 실제로는 식별자일 수도 있다.
따라서 Inspector의 자동 판별 결과를 실제 데이터 Schema의 검증으로 사용해서는 안 된다.
Topic Tree
MQTT Topic은 /로 구분된 계층적인 이름을 사용할 수 있다.
예를 들어 다음 Topic이 존재할 수 있다.
sensors/room1/temperature
sensors/room1/humidity
sensors/room2/temperature
저자는 각 Topic을 /로 분리한다.
그리고 Tree 구조로 저장한다.
각 Node에는 다음 정보를 유지한다.
- 마지막 Payload
- 메시지 수
이렇게 하면 사용자는 Topic의 계층 구조를 탐색할 수 있다.
예를 들어 sensors 아래에 room1과 room2가 존재한다는 사실을 확인할 수 있다.
또한 숫자형 메시지에는 최근 값을 기록해 작은 Sparkline을 표시한다.
Sparkline은 데이터의 변화를 작은 선 그래프로 나타내는 방식이다.
빈번한 메시지에서 UI 갱신 제한
MQTT Broker에서는 매우 많은 메시지가 짧은 시간에 발생할 수 있다.
예를 들어 센서가 100ms마다 새로운 값을 전송한다고 가정해 보자.
여러 Topic을 동시에 구독하면 브라우저는 많은 메시지를 처리해야 한다.
이때 모든 메시지가 도착할 때마다 React 컴포넌트 전체를 다시 렌더링하면 성능 문제가 발생할 수 있다.
저자는 이를 줄이기 위해 메시지의 실제 개수는 별도로 관리하고 초당 메시지 수 같은 통계는 Timer를 이용해 계산했다.
즉 매 메시지마다 모든 통계를 새로 계산하지 않는다.
이러한 방식은 고빈도 이벤트 처리와 UI 렌더링의 갱신 주기를 분리하는 사례다.
인증 정보가 Bridge 서버를 통과하는 문제
이번 구현에는 중요한 보안상 트레이드오프가 존재한다.
WebSocket 경로에서는 브라우저가 Broker에 직접 연결한다.
반면 TCP Bridge 경로에서는 실제 MQTT 연결을 Bridge 서버가 유지한다.
따라서 다음 정보가 Bridge 서버를 통과할 수 있다.
- MQTT Username
- MQTT Password
- 메시지 Payload
저자는 해당 정보를 디스크나 데이터베이스, 로그에 저장하지 않는다고 설명한다.
Connection이 유지되는 동안 메모리에만 저장한다는 정책이다.
하지만 서버에서 데이터를 저장하지 않는다는 사실과 서버가 데이터를 볼 수 없다는 사실은 다르다.
실제로 Bridge 서버는 MQTT Broker에 연결하기 위해 Credential을 사용해야 한다.
따라서 사용자가 중간 서버를 신뢰하지 않는다면 WebSocket을 이용해 Broker에 직접 연결하는 편이 적합할 수 있다.
저자는 이러한 제한을 명시적으로 설명한다.
이번 글은 정량적인 성능 Benchmark를 제공하지 않는다.
하지만 브라우저에서 지원하지 않는 네트워크 프로토콜을 서버 측 Bridge로 연결할 때 발생하는 보안과 API 설계, 프론트엔드 추상화 문제를 실제 구현으로 설명한다는 점에서 참고할 만하다.
Four Ways a Test Can Lie to You
- 저자: Ken W Alger
- 매체: DEV Community
- 게시일: 2026-10-28
- 원문: https://dev.to/kenwalger/four-ways-a-test-can-lie-to-you-2ibf
AI 코딩 에이전트가 대부분의 코드를 작성한 실제 애플리케이션에서 약 530개의 테스트가 존재했음에도 중요한 버그를 발견하지 못했던 이유를 분석한 기술 글이다.
이번 글의 핵심은 테스트의 개수가 많다는 사실과 실제 테스트가 유효하다는 사실이 서로 다르다는 점이다.
저자는 약 2주 동안 와인 저장고 관리 애플리케이션을 개발했다.
이 애플리케이션에서는 특정 날짜의 와인 보관 상태와 관련 정보를 확인할 수 있다.
또한 AI 에이전트가 새로운 정보를 제안할 수 있지만 실제 데이터에 반영하려면 사람의 승인이 필요하다.
따라서 다음과 같은 규칙이 중요하다.
AI가 제안한 정보는 사람이 승인하기 전까지 실제 결과에 영향을 주지 않아야 한다.
저자는 이러한 규칙을 검사하는 테스트를 작성했다.
테스트는 통과했다.
하지만 실제로는 해당 규칙을 제거해도 테스트가 계속 통과하는 문제가 존재했다.
네 가지 잘못된 검증
저자는 테스트가 잘못된 확신을 제공하는 상황을 네 가지로 구분한다.
| 유형 | 문제 |
|---|---|
| 절대 실패하지 않는 테스트 | 검사하려는 규칙을 제거해도 통과 |
| 항상 실패하는 검사 | 정상적인 입력에서도 계속 실패 |
| 잘못된 요구사항을 정확하게 검사 | 테스트 자체는 정상적이지만 Contract가 잘못됨 |
| 다른 실행 환경을 검사 | 올바른 테스트를 잘못된 데이터나 시스템에서 실행 |
이러한 구분은 단순히 Assertion이 부족하다는 문제보다 더 구체적이다.
저자는 실험에서 사용하는 Positive Control과 Negative Control의 개념으로 설명한다.
Positive Control은 실제 문제가 존재할 때 검사가 이를 발견할 수 있는지 확인하는 방법이다.
Negative Control은 문제가 존재하지 않을 때 검사가 잘못된 경고를 발생시키지 않는지 확인하는 방법이다.
첫 번째: 실패할 수 없는 테스트
저자의 애플리케이션에서는 여러 정보를 바탕으로 와인의 상태를 계산한다.
각 정보에는 다음 속성이 포함될 수 있다.
- 정보의 출처
- 신뢰 수준
- 날짜
- 승인 상태
AI가 제안한 정보는 승인되기 전까지 실제 결과에 영향을 주지 않아야 한다.
저자는 이를 검증하기 위해 제안된 정보를 추가한 뒤 전체 데이터의 결과가 변경되지 않았는지 확인했다.
총 542개 와인의 상태가 기존 결과와 완전히 동일했다.
따라서 테스트는 통과했다.
하지만 실제로는 Fixture가 잘못 구성돼 있었다.
기존 정보는 2024년의 데이터였다.
새롭게 제안한 정보는 2019년의 데이터였다.
두 정보는 같은 신뢰 수준을 가지고 있었다.
애플리케이션은 같은 신뢰 수준의 정보가 여러 개 존재하면 더 최근의 정보를 선택했다.
따라서 2019년의 정보는 승인 여부와 관계없이 결과에 영향을 줄 수 없었다.
즉 실제 승인 상태를 검사하지 않고 있었던 것이다.
규칙을 제거해도 테스트가 통과
저자는 승인 상태를 확인하는 실제 코드를 제거했다.
하지만 테스트는 계속 통과했다.
왜냐하면 제안된 정보가 승인되지 않았기 때문이 아니라 오래된 정보였기 때문에 무시됐기 때문이다.
이러한 테스트는 실제로는 중요한 규칙을 검사하지 못한다.
저자는 이를 해결하기 위해 두 가지 Assertion을 함께 사용했다.
첫 번째는 제안 상태의 정보가 결과를 변경하지 않는지 확인하는 것이다.
두 번째는 같은 정보를 승인하면 실제 결과가 변경되는지 확인하는 것이다.
이렇게 하면 테스트가 승인 상태에 실제로 반응하는지 검증할 수 있다.
저자는 다른 테스트에서도 같은 종류의 문제를 발견했다.
그리고 실제 규칙을 제거했을 때 테스트가 실패하는지 확인했다.
이러한 방식은 Mutation Testing과 연결된다.
두 번째: 항상 실패하는 보안 검사
저자는 정적 빌드 결과물에 API Credential이 포함되지 않았는지 확인하는 검사를 작성했다.
검사는 정규식을 사용했다.
대표적인 패턴은 다음과 같았다.
sk[A-Za-z0-9]{20,}
이 패턴은 sk로 시작하고 이후 영문자와 숫자가 20개 이상 이어지는 문자열을 찾는다.
문제는 정상적인 빌드 결과물에도 해당 패턴에 일치하는 문자열이 존재했다는 점이다.
대표적인 예는 다음 변수 이름이었다.
skipCrossDatasetReferenceValidation
이 문자열은 실제 API Key가 아니다.
하지만 정규식에는 일치했다.
따라서 검사 결과는 항상 실패했다.
저자는 이러한 검사가 실제 운영에서는 오히려 위험할 수 있다고 설명한다.
정상적인 코드에서도 검사가 계속 실패하면 개발자는 해당 오류를 무시하는 데 익숙해질 수 있다.
그 결과 실제 인증 정보가 노출됐더라도 기존의 False Positive와 구분하지 못할 수 있다.
저자는 검사 패턴의 최소 길이를 조정했다.
그리고 정상적인 빌드에서는 경고가 발생하지 않도록 수정했다.
하지만 패턴을 수정했다고 해서 모든 인증 정보를 탐지하는 것은 아니다.
실제 Secret의 종류에 따라 형식이 다르기 때문이다.
따라서 정규식 검사는 알려진 패턴에 관한 제한적인 검사로 이해해야 한다.
세 번째: 올바른 테스트, 잘못된 요구사항
세 번째 문제는 HTML Date Input에서 발생했다.
저자의 애플리케이션에는 날짜를 입력하는 기능이 존재했다.
허용하는 날짜 범위는 다음과 같았다.
- 1996년
- 2042년
입력한 날짜가 이 범위를 벗어나면 가장 가까운 허용 날짜로 변경하도록 구현했다.
겉으로 보기에는 합리적인 기능이다.
하지만 실제 브라우저에서 날짜를 직접 입력할 때 문제가 발생했다.
예를 들어 사용자가 2023년을 입력하려고 한다고 가정해 보자.
브라우저는 입력 중간에 다음과 같은 값을 생성할 수 있다.
0002-12-310020-12-310202-12-312023-12-31
처음 세 값은 실제로 허용된 날짜 범위를 벗어난다.
애플리케이션은 이를 잘못된 날짜로 판단해 1996년으로 변경했다.
그 결과 사용자는 2023년을 끝까지 입력할 수 없었다.
테스트는 실제 구현을 정확하게 검사했다
문제는 기존 테스트가 정상적으로 동작했다는 사실이다.
테스트는 허용 범위보다 오래된 날짜가 전달되면 1996년으로 변경되는지 확인했다.
그리고 실제 구현은 해당 규칙을 충족했다.
따라서 테스트는 통과했다.
하지만 요구사항 자체에 중요한 조건이 누락돼 있었다.
사용자가 날짜 입력을 완료한 상태와 아직 입력 중인 상태는 서로 다르다.
날짜를 모두 입력한 이후에는 범위 제한을 적용하는 것이 적절할 수 있다.
반면 사용자가 연도를 입력하는 도중에 같은 제한을 적용하면 정상적인 입력을 방해할 수 있다.
이는 테스트 구현의 문제가 아니라 실제 사용자 동작을 충분히 반영하지 못한 요구사항의 문제다.
저자는 이를 Correct Test of a Wrong Contract라고 설명한다.
즉 잘못 정의된 규칙을 테스트가 정확하게 검증한 것이다.
네 번째: 잘못된 데이터 환경에서 실행한 테스트
네 번째 문제는 Staging과 Production의 데이터 연결에서 발생했다.
저자는 실제 운영 데이터를 변경하지 않기 위해 별도의 Staging 데이터셋을 준비했다.
그리고 애플리케이션과 데이터 작성 도구를 모두 Staging에 연결하려고 했다.
환경 변수로 데이터셋을 선택하도록 구성했다.
하지만 두 프로그램의 동작이 달랐다.
데이터 작성 도구는 환경 변수를 정상적으로 사용했다.
반면 실제 웹 애플리케이션은 해당 환경 변수를 무시했다.
그 결과 애플리케이션은 Production 데이터셋을 계속 사용했다.
문제는 빌드와 테스트가 모두 정상적으로 완료됐다는 점이다.
실제로 데이터가 존재했기 때문이다.
따라서 화면에 표시되는 값도 정상적인 것처럼 보였다.
하지만 실제로는 잘못된 데이터셋을 검사하고 있었다.
저자는 이 문제를 해결하기 위해 데이터셋 이름을 실행 설정에서 명확하게 관리하도록 변경했다.
또한 Production이 아닌 데이터셋을 사용하는 경우 화면에 해당 이름을 표시하도록 했다.
이렇게 하면 개발자는 현재 어떤 데이터를 보고 있는지 확인할 수 있다.
Property-based Test가 발견한 버그
저자는 날짜 계산 기능에서도 실제 버그를 발견했다.
애플리케이션은 특정 기간 사이의 날짜 차이를 계산했다.
하지만 1월 31일과 3월 1일 사이의 계산에서 오류가 발생했다.
기존 테스트에는 미리 선택한 다섯 개의 날짜가 포함돼 있었다.
이 다섯 개의 날짜에서는 모두 정상적으로 동작했다.
하지만 실제 날짜 전체를 검사하면 다른 결과가 나타날 수 있었다.
저자는 이를 해결하기 위해 Property-based Test와 유사한 검증을 수행했다.
날짜 Slider가 가질 수 있는 17,167개의 위치를 검사했다.
그리고 모든 위치에서 성립해야 하는 계산 규칙을 확인했다.
이 과정에서 기존 예제 테스트가 발견하지 못한 오류를 찾아냈다.
이는 미리 선택한 몇 개의 성공 사례가 전체 입력 공간의 정확성을 보장하지 않는다는 점을 보여준다.
성능 최적화도 실제로 측정
저자는 Memoization과 관련된 성능 문제도 발견했다.
Memoization은 이전 계산 결과를 저장해 같은 작업을 반복하지 않도록 하는 최적화다.
하지만 어떤 작업을 Memoization해야 하는지 잘못 판단할 수 있다.
저자의 프로젝트에서는 인덱스를 다시 생성하는 작업이 약 6ms 걸렸다.
반면 해당 인덱스를 통해 얻으려던 계산 결과를 직접 다시 계산하는 작업은 약 0.07ms 걸렸다.
즉 인덱스를 생성하는 비용이 실제 계산보다 훨씬 컸다.
하지만 기존 코드는 인덱스를 재사용하도록 최적화돼 있었다.
저자는 실제 Benchmark를 수행한 이후 이러한 차이를 발견했다.
이 사례는 코드가 복잡하다는 이유만으로 해당 작업이 느릴 것이라고 판단해서는 안 된다는 점을 보여준다.
실제 실행 시간을 측정해야 한다.
AI가 작성한 테스트의 한계
저자의 프로젝트에서는 AI 에이전트가 대부분의 코드와 테스트를 작성했다.
하지만 코드와 테스트가 같은 요구사항을 바탕으로 생성되면 동일한 잘못된 가정을 공유할 수 있다.
예를 들어 요구사항에 입력 도중의 상태를 구분해야 한다는 내용이 없다면 AI는 해당 조건을 구현하지 않을 수 있다.
테스트 역시 같은 조건을 검사하지 않을 수 있다.
그 결과 코드와 테스트는 서로 일치한다.
하지만 실제 사용자에게는 문제가 발생한다.
저자는 이러한 문제를 줄이기 위해 중요한 규칙에 별도의 검증 자료를 제공했다.
또한 테스트가 실제로 문제를 발견할 수 있는지 확인하기 위해 관련 코드를 의도적으로 변경했다.
검증 결과의 세 가지 상태
이번 글에서 중요한 제안은 검증 결과를 다음 세 상태로 구분하는 것이다.
- Pass
- Fail
- Not Verified
예를 들어 AI 에이전트가 실제 브라우저를 사용할 수 없는 환경에서 UI를 구현했다고 가정해 보자.
코드를 작성했다는 사실만으로 실제 화면이 정상적으로 표시된다고 말해서는 안 된다.
브라우저에서 실행하지 않았다면 검증하지 않은 상태다.
따라서 에이전트는 실제로 수행한 검사와 수행하지 못한 검사를 구분해야 한다.
저자는 이러한 상태를 명시적으로 기록하도록 했다.
이번 글은 AI 코딩 에이전트가 많은 테스트를 작성하더라도 해당 테스트가 실제 오류를 발견할 수 있는지 독립적으로 검증해야 한다는 점을 구체적인 실패 사례와 수치로 설명한다.
특히 테스트 자체의 품질과 실제 실행 환경의 정확성을 검토하는 데 참고할 만하다.