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

기준 시점: 2026-11-06 06:00 KST
조사 범위: 전일 2026-11-05 06:00 ~ 당일 2026-11-06 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
Vue 3.5.45 정식 출시 — Suspense·비동기 컴포넌트 오류 복구 강화 및 컴파일러 최적화
- 발표일: 2026-11-05
- GitHub 릴리스 표시 시각: 2026-11-05 09:15 — 표시 시간대 미확인
- 버전: Vue 3.5.45
- 안정화 단계: 정식 안정 버전의 패치 릴리스
- 적용 대상: Vue Runtime, Suspense, KeepAlive, Hydration, HMR, SFC Compiler
- 원문: https://github.com/vuejs/core/releases/tag/v3.5.45
- 상세 변경 기록: https://github.com/vuejs/core/blob/main/CHANGELOG.md
Vue가 3.5 계열의 새로운 안정 패치인 3.5.45를 공개했다.
이번 릴리스는 새로운 컴포넌트 API나 렌더링 방식을 도입하는 업데이트가 아니다. 대신 비동기 컴포넌트와 Suspense가 결합된 상황에서 발생할 수 있는 여러 오류를 수정하고, 컴파일러의 일부 불필요한 작업을 줄였다.
특히 Suspense와 KeepAlive, Transition을 함께 사용하는 애플리케이션에서 의미가 있는 수정이 많다.
Suspense의 기본 동작
Vue의 Suspense는 비동기 의존성을 가진 컴포넌트를 처리하기 위한 기능이다.
예를 들어 어떤 페이지가 사용자 정보를 가져온 이후 화면을 표시해야 한다고 가정해 보자.
데이터가 준비되기 전에는 로딩 화면을 표시할 수 있다.
이후 데이터가 준비되면 실제 콘텐츠를 표시한다.
Suspense는 이러한 과정을 관리할 수 있도록 한다.
하지만 다음 상황에서는 실행 흐름이 복잡해진다.
- 데이터 요청이 완료되기 전에 사용자가 다른 페이지로 이동
- 비동기 컴포넌트의 로딩 중 다른 컴포넌트로 교체
- 로딩 중이던 컴포넌트를 KeepAlive로 보관
- Transition의 종료 애니메이션이 진행 중인 상태에서 컴포넌트 제거
- 비동기 Setup 함수에서 오류 발생
이러한 상황에서는 단순히 로딩이 완료됐는지만 확인하는 것으로 충분하지 않다.
현재 해당 컴포넌트가 여전히 화면에 존재해야 하는지, 이전의 비동기 결과를 적용해도 되는지, 관련 리소스를 정리해야 하는지 판단해야 한다.
이번 업데이트는 이러한 상태 전환의 정확성을 개선한다.
제거된 Suspense에 Fallback을 다시 표시하지 않도록 수정
대표적인 수정은 다음과 같다.
do not mount the fallback after the boundary is unmounted
예를 들어 사용자가 상품 상세 페이지에 접근했다고 가정해 보자.
페이지는 데이터를 기다리는 동안 로딩 UI를 표시한다.
하지만 데이터가 준비되기 전에 사용자가 다른 페이지로 이동했다.
이 경우 기존 Suspense Boundary는 더 이상 화면에 존재하지 않아야 한다.
문제는 비동기 작업의 완료와 컴포넌트 제거 시점이 서로 겹칠 수 있다는 점이다.
기존 구현의 일부 상황에서는 이미 제거된 Boundary의 Fallback이 다시 Mount될 가능성이 있었다.
이번 수정은 이러한 상황을 방지한다.
또한 Pending 상태의 Branch가 제거됐을 때 관련 Unmount Hook을 잘못 유지하지 않도록 수정했다.
이러한 변경은 실제 DOM에 더 이상 존재하지 않아야 하는 컴포넌트가 남거나 불필요한 Lifecycle 처리가 발생하는 문제를 줄인다.
비동기 Setup 오류 처리
다음 수정도 포함됐다.
render nothing for a component whose async setup rejects
Vue의 setup()은 비동기 작업을 수행할 수 있다.
예를 들어 필요한 데이터를 먼저 가져온 뒤 컴포넌트를 준비하는 구조다.
하지만 해당 작업에서 오류가 발생할 수 있다.
이번 변경에서는 비동기 Setup이 Reject된 컴포넌트에 대해 잘못된 콘텐츠를 렌더링하지 않도록 처리한다.
또한 다음 상황도 수정했다.
- 이미 중단된 Error Boundary가 비동기 오류를 처리하지 않도록 변경
- 오류 처리기가 없는 비동기 Setup 실패 이후 Suspense Boundary가 적절하게 완료 상태로 이동하도록 수정
- 폐기된 Pending Cycle에 속한 비동기 의존성의 처리 상태 정리
여기서 중요한 점은 비동기 작업에서 오류가 발생했다는 사실과 Suspense Boundary가 영구적으로 Pending 상태에 남아야 한다는 사실은 다르다는 것이다.
실제 작업이 실패했다면 해당 상태를 적절하게 처리하고 관련 리소스를 정리해야 한다.
이번 업데이트는 그러한 오류 처리 경로를 보완한다.
KeepAlive와 Suspense의 관계
KeepAlive는 컴포넌트를 화면에서 제거하더라도 내부 상태를 보존할 수 있도록 하는 기능이다.
예를 들어 사용자가 여러 탭을 전환하는 화면을 생각할 수 있다.
사용자가 첫 번째 탭에서 Form을 작성한다.
두 번째 탭으로 이동한다.
다시 첫 번째 탭으로 돌아온다.
KeepAlive를 사용하면 기존 Form 상태를 유지할 수 있다.
하지만 KeepAlive 안의 컴포넌트가 비동기 의존성을 가진다면 문제가 복잡해진다.
이번 릴리스에서는 다음 수정이 포함됐다.
- 해결된 Suspense Child를 즉시 Cache에 저장
- Cache에 저장되기 전에 비활성화된 Child를 적절하게 Unmount
이러한 변경은 컴포넌트가 이미 준비됐는지와 실제로 Cache에 등록됐는지를 구분하는 데 관련된다.
예를 들어 비동기 컴포넌트가 로딩을 완료했지만 아직 Cache에 저장되기 전에 사용자가 다른 탭으로 이동할 수 있다.
이 경우 기존 실행 상태가 잘못 유지되면 실제 화면과 Cache가 서로 다른 컴포넌트 상태를 가지고 있을 수 있다.
이번 수정은 이러한 불일치를 줄인다.
중첩된 Suspense와 Transition
이번 릴리스에는 중첩된 Suspense Boundary에 관한 여러 수정도 포함됐다.
대표적인 내용은 다음과 같다.
| 영역 | 수정 내용 |
|---|---|
| 중첩 Boundary | 유지 중인 콘텐츠의 같은 Root 업데이트 처리 |
| Pending Branch | 표시 시점의 Enter Transition 처리 |
| Root 변경 | 기존 Pending 상태와 재시도 작업의 연결 유지 |
| Timeout | Pending 상태에서 Root가 다시 변경돼도 기존 Timeout 계산 유지 |
| Unmount | 제거된 Boundary의 Transition Callback 실행 방지 |
| 의존성 | 자식 Boundary 제거 시 부모의 Pending Dependency 정리 |
예를 들어 모달 안에 비동기 컴포넌트가 있다고 가정해 보자.
모달이 열릴 때 데이터를 요청한다.
하지만 사용자가 데이터를 받기 전에 모달을 닫았다.
이때 실제 컴포넌트는 제거돼야 한다.
그런데 비동기 작업이 완료됐다는 이유로 이전 Enter Transition이 실행되면 잘못된 DOM 변경이 발생할 수 있다.
이번 변경은 이러한 비동기 작업과 Transition Lifecycle의 관계를 보다 정확하게 처리하려는 목적이다.
Hydration과 HMR
Suspense 외에도 Hydration과 HMR 관련 수정이 포함됐다.
Hydration에서는 기존 서버 DOM을 교체하는 상황에 한정해 컴포넌트의 Root Element 참조를 갱신하도록 변경했다.
이는 서버가 생성한 HTML과 클라이언트에서 연결한 컴포넌트의 실제 DOM 참조를 일관되게 관리하기 위한 것이다.
HMR(Hot Module Replacement)에서는 아직 해결되지 않은 비동기 Root가 존재할 때 파일 변경에 따른 Reload를 재시도하도록 수정했다.
예를 들어 비동기 컴포넌트가 준비되기 전에 개발자가 해당 파일을 수정할 수 있다.
이 경우 기존 Reload 작업이 무시되면 수정한 코드가 화면에 반영되지 않을 수 있다.
새로운 구현은 비동기 Root가 준비된 이후 필요한 Reload를 다시 시도하도록 한다.
컴파일러와 DOM 처리 수정
컴파일러에서도 여러 변경이 이루어졌다.
대표적인 항목은 다음과 같다.
catch와switch의 Lexical Scope를 올바르게 처리- Static Boolean Binding의 의미 유지
- 정적 Style Attribute를 SSR 컴파일 시점에 처리
- 불필요한
tsconfigInclude 목록 열거 생략 - 내부 초기화 함수에 Pure Annotation 적용
또한 DOM 관련 수정도 포함됐다.
disablepictureinpicture와 disableremoteplayback을 Boolean Attribute로 처리하도록 변경했다.
이러한 속성은 Video와 관련된 브라우저 기능을 제어하는 데 사용할 수 있다.
TypeScript의 HTML Dialog 관련 타입에는 다음 속성이 추가됐다.
closedby
또한 v-model에서 Set 형태의 Select Value를 사용할 때 Number Modifier를 올바르게 처리하도록 수정했다.
안정화 단계와 성능 수치
Vue 3.5.45는 정식 안정 패치다.
따라서 앞서 공개된 Vue 3.6 Release Candidate와 구분해야 한다.
이번 변경은 Vue 3.5를 사용하는 프로젝트에 적용되는 수정이다.
특히 비동기 컴포넌트와 Suspense를 많이 사용하는 애플리케이션에서 관련 문제가 발생할 가능성을 줄일 수 있다.
하지만 이번 공식 릴리스에는 빌드 시간과 메모리 사용량, 렌더링 성능의 전후 차이를 측정한 Benchmark가 포함되지 않았다.
따라서 업데이트 이후 모든 Vue 프로젝트의 성능이 특정 비율만큼 개선된다고 단정할 수 없다.
이번 릴리스는 비동기 렌더링과 컴포넌트의 생성·제거 과정에서 발생하는 상태 불일치를 수정하고 컴파일러의 일부 반복 작업을 줄인 안정화 패치다.
Astro 7.4.6·7.4.7 공개 — astro check의 잘못된 성공 종료와 다국어 SSR Fallback 오류 수정
- 발표일: 2026-11-05
- GitHub 릴리스 표시 시각: 7.4.6은 16:16, 7.4.7은 19:57 — 표시 시간대 미확인
- 버전: Astro 7.4.6, 7.4.7
- 안정화 단계: 정식 안정 패치
- 적용 대상: Astro CLI, TypeScript 검사, SSR, i18n Routing, 정적 자산 처리
- 원문: https://github.com/withastro/astro/releases/tag/astro@7.4.6
- 후속 패치: https://github.com/withastro/astro/releases/tag/astro@7.4.7
Astro가 7.4 계열의 두 패치를 공개했다.
이번 업데이트는 새로운 UI 컴포넌트 API를 추가하는 릴리스가 아니다.
주요 변경은 CI에서 타입 검사가 실제로 실행되지 않았는데도 성공으로 처리할 수 있던 문제와 다국어 SSR 페이지의 Fallback 처리 오류를 수정한 것이다.
또한 일부 페이지에서 불필요한 클라이언트 JavaScript가 포함될 수 있던 문제도 수정했다.
astro check가 성공 코드 0을 반환하던 문제
Astro에서는 다음 명령을 사용할 수 있다.
astro check
이 명령은 TypeScript 및 Astro 컴포넌트의 진단을 수행하는 데 사용된다.
실제 프로젝트에서는 CI에서 해당 명령을 실행할 수 있다.
예를 들어 다음 순서다.
- 의존성 설치
- TypeScript 및 Astro 진단
- 테스트 실행
- 프로덕션 빌드
- 배포
문제는 실제 검사 도구가 설치되지 않은 상황이었다.
예를 들어 다음 패키지가 존재하지 않는다고 가정해 보자.
@astrojs/checktypescript
이 경우 astro check는 정상적인 타입 검사를 수행할 수 없다.
하지만 기존 구현에서는 일부 상황에서 명령이 성공 종료 코드인 0을 반환했다.
이는 CI에서 중요한 문제를 만든다.
타입 검사가 수행되지 않았는데도 CI가 타입 검사를 통과했다고 판단할 수 있기 때문이다.
이번 Astro 7.4.6에서는 필요한 패키지가 설치되지 않은 경우 종료 코드 1을 반환하도록 변경했다.
따라서 실제 검사를 수행할 수 없는 상황이 CI에서 실패로 처리된다.
이는 새로운 타입 검사 기능이 아니라 기존 검사 명령의 성공과 실패 판정을 수정한 것이다.
Pipe를 사용하는 환경의 오류 처리
관련 수정도 포함됐다.
예를 들어 다음 명령을 사용할 수 있다.
astro check | head
이 명령은 검사 결과의 일부만 표시한다.
하지만 head가 필요한 출력을 읽은 뒤 Pipe를 닫으면 astro check가 계속 출력하려는 과정에서 오류가 발생할 수 있다.
대표적인 오류는 다음과 같다.
EPIPE
기존에는 이러한 상황에서 Stack Trace가 출력되거나 필요한 최종 요약이 누락될 수 있었다.
이번 수정에서는 느린 출력 소비자와 조기에 종료되는 Pipe를 더 안정적으로 처리한다.
Pipe가 조기에 닫히면 불필요한 오류 Stack Trace 대신 실패 종료 코드를 반환한다.
다만 이 변경이 모든 Shell Pipeline의 종료 코드를 자동으로 검사하도록 만든다는 의미는 아니다.
실제 CI에서는 Shell의 pipefail 설정 등도 결과에 영향을 줄 수 있다.
다국어 SSR Fallback
두 번째 주요 수정은 Astro의 다국어 라우팅과 관련된다.
예를 들어 다음 Locale이 존재한다고 가정해 보자.
- 영어:
en - 독일어:
de
하지만 모든 페이지가 두 언어로 번역돼 있지는 않을 수 있다.
예를 들어 /about 페이지는 영어로만 존재한다.
사용자는 다음 주소로 접근했다.
/de/about
애플리케이션은 독일어 페이지가 없다면 영어 페이지를 보여주도록 설정할 수 있다.
대표적인 설정은 다음과 같다.
fallback: { de: "en" }
문제는 On-demand Rendering을 사용하는 경우였다.
기존 Astro에서는 해당 요청에 다음 응답을 반환할 수 있었다.
302 Found
하지만 응답에 Location Header가 없었다.
HTTP Redirect는 클라이언트가 이동할 대상 주소를 알아야 한다.
따라서 Location이 없는 302 응답은 예상한 페이지 이동을 수행하지 못할 수 있다.
이번 수정에서는 다국어 Fallback 요청을 실제 설정에 따라 처리한다.
Rewrite와 Redirect의 차이
Astro는 다음 설정에 따라 Fallback을 처리한다.
i18n.routing.fallbackType
설정에 따라 Rewrite 또는 Redirect 방식이 사용될 수 있다.
두 방식은 다르다.
Rewrite에서는 사용자가 요청한 URL을 유지하면서 다른 콘텐츠를 제공할 수 있다.
반면 Redirect에서는 브라우저가 다른 URL로 이동한다.
예를 들어 /de/about에 접근했을 때 영어 콘텐츠를 보여준다고 가정해 보자.
Rewrite 방식에서는 URL이 /de/about으로 유지될 수 있다.
Redirect 방식에서는 실제 영어 페이지의 URL로 이동할 수 있다.
따라서 애플리케이션의 SEO와 Canonical URL, 사용자에게 표시되는 주소에도 차이가 발생할 수 있다.
이번 수정에서는 다음 조건도 처리하도록 개선됐다.
- Percent Encoding이 포함된 경로
- Astro의
base설정을 사용하는 사이트 - Query String이 포함된 요청
- 기존 응답에서 명시적으로 설정한 HTTP Status 및 Redirect
특히 Query String을 유지하는 것은 중요하다.
예를 들어 사용자가 다음 주소에 접근했다고 가정해 보자.
/de/products?category=tools
Fallback을 처리하면서 category=tools가 제거되면 사용자의 필터 조건이 사라질 수 있기 때문이다.
개발 서버의 Route 매칭
이번 릴리스에서는 개발 서버의 matchAllRoutes도 수정됐다.
기존 구현은 일부 Fallback Route를 충분히 고려하지 못했다.
그 결과 개발 환경에서 다국어 Fallback 요청이 실제 원본 Route 대신 404 Route의 Metadata와 연결될 수 있었다.
새로운 구현은 Production Router와 동일한 방식으로 Fallback Route를 고려하도록 변경했다.
이는 개발 환경과 실제 배포 환경 사이의 동작 차이를 줄이기 위한 수정이다.
불필요한 클라이언트 JavaScript 포함 방지
Astro는 기본적으로 서버에서 HTML을 생성하고 필요한 컴포넌트에만 클라이언트 JavaScript를 제공하는 방식을 지원한다.
예를 들어 대부분의 페이지가 정적 콘텐츠로 구성돼 있다면 많은 JavaScript를 실행할 필요가 없을 수 있다.
하지만 일부 페이지에서는 실제로 상호작용하는 컴포넌트가 필요하다.
이번 패치에서는 astro:i18n을 Import하는 페이지 사이에서 Module Graph가 잘못 연결될 수 있던 문제를 수정했다.
기존에는 특정 페이지에 필요한 클라이언트 전용 컴포넌트나 Script가 다른 페이지에도 포함될 수 있었다.
새로운 구현은 Top-level Page Graph를 탐색할 때 가상 Page Entry에서 범위를 제한한다.
이렇게 하면 관련이 없는 페이지에 불필요한 클라이언트 코드가 연결되는 상황을 줄일 수 있다.
하지만 공식 발표에는 JavaScript Bundle 크기가 몇 KB 감소했는지에 관한 수치가 포함되지 않았다.
따라서 모든 Astro 프로젝트의 클라이언트 JavaScript가 동일한 비율로 감소한다고 판단할 수 없다.
정적 파일의 수정 시각 보존
이번 업데이트에서는 빌드 결과물에 파일을 복사할 때 수정 시각도 더 정확하게 보존하도록 변경했다.
대상에는 다음이 포함된다.
- Cache에 저장된 Font 파일
- 변경되지 않은
public/파일
파일의 수정 시각은 일부 빌드 시스템과 배포 환경에서 변경 여부를 판단하는 데 활용할 수 있다.
따라서 내용이 변경되지 않은 파일의 시각이 불필요하게 변경되면 Cache나 증분 빌드의 동작에 영향을 줄 수 있다.
이번 수정은 실제 원본 파일과 빌드 결과물의 관계를 더 정확하게 유지하도록 한다.
다만 Build Plugin이 파일을 실제로 덮어쓴 경우에는 해당 파일의 수정 시각을 무조건 이전 값으로 되돌리지 않는다.
Astro 7.4.7의 후속 수정
7.4.7에서는 이미지 Endpoint 관련 수정이 추가됐다.
문제는 대소문자를 구분하는 파일시스템에서 발생할 수 있었다.
예를 들어 다음 두 디렉터리를 생각해 보자.
Assetsassets
Linux와 같은 대소문자를 구분하는 파일시스템에서는 두 이름이 서로 다른 디렉터리를 나타낼 수 있다.
하지만 기존 이미지 Endpoint에서는 디렉터리 이름의 대소문자만 다른 경우를 빌드 출력 디렉터리 안에 존재하는 경로로 잘못 판단할 수 있었다.
7.4.7은 이러한 경로 판정을 수정했다.
이번 변경을 새로운 이미지 최적화 기능이나 특정 보안 취약점의 공식 수정으로 단정해서는 안 된다.
공식 릴리스에서 확인되는 것은 대소문자를 구분하는 파일시스템에서 경로를 잘못 판정하던 문제의 수정이다.
이번 두 릴리스는 Astro의 CI 검증 결과와 다국어 SSR, 클라이언트 자산 포함 범위 및 파일 경로 처리의 정확성을 개선한 정식 패치다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome·Chromium, Firefox·Mozilla, WebKit·Safari의 공식 공개 정보와 JavaScript·TypeScript·CSS, 웹 성능 및 접근성 관련 발표를 확인했다.
이번 기간에 새로운 웹 표준이 정식 채택됐거나 주요 브라우저 API가 새롭게 안정화됐다는 발표는 확인하지 못했다.
이미 이전 브리핑에서 다룬 Firefox 159의 정식 출시와 Chrome 158 Beta는 반복하지 않았다.
또한 브라우저의 Canary 또는 Nightly 개발 빌드에서 발견한 개별 변경을 일반 사용자가 사용하는 안정적인 API의 출시로 취급하지 않았다.
다만 이번 추천 글에서는 기존 브라우저 기술을 실제 서비스에서 활용하는 방법을 다룬다.
특히 Browser AI API의 지원 여부와 모델 다운로드 상태를 확인하는 방법, Svelte 5의 반응형 실행 구조, Headless Chromium의 Font Fallback과 브라우저에서 직접 동영상을 편집할 때의 WebAssembly 제약을 자세히 살펴본다.
이러한 기술은 별도의 신규 브라우저 출시가 없어도 실제 웹 애플리케이션의 성능과 호환성에 영향을 줄 수 있다.
3️⃣ 백엔드·인프라
Vercel Schedules 공개 Beta — 일회성·반복 예약 작업과 비공개 Function, Queue 실행 지원
- 발표일: 2026-11-05 — 정확한 게시 시각 미확인
- 안정화 단계: Public Beta
- 적용 대상: Vercel Functions, Vercel Queues, Vercel 프로젝트의 배포 환경
- 지원 요금제: Hobby, Pro, Enterprise
- 원문: https://vercel.com/changelog/vercel-schedules-is-now-in-public-beta
- 상세 기술 설명: https://vercel.com/blog/introducing-vercel-schedules
Vercel이 서버리스 애플리케이션에서 정해진 시각에 작업을 실행하는 Schedules 기능을 Public Beta로 공개했다.
이번 기능은 기존 Cron Jobs의 후속 서비스다.
기존 Cron Jobs는 주기적인 작업을 실행하는 데 사용됐다.
하지만 실제 애플리케이션에서는 다음과 같은 요구사항도 존재한다.
- 정확히 한 번만 실행할 작업 예약
- 사용자별로 다른 시각에 실행
- 실제 배포 환경과 연결되지 않은 작업 예약
- HTTP로 공개되지 않은 Function 실행
- Queue를 이용한 백그라운드 작업
- 배포 없이 실행 시각 변경
Schedules는 이러한 요구사항을 하나의 예약 실행 서비스에서 처리하도록 설계됐다.
Cron과 일회성 예약
Schedules는 두 종류의 실행을 지원한다.
첫 번째는 반복 실행이다.
예를 들어 매일 오전 9시에 보고서를 생성하는 작업이다.
이러한 작업에는 Cron 표현식을 사용할 수 있다.
대표적인 표현은 다음과 같다.
0 9 * * *
두 번째는 일회성 실행이다.
예를 들어 사용자가 특정 날짜와 시각에 알림을 예약할 수 있다.
이 경우 작업을 한 번만 실행하도록 구성한다.
따라서 매분 데이터베이스를 조회해 실행할 작업이 있는지 확인하는 Polling 구조가 필요하지 않을 수 있다.
정적 Schedule
정적 Schedule은 프로젝트의 설정 파일에서 정의한다.
주요 파일은 다음과 같다.
vercel.json
예를 들어 다음 정보를 설정할 수 있다.
- Schedule 이름
- Cron 표현식
- 실행할 Function
- Timezone
대표적인 설정은 다음과 같다.
schedule: "0 * * * *"
function: "api/cleanup.ts"
이렇게 하면 매시간 정해진 시점에 특정 Function을 실행하도록 구성할 수 있다.
하지만 실제 Function의 실행이 성공했는지는 별도의 문제다.
예를 들어 예약된 작업이 시작됐더라도 데이터베이스 오류 때문에 실패할 수 있다.
따라서 예약 실행의 성공 여부와 실제 비즈니스 작업의 완료 여부는 구분해야 한다.
비공개 Function 실행
이번 기능에서 중요한 차이는 공개 HTTP Endpoint 없이도 예약 작업을 실행할 수 있다는 점이다.
기존 예약 실행 구조에서는 특정 URL로 HTTP 요청을 보내는 방식이 사용될 수 있었다.
예를 들어 다음 Endpoint를 생각해 보자.
/api/cleanup
이 Endpoint는 만료된 데이터를 삭제한다.
문제는 HTTP Endpoint가 공개돼 있다면 다른 사용자도 해당 URL에 접근하려고 시도할 수 있다는 점이다.
따라서 별도의 Secret이나 인증 검증이 필요할 수 있다.
새로운 Schedules에서는 다음 Trigger를 설정할 수 있다.
experimentalTriggers: [{ type: "schedule/v1beta" }]
이 설정을 적용한 Function은 공개 URL을 가지지 않는다.
Vercel의 Schedule 실행 인프라에서만 호출할 수 있도록 구성된다.
Function에서는 다음 Handler를 사용할 수 있다.
handleSchedule()
이 Handler는 예약 실행과 관련된 Metadata를 전달받는다.
대표적인 정보는 다음과 같다.
- Schedule 이름
- 예정된 실행 시각
따라서 어떤 예약 작업이 실행됐는지 확인할 수 있다.
하지만 비공개 Function이라는 사실만으로 해당 Function 내부의 모든 작업이 안전해지는 것은 아니다.
외부 API 인증 정보와 데이터베이스 접근 권한은 별도로 관리해야 한다.
동적 Schedule
정적 Schedule은 프로젝트 설정에 정의한다.
반면 동적 Schedule은 실제 애플리케이션 실행 중에 생성할 수 있다.
예를 들어 사용자가 자신의 알림 시각을 직접 선택한다고 가정해 보자.
사용자 A는 오전 9시를 선택한다.
사용자 B는 오후 7시를 선택한다.
사용자마다 별도의 Schedule을 생성할 수 있다.
이번 기능에서는 다음 SDK를 사용할 수 있다.
@vercel/schedules
대표적인 API는 다음과 같다.
Schedules.create()
동적 Schedule에는 다음 정보를 전달할 수 있다.
- 이름
- 실행 대상
- 실행 표현식
- Timezone
- Payload
예를 들어 daily-digest라는 Schedule을 생성할 수 있다.
실행 대상은 다음과 같은 Queue Topic이 될 수 있다.
digests
그리고 다음 Cron 표현식을 지정한다.
0 9 * * *
Timezone은 다음 값으로 지정할 수 있다.
America/Los_Angeles
이렇게 하면 해당 지역의 시각을 기준으로 실행할 수 있다.
다만 런타임에서 생성하는 동적 Schedule은 현재 Queue Topic만 대상으로 사용할 수 있다.
정적 Schedule과 동적 Schedule의 대상 지원 범위가 동일하지 않다.
Queue 기반 처리
동적 Schedule은 Queue에 메시지를 전달한다.
Queue Consumer는 해당 메시지를 받아 실제 작업을 수행한다.
예를 들어 다음과 같은 흐름이다.
- 사용자가 매일 보고서 생성을 예약한다.
- 애플리케이션이 동적 Schedule을 생성한다.
- 예약 시각에 Queue에 메시지가 전달된다.
- Queue Consumer가 보고서를 생성한다.
- 결과를 데이터베이스에 기록한다.
이러한 구조에서는 예약 실행과 실제 작업 처리를 분리할 수 있다.
하지만 메시지 기반 시스템에서는 중복 전달이나 재시도가 발생할 가능성을 고려해야 한다.
예를 들어 같은 보고서 생성 메시지가 두 번 전달될 수 있다.
따라서 실제 작업이 중복 실행되면 문제가 되는 경우에는 Idempotency Key나 데이터베이스의 고유 제약을 활용할 수 있다.
배포 없이 예약 관리
새로운 Schedules는 다음 작업을 지원한다.
- 활성화
- 비활성화
- 수동 실행
이러한 작업은 애플리케이션을 다시 배포하지 않아도 수행할 수 있다.
예를 들어 매일 실행하던 데이터 정리 작업에 문제가 발생했다고 가정해 보자.
관리자는 해당 Schedule을 일시적으로 비활성화할 수 있다.
이후 문제가 해결되면 다시 활성화할 수 있다.
또한 실제 예정 시각이 아니더라도 수동으로 실행해 동작을 확인할 수 있다.
이는 작업의 실행 시각과 애플리케이션 코드의 배포 주기를 분리하는 기능이다.
여러 배포 환경 지원
Schedules는 다음 환경에서 사용할 수 있다.
- Production
- Preview
- Custom Environment
예를 들어 실제 운영 환경과 테스트 환경에서 같은 종류의 예약 작업을 실행하고 싶을 수 있다.
하지만 테스트 환경의 작업이 운영 데이터베이스를 변경해서는 안 된다.
따라서 어떤 배포 환경에서 실제 Schedule을 실행하는지 구분해야 한다.
Schedules는 이러한 환경을 지원한다.
하지만 실제 Function이 사용하는 환경 변수와 데이터베이스의 연결 대상은 애플리케이션에서 관리해야 한다.
기존 Cron Jobs와의 관계
Vercel은 Schedules를 기존 Cron Jobs의 후속 서비스로 설명한다.
하지만 기존 Cron Jobs가 이번 발표와 동시에 중단된 것은 아니다.
기존 Cron Jobs는 계속 사용할 수 있다.
새로운 Schedules로 이전하려는 프로젝트에는 별도의 마이그레이션 가이드가 제공된다.
따라서 기존 프로젝트가 즉시 모든 예약 작업을 변경해야 하는 것은 아니다.
요금과 제한사항
이번 기능은 모든 Vercel 요금제에서 사용할 수 있다.
공식 발표에 따른 사용량 정책은 다음과 같다.
| 요금제 | 사용량 및 요금 |
|---|---|
| Hobby | 월 Schedule 생성 500회, 실행 5,000회 포함 |
| Pro·Enterprise | 실행 100만 회당 5달러부터 |
| Pro·Enterprise | 생성 100만 회당 10달러부터 |
이 수치는 Schedule 관리와 실행에 관한 것이다.
실제 Function과 Queue 사용에 발생하는 다른 비용은 별도로 고려해야 할 수 있다.
또한 Public Beta이므로 일부 API와 지원 범위가 이후 변경될 수 있다.
이번 발표에는 기존 Cron Jobs와 비교한 실행 지연 시간이나 정확도에 관한 독립적인 Benchmark가 포함되지 않았다.
일회성 및 반복 예약 작업을 비공개 Function과 Queue에 연결하고, 배포 없이 실행 설정을 관리할 수 있도록 한 서버리스 스케줄링 기능이다.
Cloudflare Durable Objects, RPC·fetch() 자동 재시도 기본 제공 — 중복 실행 방지와 재시도 제한 설정 추가
- 발표일: 2026-11-05 — 정확한 게시 시각 미확인
- 안정화 단계: 정식 Runtime 기능
- 적용 대상: Cloudflare Workers, Durable Objects
- 기본 재시도: 최대 4회
- 기본 시간 제한: 첫 호출 이후 10초가 지나면 새 재시도 시작하지 않음
- 필요 버전: 새로운 설정 API 사용 시 Wrangler 4.158.0 이상
- 원문: https://developers.cloudflare.com/changelog/post/2026-11-05-durable-objects-automatic-retries/
Cloudflare가 Durable Object에 전달하는 RPC와 fetch() 호출에서 일시적인 통신 오류가 발생하면 Runtime이 자동으로 재시도하도록 변경했다.
이번 기능은 단순히 재시도 횟수를 설정하는 API를 추가한 것이 아니다.
일부 실패 상황에서 개발자가 직접 Retry Loop를 작성하지 않아도 되도록 기본 Runtime 동작을 변경했다.
특히 다른 Durable Object를 호출하는 서비스에서 의미가 크다.
Durable Object 호출의 기본 구조
예를 들어 쇼핑몰에서 사용자별 장바구니를 Durable Object로 관리한다고 가정해 보자.
사용자마다 하나의 Durable Object가 존재할 수 있다.
장바구니의 상태는 해당 객체가 관리한다.
일반적인 Worker는 해당 Durable Object에 RPC를 호출할 수 있다.
예를 들어 다음 작업이다.
- 상품 수량 조회
- 상품 수량 변경
- 장바구니 초기화
하지만 이러한 호출도 내부 네트워크에 의존한다.
따라서 일시적인 연결 오류가 발생할 수 있다.
기존에는 이러한 상황을 처리하기 위해 개발자가 직접 재시도 로직을 작성해야 했다.
새로운 Runtime은 일부 오류를 자동으로 처리한다.
자동 재시도 대상
Cloudflare는 다음 호출에서 자동 재시도를 지원한다.
- Durable Object
fetch() - Durable Object RPC
호출 주체는 다음 두 환경 모두 가능하다.
- 일반 Worker
- 다른 Durable Object
예를 들어 Worker A가 Durable Object B에 RPC를 호출한다고 가정해 보자.
요청이 B에 도착하기 전에 일시적인 연결 오류가 발생했다.
Runtime은 해당 오류를 재시도할 수 있는 오류로 판단한다.
그리고 호출을 다시 시도한다.
따라서 애플리케이션이 직접 Retry Loop를 작성하지 않아도 해당 작업이 성공할 가능성이 있다.
기본 제한
공식 발표에서 확인한 기본값은 다음과 같다.
| 항목 | 기본 동작 |
|---|---|
| 최대 재시도 횟수 | 4회 |
| 재시도 시작 제한 | 최초 호출 시작 후 10초 |
| Durable Object 내부 코드에서 발생한 오류 | 자동 재시도하지 않음 |
.overloaded === true 오류 | 자동 재시도하지 않음 |
여기서 중요한 점은 모든 종류의 오류를 무조건 재시도하지 않는다는 사실이다.
예를 들어 Durable Object의 메서드가 실제로 오류를 발생시켰다고 가정해 보자.
이 오류는 단순한 네트워크 연결 실패와 다르다.
같은 호출을 다시 실행해도 동일한 오류가 발생할 가능성이 있다.
또한 실제 데이터가 이미 변경됐을 수도 있다.
따라서 Runtime은 Durable Object 코드에서 직접 발생한 오류를 자동 재시도 대상에서 제외한다.
요청이 도착했는지 알 수 없는 경우
이번 업데이트에서 가장 중요한 부분은 첫 번째 요청이 실제로 Durable Object에 도착했는지 확인할 수 없는 상황이다.
예를 들어 다음 흐름을 생각해 보자.
- Worker가 장바구니 수량 변경 RPC를 전송한다.
- Durable Object가 해당 요청을 받는다.
- 응답을 전달하는 과정에서 연결이 끊어진다.
- Worker는 요청이 실제로 처리됐는지 확인하지 못한다.
이 경우 Runtime은 요청을 다시 시도할 수 있다.
하지만 같은 메서드가 다시 실행되면 문제가 발생할 수 있다.
예를 들어 다음 두 작업은 서로 다르다.
첫 번째는 수량을 3으로 설정하는 작업이다.
setQuantity(itemId, 3)
이 작업은 같은 입력으로 두 번 실행하더라도 최종 수량이 3이다.
두 번째는 수량을 1 증가시키는 작업이다.
addOne(itemId)
이 작업이 두 번 실행되면 수량이 2 증가할 수 있다.
따라서 모든 메서드를 같은 방식으로 재시도해서는 안 된다.
기본적으로 중복 메서드 실행 방지
Cloudflare는 재시도 중복 실행을 방지하는 구조를 제공한다.
첫 번째 시도가 실제 Durable Object에 도착했다면 Runtime은 중복 재시도가 해당 메서드를 다시 실행하지 않도록 처리한다.
따라서 연결이 끊겼다는 사실만으로 같은 메서드가 두 번 실행되는 문제를 줄일 수 있다.
하지만 개발자가 특정 메서드는 다시 실행해도 된다고 명시할 수도 있다.
이를 위해 다음 함수를 사용한다.
retryable()
이 함수는 다음 Module에서 가져온다.
cloudflare:durable-objects
예를 들어 setQuantity()처럼 같은 입력의 반복 실행이 안전한 메서드에 적용할 수 있다.
반면 addOne()처럼 호출 횟수에 따라 결과가 달라지는 메서드에는 신중하게 적용해야 한다.
재시도 설정
개별 Durable Object Stub의 재시도 정책도 변경할 수 있다.
대표적인 API는 다음과 같다.
get()
getByName()
새로운 옵션은 다음과 같다.
retry
여기에 다음 값을 설정할 수 있다.
| 옵션 | 허용 범위 |
|---|---|
maxRetries | 0~10회 |
timeoutMs | 500~60,000ms |
예를 들어 다음 설정을 사용할 수 있다.
retry: { maxRetries: 1, timeoutMs: 2000 }
이렇게 하면 해당 Stub의 호출에서 재시도 횟수와 Timeout을 제한할 수 있다.
또한 다음 값을 지정할 수 있다.
maxRetries: 0
이 경우 Runtime의 자동 재시도를 비활성화한다.
기존 Retry Loop와의 관계
이번 업데이트에는 기존 프로젝트에서 고려해야 할 호환성 문제가 존재한다.
이미 애플리케이션에 직접 작성한 Retry Loop가 있다고 가정해 보자.
예를 들어 RPC를 최대 세 번 실행하도록 구현했다.
하지만 이제 각 RPC 호출 자체에도 Runtime의 자동 재시도가 적용될 수 있다.
따라서 애플리케이션이 예상한 것보다 더 많은 실제 연결 시도가 발생할 가능성이 있다.
Cloudflare는 이러한 경우 다음 두 방법을 안내한다.
- 애플리케이션의 기존 재시도 횟수 감소
- Durable Object Stub에서
maxRetries: 0설정
이렇게 하면 중복된 재시도 계층 때문에 불필요한 요청이 증가하는 문제를 줄일 수 있다.
다만 Runtime의 자동 재시도는 외부 API 호출 전체를 보호하는 범용 Idempotency 시스템이 아니다.
예를 들어 Durable Object 내부에서 외부 결제 API를 호출했다면 실제 결제의 중복 처리는 해당 API의 정책에 영향을 받는다.
적용 범위와 성능
이번 기본 재시도 기능은 Runtime 수준의 변경이다.
새로운 retryable()과 retry 옵션을 사용하려면 Wrangler 4.158.0 이상이 필요하다.
이번 발표에는 재시도 기능이 실제 오류율을 몇 퍼센트 감소시키는지에 관한 정량적인 Benchmark가 포함되지 않았다.
또한 재시도가 발생하면 실제 응답까지 걸리는 시간이 증가할 수 있다.
따라서 재시도는 가용성을 높이는 방법이지만 모든 요청의 응답 속도를 개선하는 기능은 아니다.
이번 업데이트는 Durable Object 사이의 일시적인 통신 오류를 Runtime에서 처리하면서 실제 메서드의 중복 실행을 제한하고, 애플리케이션이 재시도 정책을 직접 조정할 수 있도록 만든 기능이다.
Cloudflare workerd v1.20261105.1, Durable Object의 일반 WebSocket 연결을 32,768개로 제한
- 발표일: 2026-11-05
- GitHub 릴리스 표시 시각: 2026-11-05 00:45 — 표시 시간대 미확인
- 버전: workerd v1.20261105.1
- 안정화 단계: 오픈소스 Runtime 릴리스
- 적용 대상: Cloudflare workerd, Durable Objects, WebSocket
- 주요 변경: Non-hibernatable WebSocket 연결 수 제한
- 원문: https://github.com/cloudflare/workerd/releases/tag/v1.20261105.1
- 상세 구현: https://github.com/cloudflare/workerd/pull/8169
Cloudflare가 Workers Runtime의 오픈소스 구현체인 workerd의 새로운 버전을 공개하면서 Durable Object 하나가 유지할 수 있는 일반 WebSocket 연결 수를 제한했다.
이번 변경은 새로운 WebSocket API를 추가하는 기능이 아니다.
한 Durable Object가 지나치게 많은 연결을 유지할 때 같은 서버에서 실행되는 다른 객체까지 영향을 받을 수 있는 문제를 줄이기 위한 리소스 제한이다.
특히 대규모 실시간 서비스에서 중요하다.
기존 WebSocket 연결의 문제
Durable Object는 WebSocket 기반 서비스를 구현하는 데 사용할 수 있다.
예를 들어 실시간 채팅방을 하나의 Durable Object로 관리할 수 있다.
사용자는 해당 채팅방의 WebSocket에 연결한다.
Durable Object는 연결된 사용자의 목록과 메시지 전달을 관리한다.
하지만 하나의 객체에 매우 많은 사용자가 연결되면 리소스 문제가 발생할 수 있다.
예를 들어 하나의 Durable Object에 5만 개의 연결이 존재한다고 가정해 보자.
각 연결은 메모리와 네트워크 처리 자원을 사용한다.
또한 Durable Object가 재시작되면 많은 클라이언트가 동시에 재연결을 시도할 수 있다.
이러한 현상은 같은 물리 서버에서 실행되는 다른 Durable Object에도 영향을 줄 가능성이 있다.
이번 변경은 이러한 위험을 제한한다.
두 종류의 WebSocket
Cloudflare Durable Objects에서는 WebSocket을 크게 두 방식으로 사용할 수 있다.
첫 번째는 일반적인 WebSocket 연결이다.
이번 제한에서는 Non-hibernatable WebSocket이라고 부른다.
두 번째는 Hibernatable WebSocket이다.
Hibernatable WebSocket은 Durable Object의 실행 인스턴스가 계속 활성 상태에 존재하지 않더라도 연결을 유지할 수 있도록 설계됐다.
두 방식은 메모리 및 실행 자원 관리에서 차이가 있다.
기존에는 Hibernatable WebSocket에 이미 다음 제한이 존재했다.
Durable Object 인스턴스당 32,768개
반면 일반 WebSocket에서는 동일한 연결 수 제한이 명시적으로 적용되지 않았다.
이번 업데이트는 이러한 차이를 줄인다.
새로운 연결 제한
새로운 제한은 다음과 같다.
| 연결 방식 | 인스턴스당 제한 |
|---|---|
| Hibernatable WebSocket | 32,768개 |
| Non-hibernatable WebSocket | 32,768개 |
두 제한은 별도로 계산한다.
따라서 이번 변경을 하나의 Durable Object에서 모든 종류의 WebSocket을 합산해 32,768개까지만 허용하는 정책으로 해석해서는 안 된다.
현재 구현에서는 두 종류의 연결 수를 별도로 관리한다.
연결 수 계산 시점
일반 WebSocket은 다음 시점부터 연결 수에 포함된다.
첫 번째는 다음 메서드로 연결을 수락하는 경우다.
accept()
두 번째는 다음 생성자로 연결한 WebSocket이 실제로 Open 상태가 되는 경우다.
new WebSocket(url)
연결은 실제로 해제될 때까지 제한 수에 포함된다.
따라서 단순히 JavaScript 변수에서 WebSocket 참조를 제거했다고 해서 모든 경우에 즉시 연결 수가 감소한다고 가정해서는 안 된다.
실제 연결의 종료와 리소스 해제 과정을 고려해야 한다.
제한 초과 시 동작
연결 수가 32,768개를 초과하면 새로운 연결을 수락할 수 없다.
accept()를 사용하는 경우에는 오류가 발생한다.
반면 new WebSocket(url)로 생성한 연결은 Open 상태에 도달한 이후 종료된다.
그리고 WebSocket의 error 이벤트를 통해 오류가 전달된다.
따라서 실제 애플리케이션에서는 두 연결 방식의 오류를 서로 다르게 처리해야 할 수 있다.
예를 들어 채팅방에 새로운 사용자가 연결하려고 했지만 제한을 초과했다고 가정해 보자.
서버는 해당 사용자의 연결을 정상적으로 수락하지 못할 수 있다.
클라이언트에서는 재연결을 시도할 수 있다.
하지만 모든 사용자가 무조건 즉시 재연결을 시도하면 서버에 더 많은 부하가 발생할 수 있다.
따라서 실제 대규모 실시간 서비스에서는 재연결 간격과 Backoff 정책도 고려해야 한다.
Compatibility Flag가 없는 변경
이번 변경에서 중요한 부분은 호환성 정책이다.
공식 PR에 따르면 새로운 제한은 별도의 Compatibility Flag 없이 적용하도록 설계됐다.
이유는 기존 Durable Object의 연결 수를 계속 무제한으로 허용하면 리소스 고갈 위험을 줄이기 어렵기 때문이다.
따라서 기존 구현과의 호환성보다 실행 환경 전체의 안정성을 우선한 결정이다.
다만 Cloudflare는 내부적으로 해당 제한을 비활성화할 수 있는 Kill Switch를 제공한다.
이는 고객이 일반적으로 사용하는 Compatibility Flag와는 다르다.
실제 Cloudflare 운영 환경과의 구분
이번 발표에서 직접 확인한 것은 workerd v1.20261105.1의 오픈소스 릴리스와 해당 구현에 포함된 변경이다.
Cloudflare의 전 세계 운영 Runtime에 동일한 변경이 언제 모두 적용되는지는 이번 릴리스 정보만으로 확인할 수 없다.
따라서 모든 Cloudflare Workers 사용자에게 해당 제한이 이미 적용됐다고 단정해서는 안 된다.
또한 이번 릴리스에는 다음 수정도 포함됐다.
- Zstandard 압축 Frame을 끝까지 Flush하지 못하던 문제 수정
- Hibernation Handler를 별도의 Trace Event로 기록
- JavaScript 및 Rust 관련 내부 Runtime 처리 개선
이번 변경에는 WebSocket 처리량이나 메모리 사용량에 관한 정량적인 Benchmark가 포함되지 않았다.
대규모 WebSocket 연결이 하나의 Durable Object와 같은 실행 서버의 리소스를 지나치게 점유하지 않도록 연결 수에 상한을 추가한 Runtime 변경이다.
Amazon Cognito Plus, 월간 가용성 SLA 99.99%로 상향 — 기존 Plus 사용자에게 자동 적용
- 발표일: 2026-11-05 — 정확한 게시 시각 미확인
- 실제 계약 적용일: 2026-11-01
- 적용 대상: Amazon Cognito User Pool의 Plus 요금제
- 변경 전 SLA: 99.9%
- 변경 후 SLA: 99.99%
- 원문: https://aws.amazon.com/about-aws/whats-new/2026/11/amazon-cognito-sla-update/
AWS가 Amazon Cognito Plus 요금제의 월간 가용성 SLA를 99.9%에서 99.99%로 상향했다고 발표했다.
이번 소식은 인증 API나 사용자 관리 기능을 새롭게 추가하는 업데이트가 아니다.
관리형 인증 서비스의 가용성에 대한 AWS의 계약상 약속을 변경한 것이다.
특히 실제 계약 적용일은 11월 1일이고 공식 발표일은 11월 5일이라는 점을 구분해야 한다.
Cognito와 웹서비스 인증
Cognito는 사용자 인증과 관련된 기능을 제공한다.
예를 들어 다음 작업에 사용할 수 있다.
- 회원가입
- 로그인
- Access Token 발급
- 사용자 인증 상태 관리
- Multi-factor Authentication
이러한 기능은 웹 애플리케이션의 주요 진입점과 연결된다.
예를 들어 인증 서비스에 장애가 발생하면 새로운 사용자가 로그인하지 못할 수 있다.
또한 Token 갱신이 실패하면 이미 로그인한 사용자의 서비스 이용에도 영향을 줄 수 있다.
따라서 인증 서비스의 가용성은 전체 웹서비스의 안정성과 연결된다.
SLA의 의미
SLA(Service Level Agreement)는 서비스 제공업체가 약속하는 서비스 수준에 관한 계약이다.
이번 변경에서는 월간 가용성의 약속 수준이 높아졌다.
| 요금제 | 기존 SLA | 변경 후 SLA |
|---|---|---|
| Cognito Plus | 99.9% | 99.99% |
| Cognito Essentials | 99.9% | 99.9% |
| Cognito Lite | 99.9% | 99.9% |
다만 SLA가 99.99%라는 사실이 실제 측정한 가용성이 99.99%였다는 의미는 아니다.
또한 모든 장애 상황에서 계약상의 보상이 자동으로 제공된다는 의미도 아니다.
실제 적용 범위와 제외 조건은 Cognito SLA 문서에 따라 달라진다.
기존 사용자에게 자동 적용
이미 Plus 요금제를 사용하는 User Pool에는 새로운 Service Commitment가 자동 적용된다.
따라서 기존 Plus 사용자가 별도의 API를 호출하거나 새 Cognito User Pool을 생성할 필요는 없다.
Essentials나 Lite를 사용하는 사용자가 새로운 SLA 적용을 원한다면 Plus 요금제로 변경해야 한다.
AWS는 다음 방법을 지원한다.
- AWS Management Console
- AWS CLI
- AWS SDK
하지만 요금제를 변경하면 실제 Cognito 사용 비용과 제공되는 기능도 달라질 수 있다.
Plus 요금제에는 Adaptive Authentication과 유출된 인증 정보 탐지 등의 기능도 포함돼 있다.
실제 운영상의 한계
SLA는 서비스의 전체 설계에서 고려해야 하는 여러 요소 가운데 하나다.
예를 들어 Cognito의 인증 기능이 정상적으로 동작하더라도 애플리케이션의 데이터베이스에 장애가 발생하면 실제 사용자는 서비스를 이용하지 못할 수 있다.
또한 특정 네트워크에서만 발생한 연결 문제와 Cognito 서비스 전체의 가용성 문제는 서로 다르다.
따라서 이번 변경이 애플리케이션 전체의 가용성을 자동으로 99.99%로 높여주는 것은 아니다.
이번 발표는 Cognito Plus의 관리형 인증 서비스에 적용되는 계약상 가용성 약속을 상향한 정책 변경이다.
4️⃣ 개발 도구 및 보안
GitHub Secret Scanning, SpaceX 신규 파트너 추가 — Starlink API Secret 탐지 및 AWS·Google 등 오탐 감소
- 발표일: 2026-11-05 — 정확한 게시 시각 미확인
- 안정화 단계: 정식 Secret Scanning 탐지기 업데이트
- 적용 대상: GitHub Secret Scanning
- 신규 파트너: SpaceX
- 신규 Secret 유형:
starlink_public_api_v2_secret - 원문: https://github.blog/changelog/2026-11-05-secret-scanning-upgrades-detectors-adds-spacex-as-partner/
GitHub가 Secret Scanning의 신규 탐지 대상을 추가하고 AWS와 Google 등 여러 서비스의 기존 탐지기에서 발생하던 False Positive를 줄이는 업데이트를 공개했다.
이번 변경은 새로운 GitHub 보안 제품의 출시가 아니다.
기존 Secret Scanning의 탐지 범위를 확대하고 일부 패턴의 정확성을 개선한 것이다.
특히 새로운 파트너 Secret과 일반 사용자에게 제공되는 Secret Alert를 구분해야 한다.
Secret Scanning의 역할
웹 개발 프로젝트에는 여러 종류의 인증 정보가 존재할 수 있다.
예를 들어 다음과 같은 정보다.
- 클라우드 API Key
- 데이터베이스 Credential
- 외부 서비스 Access Token
- 인증용 Secret
이러한 정보는 일반적으로 소스코드에 직접 작성하지 않는 편이 적절하다.
하지만 실제 프로젝트에서는 실수로 인증 정보가 Commit에 포함될 수 있다.
예를 들어 개발자가 로컬 환경 변수 파일을 Git에 추가했다고 가정해 보자.
이후 해당 Commit을 공개 Repository에 Push한다.
실제 Secret이 노출될 수 있다.
GitHub Secret Scanning은 이러한 정보를 탐지하는 기능이다.
SpaceX 파트너 추가
이번 업데이트에서는 SpaceX가 Secret Scanning Partner Program에 참여했다.
새롭게 탐지하는 Secret 유형은 다음과 같다.
starlink_public_api_v2_secret
이는 Starlink 관련 API Secret을 식별하기 위한 패턴이다.
GitHub가 공개 Repository에서 해당 Secret을 발견하면 발급 기관에 관련 정보를 전달할 수 있다.
발급 기관은 필요한 경우 해당 인증 정보를 폐기하거나 교체할 수 있다.
이러한 방식은 단순히 Repository 관리자에게 경고를 표시하는 것과 다르다.
인증 정보를 발급한 서비스 제공업체가 직접 노출 사실을 확인하고 대응할 수 있도록 연결하는 구조다.
Partner Secret과 User Secret
GitHub는 Secret을 여러 종류로 구분한다.
이번 변경에서 중요한 차이는 다음 두 종류다.
| 구분 | 동작 |
|---|---|
| Partner Secret | 공개 Repository에서 발견하면 Secret 발급 기관에 보고 |
| User Secret | 공개 또는 비공개 Repository에서 발견하면 관련 Secret Scanning Alert 생성 |
따라서 새로운 Partner Secret 탐지가 추가됐다는 사실만으로 모든 비공개 Repository의 Secret이 발급 기관에 자동으로 전달된다고 단정해서는 안 된다.
실제 보고와 Alert 생성 방식은 Secret의 분류에 따라 달라진다.
AWS 등 기존 탐지기의 오탐 개선
이번 업데이트에서는 여러 기존 탐지기도 개선됐다.
주요 대상은 다음과 같다.
- Alibaba Cloud Access Key ID
- AWS Access Key ID
- AWS Session Token
- AWS Temporary Token
- Google Storage Key 관련 패턴
- OpenVSX
- Pydantic Logfire
- Tencent Cloud Access Key ID
특히 일부 탐지기에서는 서로 연관된 Secret을 함께 검사하는 과정이 개선됐다.
예를 들어 Access Key ID와 Secret Access Key가 함께 존재하는 상황을 생각할 수 있다.
탐지기는 두 값의 형태와 관계를 이용해 실제 인증 정보인지 판단할 수 있다.
하지만 Dictionary Filter가 관련 Secret을 제거한 이후에도 남은 문자열을 독립적인 Secret으로 보고할 수 있었다.
그 결과 실제로는 인증 정보가 아닌 문자열이 Alert로 나타날 수 있었다.
이번 변경은 이러한 불필요한 탐지를 줄인다.
오탐 감소의 효과와 한계
False Positive가 많으면 개발자가 보안 경고를 처리하는 데 불필요한 시간을 사용하게 된다.
예를 들어 실제로 문제가 없는 문자열이 계속 Secret으로 분류된다고 가정해 보자.
개발자는 반복적으로 Alert를 확인하고 종료해야 할 수 있다.
그 결과 실제로 중요한 Secret 노출을 발견했을 때도 다른 경고와 구분하기 어려워질 수 있다.
따라서 탐지기의 정확성을 높이는 것은 중요한 관리 작업이다.
하지만 오탐을 줄이는 과정에서는 실제 Secret을 놓치지 않도록 해야 한다.
이번 발표에는 False Positive가 몇 퍼센트 감소했는지에 관한 정량적인 수치가 포함되지 않았다.
또한 이번 업데이트로 모든 종류의 인증 정보 노출을 탐지할 수 있게 됐다는 의미는 아니다.
새로운 API Secret 탐지 범위를 확대하면서 기존 탐지기의 불필요한 경고를 줄이도록 개선한 공급망 보안 및 인증 정보 보호 업데이트다.
GitHub Pull Request Overview 공개 Preview — Merge 가능 여부와 미해결 리뷰를 한 화면에서 확인
- 발표일: 2026-11-05 — 정확한 게시 시각 미확인
- 안정화 단계: Public Preview, Opt-in
- 적용 대상: GitHub Pull Request 웹 UI
- 변경 유형: PR 검토 화면 및 탐색 방식 개선
- 원문: https://github.blog/changelog/2026-11-05-new-pull-request-overview-page-in-public-preview/
GitHub가 Pull Request의 Merge 상태와 Review, CI 결과를 더 쉽게 확인할 수 있도록 새로운 Overview 화면을 Public Preview로 공개했다.
이번 업데이트는 GitHub의 Pull Request API나 Merge 규칙을 변경하는 기능이 아니다.
기존 정보를 개발자가 더 빠르게 이해할 수 있도록 화면 구조를 변경한 것이다.
특히 리뷰 댓글이 많고 CI 검사도 여러 개 실행되는 대규모 PR에서 유용할 수 있다.
기존 PR 화면의 문제
예를 들어 변경 파일이 많은 Pull Request를 생각해 보자.
개발자는 다음 내용을 확인해야 한다.
- CI 검사가 모두 통과했는가?
- 아직 승인하지 않은 Reviewer가 있는가?
- 해결되지 않은 Review Comment가 있는가?
- Merge Conflict가 존재하는가?
- 현재 PR을 Merge할 수 있는가?
하지만 기존 화면에서는 여러 댓글과 이벤트를 지나 실제 Merge 상태를 확인해야 하는 경우가 있었다.
특히 오랫동안 유지된 PR에는 많은 변경 기록이 포함될 수 있다.
이러한 상황에서는 현재 어떤 작업이 남아 있는지 확인하기 어려울 수 있다.
새로운 Overview는 이러한 문제를 줄이도록 설계됐다.
Merge Readiness Summary
새로운 화면에서는 PR 상단에 Merge 상태를 요약해 표시한다.
여기에는 다음 정보가 포함될 수 있다.
- CI 검사 결과
- Review 승인 상태
- 미해결 댓글
- Merge 가능 여부
- 다음으로 수행할 수 있는 작업
GitHub는 기존 Merge Box와 비교해 같은 정보를 절반 이하의 공간에 표시하도록 변경했다고 설명한다.
하지만 이는 UI가 차지하는 공간에 관한 설명이다.
실제 개발자의 리뷰 시간이 절반으로 감소했다는 성능 Benchmark는 아니다.
댓글 필터와 정렬
새로운 Overview에서는 Review Comment를 더 세밀하게 필터링할 수 있다.
대표적인 필터는 다음과 같다.
- 전체 댓글
- 해결되지 않은 댓글
- 자신이 작성한 댓글
또한 최신 댓글부터 표시하도록 정렬할 수 있다.
예를 들어 다음과 같은 상황이다.
PR에는 댓글이 100개 존재한다.
하지만 실제로 해결되지 않은 댓글은 3개뿐이다.
이 경우 모든 댓글을 읽지 않고 미해결 댓글만 확인할 수 있다.
또한 자신이 작성한 댓글의 상태를 확인하기 쉬워진다.
최근 활동 요약
화면 오른쪽 위에는 최근 활동의 요약을 제공한다.
대표적으로 다음 정보가 포함될 수 있다.
- Review Comment
- 새로운 Review 요청
- 최근 상태 변경
따라서 오래된 PR을 다시 확인하는 개발자가 처음부터 모든 이벤트를 읽지 않아도 현재 상태를 이해하는 데 도움을 받을 수 있다.
하지만 이번 기능이 실제 코드의 변경 사항을 AI로 자동 분석해 정확한 리뷰를 제공한다는 의미는 아니다.
현재 Preview는 기존 PR의 상태와 활동 정보를 탐색하는 방식에 관한 것이다.
활성화와 제한사항
새로운 Overview는 Opt-in 방식으로 제공된다.
PR 화면의 상단에서 다음 버튼을 선택할 수 있다.
Try the Overview page
Preview 기간에는 기존 화면으로 되돌릴 수도 있다.
또한 기능은 순차적으로 배포된다.
따라서 발표 직후 모든 사용자의 모든 PR에서 동일하게 버튼이 나타난다고 보장할 수 없다.
이번 변경은 실제 Merge 가능 여부와 미해결 리뷰, 최근 활동을 더 쉽게 확인하도록 Pull Request의 정보 구조를 개선한 UI Preview다.
이번 조사 범위에서 이외에 포함할 만한 주요 신규 개발 도구 및 보안 발표는 확인하지 못했습니다.
Node.js, Deno, Bun, Vite, webpack, Rollup, esbuild, Babel, Biome, ESLint, Prettier, npm, pnpm, Yarn 및 GitLab의 공식 릴리스와 변경 기록을 검토했다.
소규모 유지보수 업데이트와 개발 빌드의 변경은 정식 기능 출시 및 중요한 보안 수정과 구분했다.
이전 브리핑에서 이미 소개한 GitHub Actions Workflow Lockfile, CodeQL 2.27.3·2.27.4, Dependabot 관련 업데이트와 Vercel의 기존 보안 기능도 반복하지 않았다.
📚 추천 글
Browser AI APIs in 2026: A practical guide
- 저자: Rahul Chhodde
- 매체: LogRocket
- 게시일: 2026-11-05
- 기술: JavaScript, Browser AI API, Summarizer, Translator, LanguageDetector, React
- 원문: https://blog.logrocket.com/browser-ai-apis-2026/
브라우저에서 제공하는 AI API를 이용해 텍스트를 요약하고 언어를 감지하며 번역하는 기능을 실제 웹 애플리케이션에 연결하는 방법을 설명한 글이다.
이번 글은 새로운 AI 모델의 출시를 소개하는 자료가 아니다.
이미 제공되기 시작한 Browser AI API를 실제 서비스에서 사용하려면 어떤 조건을 확인해야 하는지 설명한다.
특히 브라우저가 API를 제공한다는 사실과 실제 사용자의 기기에서 모델을 실행할 수 있다는 사실은 다르다는 점을 강조한다.
Browser AI API의 기본 구조
일반적인 AI 기반 웹 애플리케이션에서는 다음과 같은 구조를 사용한다.
- 사용자가 텍스트 입력
- 브라우저에서 백엔드 API 호출
- 백엔드에서 AI 모델 제공업체 호출
- 모델이 결과 생성
- 브라우저로 결과 반환
이러한 구조에서는 서버와 외부 모델 API의 사용량에 따라 비용이 발생할 수 있다.
또한 네트워크 연결 상태에 영향을 받는다.
반면 Browser AI API는 일부 AI 작업을 브라우저가 제공하는 기능으로 처리할 수 있다.
예를 들어 사용자가 고객지원 메시지를 입력했다고 가정해 보자.
브라우저는 해당 메시지의 언어를 감지할 수 있다.
이후 메시지를 요약하거나 다른 언어로 번역할 수 있다.
지원 환경에서는 이러한 작업을 로컬 모델로 처리할 수 있다.
하지만 모든 Browser AI API가 모든 브라우저에서 동일하게 지원되는 것은 아니다.
세 가지 Task API
저자는 다음 세 API를 중심으로 설명한다.
| API | 주요 역할 |
|---|---|
| Summarizer | 긴 텍스트 요약 |
| LanguageDetector | 입력 텍스트의 언어 감지 |
| Translator | 텍스트 번역 |
예를 들어 사용자가 영어로 작성한 고객 문의를 입력했다고 가정해 보자.
애플리케이션은 먼저 LanguageDetector로 언어를 확인한다.
그다음 Summarizer로 주요 내용을 요약할 수 있다.
마지막으로 Translator를 이용해 한국어 등 다른 언어로 변환할 수 있다.
이러한 구조에서는 모든 작업을 하나의 범용 LLM에 전달하지 않아도 된다.
특정 작업에 적합한 API를 선택할 수 있기 때문이다.
하지만 실제 모델의 지원 언어와 품질은 브라우저마다 다를 수 있다.
첫 번째 검사: API가 존재하는가?
저자는 다음 조건으로 기능을 확인한다.
"LanguageDetector" in self
이렇게 하면 현재 JavaScript 실행 환경에 LanguageDetector가 존재하는지 확인할 수 있다.
하지만 API가 존재한다는 사실만으로 해당 기능을 실제로 실행할 수 있다는 의미는 아니다.
예를 들어 브라우저는 API를 제공하지만 사용자의 기기가 필요한 모델 실행 조건을 충족하지 못할 수 있다.
또는 실제 모델 파일을 아직 다운로드하지 않았을 수 있다.
따라서 추가적인 검사가 필요하다.
두 번째 검사: 실제로 사용할 수 있는가?
저자는 다음 메서드를 사용한다.
availability()
예를 들어 다음과 같다.
Summarizer.availability(options)
이 API는 실제 기능의 사용 가능 상태를 확인하는 데 사용된다.
대표적으로 다음 상황을 구분해야 한다.
- 현재 기기에서 사용할 수 없음
- 모델 다운로드가 필요함
- 이미 사용할 준비가 완료됨
따라서 단순히 API의 존재 여부만 확인하는 방식보다 정확하게 실제 실행 조건을 판단할 수 있다.
하지만 Browser AI API의 세부 상태와 옵션은 사용하는 브라우저 및 API 버전에 영향을 받을 수 있다.
따라서 실제 애플리케이션에서는 지원하는 브라우저의 공식 문서와 런타임 결과를 함께 확인해야 한다.
모델 다운로드
이번 글에서 중요한 부분은 AI 모델의 다운로드 과정이다.
브라우저가 API를 지원하더라도 필요한 모델 파일이 사용자의 기기에 존재하지 않을 수 있다.
이 경우 처음 기능을 사용할 때 모델을 다운로드해야 한다.
문제는 모델의 크기와 다운로드 시간이다.
예를 들어 사용자가 모바일 네트워크를 이용하고 있다고 가정해 보자.
브라우저가 모델을 다운로드하기 시작하면 상당한 네트워크 사용량이 발생할 수 있다.
따라서 사용자에게 아무런 안내 없이 다운로드를 시작하는 것은 적절하지 않을 수 있다.
저자는 다음 방법을 제안한다.
- 사용자에게 모델 다운로드가 필요하다는 사실 표시
- 다운로드 진행 상태 제공
- 실제 사용자의 클릭 이후 다운로드 시작
- 다운로드할 수 없는 환경에서는 대체 경로 사용
예를 들어 요약 버튼을 누르면 필요한 모델을 준비하도록 구성할 수 있다.
이러한 방식은 사용자가 자신이 어떤 작업을 시작했는지 이해할 수 있도록 한다.
Secure Context
Browser AI API의 사용에는 보안 조건도 존재한다.
대표적인 조건은 HTTPS다.
개발 환경에서는 localhost가 Secure Context로 취급될 수 있다.
하지만 실제 운영 환경에서는 HTTPS를 사용해야 한다.
저자는 다음 속성으로 실행 환경을 확인하도록 안내한다.
isSecureContext
예를 들어 브라우저가 지원하는 모델을 사용할 수 있어도 현재 페이지가 Secure Context가 아니라면 관련 API를 사용할 수 없을 수 있다.
따라서 브라우저 버전뿐 아니라 실제 사이트의 실행 환경도 고려해야 한다.
iframe의 Permissions Policy
Browser AI API를 iframe 안에서 사용하는 경우에는 추가적인 제약이 존재할 수 있다.
예를 들어 개발 문서에서 외부 CodePen 예제를 iframe으로 표시한다고 가정해 보자.
원래 페이지에서는 API를 사용할 수 있다.
하지만 iframe에서는 접근 권한이 제한될 수 있다.
이러한 경우 다음 Permission을 명시적으로 설정해야 할 수 있다.
summarizertranslatorlanguage-detector
대표적인 HTML 속성은 다음과 같다.
allow="summarizer; translator; language-detector"
하지만 해당 Permission을 추가했다고 해서 브라우저가 지원하지 않는 API까지 사용할 수 있는 것은 아니다.
Permissions Policy는 실제로 지원하는 기능에 대한 접근을 제어하는 수단이다.
고객지원 메시지 처리 예제
저자는 React로 작성한 고객지원 메시지 처리 예제도 제공한다.
실행 흐름은 다음과 같다.
- 사용자가 메시지를 입력한다.
- Browser AI API 지원 여부를 확인한다.
- LanguageDetector로 언어를 감지한다.
- Summarizer로 내용을 요약한다.
- Translator로 필요한 언어로 번역한다.
- 결과를 화면에 표시한다.
실제 테스트에서는 짧고 구어체가 포함된 메시지도 언어를 잘 감지했다고 설명한다.
또한 여러 언어의 번역을 테스트했다.
하지만 일부 의미 없는 문자열이나 비정상적인 표현에서는 기대한 결과가 나오지 않았다.
이러한 결과는 저자가 직접 사용한 브라우저와 기기의 관찰 결과다.
여러 언어와 도메인에서 정확도를 정량적으로 평가한 독립적인 Benchmark는 아니다.
서버 Fallback
이번 글에서 가장 중요한 설계 선택은 Browser AI API를 유일한 실행 경로로 사용하지 않는다는 점이다.
예를 들어 Firefox나 Safari에서 해당 API를 사용할 수 없다고 가정해 보자.
이 경우 같은 기능을 서버 API로 처리하도록 구성할 수 있다.
또한 Chrome이나 Edge에서도 실제 사용자의 기기가 모델 실행 조건을 충족하지 못할 수 있다.
따라서 다음 구조를 사용한다.
- 브라우저 API 사용 가능: 로컬 실행
- 브라우저 API 사용 불가: 서버 API 호출
이러한 방식에서는 지원하는 브라우저에서 로컬 처리의 이점을 얻으면서 다른 환경에서도 기능을 제공할 수 있다.
하지만 Fallback이 서버로 데이터를 전송한다면 개인정보 처리 특성이 달라질 수 있다.
로컬에서만 처리하는 경우와 외부 API에 사용자 데이터를 전달하는 경우는 구분해야 한다.
따라서 사용자에게 어떤 경로로 데이터를 처리하는지 안내할 필요가 있을 수 있다.
모델 인스턴스 재사용
저자는 create()를 필요할 때마다 반복해서 호출하지 않도록 권장한다.
모델 인스턴스를 생성하는 과정에는 초기화 비용이 존재할 수 있다.
따라서 동일한 작업을 반복하는 애플리케이션에서는 생성한 인스턴스를 재사용하는 편이 적합할 수 있다.
하지만 장시간 유지하는 인스턴스에는 메모리 사용량 등의 비용이 존재할 수 있다.
따라서 실제 애플리케이션의 사용 패턴에 따라 수명을 관리해야 한다.
하드웨어 및 브라우저 지원의 한계
이번 글은 Chrome과 Edge를 중심으로 Browser AI API의 지원 범위를 비교한다.
하지만 다음 조건이 실제 실행 가능 여부에 영향을 준다.
- 브라우저 버전
- 운영체제
- GPU 및 CPU
- 사용 가능한 디스크 공간
- 필요한 모델의 다운로드 여부
- 실험 기능의 활성화 여부
또한 모든 API가 같은 안정화 단계에 존재하는 것은 아니다.
예를 들어 일부 Task API는 안정 버전에서 제공될 수 있지만 범용 Prompt API나 Writer 계열 기능은 실험 단계일 수 있다.
따라서 브라우저가 특정 버전 이상이라는 이유만으로 모든 Browser AI API를 동일하게 사용할 수 있다고 판단해서는 안 된다.
이번 글은 브라우저에서 AI 모델을 직접 실행하는 기능을 구현할 때 API 지원 여부와 실제 기기 조건, 모델 다운로드, 보안 정책, 서버 Fallback을 하나의 설계로 관리하는 방법을 설명한다.
Svelte 5 Isn't Pure Compiler Magic: How Runes, Fine-Grained Signals, and Direct DOM Mutations Actually Work Under the Hood
- 저자: S M Tahosin
- 매체: DEV Community
- 게시일: 2026-11-04
- 기술: Svelte 5, JavaScript, TypeScript, Runes, Fine-grained Reactivity
- 원문: https://dev.to/smtahosin/svelte-5-isnt-pure-compiler-magic-how-runes-fine-grained-signals-and-direct-dom-mutations-36a3
Svelte 5가 기존 Svelte 3·4의 컴파일 시점 반응성 처리 방식에서 벗어나 Runtime Signal과 컴파일러를 결합하는 구조로 변경된 이유를 설명한 기술 글이다.
이번 글의 특징은 새로운 Svelte 릴리스를 소개하는 데 있지 않다.
Svelte 5의 기존 기능인 Runes를 실제 실행 구조 관점에서 분석한다.
특히 다음 세 가지를 구분한다.
- 컴파일 시점에 할 수 있는 작업
- 실행 중 반응형 의존성을 추적하는 작업
- 실제 DOM을 변경하는 작업
이는 Svelte가 다른 프론트엔드 프레임워크와 어떤 실행 구조의 차이를 가지는지 이해하는 데 도움이 된다.
Svelte 3·4의 반응성 처리
기존 Svelte에서는 컴포넌트의 변수가 변경되면 해당 변경을 감지하도록 코드를 컴파일했다.
예를 들어 다음 변수가 있다고 가정해 보자.
let count = 0
이후 다음 작업을 수행한다.
count += 1
Svelte 컴파일러는 이러한 Assignment를 분석해 내부 갱신 함수를 호출하도록 변환할 수 있었다.
대표적인 내부 Helper는 다음과 같다.
$$invalidate
이러한 방식에서는 컴포넌트의 어떤 값이 변경됐는지 Bitmask 등으로 관리할 수 있었다.
이후 변경된 값에 의존하는 DOM을 갱신한다.
하지만 이 방식에는 제약이 존재했다.
컴포넌트 밖의 반응성
기존 방식에서는 반응성이 .svelte 컴포넌트의 컴파일 과정에 강하게 연결돼 있었다.
예를 들어 장바구니 상태를 여러 컴포넌트에서 공유한다고 가정해 보자.
기존에는 Svelte Store를 사용할 수 있었다.
대표적인 API는 다음과 같다.
writable()derived()
하지만 일반적인 JavaScript 또는 TypeScript Module에서 같은 방식의 반응형 변수를 사용하기는 어려웠다.
따라서 컴포넌트의 내부 상태와 애플리케이션의 공유 상태를 작성하는 방법이 달라질 수 있었다.
Svelte 5는 이러한 차이를 줄이기 위해 Runes를 도입했다.
$state
대표적인 Rune은 다음과 같다.
$state
예를 들어 다음과 같이 작성할 수 있다.
let count = $state(0)
이 값은 단순한 일반 변수와 다르게 반응형 상태로 관리된다.
따라서 해당 값을 읽는 렌더링 표현과 연결할 수 있다.
Svelte 5는 이러한 반응성을 컴포넌트뿐 아니라 다음 파일에서도 사용할 수 있도록 한다.
.svelte.js.svelte.ts
이렇게 하면 공통 상태 로직을 컴포넌트 밖으로 분리할 수 있다.
예를 들어 장바구니 상태를 관리하는 Class를 만들 수 있다.
각 컴포넌트는 해당 Class의 상태를 읽는다.
그리고 특정 상태가 변경됐을 때 관련 UI를 갱신할 수 있다.
$derived
두 번째 중요한 Rune은 다음과 같다.
$derived
이는 기존 상태에서 계산할 수 있는 값을 표현한다.
예를 들어 다음 두 값이 존재한다고 가정해 보자.
count
double
double은 항상 count * 2다.
이 경우 두 값을 독립적인 State로 관리할 필요가 없다.
대신 다음처럼 표현할 수 있다.
let double = $derived(count * 2)
이러한 방식에서는 count가 변경되면 관련 Derived Value를 다시 계산할 수 있다.
하지만 실제로 값이 필요하지 않은 상황에서는 계산을 지연할 수 있다.
저자는 이러한 구조를 Push-Pull Signal Graph로 설명한다.
Push-Pull Signal Graph
Push 단계에서는 기존 상태가 변경됐다는 사실을 관련 의존성에 전달한다.
예를 들어 count가 변경됐다고 가정해 보자.
double은 더 이상 이전 계산 결과를 그대로 신뢰할 수 없다.
따라서 갱신이 필요한 상태로 표시할 수 있다.
하지만 모든 Derived Value를 그 순간 즉시 다시 계산할 필요는 없다.
Pull 단계에서는 실제로 해당 값을 읽는 시점에 필요한 계산을 수행한다.
이러한 구조는 여러 Derived Value가 서로 연결된 상황에서 불필요한 계산을 줄이는 데 도움이 된다.
예를 들어 다음 관계를 생각할 수 있다.
countdouble = count * 2triple = count * 3total = double + triple
count가 변경됐다고 해서 모든 계산을 여러 번 중복 실행할 필요는 없다.
필요한 값을 실제로 읽는 시점에 최신 의존성의 결과를 이용할 수 있다.
컴파일러의 역할
Svelte 5에서 Runtime Signal을 사용한다고 해서 컴파일러의 역할이 사라지는 것은 아니다.
컴파일러는 여전히 Template을 분석한다.
특히 다음 작업을 수행할 수 있다.
- 정적인 HTML 구조 식별
- 동적으로 변경되는 표현 식별
- 실제 DOM에서 변경할 위치 결정
- 필요한 반응형 연결 생성
예를 들어 다음 구조의 컴포넌트가 있다고 가정해 보자.
- 정적인 제목
- 정적인 설명 문단
count를 표시하는 Button
이 경우 count가 변경될 때 정적인 문단 전체를 다시 생성할 필요는 없다.
컴파일러는 어떤 DOM Node가 해당 값에 의존하는지 미리 분석할 수 있다.
이후 실제로 필요한 Text Node를 갱신하는 코드를 생성할 수 있다.
저자는 이러한 구조를 기존 Virtual DOM 기반 렌더링과 비교한다.
다만 글의 컴파일 결과 예시는 내부 동작을 설명하기 위한 개념적 표현이다.
실제 Svelte 컴파일러가 모든 버전에서 정확히 같은 JavaScript를 생성한다고 보장하는 자료는 아니다.
깊은 객체 반응성
Svelte 5의 $state는 객체와 배열에도 사용할 수 있다.
예를 들어 다음 구조가 있다고 가정해 보자.
settings.user.notifications.email
애플리케이션은 이 중첩된 값을 변경할 수 있다.
Svelte는 Proxy 기반의 반응성을 이용해 관련 변경을 추적할 수 있다.
따라서 항상 최상위 객체를 새로운 객체로 교체해야 하는 것은 아니다.
하지만 큰 데이터 구조에서는 다른 문제가 발생할 수 있다.
예를 들어 5만 개의 객체를 가진 배열을 반응형으로 관리한다고 가정해 보자.
이러한 데이터를 모두 깊은 반응성 처리의 대상으로 만들면 불필요한 Proxy 관리 비용이 발생할 수 있다.
$state.raw
이러한 상황에서는 다음 Rune을 사용할 수 있다.
$state.raw
이 방식은 객체 내부를 깊게 반응형으로 처리하지 않는다.
따라서 배열 안의 객체를 직접 수정하더라도 해당 변경이 자동으로 UI에 반영되지 않을 수 있다.
반면 최상위 참조를 새로운 값으로 교체하면 관련 상태가 갱신될 수 있다.
예를 들어 다음 데이터에 활용할 수 있다.
- 대규모 API 응답
- 대부분 읽기만 하는 데이터
- 외부 라이브러리가 자체적으로 관리하는 객체
- WebGL 또는 Chart 관련 데이터
하지만 $state.raw를 사용한다는 사실만으로 모든 데이터 처리 작업이 반드시 빨라진다는 의미는 아니다.
실제 데이터의 크기와 갱신 방식에 따라 결과가 달라진다.
$effect의 역할
저자는 $effect를 상태 계산에 무분별하게 사용하는 문제도 설명한다.
예를 들어 다음과 같은 구조를 생각할 수 있다.
count가 변경되면 Effect에서 double을 갱신한다.
이 경우 count와 double을 별개의 State로 유지해야 한다.
또한 Effect의 실행과 추가 상태 변경이 발생할 수 있다.
하지만 double은 실제로 count에서 계산할 수 있는 값이다.
따라서 $derived가 더 적합하다.
반면 $effect는 다음과 같은 외부 작업에 사용할 수 있다.
- DOM 직접 변경
- Canvas 작업
- 외부 이벤트 구독
- WebSocket 연결
- Local Storage 동기화
즉 상태로부터 계산할 수 있는 값과 실제 외부 시스템에 영향을 주는 작업을 구분해야 한다는 것이다.
실제 설계에서의 제한사항
이번 글은 Svelte 5의 반응성 실행 구조를 설명한다.
하지만 특정 앱에서 Svelte가 React보다 몇 배 빠르다는 독립적인 Benchmark를 제공하지는 않는다.
또한 Runtime Signal과 Compiler의 실제 내부 구현은 Svelte 버전에 따라 변경될 수 있다.
따라서 개념적 컴파일 결과를 실제 Source Output과 동일하게 취급해서는 안 된다.
이번 글은 Svelte 5에서 반응형 상태와 계산된 값, Effect, DOM 업데이트가 서로 어떤 역할을 담당하는지 이해하는 데 필요한 아키텍처 자료다.
Building with Topcoat, the Rust framework that skips WebAssembly entirely
- 저자: Ikeh Akinyemi
- 매체: LogRocket
- 게시일: 2026-11-03
- 기술: Rust, Topcoat 0.10.0, Toasty 0.11.0, SQLite, WebSocket, SSR
- 원문: https://blog.logrocket.com/building-with-topcoat-the-rust-framework-that-skips-webassembly-entirely/
- 실험 코드: https://github.com/Ikeh-Akinyemi/reimagined-octo-giggle
Rust로 웹 애플리케이션을 개발하면서 브라우저에서 WebAssembly를 실행하지 않고도 서버 렌더링과 클라이언트 반응성을 제공하는 Topcoat 프레임워크를 실제 프로젝트에 적용한 글이다.
저자는 단순한 카운터 예제 대신 실시간 작업 보드를 만들었다.
작업 보드에는 다음 기능이 존재한다.
- 작업 목록
- 작업 상태 변경
- 검색
- 작업 이동
- 여러 브라우저 탭 사이의 실시간 동기화
특히 Rust에서 작성한 표현식을 브라우저의 JavaScript로 변환하는 과정에서 실제로 발견한 호환성 문제를 공개했다는 점이 중요하다.
일반적인 Rust 웹 프레임워크의 접근
Rust를 이용해 웹 프론트엔드를 개발하는 경우 WebAssembly를 사용할 수 있다.
WebAssembly는 브라우저에서 실행할 수 있는 바이너리 형식이다.
Rust 코드를 WebAssembly로 컴파일하면 브라우저에서 일부 애플리케이션 로직을 실행할 수 있다.
하지만 실제 프로젝트에서는 다음 작업이 필요할 수 있다.
- WebAssembly 빌드
- JavaScript와 WebAssembly 사이의 데이터 전달
- 브라우저 API와의 연결
- 서버 렌더링과 클라이언트 Hydration의 조합
Topcoat는 다른 접근을 사용한다.
서버에서 HTML 렌더링
Topcoat는 Rust에서 HTML을 생성한다.
컴포넌트는 비동기 Rust 함수로 작성할 수 있다.
따라서 컴포넌트 안에서 데이터베이스 Query를 기다린 뒤 결과를 HTML로 반환하는 방식이 가능하다.
저자는 Toasty를 이용해 SQLite에서 데이터를 가져왔다.
작업 보드에는 다음 네 상태가 존재한다.
- To do
- Doing
- Review
- Done
각 작업에는 다음 정보가 포함된다.
- ID
- 제목
- 담당자
- 현재 상태
- Blocked 여부
서버는 실제 데이터를 조회한 뒤 작업 보드를 HTML로 생성한다.
브라우저는 이미 데이터가 포함된 HTML을 받을 수 있다.
브라우저의 반응성을 제공하는 방법
이번 구조의 특징은 전체 Rust 애플리케이션을 WebAssembly로 컴파일하지 않는다는 사실이다.
대신 Topcoat는 특정 Rust 표현식을 JavaScript로 변환한다.
대표적인 문법은 다음과 같다.
$(...)
이 표현은 Topcoat의 view! 안에서 사용한다.
예를 들어 사용자가 Done 상태의 작업을 숨기는 버튼을 누른다고 가정해 보자.
버튼의 클릭 이벤트는 브라우저에서 실행된다.
Topcoat는 관련 Rust 표현식을 JavaScript로 변환한다.
그리고 필요한 반응형 Signal과 연결한다.
따라서 단순한 UI 상태 변경을 위해 항상 서버에 요청을 보낼 필요는 없다.
하지만 모든 Rust 문법을 JavaScript로 변환할 수 있는 것은 아니다.
Topcoat가 지원하는 표현식의 범위에는 제한이 존재한다.
브라우저 Runtime의 크기
저자는 실제 예제 프로젝트에 포함된 Browser Runtime의 압축 크기를 측정했다.
결과는 다음과 같았다.
15,160바이트
약 15KB다.
하지만 이 수치는 Topcoat 0.10.0의 공유 Runtime Script에 관한 것이다.
실제 웹페이지의 전체 JavaScript 다운로드 크기가 항상 15KB라는 의미는 아니다.
애플리케이션이 생성한 표현식과 데이터, 기타 정적 자산이 추가될 수 있기 때문이다.
또한 이번 수치는 다른 프레임워크와 완전히 동일한 UI를 구현한 뒤 측정한 독립적인 성능 비교는 아니다.
서버에 요청하는 두 가지 방법
Topcoat는 다음 개념을 제공한다.
- Shard
- Procedure
Shard는 서버에서 다시 렌더링할 수 있는 UI 영역이다.
예를 들어 사용자가 작업 목록을 검색한다고 가정해 보자.
브라우저는 검색어를 서버로 전달할 수 있다.
서버는 해당 검색어에 맞는 작업을 조회한다.
이후 관련 HTML을 다시 생성한다.
브라우저는 해당 결과를 기존 화면에 반영한다.
Procedure는 브라우저에서 호출할 수 있는 서버 함수다.
예를 들어 작업을 다른 상태로 이동시키는 기능이 해당한다.
브라우저는 Procedure를 호출한다.
서버는 실제 작업의 상태를 변경한다.
이러한 구조에서는 단순한 UI 표현과 실제 데이터 변경을 구분할 수 있다.
서버 측 검증
저자는 작업을 이동시키는 Procedure에서 다음 조건을 검사했다.
- 존재하는 작업인가?
- 이동할 대상 상태가 유효한가?
- Blocked 상태인가?
- 인접한 상태로 이동하는가?
예를 들어 To do에서 바로 Done으로 이동할 수 없도록 규칙을 만들 수 있다.
브라우저에서는 해당 버튼을 비활성화할 수 있다.
하지만 버튼이 비활성화됐다는 사실만으로 서버의 규칙을 보호할 수는 없다.
사용자는 실제 Procedure를 직접 호출할 수 있기 때문이다.
따라서 서버에서도 같은 규칙을 검증해야 한다.
동시성 문제
저자는 실제 구현의 한계도 명시했다.
현재 작업 이동 로직은 다음 순서로 동작한다.
- 현재 작업 상태 조회
- 이동 가능한지 검사
- 데이터베이스에 변경 기록
문제는 두 요청이 동시에 실행되는 경우다.
예를 들어 두 사용자가 같은 작업을 다른 상태로 이동시키려고 할 수 있다.
두 요청 모두 이전 상태를 읽었다면 같은 기준으로 검증할 수 있다.
이후 실제 변경 순서에 따라 예상하지 못한 결과가 발생할 수 있다.
따라서 실제 운영 서비스에서는 Atomic Update나 적절한 Transaction이 필요하다.
또한 현재 예제에는 실제 사용자 인증과 작업별 권한 검증이 완전히 포함되지 않았다.
WebSocket 기반 실시간 갱신
저자는 작업 이동 결과를 여러 브라우저 탭에 전달하도록 구현했다.
이를 위해 다음 기능을 사용했다.
live!emit!
서버에서는 작업 보드의 변경을 구독한다.
실제로 작업 상태가 변경되면 새로운 보드의 HTML을 생성한다.
그리고 연결된 브라우저에 전달한다.
저자의 테스트에서는 한 탭에서 작업을 이동한 결과가 다른 탭에도 표시됐다.
관련 WebSocket Frame은 작은 테스트 데이터 기준 약 4KB였다.
하지만 데이터가 증가하면 전달하는 HTML 크기도 증가할 수 있다.
또한 이번 구현은 변경된 행 하나만 전달하는 방식이 아니다.
작업이 변경될 때 전체 작업 보드를 다시 조회하고 렌더링하는 구조다.
따라서 작업 수와 연결된 사용자 수가 증가하면 서버의 처리 비용도 증가할 수 있다.
하나의 프로세스에 의존하는 이벤트 전달
저자는 Tokio Broadcast Channel을 이용했다.
이 방식은 하나의 서버 프로세스 안에서 이벤트를 전달하는 데 사용할 수 있다.
하지만 여러 서버 인스턴스를 사용하는 환경에서는 문제가 발생할 수 있다.
예를 들어 사용자 A는 서버 인스턴스 1에 연결돼 있다.
사용자 B는 서버 인스턴스 2에 연결돼 있다.
인스턴스 1에서 작업 변경 이벤트가 발생했다고 가정해 보자.
해당 이벤트가 인스턴스 2에도 전달되지 않으면 사용자 B의 화면은 갱신되지 않을 수 있다.
따라서 실제 분산 환경에서는 공유 메시지 브로커 등의 구성이 필요할 수 있다.
Rust와 JavaScript의 실행 결과 불일치
이번 글에서 가장 흥미로운 부분은 두 실행 언어의 차이다.
Topcoat는 특정 Rust 표현식을 서버에서 실행한다.
그리고 같은 표현식을 JavaScript로 변환해 브라우저에서도 실행한다.
하지만 Rust와 JavaScript는 완전히 같은 언어가 아니다.
따라서 같은 코드처럼 보이는 표현식에서도 실제 실행 결과가 달라질 수 있다.
저자는 Topcoat 0.10.0에서 두 가지 문제를 발견했다.
조기 return 처리 오류
첫 번째는 if 안의 return이다.
예를 들어 사용자가 Blocked 상태인 작업을 이동하려고 했다고 가정해 보자.
코드에서는 다음 규칙을 작성할 수 있다.
if blocked { return; }
이후 실제 이동 횟수를 증가시키는 코드가 존재한다.
Rust에서는 return을 실행하면 해당 함수가 종료돼야 한다.
하지만 Topcoat가 생성한 JavaScript에서는 if를 별도의 Arrow Function으로 감싸는 상황이 있었다.
이 경우 return이 실제 외부 이벤트 Handler가 아니라 내부 Arrow Function만 종료했다.
따라서 Blocked 상태에서도 이후 코드가 실행될 수 있었다.
저자는 이를 해결하기 위해 다음과 같이 작성했다.
if !blocked { ... }
이렇게 하면 문제가 되는 조기 return을 사용할 필요가 없다.
하지만 이는 특정 버전에서 발견한 문제의 우회 방법이다.
모든 Topcoat 버전에서 같은 오류가 계속 발생한다고 단정해서는 안 된다.
NaN 직렬화 문제
두 번째는 NaN이다.
예를 들어 비어 있는 작업 목록에서 Blocked 비율을 계산한다고 가정해 보자.
계산은 다음과 같을 수 있다.
0.0 / 0.0
결과는 NaN이다.
서버에서는 해당 값을 렌더링할 수 있었다.
하지만 JavaScript로 전달하기 위해 직렬화하는 과정에서 문제가 발생했다.
일부 표현에서는 NaN이 null 형태로 변환됐다.
이후 브라우저에서 복원할 때 실제 Rust에서 계산한 값과 다른 표현이 생성됐다.
그 결과 초기 서버 렌더링 결과와 클라이언트에서 다시 계산한 결과가 달라질 수 있었다.
저자는 분모가 0인 경우를 서버에서 명시적으로 처리하도록 변경했다.
이러한 방식은 실제 계산에 필요한 데이터가 부족한 상태를 별도로 처리한다.
서버와 브라우저의 의미 일치
이번 글은 단순히 Rust 코드를 JavaScript로 변환할 수 있다는 사실만으로 두 환경이 동일하게 동작한다고 보장할 수 없다는 점을 보여준다.
특히 다음 항목은 주의가 필요하다.
- 제어 흐름
- 숫자의 특수 값
- 직렬화
- 이벤트 Handler의 실행 순서
- 브라우저에서 지원하지 않는 Rust 표현식
저자는 이러한 차이를 검증하기 위해 Rust와 V8의 실행 결과를 비교하는 테스트 구조도 소개한다.
하지만 해당 테스트가 모든 사용자 이벤트와 외부 Side Effect까지 검증하는 것은 아니다.
실제 배포의 한계
예제에서는 메모리 기반 SQLite를 사용한다.
따라서 프로세스가 종료되면 데이터가 초기화된다.
실제 서비스에서는 지속적인 저장소가 필요하다.
또한 여러 인스턴스를 사용하는 경우 공유 이벤트 전달 구조가 필요하다.
사용자 인증과 권한도 별도로 구현해야 한다.
이번 글은 Rust 기반 SSR과 브라우저 반응성을 결합하면서 어떤 코드를 서버에서 실행하고 어떤 코드를 JavaScript로 변환해야 하는지, 두 실행 환경의 의미가 일치하는지 어떻게 검증할 수 있는지 실제 구현과 오류 사례로 설명한다.
Fonts in headless Chromium: what your PDF really carries
- 저자: Philipp Staudt
- 매체: Formfeed 기술 블로그
- 게시일: 2026-11-05
- 기술: CSS Font Loading, Headless Chromium 153, PDF, WOFF2, Font Fallback
- 원문: https://formfeed.dev/blog/fonts-in-headless-chromium
HTML을 PDF로 변환하는 서비스에서 CSS에 명시한 Font와 실제 생성된 PDF에 포함된 Font가 서로 다를 수 있다는 점을 네 가지 실제 템플릿으로 검증한 글이다.
저자는 자신이 운영하는 PDF 렌더링 서비스에서 Headless Chromium을 사용한다.
그리고 실제 PDF가 어떤 Font를 포함하는지 분석했다.
특히 브라우저의 개발 환경에서 정상적으로 보였다는 사실이 서버에서 생성한 PDF에서도 동일한 Font가 사용된다는 보장은 아니라는 점을 보여준다.
Font Family의 의미
CSS에서는 다음 속성을 사용할 수 있다.
font-family
예를 들어 다음과 같은 설정이다.
font-family: "Segoe UI", Helvetica, sans-serif
이 설정은 브라우저에 사용할 수 있는 Font의 우선순위를 알려준다.
하지만 실제 Font 파일이 존재하지 않는다면 브라우저는 해당 Font를 사용할 수 없다.
예를 들어 Windows에서는 Segoe UI가 기본적으로 설치돼 있을 수 있다.
하지만 Linux 서버에서는 해당 Font가 존재하지 않을 수 있다.
이 경우 브라우저는 다음 Font를 확인한다.
Helvetica도 없다면 일반적인 Sans-serif Font를 선택한다.
따라서 같은 HTML과 CSS라도 실제 실행 환경에 따라 다른 Font가 사용될 수 있다.
네 가지 실제 테스트
저자는 Headless Chromium 153을 사용해 네 가지 템플릿을 렌더링했다.
결과는 다음과 같았다.
| CSS 또는 콘텐츠 | PDF에 실제 포함된 Font |
|---|---|
| Segoe UI, Helvetica, sans-serif | Liberation Sans |
| Inter를 지정했지만 실제 파일을 로딩하지 않음 | Liberation Sans |
| Google Fonts에서 Inter를 로딩 | Inter |
| Inter와 체크·X·화살표 기호 사용 | Inter, DejaVu Sans, Liberation Sans |
두 번째 결과가 특히 중요하다.
개발자의 컴퓨터에는 Inter가 설치돼 있을 수 있다.
따라서 브라우저 미리보기에서는 Inter가 표시된다.
하지만 실제 PDF를 생성하는 Linux 서버에는 Inter가 설치돼 있지 않을 수 있다.
이 경우 CSS에 Inter를 지정했더라도 다른 Font가 선택된다.
실제 PDF의 Font 확인
저자는 실제 생성된 PDF에서 어떤 Font가 사용됐는지 확인하도록 안내한다.
대표적인 명령은 다음과 같다.
pdffonts invoice.pdf
이 명령은 PDF에 포함된 Font 정보를 표시한다.
예를 들어 다음과 같은 Font 이름이 나타날 수 있다.
- Inter-Regular
- LiberationSans
- DejaVuSans
또한 일부 Font 이름에는 앞쪽에 여섯 글자의 Prefix가 포함될 수 있다.
이는 PDF에 Font의 일부 Glyph만 포함된 Subset을 나타낼 수 있다.
따라서 Font의 전체 파일이 아니라 실제 문서에서 사용하는 문자만 포함됐는지 확인할 수 있다.
이러한 검사는 단순히 PDF를 눈으로 확인하는 것보다 정확하다.
실제로 어떤 Font가 사용됐는지 확인할 수 있기 때문이다.
Web Font 다운로드 비용
Font를 직접 지정하려면 실제 Font 파일을 로딩해야 한다.
예를 들어 Google Fonts에서 Inter를 가져올 수 있다.
하지만 이 과정에서는 네트워크 요청이 발생한다.
저자는 실제 렌더링 시간을 측정했다.
결과는 다음과 같았다.
| 조건 | 렌더링 시간 중앙값 |
|---|---|
| Web Font가 없는 페이지 | 58ms |
| Inter 첫 렌더링, 빈 Cache | 318ms |
| Inter를 Cache에서 재사용 | 72ms |
처음 Inter를 로딩한 경우 약 318ms가 필요했다.
반면 Cache가 존재하는 이후의 렌더링에서는 72ms가 필요했다.
따라서 첫 번째 실행의 네트워크 요청이 실제 비용에서 큰 비중을 차지했다.
하지만 이번 측정은 저자의 테스트 환경에서 수행한 자체 Benchmark다.
모든 서버에서 같은 시간 차이가 발생한다는 의미는 아니다.
저자는 다른 작업으로 서버에 부하가 존재할 때 실행 시간이 네다섯 배 증가했다고 설명한다.
따라서 실제 렌더링 시간은 CPU와 네트워크 상태, Cache 정책에 영향을 받는다.
document.fonts.ready
저자는 실제 Font 파일이 준비됐는지 확인하기 위해 다음 API를 사용했다.
document.fonts.ready
이 Promise는 필요한 Font의 로딩 상태와 연결된다.
PDF를 생성하기 전에 필요한 Font를 준비할 수 있도록 한다.
하지만 Font가 로딩됐다는 사실과 해당 Font가 실제 모든 문자에 사용된다는 사실은 다르다.
Font에 특정 Glyph가 존재하지 않을 수 있기 때문이다.
문자별 Fallback
이번 글에서 가장 흥미로운 발견은 특수문자에 관한 것이다.
저자는 다음 문자를 포함했다.
✓✗→
본문의 일반적인 텍스트에는 Inter가 사용됐다.
하지만 일부 특수문자는 다른 Font에서 가져왔다.
체크 표시와 X 표시는 DejaVu Sans에서 가져왔다.
화살표는 Liberation Sans에서 가져왔다.
즉 하나의 문장 안에서도 서로 다른 Font가 사용될 수 있다.
이러한 현상은 해당 Font 파일에 필요한 Glyph가 존재하지 않을 때 발생한다.
브라우저는 실제로 사용할 수 있는 다른 Font를 찾는다.
그 결과 같은 문장 안에서 일부 기호만 굵기나 위치가 달라질 수 있다.
특수문자를 안정적으로 표시하는 방법
저자는 세 가지 방법을 소개한다.
첫 번째는 실제 필요한 기호를 포함하는 Font를 사용하는 것이다.
두 번째는 특수문자용 Font를 별도로 지정하는 것이다.
예를 들어 다음 Font를 사용할 수 있다.
Noto Sans Symbols 2
세 번째는 상태 아이콘을 SVG로 표현하는 것이다.
예를 들어 체크 표시가 단순한 텍스트가 아니라 중요한 상태 아이콘이라면 Inline SVG로 표시할 수 있다.
이렇게 하면 Font Fallback에 따라 기호의 모양이 변경되는 문제를 줄일 수 있다.
하지만 SVG를 사용하는 경우에도 실제 크기와 색상, 접근성에 관한 설정은 별도로 필요하다.
Header와 Footer의 별도 Font
PDF를 생성하는 시스템에서는 Header와 Footer가 본문과 다른 Document에서 렌더링될 수 있다.
이 경우 본문에 적용한 CSS가 Header와 Footer에 자동으로 전달되지 않을 수 있다.
따라서 본문에서 Web Font를 정상적으로 로딩했더라도 Header에서는 다른 Font가 사용될 수 있다.
저자는 이러한 상황을 해결하기 위해 Header와 Footer에도 필요한 Font 정의를 전달하도록 했다.
이번 글은 CSS의 Font Family 선언과 실제 PDF에 포함된 Font 파일, 브라우저의 Glyph Fallback, 네트워크 Cache 비용을 구분해 이해하는 데 필요한 실제 측정 자료다.
Trimming video in the browser with FFmpeg.wasm: 6 things I had to figure out
- 저자: Viktor_Dev
- 매체: DEV Community / CompressPhoto 개발 기록
- 게시일: 2026-11-04
- 기술: JavaScript, WebAssembly, FFmpeg.wasm, WORKERFS, Web Worker, HTTP Streaming
- 원문: https://dev.to/webdevs2/trimming-video-in-the-browser-with-ffmpegwasm-6-things-i-had-to-figure-out-30a1
실제 웹사이트에서 사용자가 동영상을 서버로 업로드하지 않고 브라우저 안에서 자르거나 오디오를 추출하도록 기능을 구현한 과정을 설명한 글이다.
저자는 CompressPhoto에서 다음 기능을 개발했다.
- 동영상 구간 자르기
- 동영상에서 MP3 추출
이를 위해 FFmpeg.wasm을 사용했다.
FFmpeg.wasm은 FFmpeg의 기능을 WebAssembly로 실행할 수 있도록 만든 도구다.
따라서 브라우저에서 동영상 파일을 처리할 수 있다.
하지만 실제 구현에서는 일반적인 JavaScript UI보다 더 많은 제약이 존재했다.
특히 브라우저의 메모리와 보안 정책, 코덱 지원 범위가 중요했다.
첫 번째 문제: 멀티스레드 WebAssembly의 제약
FFmpeg.wasm은 여러 Thread를 사용하는 Core를 제공한다.
멀티스레드 실행은 일부 동영상 처리에서 성능을 개선할 수 있다.
하지만 브라우저에서 여러 Thread가 데이터를 공유하려면 다음 기능이 필요할 수 있다.
SharedArrayBuffer
SharedArrayBuffer의 사용에는 Cross-origin Isolation 조건이 필요하다.
대표적으로 다음 HTTP Header를 설정할 수 있다.
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp
하지만 이러한 Header는 기존 웹사이트의 다른 기능에 영향을 줄 수 있다.
저자의 서비스에서는 Google 로그인 Popup과 일부 외부 Script의 호환성을 고려해야 했다.
따라서 저자는 멀티스레드 Core 대신 Single-thread Core를 선택했다.
대표적인 패키지는 다음과 같다.
@ffmpeg/core
이 선택은 실행 속도의 일부 이점을 포기하는 대신 기존 사이트의 호환성을 유지하기 위한 것이다.
하지만 Single-thread Core가 모든 브라우저에서 어떤 동영상이든 안정적으로 처리할 수 있다는 의미는 아니다.
실제 기기의 CPU와 메모리 제한은 계속 적용된다.
두 번째 문제: 약 32MB의 WASM 다운로드
저자가 사용한 Core의 WASM 파일은 약 32MB였다.
정확한 파일 크기는 다음과 같았다.
32,232,419바이트
이 파일은 일반적인 프론트엔드 JavaScript Bundle보다 클 수 있다.
따라서 사용자가 기능을 열 때 모델이나 실행 파일을 다운로드하는 것과 비슷한 대기 시간이 발생할 수 있다.
저자는 파일을 외부 CDN에서 로딩하지 않았다.
대신 자신의 웹사이트에서 제공했다.
대표적인 경로는 다음과 같다.
/ffmpeg/0.12.10/ffmpeg-core.wasm
경로에 버전을 포함한 이유는 Cache와 관련된다.
실제로 Core 파일의 내용이 변경되지 않는다면 브라우저가 같은 파일을 다시 다운로드할 필요가 없을 수 있다.
반면 새로운 버전이 배포되면 경로가 변경되므로 기존 Cache와 구분할 수 있다.
다운로드 진행률의 문제
저자는 사용자에게 실제 다운로드 진행률을 표시하고 싶었다.
일반적으로 다음 값을 사용할 수 있다.
Content-Length
하지만 HTTP Response가 압축돼 전송되면 문제가 발생한다.
브라우저의 JavaScript가 읽는 데이터는 압축 해제된 Byte일 수 있다.
반면 HTTP Header에 기록된 Content-Length는 압축된 전송 크기를 나타낼 수 있다.
따라서 단순히 다음 값을 계산하면 잘못된 결과가 발생할 수 있다.
loaded / contentLength
저자는 이를 해결하기 위해 실제 압축 해제된 WASM 파일의 크기를 사용했다.
그리고 ReadableStream으로 데이터를 순차적으로 읽었다.
이렇게 하면 실제 로딩 상태를 더 정확하게 표시할 수 있다.
다만 파일의 버전이 변경되면 실제 크기도 달라질 수 있다.
따라서 저자는 패키지 버전과 파일 크기가 일치하는지 테스트로 확인하도록 했다.
세 번째 문제: 700MB 파일의 메모리 복사
이번 글에서 가장 중요한 부분은 큰 동영상 파일을 처리하는 방식이다.
일반적인 예제에서는 다음 API를 사용할 수 있다.
ffmpeg.writeFile()
이 함수에 실제 동영상의 모든 Byte를 전달할 수 있다.
하지만 700MB 동영상 파일을 처리한다고 가정해 보자.
브라우저는 실제 파일 데이터를 메모리로 가져와야 할 수 있다.
이후 WebAssembly의 내부 파일시스템에도 해당 데이터를 복사할 수 있다.
결과 동영상까지 메모리에 존재한다면 전체 사용량이 크게 증가할 수 있다.
특히 모바일 기기에서는 브라우저 탭이 종료될 수 있다.
저자는 이를 해결하기 위해 다음 기능을 사용했다.
WORKERFS
WORKERFS의 역할
WORKERFS는 WebAssembly 실행 환경에서 브라우저의 파일 데이터를 가상의 파일시스템으로 연결할 수 있도록 한다.
저자는 입력 파일을 전부 복사하지 않고 Mount하는 방식을 사용했다.
대표적인 API는 다음과 같다.
ffmpeg.mount()ffmpeg.unmount()
예를 들어 사용자가 선택한 파일을 다음 경로에 연결할 수 있다.
/input/source.mp4
이후 FFmpeg는 해당 경로에서 데이터를 읽는다.
이렇게 하면 입력 동영상 전체를 WASM의 메모리 파일시스템에 복사하는 비용을 줄일 수 있다.
저자는 이러한 구조를 이용해 최대 1GB 정도의 파일을 처리하도록 구성했다.
하지만 출력 결과물은 여전히 메모리에 존재할 수 있다.
따라서 1GB 파일을 지원한다는 사실만으로 모든 기기에서 해당 크기의 모든 변환 작업이 성공한다고 보장할 수는 없다.
네 번째 문제: 빠른 자르기와 정확한 자르기
저자는 동영상을 자르는 기능을 두 종류로 제공했다.
첫 번째는 빠른 모드다.
대표적인 옵션은 다음과 같다.
-c copy
이 방식에서는 기존 동영상의 Stream을 다시 인코딩하지 않고 복사할 수 있다.
따라서 실제 처리 시간이 짧을 수 있다.
하지만 정확히 사용자가 지정한 Frame에서 잘리지 않을 수 있다.
이유는 Keyframe 때문이다.
영상 코덱은 모든 Frame을 독립적으로 저장하지 않을 수 있다.
따라서 임의의 시점에서 Stream을 복사하면 실제 자르는 위치가 가장 가까운 Keyframe에 영향을 받을 수 있다.
두 번째는 정확한 모드다.
이 방식에서는 실제 영상을 다시 인코딩한다.
저자는 H.264와 AAC를 사용했다.
대표적인 옵션은 다음과 같다.
libx264yuv420paacfaststart
이렇게 하면 더 정확한 자르기가 가능하다.
하지만 인코딩에 CPU 시간이 필요하다.
특히 모바일 환경에서는 오래 걸릴 수 있다.
따라서 저자는 빠른 모드를 기본으로 제공하고 정확한 모드는 사용자가 명시적으로 선택하도록 했다.
해상도와 코덱의 호환성
저자는 정확한 모드에서 이미지의 크기도 조정했다.
H.264와 yuv420p를 사용하는 일부 구성에서는 가로와 세로가 짝수여야 한다.
하지만 실제 모바일 동영상에는 홀수 크기가 포함될 수 있다.
이 경우 인코딩이 실패할 수 있다.
따라서 저자는 가로와 세로를 짝수로 조정하는 Filter를 사용했다.
또한 오디오가 없는 동영상도 고려했다.
일부 화면 녹화 파일에는 오디오 Track이 존재하지 않을 수 있다.
따라서 오디오 Track을 필수로 요구하면 실제 파일을 처리하지 못할 수 있다.
저자는 다음과 같은 선택적 매핑을 사용했다.
0:a:0?
이렇게 하면 오디오 Track이 없는 경우에도 작업을 계속할 수 있다.
다섯 번째 문제: 실제 작업 진행률과 취소
FFmpeg의 exec()는 실제 변환 진행률을 직접 반환하지 않는다.
하지만 실행 로그에는 처리된 동영상 시각이 포함될 수 있다.
대표적인 표현은 다음과 같다.
time=
저자는 해당 값을 분석해 전체 길이와 비교했다.
이렇게 하면 실제 변환이 어느 정도 진행됐는지 표시할 수 있다.
하지만 모든 동영상에서 전체 길이를 처음부터 정확하게 확인할 수 있는 것은 아니다.
일부 코덱과 Container에서는 별도의 Probe가 필요할 수 있다.
실행 중인 FFmpeg 취소
사용자가 변환 작업을 취소하고 싶을 수도 있다.
하지만 실행 중인 FFmpeg 명령을 항상 안전하게 중간 종료할 수 있는 것은 아니다.
저자는 변환을 취소할 때 Worker 자체를 종료하는 방식을 사용했다.
이후 새로운 작업이 시작되면 새로운 Worker를 생성한다.
이렇게 하면 이전 실행의 메모리와 상태를 정리할 수 있다.
하지만 Worker를 다시 생성해야 하므로 초기화 비용이 발생한다.
다만 WASM 파일이 브라우저의 HTTP Cache에 존재한다면 다시 다운로드하는 비용은 줄어들 수 있다.
여섯 번째 문제: 동시 실행 제한
저자는 Single-thread FFmpeg 인스턴스에서 여러 명령을 동시에 실행하지 않도록 했다.
예를 들어 사용자가 두 동영상의 변환을 연속해서 요청했다고 가정해 보자.
첫 번째 작업이 실행 중이다.
두 번째 작업이 같은 인스턴스의 파일시스템과 실행 상태를 변경하면 충돌할 수 있다.
따라서 저자는 Promise Chain을 사용해 작업을 순차적으로 실행했다.
첫 번째 작업이 완료되면 두 번째 작업을 시작한다.
첫 번째 작업이 실패하더라도 이후 작업이 계속 실행될 수 있도록 Queue 상태를 정리했다.
이러한 방식은 여러 작업을 병렬 처리하는 대신 하나의 실행 환경을 안정적으로 공유하도록 한다.
실제 구현의 한계
저자는 이 방식이 모든 동영상 처리에 적합하다고 주장하지 않는다.
특히 다음 상황에서는 서버 기반 FFmpeg가 더 적합할 수 있다.
- 매우 큰 동영상
- 복잡한 재인코딩
- 고해상도 장시간 처리
- 모바일 기기의 메모리 제한
- 여러 파일의 대량 병렬 처리
이번 글에는 서버 FFmpeg와 브라우저 FFmpeg의 실행 시간을 동일한 조건으로 비교한 정량적인 Benchmark는 포함되지 않았다.
또한 Single-thread WASM은 Native FFmpeg보다 느릴 수 있다고 설명한다.
그럼에도 사용자가 파일을 서버로 업로드할 필요가 없다는 점은 장점이다.
이번 글은 브라우저에서 대용량 파일을 직접 처리하려면 WebAssembly의 실행 성능뿐 아니라 파일 복사와 메모리 사용량, HTTP Cache, 진행률 표시, 취소 및 동시성까지 함께 설계해야 한다는 점을 실제 구현으로 보여준다.