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

기준 시점: 2026-10-31 06:00 KST
조사 범위: 전일 2026-10-30 06:00 ~ 당일 2026-10-31 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
Vue 3.6.0-rc.14 공개 — Vapor Mode의 SSR Hydration 오류 복구와 컴파일러 최적화 확대
- 발표일: 2026-10-30
- 릴리스 시각: GitHub 표시 기준 08:01 UTC, 17:01 KST
- 버전: Vue 3.6.0-rc.14
- 안정화 단계: Release Candidate, 정식 안정 버전 아님
- 적용 대상: Vue Vapor Mode, Vapor Compiler, Vapor Runtime
- 원문: https://github.com/vuejs/core/releases/tag/v3.6.0-rc.14
- 상세 변경 기록: https://github.com/vuejs/core/blob/minor/CHANGELOG.md
Vue가 3.6의 새로운 Release Candidate인 3.6.0-rc.14를 공개했다.
이번 버전은 새로운 컴포넌트 문법이나 대규모 사용자 API를 도입하는 릴리스가 아니다. 주요 변경은 Vapor Mode에서 발견된 Hydration 문제의 수정과 컴파일러의 내부 처리 효율 개선에 집중돼 있다.
특히 서버에서 생성한 HTML과 클라이언트의 렌더링 결과가 다를 때 화면을 정상적으로 복구하는 작업이 여러 건 포함됐다.
Vapor Mode와 기존 Vue 렌더링의 차이
Vue는 기존에 Virtual DOM 기반 렌더링 방식을 사용해 왔다.
개발자는 Template에 UI 구조를 선언하고, Vue는 해당 Template을 렌더링 함수로 컴파일한다.
상태가 변경되면 Vue는 필요한 UI 변경을 계산해 실제 DOM에 반영한다.
Vapor Mode는 다른 접근을 사용한다.
컴파일 단계에서 상태와 DOM의 관계를 분석하고, 실행 중에는 필요한 DOM 요소를 직접 변경하도록 코드를 생성한다.
예를 들어 하나의 반응형 값이 특정 텍스트에만 영향을 준다고 가정해 보자.
Vapor Compiler는 해당 텍스트가 어떤 상태에 의존하는지 미리 분석할 수 있다.
이후 상태가 변경되면 관련 텍스트를 직접 갱신하도록 실행 코드를 생성한다.
이러한 방식은 중간 렌더링 구조를 생성하고 비교하는 작업을 줄이려는 목적을 가진다.
하지만 컴파일러가 더 많은 책임을 담당하는 만큼 Template의 다양한 표현을 정확하게 처리해야 한다.
이번 Release Candidate에서는 이러한 정확성과 관련된 수정이 이루어졌다.
SSR Hydration의 Root 불일치 복구
이번 변경 가운데 중요한 부분은 다음 항목이다.
runtime-vapor: finish a block as a client render after a hydration root mismatch
Hydration은 서버가 생성한 HTML에 클라이언트의 JavaScript 동작을 연결하는 과정이다.
예를 들어 Nuxt 애플리케이션에서 서버가 먼저 HTML을 생성했다고 가정해 보자.
브라우저는 HTML을 받아 화면에 표시한다.
이후 JavaScript가 실행되면서 이벤트 처리와 반응형 상태가 연결된다.
문제는 서버와 클라이언트가 서로 다른 UI 구조를 생성하는 경우다.
예를 들어 서버에서는 특정 요소가 존재했지만 클라이언트에서는 해당 요소가 존재하지 않을 수 있다.
이 경우 Hydration은 기존 DOM 구조를 그대로 재사용하기 어려워진다.
이번 수정에서는 Vapor Runtime이 Root 구조의 불일치를 발견했을 때 해당 Block을 클라이언트 렌더링으로 마무리하도록 개선했다.
즉 기존 서버 DOM을 재사용할 수 없는 상황에서 렌더링 작업을 중간에 잘못된 상태로 남기지 않고 복구하도록 한 변경이다.
다만 이러한 복구 기능이 존재한다고 해서 서버와 클라이언트의 렌더링 불일치를 의도적으로 방치해도 된다는 의미는 아니다.
실제 애플리케이션에서는 서버와 클라이언트의 초기 출력이 일치하도록 만드는 것이 중요하다.
서버가 렌더링한 v-if 분기 기준으로 Hydration
두 번째 수정은 조건부 렌더링과 관련된다.
runtime-vapor: hydrate a v-if chain by the branch the server rendered
Vue에서는 v-if, v-else-if, v-else를 이용해 조건에 따라 서로 다른 UI를 표시할 수 있다.
예를 들어 다음과 같은 상황이다.
- 로그인한 사용자에게는 프로필 표시
- 로그인하지 않은 사용자에게는 로그인 버튼 표시
서버와 클라이언트가 서로 다른 상태를 사용하면 실제로 선택하는 분기가 달라질 수 있다.
이번 변경에서는 서버가 실제로 렌더링한 분기를 기준으로 Hydration을 처리하도록 수정했다.
이는 조건부 Template을 다루는 과정에서 잘못된 DOM에 이벤트나 반응형 연결을 적용하는 문제를 줄이기 위한 것이다.
Static Template 불일치 복구
다음 수정도 포함됐다.
runtime-vapor: recover a mismatched static template root during hydration
Static Template은 렌더링 과정에서 구조가 크게 변경되지 않는 Template을 의미한다.
하지만 서버가 생성한 HTML의 Root 구조가 클라이언트에서 기대한 구조와 일치하지 않을 수 있다.
이 경우 Vapor Runtime이 잘못된 DOM 구조를 계속 재사용하면 이후의 업데이트까지 영향을 받을 수 있다.
이번 수정은 이러한 상황에서도 적절하게 복구하도록 한다.
또한 서버에서 생성한 DOM Range를 하나의 단위로 처리하는 복구 로직도 개선됐다.
따라서 단순한 텍스트 불일치뿐 아니라 여러 DOM Node로 구성된 Block의 복구 정확성도 개선하려는 방향이다.
Dynamic Component와 Slot 처리
이번 Release Candidate에는 Dynamic Component 및 Slot과 관련된 수정도 포함됐다.
대표적인 변경은 다음과 같다.
| 변경 영역 | 수정 내용 |
|---|---|
| Dynamic Component | v-once가 적용된 컴포넌트의 Fragment Flag 처리 |
| Slot Props | VDOM 방식과 일치하도록 key 전달 |
| Dynamic Slot Component | Slot Prop이 변경됐을 때 컴포넌트 Tag 갱신 |
| Compiler Binding | 여러 자리 숫자로 끝나는 내부 변수 이름의 충돌 방지 |
| Component Resolution | 제거된 컴포넌트의 불필요한 Asset Resolution 생략 |
이러한 수정은 일반적인 사용자 코드에서 눈에 띄는 새로운 기능보다 컴파일러가 생성한 코드의 동작을 기존 Vue의 의미와 일치시키는 작업에 가깝다.
예를 들어 Slot Prop을 이용해 어떤 컴포넌트를 렌더링할지 결정하는 경우를 생각할 수 있다.
Slot Prop이 변경됐는데 실제 컴포넌트 Tag가 갱신되지 않는다면 화면에 오래된 컴포넌트가 남을 수 있다.
이번 변경은 이러한 종류의 불일치를 수정한다.
Vapor Compiler 성능 최적화
컴파일러에서도 여러 최적화가 이루어졌다.
대표적인 항목은 다음과 같다.
- 단순 식별자의 불필요한 재파싱 방지
- 반응형 Effect Group의 ID 연결 작업 감소
v-ifChain의 Head와 Tail 탐색 결과 재사용- 부모 Transform Context를 복사하는 대신 자식 Context 생성
- 코드 생성 과정의 객체 할당 감소
- Object 형태의
v-bind를 Directive마다 한 번만 분석 v-for선택자 비교에서 불필요한 검사 조기 종료
이러한 변경은 컴파일러가 동일한 정보를 반복해서 계산하거나 객체를 생성하는 비용을 줄이기 위한 것이다.
예를 들어 하나의 Template에 많은 v-if와 v-for가 포함돼 있다면 컴파일러는 여러 Node와 표현식을 분석해야 한다.
이 과정에서 이전에 계산한 결과를 재사용하면 반복 작업을 줄일 수 있다.
하지만 이번 공식 릴리스에는 컴파일 시간이 얼마나 감소했는지에 관한 정량적인 Benchmark가 포함되지 않았다.
따라서 모든 Vue 프로젝트에서 빌드 시간이 특정 비율만큼 감소한다고 판단할 수 없다.
Vue Devtools 연동 개선
이번 버전에는 Vapor Runtime의 Devtools Component Hook도 추가됐다.
이는 개발 도구에서 Vapor Component의 실행 상태를 관찰하는 기능과 관련된다.
기존 Vue 개발 경험에서는 Vue Devtools를 이용해 컴포넌트의 구조와 반응형 상태를 확인할 수 있었다.
Vapor Mode에서도 비슷한 개발 경험을 제공하려면 해당 실행 모델에 맞는 Devtools 연동이 필요하다.
이번 변경은 그러한 기반을 확장한 것이다.
다만 이번 Hook 추가가 기존 Vue Devtools의 모든 기능이 Vapor Mode에서 완전히 동일하게 동작한다는 의미는 아니다.
안정화 단계와 적용 범위
Vue 3.6.0-rc.14는 Release Candidate다.
Vue 3.6의 최종 안정 버전이 아니다.
따라서 운영 중인 Vue 3.5 또는 기존 안정 버전의 애플리케이션을 즉시 교체해야 한다는 의미는 아니다.
특히 Vapor Mode는 기존 Virtual DOM 방식과 내부 실행 구조가 다르므로 사용 중인 라이브러리와 컴포넌트의 호환성을 확인해야 한다.
이번 릴리스는 Vapor Mode가 실제 SSR 환경에서 안정적으로 동작할 수 있도록 Hydration 오류 복구를 강화하고 컴파일러의 반복 작업을 줄인 안정화 업데이트다.
2️⃣ 웹 플랫폼·브라우저
W3C, Geolocation Candidate Recommendation Draft 개정판 공개 — 위치 에뮬레이션과 권한 처리 규칙 명확화
- 발표일: 2026-10-30 — 정확한 게시 시각 미확인
- 발표 주체: W3C Devices and Sensors Working Group, Web Applications Working Group
- 문서: Geolocation
- 안정화 단계: Candidate Recommendation Draft
- 원문: https://www.w3.org/TR/2026/CRD-geolocation-20261030/
- 공식 발행 이력: https://www.w3.org/standards/history/geolocation/
W3C가 Geolocation 규격의 새로운 Candidate Recommendation Draft를 2026년 10월 30일에 공개했다.
이번 문서는 브라우저에서 사용하는 새로운 위치 조회 API를 처음으로 제안한 것이 아니다.
기존 Geolocation 규격을 개정하면서 위치 정보의 처리와 권한, 자동화 테스트에서의 에뮬레이션 등을 명확하게 정의한 문서다.
특히 개발자가 구분해야 할 부분은 다음과 같다.
새로운 규격 초안이 공개됐다는 사실과 브라우저에 새로운 안정 기능이 배포됐다는 사실은 다르다.
이번 발표는 규격 문서의 개정이다.
따라서 Chrome이나 Firefox, Safari의 신규 기능 출시로 취급해서는 안 된다.
기존 Geolocation API
Geolocation API는 브라우저가 기기의 위치 정보를 애플리케이션에 제공할 수 있도록 하는 기술이다.
대표적으로 다음 API를 사용한다.
navigator.geolocation.getCurrentPosition()navigator.geolocation.watchPosition()navigator.geolocation.clearWatch()
getCurrentPosition()은 현재 위치를 한 번 요청한다.
watchPosition()은 위치가 변경될 때 새로운 정보를 전달받을 수 있도록 한다.
예를 들어 사용자가 주변 매장을 검색하는 웹서비스를 이용한다고 가정해 보자.
브라우저는 사용자의 동의를 받은 뒤 현재 위치를 제공할 수 있다.
애플리케이션은 해당 좌표를 이용해 주변 매장을 검색할 수 있다.
하지만 실제 위치 정보는 개인정보와 직접 연결될 수 있다.
따라서 Geolocation API는 브라우저의 권한 정책과 보안 모델에 영향을 받는다.
사용자 권한과 Secure Context
Geolocation 규격은 다음 조건을 포함한다.
- HTTPS 등 Secure Context에서 사용
- 사용자의 위치 정보 접근 권한 확인
- Permissions API 및 Permissions Policy와 연결
- 문서의 활성 상태와 가시성 고려
예를 들어 HTTPS가 아닌 일반적인 페이지에서는 위치 정보 API가 제한될 수 있다.
또한 사용자에게 위치 정보 사용 권한을 요청할 수 있다.
사용자가 거부하면 애플리케이션은 해당 오류를 처리해야 한다.
Permissions Policy의 geolocation 기능 식별자는 기본적으로 self를 허용 목록으로 사용한다.
따라서 Cross-origin iframe에서 위치 정보에 접근하려면 별도의 정책 구성이 필요할 수 있다.
하지만 사용자가 권한을 허용했다는 사실만으로 모든 iframe에서 위치 정보를 사용할 수 있다는 의미는 아니다.
브라우저의 권한과 문서에 적용된 Permissions Policy는 서로 다른 제약이다.
위치 에뮬레이션 규칙
이번 개정 문서에서 주목할 수 있는 부분은 Geolocation Emulation에 관한 규범적 절차다.
웹 개발에서는 실제 기기의 위치를 변경하지 않고도 여러 국가와 도시에서 애플리케이션이 어떻게 동작하는지 테스트해야 할 수 있다.
예를 들어 위치 기반 서비스를 테스트할 때 다음 상황을 검사할 수 있다.
- 서울에서 접속
- 도쿄에서 접속
- 런던에서 접속
- 위치 정보 사용 거부
- 위치 정보 취득 실패
이러한 테스트를 실제로 여러 국가에서 수행하는 것은 비효율적이다.
따라서 브라우저 자동화 도구는 위치 정보를 가상으로 설정하는 기능을 제공할 수 있다.
이번 규격은 이러한 동작을 표준 알고리즘 수준에서 설명한다.
에뮬레이션 데이터는 다음 종류를 가질 수 있다.
GeolocationCoordinatesGeolocationPositionErrornull
좌표 정보를 지정하면 브라우저가 실제 위치 대신 해당 정보를 사용할 수 있다.
오류 정보를 지정하면 위치 취득 실패 상황을 테스트할 수 있다.
null은 별도의 에뮬레이션 데이터를 사용하지 않는 상태를 나타낸다.
위치 변경과 watchPosition()
위치를 지속적으로 관찰하는 watchPosition()에서는 새로운 위치가 발생했는지 판단해야 한다.
이번 규격은 에뮬레이션 데이터를 변경한 경우 이를 중요한 위치 변화로 처리하도록 정의한다.
예를 들어 자동화 테스트에서 다음 순서를 수행한다고 가정해 보자.
- 서울의 좌표를 설정한다.
watchPosition()을 실행한다.- 도쿄의 좌표로 변경한다.
- 위치 변경 Callback이 호출되는지 확인한다.
이러한 테스트에서 브라우저마다 에뮬레이션 처리 방식이 다르면 테스트 결과가 달라질 수 있다.
규격 차원의 절차를 정의하면 서로 다른 브라우저와 자동화 도구가 동작을 일관되게 구현하는 데 도움이 된다.
다만 위치 에뮬레이션은 일반 웹페이지가 실제 사용자의 위치를 임의로 변경하기 위한 새로운 공개 API가 아니다.
주로 브라우저 구현과 사용자 에이전트 자동화, 애플리케이션 테스트를 위한 규칙이다.
오류 처리와 좌표 정보의 정합성
개정 문서에는 다음과 같은 규범적 변경 사항도 정리돼 있다.
- Latitude 정의 보완
- 에뮬레이션 오류의 처리 방식 명확화
- 정지 상태에서 잘못된 숫자 대신
null사용 - 정확도 값의 단위 명확화
- 보안이 보장되지 않는 Context의 검사 강화
- 좌표와 위치 관련 객체의
toJSON()지원 - 좌표의 측지 기준과 단위 설명 보완
이 목록은 2022년 Recommendation 이후 누적된 변경 사항을 정리한 것이다.
따라서 모든 항목이 10월 30일에 처음 제안됐다고 해석해서는 안 된다.
특히 이번 문서의 공개 날짜와 각각의 규범적 변경이 처음 작성된 날짜는 다를 수 있다.
표준화 단계의 제한사항
이번 문서는 Candidate Recommendation Draft다.
Candidate Recommendation Draft는 향후 Recommendation을 준비하기 위한 작업 문서다.
문서에는 W3C와 회원사가 해당 내용을 최종적으로 승인한 것은 아니라는 설명이 명시돼 있다.
따라서 앞으로 규격 내용이 수정되거나 다른 문서로 대체될 수 있다.
또한 Geolocation API 자체는 이미 여러 브라우저에서 제공되고 있다.
이번 개정 초안의 공개만으로 새로운 JavaScript API를 추가해야 하는 것은 아니다.
기존 위치 정보 API의 동작과 자동화 테스트에 필요한 처리 규칙을 더 정확하게 정의한 표준 문서 업데이트다.
이번 조사 범위에서 이외에 포함할 만한 주요 웹 플랫폼·브라우저 신규 발표는 확인하지 못했습니다.
WHATWG, TC39, MDN, Chrome·Chromium, Firefox·Mozilla, WebKit·Safari의 주요 발표를 검토했다.
Chrome 158 Beta와 Firefox 159 정식 출시는 이전 브리핑에서 다뤘으므로 반복하지 않았다.
Geolocation 규격 개정 역시 브라우저의 신규 Stable API 출시와 구분했다.
3️⃣ 백엔드·인프라
Cloudflare Durable Objects, 코드 업데이트를 기본적으로 최대 5분 지연 — WebSocket 연결과 장시간 작업의 중단 위험 완화
- 발표일: 2026-10-30 — 정확한 게시 시각 미확인
- 발표 주체: Cloudflare
- 적용 대상: Durable Objects, Cloudflare Workers
- 변경 유형: 기본 배포 동작 변경
- 기본 지연 시간: 300초
- 설정 가능 범위: 0~86,400초
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-30-durable-objects-deferred-code-updates/
Cloudflare가 Durable Objects의 코드를 배포할 때 실행 중인 객체를 즉시 재시작하지 않고, 기본적으로 최대 5분 동안 기존 코드를 유지하도록 동작을 변경했다.
이번 업데이트는 새로운 API를 추가한 것보다 실제 운영 중인 서비스의 배포 방식에 직접적인 영향을 주는 변경이다.
Durable Objects는 특정 ID와 연결된 실행 인스턴스에서 상태를 관리할 수 있는 Cloudflare의 서버리스 기능이다.
일반적인 Worker는 HTTP 요청을 처리하는 데 사용된다.
반면 Durable Object는 같은 ID와 연결된 여러 요청을 하나의 논리적인 실행 단위에서 처리할 수 있다.
예를 들어 다음과 같은 서비스에서 사용할 수 있다.
- 실시간 채팅방
- 공동 편집
- 멀티플레이어 게임
- 사용자별 세션 관리
- 장시간 실행되는 AI 에이전트
이러한 서비스에서는 인스턴스의 메모리 상태와 실행 중인 연결이 중요할 수 있다.
문제는 새로운 코드를 배포할 때 발생한다.
기존 배포 방식
기존에는 Durable Object의 코드를 업데이트하면 새로운 코드가 즉시 적용될 수 있었다.
예를 들어 사용자가 채팅방에 연결돼 있다고 가정해 보자.
개발자가 새로운 기능을 배포한다.
Durable Object가 새로운 코드로 재시작된다.
이 과정에서 기존 WebSocket 연결이 종료되거나 실행 중인 작업이 중단될 수 있었다.
일반적인 HTTP 요청에서는 클라이언트가 다시 요청을 보내면 되는 경우가 많다.
하지만 실시간 채팅이나 장시간 작업에서는 중단의 영향이 더 클 수 있다.
예를 들어 AI 에이전트가 여러 외부 API를 호출하는 도중 실행 환경이 재시작되면 작업 상태를 복구해야 할 수 있다.
이번 변경은 이러한 중단을 줄이기 위한 것이다.
새로운 기본 동작: Deferred Update
새로운 기본 전략은 다음과 같다.
deferred
새로운 코드가 배포되면 실행 중인 Durable Object는 기존 코드를 잠시 유지할 수 있다.
기본 최대 지연 시간은 다음과 같다.
300초
즉 5분이다.
이 기간 동안 Durable Object가 Hibernation 상태에 들어가면 다음 요청에서 새로운 코드로 시작할 수 있다.
반면 지연 시간이 끝날 때까지 Hibernation하지 않았다면 객체가 재설정되고 새로운 코드를 사용하게 된다.
따라서 다음 두 상황이 존재한다.
| 상황 | 결과 |
|---|---|
| 지연 시간 안에 Hibernation | 다음 활성화 시 새로운 코드 사용 |
| 지연 시간 종료까지 계속 활성 상태 | 객체 재설정 후 새로운 코드 사용 |
이러한 방식은 모든 객체가 배포 순간에 동시에 재시작되는 상황을 줄이려는 목적을 가진다.
Hibernation API와 WebSocket 연결
Cloudflare Durable Objects는 WebSocket Hibernation API를 제공한다.
이를 사용하면 Durable Object의 실행 인스턴스가 계속 메모리에 존재하지 않더라도 WebSocket 연결을 유지할 수 있다.
새로운 코드 업데이트 전략은 이러한 기능과 연결된다.
예를 들어 채팅방의 WebSocket 연결이 존재한다고 가정해 보자.
Durable Object가 Hibernation 상태에 들어간다.
이후 새로운 코드가 적용된다.
WebSocket Hibernation API를 사용하는 연결은 해당 과정에서도 유지될 수 있다.
따라서 코드 배포 이후 WebSocket 연결을 무조건 다시 설정해야 하는 상황을 줄일 수 있다.
하지만 모든 종류의 WebSocket 연결이 자동으로 보존된다는 의미는 아니다.
Cloudflare는 Hibernation API를 사용하는 연결을 대상으로 해당 동작을 설명한다.
또한 코드 업데이트가 항상 정확히 5분 동안 지연된다는 보장도 없다.
max_delay 설정
개발자는 지연 시간을 직접 설정할 수 있다.
Wrangler 설정에서 다음 항목을 사용한다.
durable_objects.code_update_strategy
대표적인 설정은 다음과 같다.
mode: "deferred"
max_delay: 60
이 경우 최대 지연 시간을 60초로 지정한다.
설정할 수 있는 범위는 다음과 같다.
0초부터 86,400초까지
86,400초는 24시간이다.
하지만 긴 지연 시간을 사용하면 서로 다른 Durable Object가 서로 다른 코드 버전으로 실행되는 시간이 증가할 수 있다.
예를 들어 채팅방 A는 새로운 버전을 사용하고 채팅방 B는 이전 버전을 사용할 수 있다.
이러한 상황에서는 메시지 형식이나 저장 데이터의 호환성을 고려해야 한다.
특히 새로운 버전이 기존 데이터 구조를 변경한다면 이전 코드가 해당 데이터를 정상적으로 처리할 수 있는지 확인해야 한다.
즉시 업데이트가 필요한 상황
Cloudflare는 특정 배포에 한해 즉시 업데이트를 적용하는 옵션도 제공한다.
대표적인 설정은 다음과 같다.
--durable-objects-code-update-mode immediate
예를 들어 보안 취약점을 수정했다고 가정해 보자.
개발자는 새로운 코드가 가능한 한 빨리 적용되기를 원할 수 있다.
이 경우 기존 객체가 Hibernation할 때까지 기다리는 방식보다 즉시 업데이트가 적합할 수 있다.
또한 배포 직후 모든 Durable Object가 새로운 코드로 실행되고 있다고 가정하는 자동화 시스템에서도 즉시 업데이트가 필요할 수 있다.
따라서 Deferred Update가 기본값이 됐더라도 항상 지연 배포가 가장 적합한 것은 아니다.
서비스의 요구사항에 따라 선택해야 한다.
Best-effort의 의미
Cloudflare는 이번 지연 전략을 Best-effort로 설명한다.
이는 최대 지연 시간을 설정했다고 해서 해당 시간 동안 객체가 반드시 계속 실행된다는 보장이 없다는 의미다.
예를 들어 Cloudflare Runtime 자체가 업데이트되는 상황에서는 Durable Object가 설정된 지연 시간이 끝나기 전에 재설정될 수 있다.
따라서 이 기능을 장시간 작업의 유일한 복구 수단으로 사용해서는 안 된다.
실제로 완료해야 하는 중요한 작업은 Durable Storage나 별도의 작업 기록을 통해 복구할 수 있도록 설계해야 한다.
이번 발표는 실시간 연결과 장시간 실행을 사용하는 Durable Object의 배포 중단을 줄이기 위해 코드 적용 시점을 더 유연하게 제어하도록 한 운영 방식 변경이다.
Cloudflare D1, 11월 4일부터 SQL Query 결과를 최대 50MB로 제한 — 대용량 조회는 오류 반환
- 발표일: 2026-10-30 — 정확한 게시 시각 미확인
- 제한 적용 예정일: 2026-11-04
- 적용 대상: Cloudflare D1
- 변경 유형: Query Result 크기 제한
- 최대 결과 크기: 50MB
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-30-query-result-size-limit/
Cloudflare가 2026년 11월 4일부터 D1의 단일 SQL Query 결과 크기를 최대 50MB로 제한할 예정이라고 발표했다.
이번 변경은 아직 적용되지 않은 제한에 관한 사전 공지다.
따라서 10월 31일 기준으로 모든 D1 Query에 해당 제한이 이미 강제되고 있다고 해석해서는 안 된다.
D1은 Cloudflare가 제공하는 SQLite 기반 서버리스 데이터베이스다.
Cloudflare Workers와 연결해 데이터를 저장하고 조회할 수 있다.
예를 들어 다음과 같은 데이터에 사용할 수 있다.
- 사용자 설정
- 게시글
- 서비스의 일반적인 관계형 데이터
- 작은 규모의 업무 데이터
일반적으로 웹 애플리케이션에서 SQL Query를 실행할 때는 필요한 데이터만 가져오는 것이 적절하다.
하지만 일부 프로젝트에서는 매우 많은 행이나 큰 JSON 문자열을 한 번에 반환하는 Query를 사용할 수 있다.
이번 제한은 이러한 대용량 Query에 영향을 준다.
새로운 결과 크기 제한
적용되는 최대 크기는 다음과 같다.
50MB
단일 Query가 이보다 큰 결과를 반환하려고 하면 오류가 발생한다.
공식 오류 메시지는 다음과 같다.
D1 query result exceeded the maximum response size limit of 50 MB
이는 단순한 경고가 아니다.
제한을 초과한 Query는 실패한다.
따라서 기존에 정상적으로 실행되던 Query라도 반환 데이터가 증가하면 오류가 발생할 수 있다.
예를 들어 특정 테이블에서 모든 행과 모든 열을 가져오는 Query를 생각할 수 있다.
SELECT * FROM records
처음에는 데이터가 적어서 문제가 없을 수 있다.
하지만 서비스가 성장하면서 행이 수십만 개로 증가하면 결과 크기가 50MB를 초과할 수 있다.
이 경우 Query 자체는 문법적으로 올바르더라도 실행 결과를 반환하지 못할 수 있다.
사용자에게 미치는 영향
Cloudflare는 이번 제한이 전체 D1 요청의 99.999번째 백분위수까지 포함하는 수준이라고 설명한다.
따라서 대부분의 일반적인 Query에는 영향을 주지 않을 것으로 예상한다.
하지만 이 수치는 Cloudflare의 자체 서비스 사용량을 기준으로 한다.
모든 개별 프로젝트에서 영향받는 Query가 없다는 의미는 아니다.
특히 대량의 로그나 분석 데이터를 한 번에 가져오는 서비스에서는 영향이 있을 수 있다.
또한 반환 데이터가 매우 큰 JSON이나 Text 필드를 포함하는 경우에도 주의해야 한다.
예를 들어 행의 개수가 적더라도 각 행에 큰 문자열이 포함돼 있다면 결과 크기가 증가할 수 있다.
필요한 열만 조회
Cloudflare는 대용량 Query를 줄이기 위한 방법으로 필요한 열만 선택하는 방식을 권장한다.
예를 들어 다음 테이블이 있다고 가정해 보자.
idtitlebodycreated_at
목록 화면에서는 제목과 생성 시각만 필요할 수 있다.
하지만 SELECT *를 사용하면 본문 전체까지 가져오게 된다.
본문이 길다면 불필요한 데이터 전송량이 증가한다.
따라서 실제로 필요한 열만 조회하도록 변경할 수 있다.
SELECT id, title, created_at FROM posts
이렇게 하면 반환 결과의 크기를 줄일 수 있다.
Pagination
두 번째 방법은 Pagination이다.
Pagination은 많은 데이터를 여러 구간으로 나누어 가져오는 방식이다.
대표적인 SQL 구문은 다음과 같다.
LIMITOFFSET
예를 들어 게시글을 50개씩 조회할 수 있다.
이렇게 하면 전체 게시글을 한 번에 반환하지 않고 필요한 구간만 가져올 수 있다.
하지만 OFFSET 기반 Pagination은 데이터가 많아질수록 비효율적일 수 있다.
또한 새로운 행이 삽입되는 동안 여러 페이지를 순차적으로 조회하면 중복되거나 누락되는 데이터가 발생할 수 있다.
따라서 대규모 데이터에서는 정렬 기준과 고유 식별자를 이용하는 Cursor 기반 Pagination도 고려할 수 있다.
이번 공식 공지는 LIMIT과 OFFSET을 예로 제시하지만 모든 프로젝트에서 OFFSET이 최적이라는 의미는 아니다.
Import·Export에는 적용되지 않음
이번 제한의 중요한 예외가 있다.
D1 REST API를 이용한 Import 및 Export 작업에는 50MB 결과 제한이 적용되지 않는다.
따라서 데이터베이스의 백업이나 대량 데이터 이전 작업에 동일한 제한이 직접 적용되는 것은 아니다.
다만 Import·Export 자체의 다른 제한과 사용량 정책은 별도로 존재할 수 있다.
이번 변경은 일반적인 SQL Query가 반환하는 결과 크기에 관한 것이다.
적용 일정과 제한사항
제한 적용일은 2026년 11월 4일이다.
따라서 해당 날짜 이전에 큰 결과를 반환하는 Query를 사용하는 프로젝트에서는 영향을 확인할 수 있다.
이번 발표는 D1의 전체 데이터베이스 크기 제한을 50MB로 변경한 것이 아니다.
또한 하나의 테이블을 50MB까지만 저장할 수 있다는 의미도 아니다.
단일 Query가 반환하는 결과의 크기를 제한하는 변경이다.
이번 업데이트는 서버리스 데이터베이스의 응답 크기를 제한해 매우 큰 결과가 한 번에 처리되는 상황을 줄이기 위한 운영 정책 변경이다.
Vercel, Node.js 26 LTS 정식 지원 — 신규 프로젝트 기본 버전 변경과 HTTP/2 기반 fetch 도입
- 발표일: 2026-10-29 — 정확한 게시 시각 미확인
- 조사 범위 포함 근거: 게시 날짜만 확인되며 한국 시간 기준 조사 범위와 날짜가 겹침
- 안정화 단계: Generally Available
- 적용 대상: Vercel Build, Vercel Functions
- 새 기본 버전: Node.js 26
- 원문: https://vercel.com/changelog/node-js-26-lts-is-now-generally-available-on-vercel
Vercel이 Node.js 26 LTS를 Build와 Vercel Functions에서 정식으로 지원하고, 새 프로젝트의 기본 Node.js 버전을 26으로 변경했다고 발표했다.
이번 업데이트는 새로운 런타임을 사용할 수 있게 된 것뿐 아니라 기존 프로젝트에도 영향을 줄 수 있다는 점이 중요하다.
특히 package.json의 Node.js 버전 범위를 넓게 설정한 프로젝트에서는 다음 배포 시점에 사용되는 Node.js 버전이 변경될 수 있다.
새로운 프로젝트의 기본 버전
Vercel은 Node.js 26을 모든 신규 프로젝트의 기본 버전으로 설정했다.
따라서 새로운 프로젝트를 생성하면 별도의 설정 없이 Node.js 26을 사용할 수 있다.
하지만 기존 프로젝트에서도 다음 배포에서 자동으로 26을 사용할 가능성이 있다.
예를 들어 package.json에 다음 설정이 있다고 가정해 보자.
"node": ">=20"
이 설정은 Node.js 20 이상을 허용한다.
따라서 Node.js 26도 허용 범위에 포함된다.
Vercel은 이러한 경우 다음 배포에서 Node.js 26을 사용하도록 변경될 수 있다고 안내한다.
반면 특정 메이저 버전을 명시한 프로젝트는 다른 동작을 가질 수 있다.
예를 들어 다음 설정이다.
"node": "24.x"
이 설정은 Node.js 24 버전 라인을 사용하도록 제한한다.
따라서 새로운 기본값이 26이라고 해서 해당 프로젝트의 버전이 무조건 26으로 변경되는 것은 아니다.
Node.js 26으로 명시적으로 변경
기존 프로젝트에서 Node.js 26을 사용하려면 다음 설정을 사용할 수 있다.
"node": "26.x"
이후 프로젝트를 다시 배포한다.
Vercel은 Node.js 26의 마이너 및 패치 업데이트를 자동으로 적용한다고 설명한다.
따라서 한 번 Node.js 26을 사용하도록 설정한 뒤 모든 패치 버전을 직접 지정할 필요는 없다.
하지만 이 방식은 Node.js의 패치 버전이 관리형 환경에서 변경될 수 있다는 의미이기도 하다.
실제 운영 환경에서 정확한 Runtime 버전이 필요한 경우에는 배포 기록과 실행 환경을 확인할 필요가 있다.
HTTP/2를 기본으로 사용하는 fetch
Node.js 26에는 Undici 8이 포함된다.
Undici는 Node.js에서 HTTP 요청을 처리하는 데 사용되는 구성요소다.
Node.js의 내장 fetch()도 Undici와 연결된다.
이번 버전에서는 상대 서버가 HTTP/2를 지원하는 경우 내장 fetch()가 기본적으로 HTTP/2를 사용할 수 있다.
HTTP/2는 하나의 연결에서 여러 요청을 처리할 수 있는 Multiplexing 등의 기능을 제공한다.
예를 들어 서버리스 Function이 외부 API에 여러 요청을 보내는 상황을 생각해 보자.
HTTP/2를 지원하는 서버와 통신하면 여러 요청이 연결을 공유할 수 있다.
이는 연결 설정과 요청 처리의 일부 비용을 줄이는 데 도움이 될 수 있다.
하지만 모든 요청에서 지연 시간이 반드시 감소하는 것은 아니다.
실제 성능은 상대 서버의 HTTP/2 지원과 연결 상태, 요청의 크기, 네트워크 환경 등에 영향을 받는다.
HTTP/1.1로 되돌리는 설정
기존 애플리케이션이 HTTP/1.1의 특정 동작에 의존하고 있다면 문제가 발생할 수 있다.
대표적인 차이는 다음과 같다.
Response.statusText
HTTP/2에서는 HTTP/1.1의 Reason Phrase와 같은 정보를 사용하지 않는다.
따라서 statusText가 비어 있을 수 있다.
예를 들어 HTTP 오류를 사용자에게 표시하기 위해 statusText에 의존하는 코드가 있다면 기대한 문자열을 얻지 못할 수 있다.
이러한 경우 HTTP 상태 코드를 직접 사용하는 편이 적절하다.
response.status
Vercel은 필요한 경우 다음 환경 변수를 이용해 HTTP/2 사용을 비활성화하도록 안내한다.
VERCEL_FETCH_DISABLE_H2=1
환경 변수를 설정한 이후에는 다시 배포해야 한다.
다만 이는 Vercel Functions의 내장 fetch 동작에 관한 설정이다.
모든 외부 HTTP Client 라이브러리가 동일한 환경 변수에 반응한다고 가정해서는 안 된다.
Temporal API
Node.js 26에는 Temporal API도 포함된다.
Temporal은 JavaScript의 기존 Date API가 가진 여러 문제를 개선하기 위한 날짜 및 시간 처리 API다.
대표적인 특징은 다음과 같다.
- 불변 값 기반의 날짜 및 시간 표현
- Timezone 지원
- Calendar 지원
- 서로 다른 날짜 및 시간 개념의 명확한 분리
예를 들어 일반적인 예약 시스템에서는 날짜와 시간이 서로 다른 의미를 가질 수 있다.
특정 지역의 오후 3시라는 표현과 UTC 기준의 정확한 시각은 서로 다르다.
Temporal은 이러한 차이를 더 명확하게 표현하는 데 도움을 줄 수 있다.
하지만 새로운 API가 지원된다고 해서 기존 프로젝트의 모든 Date 사용을 즉시 교체해야 하는 것은 아니다.
실제로 사용하는 프레임워크와 라이브러리의 지원 범위를 확인해야 한다.
V8 14.6
Node.js 26은 V8 14.6을 사용한다.
V8은 JavaScript 코드를 실행하는 엔진이다.
이번 버전에는 다음과 같은 기능이 포함된다.
Map.prototype.getOrInsert()
이 메서드는 Map에 특정 Key가 존재하면 기존 값을 반환하고, 존재하지 않으면 기본값을 삽입하는 동작을 제공한다.
기존에는 이러한 작업을 여러 메서드 호출로 구현해야 할 수 있었다.
예를 들어 다음 작업이다.
- Key가 존재하는지 확인
- 존재하면 값 반환
- 존재하지 않으면 기본값 저장
- 저장한 값 반환
새로운 API는 이러한 동작을 간결하게 표현할 수 있도록 한다.
Prebuilt Deployment 제한사항
Vercel은 Prebuilt Deployment에서 Node.js 26 Functions를 배포하려면 다음 버전 이상의 CLI를 요구한다.
Vercel CLI 65.8.0
따라서 기존 CI에서 오래된 CLI를 사용하고 있다면 새로운 Node.js Runtime을 지원하지 못할 수 있다.
이번 업데이트는 서버리스 배포 환경의 기본 Node.js 버전을 변경하면서 HTTP/2와 최신 JavaScript 기능을 제공하는 정식 런타임 지원 확대다.
4️⃣ 개발 도구 및 보안
AWS WAF, CloudFront HTML에 보안 SDK 자동 삽입 지원 — Bot 탐지·계정 탈취 방어에 필요한 브라우저 신호 수집
- 발표일: 2026-10-30 — 정확한 게시 시각 미확인
- 발표 주체: AWS
- 적용 대상: AWS WAF, Amazon CloudFront
- 안정화 단계: 정식 기능
- 추가 통합 요금: 없음
- 원문: https://aws.amazon.com/about-aws/whats-new/2026/10/aws-waf-automatic-sdk-integration-amazon-cloudfront/
AWS가 CloudFront를 통해 제공하는 HTML에 AWS WAF JavaScript SDK를 자동으로 삽입하는 기능을 공개했다.
이번 기능은 프론트엔드 코드에 별도의 스크립트를 추가하지 않아도 브라우저 측 보안 신호를 수집할 수 있도록 한다.
AWS WAF는 웹 애플리케이션으로 들어오는 HTTP 요청을 분석하고 보안 정책을 적용하는 서비스다.
대표적으로 다음 공격에 대응하는 데 사용할 수 있다.
- 악성 Bot
- 자동화된 로그인 시도
- 계정 탈취 시도
- 비정상적인 계정 생성
- 알려진 웹 공격 패턴
하지만 모든 공격을 서버에서 관찰한 HTTP Header만으로 정확하게 구분하기는 어렵다.
일부 Bot은 일반적인 브라우저와 비슷한 요청을 생성할 수 있기 때문이다.
따라서 브라우저에서 실제 JavaScript가 실행되는지와 관련된 신호를 수집하는 방식이 사용된다.
AWS WAF JavaScript SDK의 역할
AWS WAF JavaScript SDK는 실제 브라우저에서 실행되며 보안 평가에 필요한 일부 클라이언트 신호를 수집한다.
이러한 신호는 AWS WAF의 특정 Managed Rule Group에서 사용된다.
이번 기능이 지원하는 주요 Rule Group은 다음과 같다.
| Rule Group | 목적 |
|---|---|
| Bot Control Targeted | 고도화된 Bot 탐지 |
| Fraud Control ATP | 계정 탈취 시도 방어 |
| Fraud Control ACFP | 비정상적인 계정 생성 방어 |
ATP는 Account Takeover Prevention이다.
ACFP는 Account Creation Fraud Prevention이다.
예를 들어 로그인 페이지에서 자동화된 비밀번호 대입 공격이 발생한다고 가정해 보자.
WAF는 요청의 특성과 브라우저에서 수집한 신호를 함께 이용해 위험 여부를 판단할 수 있다.
하지만 JavaScript SDK가 수집하는 신호만으로 모든 악성 Bot을 정확하게 식별할 수 있는 것은 아니다.
실제 판단은 WAF의 다른 조건과 Managed Rule의 정책에 영향을 받는다.
기존 방식의 문제
기존에는 해당 SDK를 사용하기 위해 실제 웹 애플리케이션에 Script Tag를 추가해야 했다.
예를 들어 여러 React 또는 Next.js 애플리케이션을 운영한다면 각 애플리케이션에 스크립트를 삽입해야 할 수 있었다.
문제는 운영 중인 애플리케이션의 수가 많을 때다.
새로운 프로젝트를 생성했지만 보안 SDK를 추가하지 않았을 수 있다.
또는 기존 프로젝트에서 스크립트를 제거하는 코드 변경이 발생할 수 있다.
이 경우 AWS WAF의 관련 Managed Rule은 활성화돼 있지만 필요한 브라우저 신호를 충분히 받지 못하는 상황이 발생할 수 있다.
이번 기능은 이러한 문제를 줄이기 위해 CloudFront가 전달하는 HTML에 Script Tag를 자동으로 삽입한다.
따라서 개별 애플리케이션의 소스코드와 빌드 과정을 변경할 필요가 줄어든다.
CloudFront와 Web ACL의 관계
이번 기능은 CloudFront에 연결된 Web ACL에서 활성화한다.
Web ACL은 AWS WAF의 규칙을 묶어 특정 리소스에 적용하는 구성이다.
예를 들어 하나의 CloudFront Distribution에 특정 Web ACL을 연결할 수 있다.
해당 Web ACL에는 여러 Rule Group이 포함될 수 있다.
자동 SDK 삽입은 다음 조건을 충족하는 Web ACL에서 사용할 수 있다.
- Amazon CloudFront에 사용됨
- 관련 Bot Control 또는 Fraud Control Managed Rule Group이 존재함
따라서 모든 AWS WAF 리소스에서 자동으로 동일한 기능을 사용할 수 있는 것은 아니다.
이번 업데이트는 CloudFront Distribution과 연결된 Web ACL을 대상으로 한다.
생성 방식에 따른 기본값 차이
이번 기능에는 중요한 설정 차이가 있다.
AWS Console에서 새로운 Web ACL을 생성하는 경우와 API로 생성하는 경우의 기본값이 다르다.
| 생성 방식 | 자동 SDK 통합 기본값 |
|---|---|
| Console에서 관련 Rule Group을 포함해 새 Web ACL 생성 | 활성화 |
| CLI·SDK·CDK·CloudFormation으로 생성 | 비활성화 |
| 기존에 생성한 Web ACL | 기존 설정 유지 |
따라서 Infrastructure as Code를 사용하는 팀에서는 설정을 명시적으로 활성화해야 할 수 있다.
예를 들어 CloudFormation으로 Web ACL을 생성한다면 Console에서 생성한 것과 동일한 기본 동작을 기대해서는 안 된다.
또한 기존에 생성한 Web ACL은 이번 기능 때문에 자동으로 변경되지 않는다.
Content Security Policy
HTML에 새로운 Script가 추가되면 CSP(Content Security Policy)에도 영향을 줄 수 있다.
CSP는 웹페이지가 어떤 Script와 다른 리소스를 로딩할 수 있는지 제한하는 보안 정책이다.
예를 들어 특정 Origin에서 제공하는 Script만 허용하도록 설정했다면 자동으로 삽입된 SDK가 차단될 수 있다.
따라서 AWS는 관련 CSP 요구사항을 확인하도록 안내한다.
하지만 새로운 보안 SDK를 사용하기 위해 CSP를 무조건 완화하는 것은 적절하지 않을 수 있다.
실제로 필요한 Script와 리소스만 허용하는 방식이 중요하다.
또한 자동 삽입이 성공했는지와 브라우저에서 실제 SDK가 정상적으로 실행됐는지를 구분해야 한다.
브라우저의 CSP나 확장 프로그램, 네트워크 정책에 따라 실제 실행이 제한될 가능성이 있기 때문이다.
비용과 적용 범위
AWS는 자동 SDK 통합 자체에 별도의 추가 요금을 부과하지 않는다고 설명한다.
하지만 AWS WAF의 기존 Rule Group과 요청 처리에 관한 요금은 별도로 적용될 수 있다.
따라서 자동 삽입 기능이 무료라는 사실과 전체 WAF 사용이 무료라는 사실은 다르다.
이번 발표는 SDK를 직접 삽입하지 않은 웹사이트에서도 CloudFront와 AWS WAF를 통해 브라우저 측 보안 신호를 수집할 수 있도록 통합 과정을 자동화한 기능이다.
Cloudflare Keyless SSL, ECDSA 인증서에서 TLS 1.3 지원 — 비공개 키를 자체 서버에 유지하면서 최신 TLS 사용
- 발표일: 2026-10-30 — 정확한 게시 시각 미확인
- 적용 대상: Cloudflare Keyless SSL
- 지원 조건: ECDSA 인증서, TLS 1.3 활성화
- RSA 인증서의 제한: TLS 1.2까지
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-30-keyless-ssl-tls13-ecdsa/
Cloudflare가 Keyless SSL에서 ECDSA 인증서를 사용하는 경우 TLS 1.3을 지원하도록 기능을 확장했다.
이번 업데이트는 HTTPS 연결의 암호화와 관련된다.
일반적으로 CDN을 사용하면 사용자의 브라우저가 CDN의 Edge Server에 HTTPS로 연결한다.
이 과정에서는 TLS 인증서와 해당 인증서에 연결된 비공개 키가 필요할 수 있다.
하지만 일부 기업은 보안 정책 때문에 비공개 키를 외부 CDN에 전달하고 싶지 않을 수 있다.
예를 들어 금융기관에서는 TLS 인증서의 비공개 키를 자체 인프라에서만 관리하도록 요구할 수 있다.
Cloudflare Keyless SSL은 이러한 상황을 위한 기능이다.
Keyless SSL의 역할
Keyless SSL에서는 TLS 인증서의 비공개 키를 고객이 관리하는 Key Server에 유지한다.
Cloudflare는 실제 TLS 연결을 처리하지만 필요한 암호화 작업을 Key Server와 연결한다.
따라서 비공개 키를 Cloudflare의 Edge Server에 직접 제공하지 않고 HTTPS 서비스를 운영할 수 있다.
이러한 구조는 다음 두 요구사항 사이에서 균형을 맞추기 위한 것이다.
- CDN을 이용한 글로벌 HTTPS 서비스
- 비공개 키를 자체 인프라에서 관리하는 보안 정책
하지만 Keyless SSL의 기존 지원 범위에는 TLS 버전과 인증서 알고리즘에 관한 제약이 있었다.
이번 업데이트는 이 가운데 일부를 개선한다.
ECDSA 인증서와 TLS 1.3
ECDSA는 타원곡선 암호학 기반의 디지털 서명 알고리즘이다.
TLS 인증서에서 서버의 신원을 검증하는 데 사용될 수 있다.
이번 업데이트 이후에는 ECDSA 인증서를 사용하는 Keyless SSL 연결에서 TLS 1.3을 협상할 수 있다.
TLS 1.3은 TLS 1.2보다 새로운 버전의 암호화 프로토콜이다.
암호화 알고리즘과 Handshake 구조에 여러 변경이 포함돼 있다.
예를 들어 일부 오래된 암호화 방식을 제거하고 연결 설정 과정을 단순화했다.
이러한 변경은 보안과 연결 설정의 효율성 측면에서 장점을 제공할 수 있다.
하지만 이번 발표에는 Keyless SSL에서 TLS 1.3을 사용할 때 Handshake 지연 시간이 몇 퍼센트 줄어드는지에 관한 Benchmark가 포함되지 않았다.
따라서 모든 사용자에게 특정한 성능 개선이 발생한다고 단정할 수 없다.
RSA 인증서의 제한사항
중요한 제한이 존재한다.
Keyless SSL에서 RSA 인증서를 사용하는 경우에는 여전히 TLS 1.2까지만 지원한다.
따라서 Keyless SSL을 사용하는 모든 도메인이 이번 업데이트 이후 TLS 1.3을 사용할 수 있는 것은 아니다.
실제 사용 여부는 다음 조건에 따라 달라진다.
- 인증서가 ECDSA인지
- Cloudflare Zone에 TLS 1.3이 활성화돼 있는지
- 클라이언트가 TLS 1.3을 지원하는지
또한 ECDSA 인증서를 사용한다는 사실만으로 모든 클라이언트가 TLS 1.3 연결을 사용할 수 있다는 의미는 아니다.
클라이언트의 지원 범위도 고려해야 한다.
이번 발표는 비공개 키를 자체 서버에 유지해야 하는 환경에서도 ECDSA 인증서를 이용해 TLS 1.3 연결을 제공할 수 있도록 한 기능 확대다.
Cloudflare Workers, BYOIP 대상 Subrequest에서 원래 클라이언트의 보안 신호 유지 — 잘못된 403과 Bot 판별 오류 수정
- 발표일: 2026-10-30 — 정확한 게시 시각 미확인
- 적용 대상: Cloudflare Workers, BYOIP, Bot Management
- 변경 유형: Subrequest의 보안 Context 보존
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-30-byoip-workers-subrequest-context/
Cloudflare가 Workers에서 다른 Cloudflare Proxy 도메인으로 Subrequest를 보낼 때 원래 클라이언트의 보안 정보를 유지하도록 변경했다.
이번 문제는 특히 BYOIP를 사용하는 Cloudflare 환경에서 발생했다.
BYOIP(Bring Your Own IP)는 고객이 보유한 IP 주소 대역을 Cloudflare 네트워크에서 사용할 수 있도록 하는 기능이다.
예를 들어 기업이 자체 IP 주소 대역을 보유하고 있지만 Cloudflare의 Proxy 및 보안 기능을 함께 사용하려는 경우다.
이번 변경은 이러한 BYOIP 도메인에 Workers가 요청을 전달할 때 적용된다.
Subrequest의 기본 구조
예를 들어 사용자가 웹 애플리케이션에 요청했다고 가정해 보자.
요청은 Cloudflare Worker A에 도착한다.
Worker A는 다른 Cloudflare 도메인에 HTTP 요청을 보낸다.
이를 Subrequest라고 한다.
전체 흐름은 다음과 같다.
사용자 → Worker A → Cloudflare Proxy 도메인 B
이러한 구조에서는 원래 사용자의 요청과 Worker A가 보낸 내부 요청을 구분해야 한다.
보안 정책은 실제 사용자의 특성과 Worker의 실행 Context를 함께 고려할 수 있기 때문이다.
기존 문제
Cloudflare에 따르면 일부 BYOIP Subrequest에서는 원래 Workers Context가 손실될 수 있었다.
이 경우 대상 도메인은 내부 Cloudflare 연결을 일반적인 외부 인터넷 요청처럼 처리할 수 있었다.
그 결과 다음 문제가 발생할 수 있었다.
- 실제 사용자 정보를 이용한 보안 규칙이 잘못 적용
- Bot Management가 원래 사용자가 아닌 내부 연결을 평가
- 정상적인 요청에서 HTTP 403 반환
예를 들어 실제 사용자가 정상적인 브라우저로 요청했다고 가정해 보자.
하지만 대상 Zone에서는 Cloudflare 내부 연결의 특성을 이용해 Bot Score를 계산할 수 있었다.
이 경우 실제 사용자의 요청 특성과 다른 보안 판단이 이루어질 수 있다.
Workers Context 보존
이번 업데이트에서는 다음 정보를 유지한다.
CF-WorkerHeadercf.worker.upstream_zone- 원래 클라이언트의 Bot Score
- Verified Bot 상태
- JA3 Fingerprint
- JA4 Fingerprint
JA3와 JA4는 TLS 연결의 특성을 분석하는 데 사용하는 Fingerprint다.
이러한 정보는 실제 요청의 특성을 파악하고 Bot을 분류하는 데 활용할 수 있다.
새로운 방식에서는 대상 Zone이 원래 사용자의 연결 정보를 기준으로 보안 정책을 평가할 수 있다.
따라서 Cloudflare 내부 연결의 특성을 실제 사용자의 특성으로 잘못 해석하는 문제를 줄인다.
Cross-zone의 Client IP 변경
이번 변경에는 중요한 동작 차이가 있다.
Worker가 다른 Cloudflare Zone의 도메인으로 Subrequest를 보내는 경우 CF-Connecting-IP가 특정 Workers IP 주소로 설정된다.
해당 값은 다음과 같다.
2a06:98c0:3600::103
이는 보안상 Cross-zone Subrequest의 원래 클라이언트 IP를 그대로 전달하지 않도록 하는 동작이다.
반면 같은 Zone 안의 Subrequest에서는 원래 클라이언트 IP를 계속 반영할 수 있다.
따라서 다음 두 상황을 구분해야 한다.
| 요청 유형 | CF-Connecting-IP |
|---|---|
| Same-zone Subrequest | 원래 클라이언트 IP 유지 |
| Cross-zone Subrequest | Workers Client IP 사용 |
이러한 차이는 Origin에서 IP 기반 접근 제어를 수행하는 경우 영향을 줄 수 있다.
예를 들어 특정 IP 주소에만 API 접근을 허용하는 시스템이라면 Cross-zone 요청의 실제 Header 값을 확인해야 한다.
다만 이번 변경이 모든 Workers Subrequest의 IP 전달 방식을 새롭게 변경한 것은 아니다.
BYOIP 대상 요청이 기존의 표준 Workers Subrequest 동작을 따르도록 수정한 것이다.
이번 업데이트는 Cloudflare 내부 요청과 실제 사용자 연결의 보안 정보를 올바르게 구분하고 전달하도록 한 보안 및 호환성 수정이다.
Cloudflare Browser Run, 기본 브라우저를 Chrome 152로 업데이트 — User-Agent·Link 추출·Markdown 출력 동작 변경
- 발표일: 2026-10-30 — 정확한 게시 시각 미확인
- 적용 대상: Cloudflare Browser Run, Quick Actions
- 기본 브라우저: Chrome 152
- 안정화 단계: 정식 서비스 업데이트
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-30-chrome-152/
Cloudflare가 Browser Run에서 사용하는 기본 브라우저를 Chrome 152로 업데이트했다.
Browser Run은 Cloudflare의 실행 환경에서 실제 브라우저를 자동화할 수 있도록 하는 서비스다.
예를 들어 다음 작업에 활용할 수 있다.
- 웹페이지의 Screenshot 생성
- JavaScript가 실행된 페이지의 내용 추출
- 링크 수집
- 자동화된 브라우저 테스트
- AI 에이전트의 웹페이지 탐색
이번 업데이트는 일반적인 Chrome 사용자를 대상으로 하는 Chrome Stable 릴리스가 아니다.
Cloudflare Browser Run의 내부 실행 브라우저 버전 변경이다.
따라서 최근 발표된 Chrome 158 Beta와도 별개의 소식이다.
Chrome 152 사용
새로운 Browser Run Session은 Chrome 152를 기본으로 사용한다.
이를 통해 해당 버전에서 제공하는 렌더링 수정과 보안 패치, 웹 플랫폼 기능을 사용할 수 있다.
기존 코드를 변경할 필요는 없다.
하지만 Browser Run을 통해 실제 웹페이지의 동작을 검사하는 경우에는 브라우저 버전이 변경됐다는 사실을 고려해야 한다.
예를 들어 특정 CSS 기능의 지원 여부가 달라질 수 있다.
또한 JavaScript 실행과 네트워크 요청 처리 방식에서도 이전 브라우저와 차이가 나타날 수 있다.
따라서 자동화된 Screenshot Test의 결과가 달라졌다면 실제 애플리케이션의 코드 변경뿐 아니라 Browser Run의 버전 변경도 확인해야 한다.
기본 User-Agent 변경
이번 업데이트에서는 Quick Actions의 기본 User-Agent도 변경됐다.
기존에는 Chrome 119로 표시됐다.
변경 이후에는 Chrome 152로 표시된다.
이러한 차이는 웹사이트의 접근 제어에 영향을 줄 수 있다.
예를 들어 어떤 사이트가 특정 User-Agent 문자열에 해당하는 요청만 허용한다고 가정해 보자.
기존에는 Browser Run의 User-Agent와 일치했지만 새로운 버전에서는 문자열이 달라질 수 있다.
따라서 해당 규칙이 더 이상 일치하지 않을 수 있다.
Cloudflare는 필요한 경우 userAgent 인자를 이용해 직접 값을 지정할 수 있다고 안내한다.
하지만 단순히 User-Agent 문자열을 변경한다고 해서 실제 브라우저 엔진의 버전이 바뀌는 것은 아니다.
User-Agent는 요청에 포함되는 식별 정보다.
실제 실행 브라우저와는 구분해야 한다.
/links 응답 변경
이번 업데이트에서는 링크 추출 결과도 변경됐다.
/links는 이제 상대 URL이 아니라 절대 URL을 반환한다.
예를 들어 웹페이지 안에 다음 Link가 있다고 가정해 보자.
href="/docs"
기존 구현에서는 /docs라는 상대 경로가 반환될 수 있었다.
새로운 방식에서는 다음과 같이 절대 URL이 반환된다.
https://example.com/docs
이러한 구조는 AI 에이전트가 링크를 따라 이동하는 경우에 유용하다.
상대 URL을 직접 현재 페이지의 주소와 조합하는 작업이 줄어들기 때문이다.
하지만 기존 코드가 상대 URL을 기대했다면 호환성 문제가 발생할 수 있다.
예를 들어 반환된 값 앞에 다시 도메인을 붙이는 코드가 존재한다면 잘못된 URL이 생성될 수 있다.
따라서 /links의 결과를 직접 처리하는 프로젝트에서는 데이터 형식의 변경을 확인해야 한다.
Link-only Crawl 개선
Browser Run의 Link-only Crawl은 이제 상대 URL도 따라갈 수 있다.
예를 들어 웹사이트의 여러 페이지를 탐색할 때 대부분의 내부 링크는 상대 URL로 작성될 수 있다.
새로운 동작은 이러한 링크를 올바르게 해석해 Crawl을 계속할 수 있도록 한다.
다만 링크를 따라갈 수 있다는 사실이 해당 페이지의 모든 콘텐츠를 자동으로 수집할 수 있다는 의미는 아니다.
웹사이트의 접근 제어와 Robot 정책, 네트워크 오류 등에 영향을 받을 수 있다.
Markdown 출력 개선
이번 업데이트에서는 /markdown과 Crawl Markdown 출력에서 코드 블록의 언어 정보를 유지하도록 개선됐다.
예를 들어 원본 웹페이지에 TypeScript 코드 블록이 있다고 가정해 보자.
Markdown으로 변환한 이후에도 해당 코드가 TypeScript라는 정보를 유지할 수 있다.
이는 기술 문서를 자동으로 수집하는 도구에서 유용하다.
코드 블록의 언어 정보가 사라지면 이후 Syntax Highlighting이나 문서 분석에서 문제가 발생할 수 있기 때문이다.
하지만 Markdown 변환이 원본 HTML의 모든 의미와 구조를 완벽하게 보존한다는 뜻은 아니다.
복잡한 표나 동적 UI에서는 변환 결과를 검증해야 할 수 있다.
오류 처리와 보안 인증서
그 밖에도 다음 변경이 포함됐다.
- 큰 Response Header를 사용하는 페이지의 로딩 개선
- 신뢰할 수 없는 Root Certificate 제거
- Quick Actions의 Timeout 오류 형식 통일
- Outbound Worker의 Exception을 브라우저에 HTTP 오류로 전달
이러한 변경은 자동화 도구가 오류의 원인을 구분하는 데 도움이 될 수 있다.
예를 들어 이전에는 특정 Timeout 오류가 다른 형식으로 반환됐지만 새로운 방식에서는 보다 일관되게 처리할 수 있다.
다만 실제 오류 응답을 Parsing하는 코드에서는 반환 형식이 변경됐는지 확인해야 한다.
이번 발표는 Browser Run의 성능 개선을 정량적인 수치로 제시하지 않았다.
브라우저 실행 버전을 업데이트하면서 자동화 도구가 사용하는 User-Agent와 링크 추출, Markdown 출력의 동작을 함께 개선한 개발 도구 업데이트다.
GitLab Orbit 정식 출시 — 코드·MR·CI·배포·취약점·소유자 정보를 하나의 그래프로 연결
- 발표일: 2026-10-29 — 정확한 게시 시각 미확인
- 조사 범위 포함 근거: 게시 날짜만 확인되며 한국 시간 기준 조사 범위와 날짜가 겹침
- 발표 주체: GitLab
- 안정화 단계: Generally Available
- 적용 대상: GitLab Premium, Ultimate
- 원문: https://about.gitlab.com/blog/gitlab-orbit-ga/
GitLab이 소프트웨어 개발 수명주기의 여러 데이터를 하나의 그래프로 연결해 AI 에이전트와 개발자가 조회할 수 있도록 하는 GitLab Orbit을 정식 출시했다.
이번 발표는 GitLab의 일반적인 Repository 검색 기능과 다르다.
기존 검색 기능에서는 특정 파일이나 Merge Request를 찾을 수 있다.
하지만 실제 개발 업무에서는 여러 종류의 정보가 서로 연결돼 있다.
예를 들어 하나의 보안 취약점이 존재한다고 가정해 보자.
개발자는 다음 사항을 확인해야 할 수 있다.
- 어떤 패키지에 취약점이 존재하는가?
- 어떤 서비스에서 해당 패키지를 사용하는가?
- 어떤 Merge Request에서 해당 코드가 변경됐는가?
- 해당 변경은 어느 환경에 배포됐는가?
- 누가 해당 서비스의 소유자인가?
이러한 질문에 답하려면 Repository와 Pipeline, 배포 환경, 보안 데이터 및 조직 정보를 함께 확인해야 한다.
GitLab Orbit은 이러한 데이터를 연결 관계를 가진 그래프 형태로 관리한다.
Context Graph
Orbit은 다음 데이터를 연결한다.
- 소스코드
- Merge Request
- Issue와 Work Item
- CI/CD Pipeline
- 배포 환경
- 보안 취약점
- 코드와 서비스의 소유자
예를 들어 특정 함수가 변경됐다고 가정해 보자.
Orbit은 해당 함수를 호출하는 다른 코드와 관련 서비스, 최근 변경 사항을 연결해 제공할 수 있다.
이렇게 하면 AI 에이전트가 단순히 현재 파일의 코드만 보고 판단하지 않도록 지원한다.
특히 코드 변경의 영향 범위를 분석하는 작업에서 활용할 수 있다.
예를 들어 공통 패키지를 변경할 때 어떤 프로젝트와 서비스가 영향을 받는지 확인하는 상황이다.
일반적인 RAG와의 차이
RAG(Retrieval-Augmented Generation)는 모델이 외부 문서나 데이터를 검색해 답변에 활용하도록 하는 구조다.
일반적인 코드 RAG에서는 Repository의 파일을 청크로 나누어 인덱싱할 수 있다.
이후 질문과 의미적으로 비슷한 코드를 검색한다.
하지만 검색된 코드가 어떤 Pipeline을 통과했고 실제로 어디에 배포됐는지까지 자동으로 연결되지는 않을 수 있다.
Orbit은 코드 자체뿐 아니라 코드와 관련된 개발 수명주기의 관계를 관리한다.
따라서 다음과 같은 질문을 처리하도록 설계됐다.
- 어떤 변경이 이 서비스를 배포했는가?
- 이 취약점은 실제로 어느 환경에 영향을 주는가?
- 어떤 CI Job이 여러 프로젝트에서 반복적으로 실패하는가?
이러한 정보는 단순한 파일 검색 결과보다 더 많은 운영 Context를 제공할 수 있다.
AI 에이전트의 연결 방식
Orbit은 여러 도구에서 사용할 수 있다.
대표적으로 다음과 같다.
- GitLab Duo Agent Platform
- Claude Code
- Codex
- Cursor
- OpenCode
- Gemini CLI
- Antigravity
- 사용자 정의 MCP Client
GitLab CLI에서는 다음 명령을 이용해 연결을 준비할 수 있다.
glab orbit setup
또한 REST API와 Data Explorer에서도 그래프를 조회할 수 있다.
따라서 특정 AI 모델 하나에 종속되는 구조는 아니다.
GitLab 권한 체계 유지
이번 기능에서 중요한 부분은 기존 GitLab의 접근 권한을 그대로 반영하도록 설계됐다는 점이다.
예를 들어 사용자가 특정 비공개 프로젝트를 읽을 권한이 없다고 가정해 보자.
AI 에이전트가 Orbit을 조회한다고 해서 해당 사용자가 원래 접근할 수 없던 프로젝트의 데이터를 자동으로 제공해서는 안 된다.
GitLab은 Orbit이 기존 그룹과 역할, 보안 영역의 접근 권한을 반영한다고 설명한다.
따라서 Graph를 별도로 운영하더라도 기존 GitLab 권한과 일관된 접근 제어를 제공하는 것을 목표로 한다.
하지만 권한을 반영한다는 설명이 모든 데이터 처리 과정의 보안 문제가 자동으로 해결된다는 의미는 아니다.
외부 에이전트에 전달하는 데이터와 인증 정보의 관리 역시 중요하다.
공식 성능 발표
GitLab은 자체 테스트에서 다음 결과를 보고했다.
| 측정 항목 | 발표 결과 |
|---|---|
| 에이전트의 재시도 횟수 | 최대 45배 감소 |
| 같은 모델과 작업의 토큰 사용량 | 최대 4.5배 감소 |
또한 Compare the Market의 실제 Merge Request 79건을 대상으로 코드 리뷰 결과를 비교했다고 설명했다.
해당 사례에서 Orbit을 사용하는 리뷰어는 기대한 Inline Comment 가운데 약 70%를 일치시켰다.
반면 일반적인 RAG 기반 리뷰어의 일치율은 약 58%였다.
하지만 이러한 수치는 GitLab과 고객사가 공개한 특정 테스트 결과다.
모든 Repository와 모든 AI 모델에서 같은 비율의 정확도 개선이 발생한다고 단정할 수 없다.
또한 재시도 횟수의 감소가 모든 경우에서 실제 코드 품질의 향상을 의미하는 것도 아니다.
실제 평가 기준과 데이터셋에 따라 결과가 달라질 수 있다.
데이터 규모와 운영 구조
GitLab은 내부 인프라에서 150만 개 이상의 프로젝트와 약 8TB의 그래프 데이터를 2시간 이내에 인덱싱할 수 있다고 설명한다.
이는 GitLab이 자체적으로 공개한 운영 수치다.
개별 고객의 Repository를 같은 시간 안에 처리한다는 보장은 아니다.
또한 Orbit의 인덱싱은 GitLab의 일반적인 요청 처리와 분리된 서비스에서 실행된다.
따라서 그래프 조회 부하가 기존 GitLab 서비스의 응답에 직접적인 영향을 주지 않도록 설계했다.
가격과 지원 범위
현재 정식 지원 대상은 GitLab Premium과 Ultimate다.
GitLab.com에서 먼저 제공한다.
Self-Managed와 GitLab Dedicated 환경에는 이후 몇 주에 걸쳐 제공할 예정이다.
따라서 모든 GitLab 설치 환경에서 즉시 동일한 기능을 사용할 수 있는 것은 아니다.
Orbit은 Query Pack 기반으로 과금한다.
공식 발표 기준으로 하나의 Query Pack은 다음과 같다.
- 월 30달러 상당의 GitLab Credit
- 조직의 공유 Query Pool에 1,000개 Query 추가
공유 Pool을 초과한 사용량에는 별도의 요금이 적용될 수 있다.
이번 발표는 코드 검색과 개발 수명주기 데이터를 연결해 AI 에이전트가 파일 단위의 정보뿐 아니라 실제 서비스의 변경·배포·보안 관계까지 조회할 수 있도록 한 플랫폼 기능이다.
📚 추천 글
From 10 Seconds to Under 1: How I Made a Site 10x Faster
- 저자: Isaque Silva Chaves
- 매체: DEV Community
- 게시일: 2026-10-30
- 원문: https://dev.to/isaque_silvachaves/from-10-seconds-to-under-1-how-i-made-a-site-10x-faster-5hgd
실제 운영 중인 대규모 웹 애플리케이션에서 초기 JavaScript와 CSS의 다운로드 크기를 약 60% 줄이고, 첫 화면 표시 전에 실행되는 JavaScript CPU 작업을 약 76% 감소시킨 과정을 설명한 기술 글이다.
이번 글은 일반적인 프론트엔드 성능 최적화 목록과 다르다.
저자는 실제 코드베이스에서 어떤 모듈이 불필요하게 초기 Bundle에 포함됐는지 분석했다.
그리고 각 변경 이후 실제 Bundle 크기와 CPU 실행 시간을 측정했다.
특히 다운로드해야 하는 JavaScript의 크기와 실제로 실행하는 JavaScript의 CPU 비용이 서로 다른 문제라는 점을 구체적으로 보여준다.
프로젝트의 규모
저자가 최적화한 애플리케이션은 다음과 같은 구조를 가진다.
- Meteor 2.16
- React 기반 UI 패널
- 약 1,150개의 클라이언트 Template
- 약 9,000개의 Lazy Module
- 대규모 공통 UI 및 데이터 처리 코드
이 애플리케이션에서는 여러 화면을 하나의 큰 클라이언트 코드베이스에서 제공하고 있었다.
하지만 실제로 사용자가 처음 방문한 화면에는 전체 Template이 필요하지 않았다.
그럼에도 상당수의 모듈이 초기 Bundle에 포함돼 있었다.
최적화 전후의 전체 결과
저자가 공개한 최종 번들 크기는 다음과 같다.
| 항목 | 변경 전 | 변경 후 | 변화 |
|---|---|---|---|
| 초기 JavaScript Gzip 크기 | 5,666KB | 2,042KB | 64.0% 감소 |
| JavaScript + CSS | 6,070KB | 2,434KB | 59.9% 감소 |
실제 Production에서 홈페이지가 표시되기까지 걸린 시간도 개선됐다.
초기에는 약 12.6초였다.
최적화 이후에는 약 0.8초 수준으로 감소했다.
다만 이 두 수치를 동일한 통제 조건의 Benchmark로 해석해서는 안 된다.
저자는 최종 Production 측정에서 동일 계정과 기기의 다섯 번 로딩에 대한 중앙값이 약 0.89초였다고 설명한다.
반면 Cold Cache를 사용한 로컬 테스트에서는 별도로 성능을 측정했다.
따라서 Production에서 관찰한 개선과 실제 변경 전후를 같은 환경에서 비교한 결과는 구분해야 한다.
Cold Cache 실험
저자는 동일한 환경에서 다음 결과를 측정했다.
| 지표 | 초기 | 주요 Template 이동 이후 |
|---|---|---|
| 초기 Bundle 모듈 수 | 11,177개 | 5,041개 |
| 초기 모듈 실행 CPU | 6,113ms | 1,454ms |
| 가장 긴 Main-thread Task | 6,476ms | 2,053ms |
| FCP | 8,704ms | 2,900ms |
| LCP | 8,988ms | 4,068ms |
초기 모듈 실행 CPU는 약 76.2% 감소했다.
가장 긴 Main-thread Task는 약 68.3% 감소했다.
LCP도 절반 이상 줄었다.
이 결과는 사용자가 실제로 처음 화면을 보기 전에 수행하던 JavaScript 작업이 매우 많았다는 사실을 보여준다.
하지만 측정 환경은 특정 Linux 서버나 실제 사용자 기기가 아니라 저자가 구성한 로컬 테스트 환경이다.
실제 기기의 CPU와 네트워크 상황에 따라 결과가 달라질 수 있다.
처음에는 다운로드 크기만 줄였다
저자는 초기 작업에서 여러 라이브러리를 제거하거나 Lazy Loading으로 변경했다.
대표적인 대상은 다음과 같다.
- Sentry의 일부 SDK
- 중복 포함된 PDF.js
- Timezone 처리 라이브러리
- 사용하지 않는 Feature Route
이 작업으로 초기 JavaScript 크기가 약 622KB 감소했다.
하지만 빠른 로컬 네트워크에서는 LCP가 크게 개선되지 않았다.
반면 느린 네트워크에서는 효과가 더 컸다.
왜 이러한 차이가 발생했을까?
실제 문제의 상당 부분이 네트워크 다운로드 이후의 JavaScript 실행에 존재했기 때문이다.
가장 큰 문제는 JavaScript Long Task
저자가 측정한 초기 실행 흐름은 다음과 같았다.
| 시점 | 작업 |
|---|---|
| 21ms | HTML 도착 |
| 181ms | CSS 도착 |
| 59~1,322ms | JavaScript 다운로드 |
| 1,432~6,747ms | 하나의 긴 JavaScript Task 실행 |
| 6,878ms | WebSocket 연결 |
| 6,884ms | FCP |
| 7,724ms | LCP |
JavaScript 파일은 약 1.3초 안에 다운로드됐다.
하지만 그 이후 메인 스레드는 약 5.3초 동안 긴 작업을 실행했다.
이 작업이 종료된 이후에야 첫 화면이 표시됐다.
따라서 단순히 CDN을 변경하거나 네트워크를 빠르게 만드는 것만으로는 전체 문제를 해결하기 어려웠다.
다운로드가 끝난 JavaScript를 브라우저가 실제로 실행하는 데 필요한 CPU 시간이 병목이었다.
기존 분석 도구의 한계
저자는 Source Map 기반의 Bundle Visualizer를 사용했다.
하지만 일부 생성 코드는 Source Map에서 정확한 원본 파일과 연결되지 않았다.
예를 들어 Meteor가 생성한 Template 코드는 실제로 다른 모듈의 크기로 집계될 수 있었다.
그 결과 어떤 모듈이 실제 Bundle 크기를 증가시키는지 정확하게 판단하기 어려웠다.
저자는 이를 해결하기 위해 자체 분석 도구를 작성했다.
대표적으로 다음 도구를 만들었다.
measureEagerBundle.jsmeasureModuleTime.jsedges.js
첫 번째 도구는 실제 생성된 Bundle에서 모듈별 크기를 계산했다.
두 번째 도구는 각 모듈의 실행 시간을 측정했다.
세 번째 도구는 어떤 Import가 특정 패키지를 초기 Bundle로 끌어들이는지 추적했다.
Module별 실행 시간 측정
저자는 Meteor의 모듈 설치 함수에 Instrumentation을 추가했다.
각 모듈이 실행될 때 시작 시각과 종료 시각을 기록했다.
그리고 자식 모듈의 실행 시간을 제외해 해당 모듈 자체가 사용한 CPU 시간을 계산했다.
이러한 방식으로 Sampled Profile보다 실제 초기 실행 구조를 더 정확하게 분석할 수 있었다.
예를 들어 기존 Sampling 결과에서는 특정 데이터베이스 모듈이 약 2,160ms를 사용한 것으로 나타났다.
하지만 직접 측정한 결과 해당 모듈 자체의 실행 시간은 약 280ms였다.
즉 기존 Profile에서는 자식 모듈이나 연결된 라이브러리의 실행 비용이 포함돼 있었다.
이러한 차이는 실제 최적화 대상을 선택하는 데 영향을 준다.
작은 파일이 큰 CPU 비용을 발생시킨 사례
저자가 발견한 대표적인 문제는 동영상 녹화 컴포넌트였다.
해당 컴포넌트는 react-media-recorder를 사용했다.
이 라이브러리는 내부적으로 standardized-audio-context를 가져왔다.
문제는 해당 라이브러리가 초기화 과정에서 많은 작업을 수행한다는 점이었다.
저자는 실제로 해당 컴포넌트의 초기 실행 비용이 약 1,032ms라는 사실을 확인했다.
하지만 해당 컴포넌트는 홈페이지에서 즉시 사용할 필요가 없었다.
특정 기능을 열었을 때만 필요한 컴포넌트였다.
따라서 해당 Import를 Lazy Loading으로 변경했다.
결과는 다음과 같았다.
- 초기 JavaScript 약 30KB 감소
- FCP 약 14% 개선
- LCP 약 14% 개선
- 가장 긴 Task 약 23% 감소
흥미로운 점은 Bundle 크기 감소가 상대적으로 작았다는 것이다.
하지만 실제 CPU 실행 비용은 크게 감소했다.
이는 라이브러리의 다운로드 크기만으로 초기 실행 비용을 판단할 수 없다는 사실을 보여준다.
22개의 Import 경로에서 로딩된 테이블 라이브러리
다음 문제는 material-react-table이었다.
처음에는 해당 라이브러리를 사용하는 Wrapper 하나만 Lazy Loading으로 변경했다.
하지만 기대한 효과가 나타나지 않았다.
다른 파일에서도 같은 라이브러리를 Import하고 있었기 때문이다.
저자가 확인한 Import 경로는 총 22개였다.
이 경로들은 37개의 모듈에서 연결돼 있었다.
따라서 하나의 Import만 변경하면 실제 Bundle에서 라이브러리가 제거되지 않았다.
저자는 여러 컴포넌트를 함께 Lazy Loading으로 변경했다.
그 결과 다음 효과가 나타났다.
- 초기 JavaScript 약 302KB 감소
- 모듈 실행 시간 약 774ms 감소
이 사례는 하나의 무거운 라이브러리를 제거하려면 실제로 어떤 Import가 해당 라이브러리를 가져오는지 모두 확인해야 한다는 점을 보여준다.
가장 큰 개선: Template Tree의 Lazy Loading
저자의 프로젝트에는 약 1,148개의 Template이 존재했다.
하지만 모든 화면에서 필요한 Template은 약 135개뿐이었다.
나머지 Template은 특정 Route에서만 필요했다.
저자는 이를 이용해 Template Tree를 분리했다.
초기 화면에서 필요한 Template만 처음에 로딩한다.
다른 Template은 해당 Route에 접근할 때 다운로드한다.
이 변경만으로 초기 JavaScript 크기가 약 2MB 감소했다.
초기 Bundle의 모듈 수 역시 크게 줄었다.
다만 Lazy Loading으로 이동한 코드가 필요 없어지는 것은 아니다.
사용자가 해당 Route에 처음 접근하면 코드를 다운로드해야 한다.
따라서 초기 로딩 속도와 이후 Navigation 속도 사이에 트레이드오프가 존재한다.
Lazy Loading 이후 발생한 오류
저자는 최적화 과정에서 여러 문제가 발생했다고 설명한다.
대표적인 사례는 다음과 같다.
- Side Effect Module이 실행되지 않아 Form Validation 실패
- 전역 객체를 등록하던 모듈이 Lazy Loading으로 이동해 함수가 존재하지 않음
- Lazy CSS가 나중에 로딩되면서 CSS Cascade 변경
- Route가 자신의 Chunk를 기다리는 순환 의존성 발생
- 로그인 이후 Redirect 상태의 처리 순서 변경
- Production에서만 발생하는 Dynamic Import 경로 오류
특히 마지막 문제는 중요하다.
일반적인 Lint와 Build가 성공하더라도 실제 Dynamic Import가 실행되는 시점에만 오류가 발생할 수 있다.
저자는 이를 방지하기 위해 변경한 페이지마다 다음 상황을 검사했다.
- 새로운 브라우저에서 직접 URL 열기
- Hard Reload
- 애플리케이션 내부 Navigation
- 느린 네트워크에서 재실행
이러한 테스트는 Lazy Loading 이후 실제 사용자 동작이 유지되는지 확인하기 위한 것이다.
CDN과 버전별 Chunk
Lazy Loading이 확대되면서 새로운 문제가 발생했다.
초기 Bundle은 작아졌지만 Route에 처음 접근할 때 많은 Lazy Module이 필요해졌다.
기존에는 약 2,591개의 모듈을 개별적으로 요청할 수 있었다.
이러한 방식에서는 HTTP 요청 수가 지나치게 많아질 수 있었다.
저자는 이를 해결하기 위해 모듈을 여러 Chunk로 묶었다.
최종적으로 약 2,591개의 모듈을 24개 Chunk와 하나의 Index로 제공하도록 변경했다.
또한 Chunk 이름에 빌드 버전 정보를 포함했다.
이렇게 하면 이전 배포의 브라우저 탭이 새로운 배포의 다른 코드와 혼합되는 문제를 줄일 수 있다.
하지만 Chunk를 지나치게 크게 만들면 사용자가 실제로 필요하지 않은 코드까지 다운로드할 수 있다.
반대로 너무 작게 만들면 요청 수가 증가한다.
따라서 Chunk의 크기와 요청 수 사이에서 균형을 찾아야 한다.
실제 운영 결과와 한계
저자는 최종 Production에서 약 0.89초의 로딩 중앙값을 관찰했다.
하지만 저자는 해당 수치가 전체 사용자의 P75나 P99가 아니라고 명시한다.
특정 계정과 기기에서 수행한 다섯 번의 측정 결과다.
또한 일부 개선은 Meteor 2.16의 모듈 로딩 방식에 직접적으로 영향을 받는다.
따라서 Vite나 최신 Next.js 프로젝트에서 같은 코드를 적용한다고 해서 동일한 결과가 발생하는 것은 아니다.
이번 글은 프론트엔드 성능 문제를 Bundle 크기와 CPU 실행 비용, 네트워크 요청, 실제 Route 로딩이라는 서로 다른 원인으로 분리하고 각각의 효과를 측정한 사례다.
특히 실제로 사용하지 않는 코드를 처음부터 실행하지 않도록 변경하는 과정과 그 과정에서 발생하는 호환성 문제를 구체적으로 설명한다.
Optimizing the sh*t out of TanStack Start
- 저자: Sarthak Mishra
- 매체: 개인 기술 블로그
- 게시일: 2026-10-26
- 최종 갱신일: 2026-10-30
- 원문: https://sarthakmishra.com/blog/optimizing-tanstack-start
TanStack Start로 개발한 웹사이트에서 불필요한 JavaScript와 이미지, Font 요청을 줄여 모바일 Lighthouse 성능 점수를 73점에서 100점으로 개선한 과정을 설명한 글이다.
이번 글은 앞서 소개한 대규모 Meteor 애플리케이션 최적화 사례와 문제의 규모가 다르다.
저자는 상대적으로 작은 개인 웹사이트를 대상으로 성능을 개선했다.
하지만 실제 측정에서는 간단한 사이트에서도 불필요한 JavaScript와 CSS, Font 때문에 LCP가 크게 증가할 수 있다는 사실을 확인했다.
측정한 성능 결과
저자는 세 시점의 PageSpeed Insights 결과를 비교했다.
| 지표 | 10월 24일 | 10월 26일 | 10월 30일 |
|---|---|---|---|
| Lighthouse Performance | 73 | 100 | 100 |
| FCP | 2.3초 | 1.2초 | 0.9초 |
| LCP | 5.2초 | 1.7초 | 0.9초 |
| TBT | 160ms | 0ms | 0ms |
| CLS | 0.002 | 0.001 | 0.001 |
| Speed Index | 4.5초 | 1.2초 | 2.4초 |
가장 중요한 개선은 LCP다.
초기에는 5.2초였다.
최종 측정에서는 0.9초로 감소했다.
하지만 모든 지표가 지속적으로 개선된 것은 아니다.
예를 들어 10월 30일의 Speed Index는 10월 26일보다 느렸다.
따라서 Lighthouse 점수가 동일하게 100이라고 해서 모든 지표가 항상 개선됐다고 판단할 수는 없다.
또한 이 결과는 여러 변경이 함께 적용된 상태에서 측정한 개별 PageSpeed Insights 실행 결과다.
각 변경의 효과를 완전히 분리한 통제 실험은 아니다.
실제 LCP 요소부터 확인
저자는 처음에 LCP가 큰 이미지 때문에 느릴 것이라고 예상할 수 있었다.
하지만 실제 LCP 요소는 이미지가 아니었다.
일반적인 텍스트 문단이었다.
이 문단은 이미 서버에서 생성한 HTML에 포함돼 있었다.
따라서 이미지 다운로드를 최적화해도 해당 문단의 표시 시점이 반드시 빨라지는 것은 아니었다.
저자는 브라우저의 Network Panel과 실제 HTML 응답을 확인했다.
HTML은 약 110ms 안에 도착했다.
문단 역시 HTML에 존재했다.
하지만 브라우저가 이를 화면에 표시하기까지 다른 JavaScript와 CSS 작업이 발생하고 있었다.
즉 서버 렌더링 자체가 느린 문제가 아니었다.
전체 이미지 메타데이터의 불필요한 로딩
저자는 사이트에서 이미지 정보를 Manifest로 관리하고 있었다.
이 Manifest에는 약 117개의 이미지 정보가 포함돼 있었다.
문제는 특정 카드 컴포넌트가 해당 Manifest를 Import하고 있었다는 것이다.
그 결과 실제 화면에서는 이미지 몇 개만 사용하더라도 전체 Manifest가 초기 JavaScript에 포함됐다.
Manifest의 압축 크기는 약 35KiB였다.
저자는 각 Route에서 필요한 이미지 정보만 가져오도록 변경했다.
그 결과 초기 Module Preload JavaScript가 약 234KiB에서 196KiB로 감소했다.
반면 HTML 크기는 약 42KiB에서 49KiB로 증가했다.
이는 선택한 이미지 정보가 HTML에 포함됐기 때문이다.
초기 JavaScript를 줄이는 과정에서 HTML 크기가 증가하는 트레이드오프가 발생한 것이다.
하지만 전체 Manifest를 모든 페이지에서 다운로드할 필요가 없어졌다는 점에서 이점이 있었다.
Dialog의 Lazy Loading
저자는 다음 기능이 처음부터 JavaScript를 다운로드하고 있다는 사실도 발견했다.
- 검색 Dialog
- 모바일 메뉴
- 이미지 확대 Viewer
- 프로필 Viewer
- Form 이탈 확인 Dialog
이러한 기능은 사용자가 실제로 열기 전까지 필요하지 않을 수 있다.
하지만 컴포넌트가 렌더링 트리에 포함돼 있다면 open={false} 상태이더라도 관련 코드가 다운로드될 수 있다.
저자는 Dialog를 실제로 처음 열 때 렌더링 트리에 추가하도록 변경했다.
또한 Hover나 Focus, Pointer 동작을 이용해 사용자가 열 가능성이 높아진 시점에 코드를 미리 가져오도록 했다.
하지만 모바일에서는 Hover가 일반적인 사용자 입력이 아니다.
따라서 첫 번째 Tap 이후 코드를 다운로드해야 하는 상황이 발생할 수 있다.
저자는 이를 받아들였다.
모든 사용자에게 처음부터 Dialog 코드를 다운로드하게 하는 것보다 실제 사용하는 사용자만 관련 코드를 가져오는 편이 적합하다고 판단했기 때문이다.
숨겨진 이미지도 다운로드될 수 있다
이번 글에서 중요한 발견은 CSS로 숨긴 이미지가 반드시 네트워크 요청에서 제외되는 것은 아니라는 사실이다.
예를 들어 데스크톱 전용 Navigation을 모바일 화면에서 숨겼다고 가정해 보자.
CSS에서는 해당 Navigation을 보이지 않도록 할 수 있다.
하지만 HTML에 이미지의 URL이 존재한다면 브라우저가 해당 이미지를 미리 다운로드할 수 있다.
저자의 프로젝트에서는 모바일에서 보이지 않는 Navigation 때문에 아홉 개의 이미지 요청이 발생했다.
이를 해결하기 위해 이미지의 로딩 전략을 변경했다.
또한 데스크톱 화면에서 실제로 필요한 시점에 이미지를 Decode하도록 구성했다.
그 결과 모바일 화면에서는 불필요한 이미지 요청을 제거할 수 있었다.
하지만 데스크톱에서는 Navigation의 이미지가 약간 늦게 표시될 수 있었다.
이 역시 초기 네트워크 사용량과 UI 표시 시점 사이의 트레이드오프다.
Font 최적화와 CLS
저자는 Font 파일의 크기도 줄였다.
하지만 새로운 문제가 발생했다.
Font가 더 빨리 다운로드되면서 실제 Font로 변경되는 시점이 앞당겨졌고, 해당 변경으로 텍스트의 크기가 달라졌다.
그 결과 일부 페이지에서 CLS가 약 0.21까지 증가했다.
CLS는 Cumulative Layout Shift다.
페이지가 표시되는 동안 요소의 위치가 얼마나 변경되는지 측정하는 지표다.
저자는 이를 해결하기 위해 Fallback Font의 Metrics를 조정했다.
사용한 CSS 속성은 다음과 같다.
size-adjustascent-overridedescent-overrideline-gap-override
이렇게 하면 실제 Web Font와 Fallback Font의 크기 차이를 줄일 수 있다.
하지만 해당 값은 모든 Font에서 동일하게 사용할 수 있는 상수가 아니다.
실제 Font와 Fallback Font의 조합에 따라 측정해야 한다.
Critical CSS 추출
저자는 CSS의 초기 로딩 비용도 개선하려고 했다.
먼저 불필요한 CSS Import를 제거했다.
이후 세 가지 방식을 비교했다.
- Route별 외부 CSS 유지
- Route CSS를 HTML에 Inline
- Critical CSS만 Inline하고 나머지는 나중에 로딩
Critical CSS는 처음 화면을 표시하는 데 필요한 스타일을 의미한다.
저자는 Beasties를 이용해 각 페이지의 HTML에서 필요한 CSS 규칙을 추출했다.
그 결과 Render-blocking CSS의 크기가 감소했다.
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| 페이지별 Render-blocking CSS | 20.6~29.7KiB | 7.0~14.9KiB |
그러나 이러한 개선에는 추가적인 유지보수 비용이 발생했다.
예를 들어 다음 상황을 처리해야 했다.
- Dark Mode의 CSS
- CSS Variable을 이용한 Font 선택
- Form Control의 선택 상태
- JavaScript가 실행되지 않은 환경
- 초기 HTML에 존재하지 않는 동적 콘텐츠
저자는 총 71개의 사전 렌더링 페이지를 여러 화면 크기와 테마에서 비교했다.
하지만 Critical CSS 추출 자체가 최종 Lighthouse 점수에 얼마나 기여했는지는 명확하게 구분할 수 없었다.
따라서 저자는 실제 CSS 병목이 확인되지 않은 프로젝트에 무조건 같은 구조를 도입하지 않겠다고 설명한다.
React Compiler의 번들 크기
저자는 Oxc 기반 React Compiler도 사용하고 있었다.
다섯 쌍의 비교 실험에서 실행 시간에는 큰 변화가 없었다.
반면 압축된 JavaScript 크기는 Route마다 약 9.5~14.1KiB 증가했다.
저자는 이러한 비용에도 React Compiler를 유지했다.
수동으로 작성한 Memoization 코드를 줄이는 유지보수상 이점이 있었기 때문이다.
이는 성능 최적화를 평가할 때 실행 시간과 Bundle 크기, 유지보수 비용을 함께 고려해야 한다는 사례다.
Zod의 Bundle 크기 비교
저자는 간단한 Search Parameter 검증에 사용하는 라이브러리도 비교했다.
결과는 다음과 같았다.
| 검증 방식 | Gzip 크기 |
|---|---|
| 직접 작성한 검증 함수 | 0.2KiB |
| Valibot 1.2 | 1.7KiB |
zod/mini | 6.5KiB |
| 일반 Zod | 24.3KiB |
| ArkType 2.1 | 47.4KiB |
이 결과는 해당 프로젝트의 Rolldown 구성에서 측정한 값이다.
다른 Bundler와 Tree Shaking 설정에서는 결과가 달라질 수 있다.
저자는 작은 검증에는 직접 작성한 함수를 사용했다.
반면 서버와 공유해야 하는 큰 Schema에는 zod/mini를 사용했다.
이렇게 하면 기존 Zod 생태계와의 호환성을 유지하면서 일부 Bundle 비용을 줄일 수 있었다.
최적화 이후의 유지보수
저자는 초기 JavaScript가 다시 증가하지 않도록 Build Budget도 설정했다.
예를 들어 다음 제한을 사용했다.
- 초기 JavaScript: 250,000바이트
- Module Preload: 최대 40개
- Render-blocking CSS: 17,500바이트
- 공유 콘텐츠 Index Chunk: 15,000바이트
하지만 Budget을 설정했다고 해서 성능 저하를 완전히 방지할 수 있는 것은 아니다.
저자는 Budget에 충분한 여유를 두었기 때문에 초기 JavaScript가 약 30KiB 증가해도 CI가 실패하지 않았다고 설명한다.
따라서 Budget과 함께 정기적인 Bundle Report도 확인해야 했다.
이번 글은 프론트엔드 성능 최적화가 단순히 JavaScript를 줄이는 작업이 아니라 실제 사용자에게 처음 필요한 정보와 나중에 필요한 기능을 구분하는 과정이라는 점을 보여준다.
특히 각 최적화의 효과와 유지보수 비용을 명확하게 구분하고, 측정으로 확인하지 못한 개선 효과는 단정하지 않는다는 점이 특징이다.
Fix Next.js Hydration Mismatch in Dashboards: Dates, Locale and Timezone
- 저자: Ridwan Soleh
- 매체: Dpop Studio
- 게시일: 2026-10-29
- 원문: https://dpopstudio.co/blog/fix-nextjs-hydration-mismatch-dashboard-dates-locale-timezone
실제 Next.js 대시보드에서 서버와 브라우저가 서로 다른 시간대 및 언어 설정을 사용하면서 발생한 Hydration 오류와 잘못된 매출 날짜 표시 문제를 해결한 기술 글이다.
이번 사례에서는 Hydration 경고를 단순한 개발 환경의 경고로 취급할 수 없었다.
실제로 사용자가 잘못된 날짜의 데이터를 확인하는 문제가 발생했기 때문이다.
실제로 발생한 오류
저자들이 운영한 대시보드에서는 판매 데이터를 날짜별로 표시하고 있었다.
개발 환경에서는 정상적으로 동작했다.
하지만 인도네시아 Makassar 지역의 사용자가 페이지를 열었을 때 문제가 발견됐다.
사용자는 오늘의 매출을 확인하려고 했다.
하지만 실제 화면에는 전날의 매출이 오늘의 제목 아래에 표시됐다.
원인은 서버와 클라이언트의 시간대 차이였다.
서버는 자신의 환경을 기준으로 날짜를 계산했다.
반면 브라우저는 사용자의 지역 시간대를 기준으로 날짜를 계산했다.
따라서 같은 Timestamp를 사용하더라도 서로 다른 날짜가 표시될 수 있었다.
Hydration Mismatch의 기본 구조
Next.js App Router에서는 서버에서 HTML을 생성한 뒤 브라우저에서 React가 Hydration을 수행할 수 있다.
이때 서버와 클라이언트의 첫 번째 렌더링 결과는 일치해야 한다.
예를 들어 서버에서 다음 문자열을 생성했다고 가정해 보자.
October 29
하지만 클라이언트에서는 다음 문자열을 생성한다.
October 30
React는 두 결과가 다르다는 사실을 발견한다.
그 결과 Hydration 경고가 발생할 수 있다.
문제는 단순히 Console에 빨간색 경고가 나타나는 것만이 아니다.
일부 상황에서는 React가 서버에서 생성한 HTML을 버리고 클라이언트에서 다시 렌더링할 수 있다.
이 경우 초기 렌더링에 필요한 JavaScript 작업이 증가할 수 있다.
환경에 따라 다른 결과를 생성하는 코드
저자들은 다음 종류의 코드를 점검했다.
new Date()toLocaleDateString()Intl.NumberFormat()Math.random()window.innerWidthlocalStorage
예를 들어 toLocaleDateString()을 인자 없이 호출하면 실행 환경의 기본 Locale과 Timezone을 사용할 수 있다.
하지만 서버와 브라우저의 기본값은 다를 수 있다.
또한 Math.random()을 렌더링 중에 호출하면 서버와 클라이언트가 서로 다른 값을 생성할 수 있다.
예를 들어 Chart의 Key를 난수로 생성하는 경우에는 Hydration 과정에서 불일치가 발생할 수 있다.
따라서 서버에서 렌더링하는 컴포넌트에서는 동일한 입력에 대해 동일한 초기 결과를 생성하도록 설계해야 한다.
Locale과 Timezone을 명시적으로 전달
저자들은 날짜 Formatter의 실행 환경에 의존하지 않도록 변경했다.
대표적인 방법은 다음과 같다.
Intl.DateTimeFormat()
Formatter에 다음 정보를 명시적으로 전달한다.
- Locale
- Timezone
예를 들어 사용자의 Timezone이 다음과 같다고 가정해 보자.
Asia/Makassar
서버와 클라이언트가 동일한 Timezone을 사용하면 같은 Timestamp를 같은 날짜와 시간으로 표현할 수 있다.
저자들은 사용자 프로필이나 Cookie에서 Timezone을 가져오도록 했다.
하지만 처음 방문한 사용자는 서버에서 아직 Timezone을 알 수 없을 수 있다.
이러한 경우에는 UTC를 Fallback으로 사용하고 해당 값이 UTC라는 사실을 명시적으로 표시했다.
중요한 점은 사용자의 Timezone을 추측해 잘못된 날짜를 표시하는 것보다 기준을 명확히 하는 편이 낫다는 것이다.
PostgreSQL의 Timestamp 타입
저자들은 데이터베이스에서도 관련 문제를 발견했다.
기존에는 일부 날짜 정보를 다음 타입으로 저장했다.
timestamp
하지만 해당 타입은 Timezone 정보를 별도로 포함하지 않는다.
저자들은 정확한 시각을 다루는 데이터에 다음 타입을 사용하도록 변경했다.
timestamptz
timestamptz는 PostgreSQL의 Timezone-aware Timestamp 타입이다.
다만 이 타입이 원래 입력한 지역의 Timezone 이름을 그대로 저장하는 것은 아니다.
실제 시각을 일관되게 저장하고, 표시할 때 필요한 Timezone으로 변환하도록 하는 데 적합하다.
따라서 사용자에게 표시하는 지역 시간은 별도의 Timezone 정보를 이용해 계산해야 한다.
브라우저에서만 알 수 있는 값
화면 너비와 같은 정보도 Hydration 문제를 발생시킬 수 있다.
예를 들어 다음 값을 사용한다고 가정해 보자.
window.innerWidth
이 값은 브라우저에서만 사용할 수 있다.
서버는 실제 사용자의 브라우저 너비를 직접 확인할 수 없다.
따라서 서버와 클라이언트에서 서로 다른 UI를 생성할 가능성이 있다.
저자들은 이를 해결하기 위해 다음 React Hook을 사용했다.
useSyncExternalStore()
이 Hook은 외부 상태를 React 렌더링과 연결할 수 있도록 한다.
특히 서버에서 사용할 Snapshot을 별도로 정의할 수 있다.
대표적인 구조는 다음과 같다.
subscribe: 상태 변경 구독getSnapshot: 클라이언트의 실제 값getServerSnapshot: 서버의 초기값
서버와 Hydration 초기 단계에서는 동일한 Snapshot을 사용한다.
이후 클라이언트에서 실제 화면 너비를 읽어 필요한 변경을 적용한다.
이렇게 하면 초기 렌더링 불일치를 줄일 수 있다.
suppressHydrationWarning의 한계
React는 특정 Hydration 경고를 숨기는 속성을 제공한다.
suppressHydrationWarning
하지만 이 속성은 실제 불일치의 원인을 해결하지 않는다.
저자들은 해당 속성을 제한적인 상황에서만 사용한다고 설명한다.
예를 들어 실시간 시계처럼 서버와 클라이언트의 문자열이 달라지는 것이 자연스러운 경우다.
하지만 실제 매출 날짜나 금액처럼 비즈니스 데이터의 의미가 달라지는 상황에서 경고를 숨기는 것은 적절하지 않다.
문제가 존재하는데도 해당 사실을 확인하기 어려워질 수 있기 때문이다.
실제 개선 결과
저자들은 다음 변화를 측정했다.
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| Hydration 경고 | 11건 | 0건 |
| Total Blocking Time | 약 420ms | 약 190ms |
TBT는 약 55% 감소했다.
측정에는 Lighthouse CI를 사용했다.
저자들은 약 20회의 실행 결과를 이용해 중앙값을 비교했다고 설명한다.
하지만 이는 해당 대시보드의 자체 측정 결과다.
모든 Next.js 애플리케이션에서 Hydration 오류를 수정하면 TBT가 같은 비율로 감소한다는 의미는 아니다.
또한 다른 코드 변경이나 실행 환경의 영향이 완전히 제거된 독립적인 실험이라고 볼 수는 없다.
다른 시간대에서 실행하는 E2E 테스트
저자들은 같은 문제가 다시 발생하지 않도록 Playwright 테스트에 Timezone을 지정했다.
대표적인 설정은 다음과 같다.
timezoneId: "Asia/Makassar"
이후 페이지를 열고 Console에 Hydration 관련 오류가 발생하는지 확인하도록 했다.
또한 서로 다른 두 Timezone에서 테스트할 것을 권장한다.
예를 들어 서버와 클라이언트가 모두 UTC인 환경에서만 테스트하면 실제 사용자 지역에서 발생하는 문제를 놓칠 수 있다.
이번 글은 Hydration 오류가 단순한 UI 경고가 아니라 실제 날짜와 데이터의 의미를 잘못 표시하는 버그가 될 수 있다는 점을 보여준다.
특히 날짜·숫자·Timezone과 브라우저 전용 상태를 다룰 때 서버와 클라이언트의 초기 렌더링 결과를 일관되게 만드는 방법을 구체적으로 설명한다.
Postgres Job Queues in 2026: SKIP LOCKED vs pg-boss vs BullMQ (Redis and Postgres), Benchmarked
- 저자: Waseem Anjum
- 매체: 개인 기술 블로그
- 게시일: 2026-10-28
- 원문: https://fabwaseem.com/blog/postgres-job-queue-pg-boss-vs-bullmq
Node.js 백엔드에서 비동기 작업을 실행하기 위한 Queue를 구현할 때 PostgreSQL을 직접 사용하는 방식과 pg-boss, BullMQ를 실제로 비교한 기술 글이다.
저자는 단순히 각 라이브러리의 기능 목록을 비교하지 않았다.
실제 서버에서 작업을 등록하고 처리하면서 다음 항목을 측정했다.
- 작업 등록 속도
- 작업 처리량
- 대기 중인 작업을 시작하기까지의 지연 시간
- Worker가 종료됐을 때의 복구 시간
- 데이터베이스 Transaction과의 연결
- 작업 하나당 저장 공간
특히 이번 글은 기본 설정만으로 측정한 Benchmark가 실제 라이브러리의 최대 성능을 제대로 보여주지 못할 수 있다는 점을 구체적으로 설명한다.
비교 대상
저자는 네 가지 구성을 비교했다.
| 구성 | 저장소 |
|---|---|
| 직접 구현한 SKIP LOCKED Queue | PostgreSQL |
| pg-boss 12 | PostgreSQL |
| BullMQ 6 | Redis |
| BullMQ 6 | PostgreSQL |
BullMQ는 기존에 Redis 기반 Job Queue로 널리 알려졌다.
하지만 BullMQ 6에서는 PostgreSQL Backend도 사용할 수 있다.
따라서 같은 BullMQ API를 유지하면서 Redis 대신 PostgreSQL을 선택할 수 있다.
다만 저장소가 달라지면 실제 처리량과 장애 복구 방식도 달라질 수 있다.
테스트 환경
저자는 다음 환경을 사용했다.
- Debian 13
- Intel Xeon 기반 VM
- 8 vCPU
- 약 15GB RAM
- PostgreSQL 18.6
- Redis 8.0.2
- Node.js
- Worker 10개
모든 구성요소는 같은 VM에서 실행됐다.
따라서 실제 네트워크를 통해 다른 서버와 통신하는 상황보다 네트워크 지연이 작다.
결과는 일반적으로 세 번 측정한 중앙값을 사용했다.
하지만 실제 서비스에서는 데이터베이스와 Worker가 서로 다른 서버에 존재할 수 있다.
따라서 네트워크 지연이 증가하면 결과도 달라질 수 있다.
기본 설정의 pg-boss가 초당 5개만 처리한 이유
가장 흥미로운 결과는 pg-boss의 기본 설정이었다.
Worker 10개를 실행했지만 초당 약 5개의 작업만 처리했다.
이 결과만 보면 pg-boss는 매우 느린 Queue처럼 보인다.
하지만 실제 원인은 Polling 설정이었다.
기본 설정에서는 Worker가 하나의 작업을 가져온 뒤 일정 시간 대기한다.
대표적인 설정은 다음과 같다.
batchSize: 1- Polling 간격: 2초
따라서 작업을 빠르게 완료하더라도 새로운 작업을 가져오기 전에 기다릴 수 있다.
저자는 이를 해결하기 위해 다음 설정을 사용했다.
batchSize: 50burstWhenBatchFull: true
변경 이후 처리량은 다음과 같았다.
초당 약 29,323개 작업
이는 기본 설정의 초당 5개와 매우 큰 차이다.
따라서 Benchmark에서는 라이브러리 자체뿐 아니라 실제 설정값이 중요하다.
처리량 비교
저자의 No-op 작업 처리 결과는 다음과 같다.
| 구성 | 처리량 |
|---|---|
| SKIP LOCKED, 한 번에 1개 | 12,588 jobs/s |
| SKIP LOCKED, 한 번에 50개 | 148,341 jobs/s |
| pg-boss 기본 설정 | 5 jobs/s |
| pg-boss Batch 최적화 | 29,323 jobs/s |
| BullMQ + Redis | 27,461 jobs/s |
| BullMQ + PostgreSQL | 7,045 jobs/s |
No-op 작업은 실제 업무 로직을 거의 수행하지 않는 작업이다.
따라서 Queue 자체의 오버헤드를 확인하는 데 사용할 수 있다.
하지만 실제 애플리케이션에서는 작업이 데이터베이스나 외부 API를 호출할 수 있다.
이 경우 Queue 자체의 처리량 차이가 전체 처리량에 미치는 영향은 작아질 수 있다.
실제 작업을 수행하는 경우
저자는 각 작업이 약 10ms 동안 실행되는 상황도 테스트했다.
동시에 실행할 수 있는 작업은 50개로 제한했다.
이론적인 최대 처리량은 다음과 같다.
초당 5,000개 작업
실제 결과는 다음과 같았다.
| 구성 | 처리량 |
|---|---|
| SKIP LOCKED | 4,189 jobs/s |
| pg-boss | 4,587 jobs/s |
| BullMQ + Redis | 4,469 jobs/s |
| BullMQ + PostgreSQL | 4,037 jobs/s |
네 구성의 차이가 상당히 줄었다.
실제 작업 시간이 존재하면 Queue의 내부 오버헤드보다 작업 자체의 실행 시간이 전체 처리량을 결정하는 비중이 커지기 때문이다.
따라서 No-op Benchmark에서 가장 빠른 Queue가 실제 서비스에서도 항상 가장 적합한 것은 아니다.
Polling과 LISTEN/NOTIFY
작업 처리량과 별도로 중요한 지표는 Pickup Latency다.
Pickup Latency는 새로운 작업이 등록된 이후 Worker가 해당 작업을 시작하기까지 걸린 시간이다.
예를 들어 이메일 발송 작업을 Queue에 등록했다고 가정해 보자.
Worker가 2초마다 새로운 작업을 확인한다면 이메일 발송이 지연될 수 있다.
PostgreSQL은 다음 기능을 제공한다.
LISTENNOTIFY
이를 이용하면 새로운 작업이 발생했을 때 Worker에 알림을 전달할 수 있다.
저자는 pg-boss에서 해당 기능을 활성화했다.
그 결과 대기 중인 작업의 시작 지연 시간이 크게 감소했다.
| 구성 | Pickup Latency P50 |
|---|---|
| pg-boss Polling | 약 972ms |
| pg-boss LISTEN/NOTIFY | 약 3.19ms |
| BullMQ + Redis | 약 1.25ms |
| BullMQ + PostgreSQL | 약 2.69ms |
하지만 LISTEN/NOTIFY에도 운영상 제약이 있다.
별도의 PostgreSQL 연결이 필요할 수 있다.
또한 PgBouncer의 Transaction Pooling Mode에서는 기대한 방식으로 동작하지 않을 수 있다.
따라서 실제 데이터베이스 연결 구조를 함께 고려해야 한다.
Worker 종료 이후 작업 복구
저자는 작업을 처리하는 Worker를 강제로 종료했다.
그리고 해당 작업이 언제 다시 실행 가능한 상태가 되는지 측정했다.
결과는 다음과 같았다.
| 구성 | 작업 재사용 가능 시점 |
|---|---|
| SKIP LOCKED | 약 0.01초 |
| BullMQ + Redis | 약 60초 |
| BullMQ + PostgreSQL | 약 60초 |
| pg-boss | 약 60초 |
SKIP LOCKED는 실행 중인 Transaction에서 Row Lock을 유지한다.
Worker가 종료되면 데이터베이스 연결이 닫힐 수 있다.
그러면 Transaction이 Rollback되고 Row Lock이 해제된다.
다른 Worker는 같은 작업을 다시 가져올 수 있다.
반면 BullMQ와 pg-boss는 작업의 실행 상태와 Lease 또는 Timeout을 관리한다.
따라서 Worker가 종료됐다는 사실을 발견할 때까지 일정 시간이 필요하다.
그러나 SKIP LOCKED가 항상 가장 적합한 복구 방식을 제공하는 것은 아니다.
긴 작업에서의 Transaction 문제
저자가 작성한 SKIP LOCKED 구현은 작업을 처리하는 동안 Transaction을 계속 열어 둔다.
예를 들어 작업이 10ms 안에 끝난다면 큰 문제가 아닐 수 있다.
하지만 외부 AI 모델 API를 호출해 몇 분 동안 기다리는 작업을 생각해 보자.
이 경우 Transaction이 몇 분 동안 유지될 수 있다.
오래 실행되는 Transaction은 다음 문제를 만들 수 있다.
- 데이터베이스 연결 장시간 점유
- Connection Pool 부족
- Vacuum 지연
- 다른 Query와의 자원 경쟁
따라서 장시간 작업에는 Transaction을 계속 열어 두는 방식보다 Lease 기반의 작업 관리가 적합할 수 있다.
하지만 Lease를 사용하면 Worker가 종료됐을 때 Lease가 만료될 때까지 기다려야 할 수 있다.
즉 빠른 장애 복구와 장시간 작업의 자원 사용 사이에 트레이드오프가 존재한다.
작업 등록과 데이터베이스 Transaction
이번 글에서 또 하나 중요한 부분은 작업 등록의 원자성이다.
예를 들어 사용자가 주문을 생성했다고 가정해 보자.
서비스는 다음 두 작업을 수행해야 한다.
- 주문 데이터를 저장한다.
- 주문 확인 이메일 작업을 등록한다.
문제는 두 작업 가운데 하나만 성공하는 경우다.
주문은 생성됐지만 이메일 작업이 등록되지 않을 수 있다.
반대로 이메일 작업은 등록됐지만 실제 주문 저장은 실패할 수 있다.
PostgreSQL 기반 Queue에서는 두 작업을 하나의 Transaction에 포함할 수 있다.
예를 들어 다음과 같은 구조다.
BEGIN
주문 INSERT
Job INSERT
COMMIT
이렇게 하면 두 작업이 함께 성공하거나 함께 Rollback될 수 있다.
저자는 pg-boss와 직접 구현한 SKIP LOCKED Queue에서 이러한 동작을 검증했다.
반면 BullMQ는 Redis Backend뿐 아니라 PostgreSQL Backend에서도 외부 애플리케이션의 Transaction을 직접 공유하지 못했다.
따라서 필요한 경우 Outbox Pattern을 사용해야 할 수 있다.
Outbox Pattern은 실제 비즈니스 데이터와 이벤트 기록을 하나의 데이터베이스 Transaction에서 저장하고 이후 별도 Worker가 이벤트를 처리하는 방식이다.
저장 공간 비교
저자는 대기 중인 작업 10만 개를 저장하고 저장 공간도 측정했다.
| 구성 | 작업 하나당 크기 |
|---|---|
| 직접 구현한 SKIP LOCKED | 213바이트 |
| pg-boss | 297바이트 |
| BullMQ + PostgreSQL | 576바이트 |
| BullMQ + Redis | 618바이트 |
이 결과는 작업을 많이 저장할 때 필요한 리소스를 비교하는 데 도움이 된다.
하지만 실제 작업 Payload의 크기와 Queue의 설정에 따라 값이 달라질 수 있다.
또한 Redis의 사용량은 주로 메모리로 측정된다.
PostgreSQL은 테이블과 인덱스의 디스크 크기로 측정된다.
따라서 두 수치를 완전히 동일한 저장 비용으로 해석해서는 안 된다.
실제 설계 선택
저자는 네 가지 구성을 다음과 같이 구분한다.
pg-boss는 이미 PostgreSQL을 사용하는 프로젝트에서 풍부한 Queue 기능이 필요한 경우 적합할 수 있다.
SKIP LOCKED는 비교적 짧고 단순한 작업에 적합할 수 있다.
BullMQ + Redis는 높은 처리량과 풍부한 Queue 기능이 필요한 경우 유리할 수 있다.
BullMQ + PostgreSQL은 BullMQ API를 유지하면서 Redis를 별도로 운영하고 싶지 않은 경우 검토할 수 있다.
하지만 이 결과는 하나의 VM에서 수행한 실험이다.
실제 운영 환경의 네트워크와 장애 복구 요구사항, 작업의 실행 시간에 따라 선택이 달라질 수 있다.
이번 글은 작업 Queue를 선택할 때 초당 처리량뿐 아니라 실제 작업의 실행 시간과 데이터베이스 Transaction, 장애 복구 지연, 운영 복잡성을 함께 고려해야 한다는 점을 구체적인 Benchmark로 설명한다.
SSRF Protection in Node.js: Validating User URLs, Redirects and DNS (Tested)
- 저자: Waseem Anjum
- 매체: 개인 기술 블로그
- 게시일: 2026-10-24 — 정확한 게시 시각 미확인
- 원문: https://fabwaseem.com/blog/ssrf-protection-nodejs
Node.js 백엔드에서 사용자가 전달한 URL을 가져오는 기능을 구현할 때 단순한 정규식이나 Hostname 검사만으로는 SSRF 공격을 방어하기 어렵다는 점을 실제 테스트로 보여주는 글이다.
SSRF(Server-Side Request Forgery)는 공격자가 서버가 요청할 주소를 조작하는 공격이다.
예를 들어 웹 애플리케이션에서 사용자가 URL을 입력하면 해당 페이지의 정보를 가져오는 기능이 있다고 가정해 보자.
정상적인 사용자는 공개된 웹페이지의 주소를 입력한다.
하지만 공격자는 내부 서버나 클라우드 Metadata Endpoint의 주소를 입력할 수 있다.
이 경우 외부 사용자는 직접 접근할 수 없는 내부 시스템에 서버를 통해 접근하려고 시도할 수 있다.
이번 글은 이러한 공격을 방어하기 위해 다음 세 가지 검증을 비교한다.
세 가지 URL 검증 방식
| 방식 | 설명 |
|---|---|
| 정규식 기반 검사 | 원본 URL 문자열에서 위험한 패턴 검색 |
| URL Parser + 문자열 목록 | new URL()로 파싱한 뒤 Hostname 비교 |
| URL Parser + IP BlockList | 정규화와 IP 범위 검증 결합 |
저자는 총 16개의 테스트 입력을 사용했다.
결과적으로 원본 URL 문자열에 정규식을 적용하는 방식은 16개 가운데 12개의 우회 입력을 허용했다.
반면 URL Parser와 IP BlockList를 사용하는 방식은 알려진 위험 입력을 더 많이 차단했다.
하지만 정적 검증만으로 모든 공격을 막을 수는 없었다.
특히 DNS 응답과 Redirect를 별도로 처리해야 했다.
정규식이 놓친 IP 주소 표현
IP 주소는 반드시 일반적인 점으로 구분된 숫자 형태로만 표현되는 것은 아니다.
예를 들어 다음 문자열을 생각할 수 있다.
http://2130706433/
겉으로 보기에는 일반적인 도메인과 비슷한 숫자 문자열처럼 보일 수 있다.
하지만 WHATWG URL Parser는 이를 다음 IP 주소로 정규화할 수 있다.
127.0.0.1
이는 Loopback 주소다.
같은 결과가 나타날 수 있는 표현에는 다음이 포함된다.
0x7f0000010177.0.0.1127.1
따라서 원본 문자열에 127.이 포함됐는지만 검사하면 일부 표현을 놓칠 수 있다.
저자는 먼저 다음 API를 사용하도록 한다.
new URL()
그다음 파싱된 URL의 hostname을 검사한다.
이렇게 하면 브라우저와 Node.js의 URL 처리 규칙에 따라 정규화된 Hostname을 확인할 수 있다.
IPv4-mapped IPv6
두 번째 문제는 IPv6다.
예를 들어 다음 주소를 생각할 수 있다.
::ffff:127.0.0.1
이는 IPv6 형식 안에 IPv4 주소를 포함하는 표현이다.
단순한 문자열 검사에서는 127.0.0.1을 놓칠 수 있다.
또한 다른 표기에서는 IPv4 주소가 16진수 형태로 표현될 수 있다.
저자는 Node.js의 다음 기능을 사용했다.
net.BlockList
BlockList는 IP 주소와 Subnet을 검사할 수 있는 기능이다.
이를 이용해 다음 종류의 주소를 제한할 수 있다.
- Loopback
- Private IPv4
- Link-local
- IPv6 Unique Local
- 일부 특수 목적 주소
하지만 모든 위험 주소를 몇 개의 Prefix만으로 완전히 표현할 수 있는 것은 아니다.
실제 서비스에서는 어떤 네트워크 주소를 허용할지 정책을 먼저 정의해야 한다.
Redirect 우회
저자가 발견한 중요한 공격 경로는 Redirect였다.
예를 들어 사용자가 다음 주소를 입력했다고 가정해 보자.
https://public.example.com/file
서버는 해당 주소가 공개 인터넷의 주소라고 판단한다.
하지만 실제 서버가 다음 응답을 반환할 수 있다.
302 Found
Location: http://127.0.0.1/
일반적인 fetch()는 Redirect를 자동으로 따라갈 수 있다.
이 경우 처음 URL은 정상적이었더라도 최종 요청은 내부 주소로 이동할 수 있다.
따라서 최초 URL만 검사하는 방식으로는 충분하지 않다.
저자는 이를 해결하기 위해 다음 설정을 사용한다.
redirect: "manual"
이렇게 하면 Redirect 응답을 직접 확인할 수 있다.
이후 새로운 URL을 다시 검증한 뒤 필요한 경우 다음 요청을 수행한다.
또한 Redirect 횟수에 제한을 둔다.
이렇게 하면 무한 Redirect가 발생하는 상황을 제한할 수 있다.
Cross-host Redirect에서 인증 정보 제거
Redirect를 처리할 때는 다른 문제도 존재한다.
기존 요청에 인증 정보가 포함돼 있을 수 있기 때문이다.
예를 들어 다음 Header가 존재한다고 가정해 보자.
AuthorizationCookieProxy-Authorization
이때 요청이 다른 Host로 Redirect되면 해당 인증 정보를 그대로 전달해서는 안 될 수 있다.
저자는 Host가 변경되는 Redirect에서 이러한 Header를 제거하도록 구현했다.
이는 SSRF뿐 아니라 인증 정보가 의도하지 않은 서버로 전달되는 위험을 줄이기 위한 것이다.
DNS Rebinding
이번 글에서 가장 중요한 부분은 DNS Rebinding 방어다.
예를 들어 다음 도메인을 생각할 수 있다.
safe.example.com
처음 DNS를 조회하면 공개 IP 주소가 반환된다.
애플리케이션은 해당 주소가 안전하다고 판단한다.
이후 실제 HTTP 요청을 보내는 과정에서 HTTP Client가 DNS를 다시 조회한다.
그런데 두 번째 DNS 응답에는 내부 IP 주소가 포함돼 있을 수 있다.
이 경우 최초 검증한 주소와 실제 연결한 주소가 달라진다.
따라서 다음 구조에는 위험이 존재한다.
- DNS 조회
- IP 주소 검증
- 별도
fetch()호출 - HTTP Client가 DNS를 다시 조회
저자는 이를 해결하기 위해 실제 TCP 연결에서 사용할 DNS 결과를 검증하도록 한다.
Node.js의 http.request()와 https.request()에서는 lookup 옵션을 사용할 수 있다.
해당 Hook에서 DNS 응답을 확인한다.
그리고 실제로 허용된 IP 주소만 연결에 사용하도록 한다.
이렇게 하면 검증한 IP 주소와 실제 연결하는 IP 주소가 서로 달라지는 문제를 줄일 수 있다.
DNS 응답 전체 검사
저자는 DNS가 여러 IP 주소를 반환하는 상황도 고려했다.
예를 들어 하나의 도메인이 다음 주소를 반환했다고 가정해 보자.
- 공개 IPv4
- 내부 IPv4
이 경우 공개 IP 하나가 존재한다는 이유로 전체 도메인을 안전하다고 판단하지 않는다.
저자의 구현에서는 반환된 주소 가운데 하나라도 허용하지 않는 주소가 존재하면 요청을 거부한다.
이는 보수적인 정책이다.
정상적인 서비스에서도 특정 네트워크 환경에 따라 내부 주소와 공개 주소를 함께 반환할 수 있기 때문이다.
따라서 실제 정책은 애플리케이션이 어떤 종류의 URL을 가져와야 하는지에 따라 달라질 수 있다.
하지만 공개 인터넷의 URL만 허용하는 서비스라면 이러한 방식이 적합할 수 있다.
실제 테스트 결과와 비용
저자는 net.BlockList의 검사 시간도 측정했다.
10만 번의 검사를 수행한 결과 평균 처리 시간은 약 1.8마이크로초였다.
이는 저자의 특정 실행 환경에서 측정한 값이다.
모든 Node.js 서버에서 동일한 시간이 걸린다는 의미는 아니다.
또한 실제 DNS 조회에 필요한 시간은 별도로 존재한다.
따라서 IP BlockList의 처리 시간이 짧다는 사실만으로 전체 HTTP 요청의 성능 비용이 무시할 수 있는 수준이라고 단정해서는 안 된다.
검증 목록의 추가 문제
저자는 자신의 초기 IP BlockList에서도 빠진 주소가 존재했다는 사실을 발견했다.
대표적으로 다음 범위다.
100.64.0.0/10198.18.0.0/15192.0.0.0/24- NAT64 관련 IPv6 주소
- 오래된 IPv6 Site-local 범위
일부 주소는 일반적인 사설 IP가 아니더라도 특정 네트워크 환경에서는 내부 서비스에 연결될 수 있다.
따라서 단순히 유명한 Private IP 범위만 차단하는 방식보다 허용할 수 있는 전역 IP 주소의 기준을 명확하게 정의하는 편이 적절할 수 있다.
CI에 포함할 테스트
저자는 실제로 사용한 공격 입력 목록을 자동화 테스트에 포함하도록 했다.
예를 들어 다음 상황을 검증한다.
- 10진수 IP 주소
- 16진수 IP 주소
- IPv4-mapped IPv6
- Link-local 주소
- 비정상적인 URL Scheme
- 다른 Host로 Redirect
- DNS 응답 변경
- 인증 정보가 포함된 URL
이러한 테스트는 이후 URL 검증 로직을 리팩터링할 때 기존에 막았던 우회 경로가 다시 열리지 않도록 하는 데 도움이 된다.
하지만 테스트 목록에 포함된 입력만 차단한다는 사실이 모든 SSRF 공격을 방어한다는 의미는 아니다.
실제 네트워크 접근 제어와 애플리케이션의 권한 정책도 함께 필요하다.
적용 범위와 한계
이번 글의 실험은 Node.js 20.19.2 환경에서 수행됐다.
따라서 다른 Node.js 버전과 HTTP Client에서 일부 내부 처리 방식이 달라질 수 있다.
또한 실제 운영 환경에서는 프록시와 DNS Resolver, 서비스 메시 등의 구성이 추가될 수 있다.
이 경우 애플리케이션의 URL 검증만으로 모든 네트워크 접근을 통제하기 어려울 수 있다.
이번 글은 사용자가 전달한 URL을 서버에서 가져오는 기능을 구현할 때 문자열 검사, IP 검증, Redirect 검증, DNS 조회를 각각 독립적으로 고려해야 한다는 점을 실제 공격 입력으로 검증한 자료다.
특히 이미지 다운로드와 URL Preview, Webhook, 외부 문서 수집 기능을 구현하는 Node.js 백엔드에서 참고할 만하다.