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

기준 시점: 2026-11-09 06:00 KST
조사 범위: 전일 2026-11-08 06:00 ~ 당일 2026-11-09 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
React, Next.js, Vercel, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro, Remix의 공식 릴리스와 기술 블로그를 조사했다.
이번 기간에는 새로운 주요 안정 버전이나 중요한 프론트엔드 API의 정식 출시를 확인하지 못했다.
조사 과정에서 제외한 Next.js 16.5 Canary 업데이트
- 릴리스: Next.js 16.5.0-canary.44
- GitHub 표시 시각: 2026-11-07 23:41 — 표시 시간대 미확인
- 안정화 단계: Canary Pre-release
- 원문: https://github.com/vercel/next.js/releases/tag/v16.5.0-canary.44
Next.js의 공식 릴리스에서 16.5.0-canary.44를 확인했다.
이번 버전의 주요 변경은 다음과 같다.
- Client Cache의 Refresh 동작에 관한 문서 보완
- Cache Components 설정 오류와 마이그레이션 가이드 연결
is-animated의존성 업데이트- Font 및 Image 관련 문서 수정
- Middleware에서 Proxy로 변경된 API의 문서 링크 수정
실제 프레임워크의 중요한 사용자 API를 새롭게 도입하거나 안정화하는 내용은 확인되지 않았다.
특히 Cache Components 관련 변경은 설정 오류를 설명하는 문서의 연결을 개선한 것이다. 캐시의 실행 알고리즘을 새롭게 변경한 기능으로 해석해서는 안 된다.
Canary 릴리스는 일반적인 안정 버전과 구분해야 한다. 이번 변경은 문서와 의존성 유지보수에 집중돼 있어 주요 소식으로 선정하지 않았다.
그 밖의 프론트엔드 공식 릴리스에서도 조사 기간에 해당하는 중요한 신규 발표는 확인하지 못했다.
이번 추천 글에서는 신규 릴리스 대신 실제 React 애플리케이션의 렌더링 구조를 분석한 자료를 선정했다.
특히 Next.js와 React로 작성한 코드 72,683줄에서 useMemo가 일곱 번만 사용된 이유를 조사한 글을 자세하게 소개한다. 이 글은 Memoization을 성능 최적화뿐 아니라 State의 참조 동일성과 Effect의 정확성이라는 관점에서 설명한다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome·Chromium, Firefox·Mozilla, WebKit·Safari의 공식 공개 정보와 JavaScript·TypeScript·CSS, 웹 성능 및 접근성 관련 자료를 조사했다.
이번 기간에 새롭게 정식 채택된 주요 웹 표준이나 안정화된 브라우저 API의 출시 발표는 확인하지 못했다.
Chrome 158 Beta와 Firefox 159의 정식 릴리스는 이전 브리핑에서 다뤘으므로 반복하지 않았다.
또한 다음 두 상황을 구분했다.
첫 번째는 브라우저의 개발 버전에서 새로운 기능을 구현하거나 기존 API를 수정하는 경우다.
두 번째는 해당 기능이 실제 안정 버전에 포함돼 일반 사용자에게 제공되는 경우다.
이번 조사에서는 두 번째 상황에 해당하는 중요한 신규 발표를 확인하지 못했다.
다만 이번 추천 글에서 다루는 웹 플랫폼 기술 가운데 실제 개발에서 활용할 만한 내용이 있었다.
대표적으로 다음 두 가지다.
- HTML의
command와commandfor속성으로 Dialog와 Popover를 제어하는 방법 - WebAIM의 실제 접근성 조사 결과를 React 컴포넌트와 디자인 시스템의 검증에 연결하는 방법
commandfor는 이번 조사 기간에 처음 출시된 API가 아니다. 이미 제공되는 기능을 11월 8일 공개된 새로운 기술 글에서 분석한 것이므로 추천 글에만 포함했다.
WebAIM의 2026년 통계도 이번 주에 새롭게 조사된 결과가 아니다. 실제 조사 시점은 2026년 2월이다. 이번 추천 글은 해당 통계를 여러 해의 결과와 비교해 새롭게 해석한 자료다.
3️⃣ 백엔드·인프라
Vercel AI Gateway, DeepSeek V4.1 Flash 1017 Ultrafast 공개 Preview 제공 — 초고속 추론 Endpoint와 100만 Token Context 지원
- 발표일: 2026-11-07
- 정확한 게시 시각: 미확인
- 조사 범위 판정: 게시 날짜의 시간대가 명시되지 않아 2026-11-08 06:00 KST 이후 공개됐는지 확정할 수 없음. 날짜가 조사 범위와 겹칠 가능성이 있어 조건부 포함
- 안정화 단계: Public Preview
- 적용 대상: Vercel AI Gateway, AI SDK, 코딩 에이전트 및 AI 기반 웹 애플리케이션
- 제공업체: Fireworks
- 공식 발표 성능: 초당 1,000개 이상의 출력 Token
- 측정 주체: Vercel 및 제공업체의 발표 수치. 독립적인 비교 Benchmark 미확인
- 원문: https://vercel.com/changelog/deepseek-v4-1-flash-1017-ultrafast-now-available-on-ai-gateway
Vercel이 AI Gateway에서 DeepSeek V4.1 Flash 1017 Ultrafast를 사용할 수 있도록 지원 범위를 확대했다.
이번 발표는 DeepSeek V4.1 모델 자체가 처음 출시됐다는 소식이 아니다.
실제로 제공하는 모델은 2026년 10월 17일 업데이트된 DeepSeek V4.1 Flash를 기반으로 한다.
새롭게 제공되는 것은 Fireworks의 가속화된 Serverless Endpoint를 통해 해당 모델을 Vercel AI Gateway에서 호출할 수 있다는 점이다.
따라서 모델 자체의 신규 출시와 특정 클라우드 플랫폼에서의 제공 시작을 구분해야 한다.
AI Gateway의 역할
일반적으로 AI 기반 웹서비스는 외부 모델 API를 호출한다.
예를 들어 다음과 같은 기능을 생각할 수 있다.
- 고객 문의 자동 응답
- 문서 검색 결과 요약
- 코드 생성
- 데이터 분석
- AI 에이전트의 작업 계획 수립
이러한 기능에서는 하나의 모델 제공업체를 직접 호출할 수 있다.
하지만 실제 서비스에서는 다음 문제를 관리해야 할 수 있다.
- 모델별 API Key
- 요청 사용량
- 추론 비용
- 외부 제공업체의 장애
- 요청 재시도
- 응답 지연 시간
Vercel AI Gateway는 여러 모델 제공업체에 접근하는 공통 API 계층을 제공한다.
애플리케이션은 Gateway를 통해 모델을 호출할 수 있다.
이후 모델의 사용량과 요청 기록, 비용 등을 확인할 수 있다.
이번 업데이트는 해당 Gateway에서 사용할 수 있는 모델과 Endpoint를 확장한 것이다.
Ultrafast Endpoint
Vercel은 이번 Endpoint가 초당 1,000개 이상의 출력 Token을 생성할 수 있다고 발표했다.
Token은 AI 모델이 텍스트를 처리하는 기본 단위다.
일반적으로 하나의 Token이 정확히 한 글자나 한 단어에 대응하는 것은 아니다.
모델의 Tokenizer와 실제 입력 언어에 따라 달라진다.
예를 들어 코딩 에이전트가 긴 TypeScript 파일을 생성한다고 가정해 보자.
출력 Token을 빠르게 생성할 수 있다면 실제 코드가 완성되기까지 필요한 시간을 줄일 가능성이 있다.
또한 에이전트가 여러 작업을 순차적으로 실행하는 구조에서도 영향을 줄 수 있다.
예를 들어 다음 흐름이다.
- 현재 코드를 분석한다.
- 수정 계획을 생성한다.
- 파일을 변경한다.
- 테스트 결과를 확인한다.
- 추가 수정 사항을 생성한다.
이러한 구조에서는 모델의 출력 속도가 빨라지면 다음 단계로 이동하는 대기 시간이 줄어들 수 있다.
하지만 출력 Token 속도와 실제 작업 완료 시간은 서로 다르다.
초당 1,000 Token의 의미와 한계
Vercel이 발표한 수치는 모델의 출력 처리량에 관한 것이다.
다음과 같은 성능 수치는 제공되지 않았다.
- 실제 웹 애플리케이션의 전체 응답 시간
- 첫 번째 Token이 도착하기까지 걸리는 시간
- 동시 요청이 많은 환경의 P95 또는 P99 지연
- 다른 추론 제공업체와 동일 조건에서 비교한 결과
- 코딩 작업의 정답률이나 실제 테스트 통과율
따라서 모든 AI 요청이 초당 1,000 Token 이상으로 처리된다고 단정해서는 안 된다.
특히 실제 요청에는 다음 시간이 추가될 수 있다.
- Gateway를 통한 요청 전달
- 입력 Token 처리
- 모델의 초기 응답 준비
- Tool Calling
- 외부 API 실행
- 결과 검증
또한 모델이 빠르게 코드를 생성한다는 사실이 해당 코드의 정확성을 보장하지 않는다.
이번 성능 수치는 Vercel과 제공업체가 공개한 것으로, 독립적인 제3자의 통제된 Benchmark는 확인되지 않았다.
100만 Token Context Window
이번 Endpoint는 100만 Token의 Context Window를 제공한다고 발표됐다.
Context Window는 모델이 하나의 요청에서 처리할 수 있는 문맥의 크기를 나타낸다.
예를 들어 큰 Repository의 여러 파일을 한 번에 분석하는 작업에서 활용할 수 있다.
또한 긴 문서와 이전 대화 기록을 함께 전달하는 경우에도 사용할 수 있다.
하지만 Context Window가 크다는 사실만으로 모델이 모든 정보를 동일한 수준으로 정확하게 이해하거나 활용한다고 보장할 수는 없다.
실제로 다음 두 문제는 별개다.
첫 번째는 특정 크기의 입력을 기술적으로 처리할 수 있는가 하는 문제다.
두 번째는 그 입력 가운데 필요한 정보를 정확하게 찾아 결과에 반영할 수 있는가 하는 문제다.
따라서 100만 Token을 지원한다고 해서 모든 Repository의 전체 코드를 무조건 하나의 Prompt에 포함하는 것이 적절한 것은 아니다.
실제 프로젝트에서는 필요한 파일을 선별하는 방법과 검색 전략이 중요할 수 있다.
AI SDK에서 사용
Vercel AI SDK에서는 다음 모델 식별자를 사용할 수 있다.
deepseek/deepseek-v4.1-flash-1017-ultrafast
대표적인 API는 다음과 같다.
streamText()
개발자는 해당 모델 식별자를 지정해 스트리밍 응답을 생성할 수 있다.
예를 들어 TypeScript 코드를 생성하는 작업에서 사용할 수 있다.
모델을 변경하기 위해 별도의 SDK API를 새롭게 작성할 필요는 없다.
다만 실제 모델이 지원하는 입력과 출력 형식, Tool Calling 기능은 모델 및 제공업체의 지원 범위를 확인해야 한다.
모든 AI 모델이 동일한 API 기능을 제공하는 것은 아니기 때문이다.
코딩 에이전트 연결
이번 모델은 다음 코딩 도구에서도 사용할 수 있도록 안내됐다.
- Claude Code
- Codex
- Cursor
Vercel은 CLI에서 다음 명령을 제공한다.
npx vercel ai-gateway setup
이후 해당 모델을 코딩 에이전트에서 선택할 수 있다.
하지만 모델을 선택할 수 있다는 사실이 모든 에이전트의 기능과 완전히 호환된다는 의미는 아니다.
예를 들어 에이전트가 특정 Tool Calling 형식이나 구조화된 출력을 요구할 수 있다.
실제 사용 전에 지원 범위를 확인해야 한다.
Public Preview의 제한사항
이번 Endpoint는 Public Preview다.
Vercel은 별도의 대기 목록이나 Session 시간 제한 없이 사용할 수 있다고 설명한다.
하지만 이는 특정 Endpoint의 사용 조건에 관한 설명이다.
실제 모델 요청의 Token 사용량과 비용, 서비스의 일반적인 Rate Limit 등이 모두 무제한이라는 의미는 아니다.
또한 Vercel AI Gateway의 기존 기능인 다음 항목이 함께 제공된다.
- 사용량 추적
- 요청 Trace
- 비용 확인
- 재시도
- Failover
- Routing 정책
이러한 Gateway 기능이 이번 모델의 추가와 동시에 처음 출시된 것은 아니다.
Vercel은 AI Gateway에서 추론에 별도 플랫폼 수수료를 추가하지 않고 제공업체의 가격을 반영한다고 설명한다.
하지만 실제 추론 요금은 모델과 제공업체, 입력 및 출력 Token 사용량에 따라 달라질 수 있다.
이번 발표는 초고속 추론을 제공하는 기존 DeepSeek 모델의 새로운 Endpoint를 Vercel AI Gateway에 연결해 코딩 에이전트와 AI 웹서비스에서 사용할 수 있도록 한 Public Preview 확대다.
이번 조사 범위에서 이외에 포함할 만한 주요 신규 백엔드·인프라 발표는 확인하지 못했습니다.
Cloudflare, AWS, Google Cloud, Azure의 주요 공식 Changelog와 서버리스·엣지 플랫폼, 데이터베이스 및 API 관련 공개 정보를 검토했다.
Cloudflare의 공식 Changelog에서는 11월 6일의 기존 발표 이후 이번 조사 기간에 해당하는 중요한 신규 항목을 확인하지 못했다.
Vercel의 공식 Changelog에서도 위에서 소개한 11월 7일 게시물보다 늦게 발표된 중요한 신규 인프라 기능을 확인하지 못했다.
Node.js와 Deno, Bun의 공식 안정 릴리스에서도 이번 기간에 해당하는 중요한 신규 버전을 확인하지 못했다.
이전 브리핑에서 소개한 Durable Objects의 자동 재시도, Vercel Schedules, Node.js 26 지원 등의 발표는 반복하지 않았다.
4️⃣ 개발 도구 및 보안
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
GitHub, GitLab, Vite, webpack, Rollup, esbuild, Babel, Biome, ESLint, Prettier, npm, pnpm, Yarn과 주요 테스트·배포 도구의 공식 릴리스 및 변경 기록을 검토했다.
또한 OWASP와 주요 공급망 보안 권고, 클라우드 서비스의 보안 발표를 조사했다.
이번 기간에 신규로 공개된 중요한 보안 취약점과 해당 수정 사항을 공식 원문으로 함께 확인할 수 있는 항목은 발견하지 못했다.
개발 버전과 정식 보안 수정의 구분
이번 조사 과정에서는 개발 브랜치의 코드 변경과 신규 Issue, Pull Request 등이 확인됐다.
하지만 이러한 활동을 모두 새로운 정식 릴리스로 취급하지 않았다.
예를 들어 특정 패키지의 내부 구현에 보안 관련 수정이 반영됐다고 가정해 보자.
실제 사용자가 설치하는 정식 버전에 해당 변경이 포함됐는지 확인되지 않았다면 이미 패치가 배포됐다고 단정할 수 없다.
또한 새로운 Issue가 등록됐다는 사실만으로 취약점이 검증됐다고 판단할 수도 없다.
따라서 다음 조건을 충족하지 않는 변경은 제외했다.
- 공식 릴리스 또는 보안 권고가 존재하는가?
- 실제 영향을 받는 버전과 환경이 명시돼 있는가?
- 변경 내용이 확인되는가?
- 수정 버전 또는 적용 시점을 확인할 수 있는가?
- 조사 기준 시점 이전에 공개됐는가?
Next.js 16.5.0-canary.44 역시 주요 사용자 기능이나 보안 수정이 아니라 문서 및 의존성 유지보수에 집중돼 있어 앞선 프론트엔드 섹션에서 제외했다.
GitHub의 공식 Changelog에서 확인한 최근 주요 발표는 11월 6일 이전에 집중돼 있었다.
이전 브리핑에서 다룬 다음 항목도 반복하지 않았다.
- GitHub Actions Workflow Lockfile
- Dependabot Reachability Analysis
- GitHub Agentic Workflows
- CodeQL 2.27.3 및 2.27.4
- GitHub Secret Scanning 개선
- 공급망 보안 관련 기존 권고
이번 조사에서는 새롭게 검증할 수 있는 중요한 발표가 없는 분야를 억지로 채우지 않았다.
대신 추천 글에서는 실제 개발자가 겪은 비동기 실행 오류와 HTML API의 활용 방법, API 응답 캐싱의 성능 측정 결과를 자세하게 소개한다.
📚 추천 글
Seven useMemo calls in 72,683 lines, six of them in one file, and the one that matters is not about speed
- 저자: Daniel Pertu
- 매체: DEV Community
- 게시일: 2026-11-08
- 기술: Next.js 16, React 19, React Server Components,
useMemo,useCallback,useOptimistic - 원문: https://dev.to/daniel_pertu/seven-usememo-calls-in-72683-lines-six-of-them-in-one-file-and-the-one-that-matters-is-not-about-ij7
실제 운영 중인 Next.js 애플리케이션에서 72,683줄의 TypeScript 코드 가운데 useMemo가 일곱 번만 사용된 이유를 조사하고, 각각의 사용 목적을 분석한 기술 글이다.
이번 글의 특징은 React 성능 최적화에 관한 일반적인 조언을 반복하지 않는다는 점이다.
저자는 실제 코드베이스 전체에서 React Hook의 사용 횟수를 조사했다.
그리고 어떤 부분에 Memoization이 필요했는지 실제 컴포넌트의 구조를 확인했다.
특히 Memoization의 목적이 항상 계산 속도를 줄이는 것은 아니며, 일부 상황에서는 React State의 참조 동일성과 실행 정확성을 유지하는 데 더 중요한 역할을 한다는 점을 설명한다.
실제 프로젝트 규모
저자가 운영하는 애플리케이션은 Nakodo다.
사용한 기술은 다음과 같다.
- Next.js 16
- React 19.2.8
- TypeScript
- React Server Components
전체 코드베이스의 규모는 다음과 같다.
| 항목 | 수치 |
|---|---|
| TypeScript 파일 | 582개 |
| TypeScript 코드 | 72,683줄 |
| TSX 파일 | 226개 |
"use client"가 선언된 파일 | 80개 |
| shadcn UI Primitive | 17개 |
| 공개 페이지 | 76개 |
저자는 이 코드베이스에서 React Hook의 사용 횟수를 검색했다.
결과는 다음과 같았다.
| Hook 또는 API | 사용 횟수 |
|---|---|
useMemo | 7회 |
useCallback | 2회 |
React.memo | 0회 |
useDeferredValue | 0회 |
useLayoutEffect | 0회 |
useImperativeHandle | 0회 |
반면 실제 State를 사용하는 코드는 존재했다.
대표적으로 다음과 같다.
useEffect: 23개 파일useState: 33개 파일useTransition: 11개 파일useActionState: 11개 파일
따라서 Memoization을 거의 사용하지 않았다는 사실이 클라이언트의 상태 관리 자체가 존재하지 않는다는 의미는 아니다.
React Server Components가 미치는 영향
저자는 Memoization이 적은 가장 큰 이유로 실제 렌더링 구조를 제시한다.
이 프로젝트에서는 많은 화면을 서버에서 렌더링한다.
예를 들어 다음 화면이다.
- 캠페인 목록
- 대화 목록
- 크리에이터 페이지
- 공개 콘텐츠 페이지
일반적인 클라이언트 컴포넌트에서는 부모 컴포넌트가 다시 렌더링되면서 자식도 다시 렌더링될 수 있다.
이때 React.memo로 자식의 불필요한 렌더링을 줄일 수 있다.
하지만 서버에서 생성한 정적인 컴포넌트 트리는 클라이언트에서 같은 방식으로 반복 렌더링되지 않는다.
따라서 클라이언트의 부모 렌더링을 막기 위해 작성한 Memoization이 의미가 없을 수 있다.
저자는 코드베이스에서 .map()을 사용하는 파일이 103개였다고 설명한다.
하지만 대부분은 서버에서 목록을 HTML로 생성하는 작업이었다.
따라서 목록을 렌더링한다는 사실만으로 React.memo를 추가할 필요는 없었다.
여섯 개의 useMemo가 한 파일에 집중된 이유
일곱 개의 useMemo 가운데 하나는 shadcn의 Chart 컴포넌트에 포함된 코드였다.
저자가 직접 작성한 useMemo 여섯 개는 모두 하나의 파일에 존재했다.
파일은 다음과 같다.
results-view.tsx
이 파일의 크기는 1,153줄이다.
프로젝트에서 가장 큰 클라이언트 컴포넌트였다.
이 컴포넌트에는 다음 기능이 존재했다.
- 검색
- 필터
- 정렬
- 선택
- Optimistic Update
- 결과 목록 표시
이러한 기능은 서버에서 생성한 HTML만으로 처리하기 어렵다.
사용자가 검색어를 입력할 때마다 즉시 결과가 변경돼야 하기 때문이다.
따라서 실제로 클라이언트에서 목록을 처리하는 부분에 Memoization이 집중됐다.
계산 결과보다 참조 동일성이 중요했던 shown
이번 글에서 가장 중요한 부분은 다음 값이다.
shown
이 값은 서버에서 받은 행과 기존 Snapshot을 비교해 화면에 표시할 데이터를 계산한다.
저자는 해당 값을 useMemo로 관리한다.
의존성은 다음과 같다.
[rows, snapshot]
문제는 사용자가 검색창에 글자를 입력하는 상황이다.
검색어가 변경되면 컴포넌트가 다시 렌더링된다.
하지만 실제 서버에서 받은 행과 Snapshot이 변경된 것은 아니다.
따라서 shown은 같은 배열을 유지해야 한다.
이 배열은 다음 Hook의 기본값으로 사용된다.
useOptimistic
만약 검색어를 입력할 때마다 새로운 shown 배열이 생성된다면 Optimistic State의 기반이 되는 값의 참조가 변경될 수 있다.
저자는 이를 방지하기 위해 shown의 참조를 안정적으로 유지했다.
이렇게 하면 검색어 변경이 Optimistic State의 불필요한 재구성으로 이어지는 문제를 줄일 수 있다.
여기서 Memoization의 핵심 목적은 배열을 계산하는 몇 밀리초를 절약하는 것이 아니라 같은 데이터를 같은 참조로 유지하는 것이다.
검색어에 따라 변경해야 하는 계산만 다시 실행
저자는 계산을 여러 단계로 나누었다.
대표적으로 다음과 같은 흐름이다.
- URL의 검색 조건 분석
- 현재 검색어와 URL 조건 결합
- 서버 데이터와 Snapshot 비교
- Optimistic State 계산
- 결과별 통계 계산
- 실제 필터와 정렬 적용
사용자가 검색어를 입력하면 모든 단계가 변경되는 것은 아니다.
예를 들어 서버 데이터와 Snapshot은 그대로 유지된다.
Optimistic State도 변경되지 않는다.
반면 검색어에 영향을 받는 필터와 정렬 결과는 변경돼야 한다.
저자는 각 계산의 의존성을 분리해 필요한 부분만 다시 실행하도록 만들었다.
이러한 구조에서는 하나의 큰 계산 함수를 Memoization하는 것보다 데이터의 관계를 더 명확하게 표현할 수 있다.
일부 계산은 의도적으로 Memoization하지 않음
흥미로운 점은 저자가 모든 배열 계산에 useMemo를 사용하지 않았다는 것이다.
예를 들어 다음 작업은 매 렌더링에서 실행할 수 있도록 유지했다.
- 결과 배열의 앞부분을 선택하는
slice() - 선택된 행을 찾는
filter() - 특정 조건을 만족하는 행의 개수 계산
저자는 이러한 작업이 상대적으로 단순하다고 판단했다.
반면 필터와 정렬을 결합한 applyFilters()는 Memoization했다.
이 함수에서는 다음 작업이 발생한다.
- 필터 적용
- 정렬 값을 포함한 임시 객체 생성
- 정렬 수행
- 원래 데이터 배열로 변환
정렬에는 일반적으로 O(n log n)의 시간 복잡도가 발생할 수 있다.
또한 중간 배열도 생성한다.
따라서 단순한 slice()와 같은 작업보다 반복 실행 비용이 클 수 있다.
하지만 이번 글에는 이러한 계산을 실제 Profiler로 비교한 정량적인 Benchmark는 포함되지 않았다.
따라서 저자의 선택이 모든 데이터 규모에서 가장 빠르다고 판단할 수는 없다.
useCallback의 목적도 달랐다
이 프로젝트에서는 useCallback도 두 번만 사용됐다.
두 경우 모두 자식 컴포넌트의 렌더링을 줄이기 위한 목적이 아니었다.
실제로는 useEffect의 의존성으로 사용하는 함수의 참조를 안정적으로 유지하기 위한 것이었다.
예를 들어 다음 상황이다.
컴포넌트의 Effect에서 특정 함수를 실행한다.
하지만 함수가 매 렌더링마다 새로 생성된다면 Effect의 의존성이 계속 변경될 수 있다.
그 결과 같은 Effect가 불필요하게 반복 실행될 수 있다.
저자의 프로젝트에서는 해당 Effect가 서버에 쓰기 작업을 요청할 수 있었다.
따라서 함수의 참조를 안정적으로 유지하는 것은 단순한 렌더링 성능뿐 아니라 불필요한 서버 요청을 방지하는 문제와 연결됐다.
다만 Effect의 의존성 문제는 useCallback 외에도 로직을 Effect 내부로 이동하거나 실제 상태 구조를 변경하는 방식으로 해결할 수 있다.
이번 글은 해당 프로젝트에서 선택한 방법을 설명하는 사례다.
실제 DOM의 크기 제한
저자의 결과 화면은 한 번에 표시하는 DOM 행의 수를 제한한다.
기본값은 다음과 같다.
PAGE = 50
따라서 데이터의 전체 행이 많더라도 한 번에 렌더링하는 DOM은 일정한 크기로 유지할 수 있다.
하지만 필터와 정렬은 실제 캠페인의 모든 행에 대해 실행될 수 있다.
따라서 데이터가 매우 커지면 검색어를 입력할 때마다 계산 시간이 증가할 수 있다.
저자는 이러한 상황에서 useDeferredValue 등을 검토할 수 있다고 설명한다.
현재는 실제 사용자가 입력 지연을 경험할 정도의 문제가 나타나지 않았기 때문에 해당 Hook을 추가하지 않았다.
이번 분석의 한계
이번 글의 수치는 실제 코드베이스에서 Hook의 사용 횟수를 검색한 결과다.
따라서 코드의 구조에 관한 정량적인 분석은 존재한다.
하지만 다음 성능 수치는 제공되지 않았다.
- Memoization 적용 전후의 렌더링 시간
- 실제 입력 지연 시간
- React Profiler에서 측정한 Commit 시간
- 메모리 사용량의 변화
따라서 Memoization이 일곱 개뿐이기 때문에 해당 애플리케이션이 빠르다는 인과관계를 증명한 것은 아니다.
또한 서버 렌더링 비중이 높은 이 프로젝트의 특성을 다른 모든 React 애플리케이션에 일반화해서는 안 된다.
이번 글은 React의 Memoization을 사용 횟수나 코드 스타일의 문제가 아니라 실제 렌더링 구조와 데이터 의존성, 참조 동일성의 관점에서 판단해야 한다는 점을 구체적인 코드베이스 분석으로 보여준다.
The Web Got Less Accessible in 2026. Here's What Four Years of WebAIM Data Mean for React Devs
- 저자: Andrei Kornev
- 매체: DEV Community
- 게시일: 2026-11-08
- 기술: React, Next.js, WCAG 2.2, ARIA, ESLint, axe-core, 접근성 자동화
- 원문: https://dev.to/andrei-kornev-frontend/the-web-got-less-accessible-in-2026-heres-what-four-years-of-webaim-data-mean-for-react-devs-2h2g
- 원본 조사 자료: https://webaim.org/projects/million/
React 기반 웹 애플리케이션의 접근성을 연구하던 개발자가 WebAIM의 2023~2026년 조사 결과를 비교하고 실제 프론트엔드 개발에서 반복적으로 발생하는 접근성 오류를 분석한 글이다.
이번 글은 새로운 보안이나 접근성 표준이 발표됐다는 소식이 아니다.
WebAIM이 기존에 발표한 여러 해의 조사 결과를 종합해 최근 웹사이트의 접근성 상태가 어떻게 변화했는지 분석한 자료다.
특히 2026년 WebAIM Million 조사 자체는 2026년 2월에 수행됐다.
따라서 11월 8일은 이 분석 글의 게시일이며, 원본 통계의 조사일과는 다르다.
WebAIM Million의 조사 방법
WebAIM은 주요 웹사이트 100만 개의 홈페이지를 분석한다.
자동화된 접근성 검사에는 WAVE를 사용한다.
검사 대상은 브라우저에서 렌더링한 DOM이다.
따라서 실제 JavaScript와 CSS가 적용된 이후의 상태를 검사할 수 있다.
주요 검사 기준은 WCAG 2.2의 A 및 AA 수준과 관련된 오류다.
하지만 자동화 도구에는 중요한 제한사항이 있다.
자동 검사에서 오류를 발견하지 못했다는 사실만으로 해당 페이지가 완전히 접근 가능하다고 판단할 수는 없다.
예를 들어 키보드 사용 흐름과 실제 스크린 리더의 사용 경험은 별도의 검증이 필요할 수 있다.
따라서 이번 통계는 자동으로 탐지할 수 있는 접근성 문제에 관한 것이다.
실제 조사 결과
저자는 네 해의 결과를 비교했다.
| 지표 | 2023 | 2024 | 2025 | 2026 |
|---|---|---|---|---|
| 접근성 오류가 발견된 홈페이지 | 96.3% | 95.9% | 94.8% | 95.9% |
| 페이지당 평균 탐지 오류 | 50.0 | 56.8 | 51.0 | 56.1 |
| 페이지당 평균 DOM 요소 | 1,050 | 1,173 | 1,257 | 1,437 |
2026년에는 조사한 홈페이지 가운데 95.9%에서 적어도 하나의 자동 탐지 접근성 오류가 발견됐다.
이는 2025년의 94.8%보다 높은 값이다.
페이지당 평균 오류 수도 51.0개에서 56.1개로 증가했다.
또한 평균 DOM 요소 수는 1,257개에서 1,437개로 증가했다.
저자는 이러한 결과를 바탕으로 최근 웹페이지의 복잡성이 증가하면서 접근성 문제를 발견할 수 있는 지점도 늘어났다고 설명한다.
하지만 DOM 요소의 수가 증가했다는 사실만으로 접근성 오류가 증가한 원인을 완전히 설명할 수는 없다.
실제 컴포넌트의 구조와 콘텐츠의 종류, 디자인 시스템 등 다른 요인도 영향을 줄 수 있다.
가장 자주 발견된 여섯 가지 문제
저자가 주목한 것은 매년 반복되는 오류의 종류다.
2026년 결과는 다음과 같다.
| 문제 유형 | 영향을 받은 홈페이지 비율 |
|---|---|
| 텍스트 대비 부족 | 83.9% |
| 이미지의 Alt Text 누락 | 53.1% |
| Form Input Label 누락 | 51.0% |
| 이름이나 내용이 없는 Link | 46.3% |
| 이름이 없는 Button | 30.6% |
| 문서의 언어 정보 누락 | 13.5% |
이번 결과에서 중요한 부분은 이러한 문제가 모두 새로운 기술 때문에 발생한 것이 아니라는 사실이다.
예를 들어 이미지는 오래전부터 대체 텍스트를 제공할 수 있었다.
Form Input에는 Label을 연결할 수 있다.
Button에는 사용자가 이해할 수 있는 이름을 제공할 수 있다.
하지만 실제 웹사이트에서는 이러한 기본적인 문제가 계속 발견됐다.
저자는 특히 컴포넌트 라이브러리의 기본값을 주목한다.
이름이 없는 Icon Button
예를 들어 다음과 같은 버튼이 있다고 가정해 보자.
<button><svg>...</svg></button>
사용자는 SVG의 모양을 보고 해당 버튼이 검색 기능을 수행한다고 이해할 수 있다.
하지만 스크린 리더는 해당 SVG만으로 버튼의 의미를 올바르게 전달하지 못할 수 있다.
따라서 버튼에는 접근 가능한 이름이 필요하다.
대표적으로 다음 속성을 사용할 수 있다.
aria-label="검색"
문제는 이러한 Icon Button이 컴포넌트 라이브러리에서 공통으로 제공될 수 있다는 점이다.
예를 들어 하나의 IconButton 컴포넌트가 접근 가능한 이름을 요구하지 않는다고 가정해 보자.
개발자는 해당 컴포넌트를 여러 페이지에서 재사용한다.
그 결과 같은 접근성 문제가 전체 서비스에 반복적으로 나타날 수 있다.
반대로 공통 컴포넌트에서 적절한 검증을 추가하면 여러 화면의 문제를 함께 방지할 수 있다.
React와 Next.js가 접근성 문제의 원인인가?
저자는 자신의 초기 가정도 검토했다.
처음에는 클라이언트 중심 프레임워크가 접근성 문제를 증가시킬 수 있다고 생각했다.
하지만 실제 통계는 단순한 설명과 일치하지 않았다.
2026년 조사에서 프레임워크별 평균 탐지 오류는 다음과 같았다.
| 프레임워크 | 페이지당 평균 탐지 오류 |
|---|---|
| Next.js | 40.9 |
| React | 43.5 |
| Nuxt | 54.7 |
| Vue | 64.6 |
| Astro | 9.0 |
| 전체 평균 | 56.1 |
Next.js와 React를 사용하는 페이지의 평균 탐지 오류는 전체 평균보다 낮았다.
Astro를 사용하는 페이지에서는 더 낮은 수치가 관찰됐다.
하지만 이러한 결과를 해석할 때 중요한 제한사항이 있다.
특정 프레임워크를 사용했기 때문에 접근성 오류가 감소했다는 인과관계가 증명된 것은 아니다.
예를 들어 Next.js를 사용하는 서비스에는 더 큰 개발팀과 QA 조직이 존재할 수 있다.
또한 각 프레임워크를 사용하는 사이트의 목적과 복잡도, 사용자 집단도 다를 수 있다.
따라서 실제 결과는 여러 조건의 영향을 받는다.
저자는 프레임워크의 이름보다 실제 UI를 어떻게 설계하고 검증하는지가 중요하다고 설명한다.
ARIA가 많을수록 접근성이 좋은가?
이번 글에서 또 하나 흥미로운 부분은 ARIA 사용량이다.
2026년 조사에서는 홈페이지의 82.7%가 ARIA를 사용했다.
평균 ARIA Attribute 수는 133개를 초과했다.
하지만 ARIA를 사용하는 페이지에서 발견한 평균 오류는 59.1개였다.
반면 ARIA를 사용하지 않는 페이지의 평균 오류는 42개였다.
이 결과만 보면 ARIA를 사용하면 접근성 오류가 증가하는 것처럼 보일 수 있다.
하지만 저자는 그러한 인과관계를 주장하지 않는다.
복잡한 웹 애플리케이션일수록 ARIA를 많이 사용할 가능성이 있기 때문이다.
중요한 것은 ARIA의 개수가 아니라 실제 HTML의 의미를 올바르게 표현하는 것이다.
예를 들어 일반적인 Button을 생각해 보자.
<button>은 기본적으로 다음 기능을 제공한다.
- 키보드 Focus
- Button 역할
- Enter 및 Space 키를 이용한 활성화
반면 다음 구조를 사용하면 추가적인 구현이 필요하다.
<div role="button">
이 경우 개발자는 키보드 처리와 Focus 등을 직접 구현해야 할 수 있다.
따라서 실제로 사용할 수 있는 Semantic HTML이 존재한다면 해당 요소를 사용하는 편이 적합할 수 있다.
ESLint와 axe-core의 역할
저자는 접근성 문제를 두 단계로 검사하도록 설명한다.
첫 번째는 코드 작성 시점의 Lint다.
대표적인 도구는 다음과 같다.
eslint-plugin-jsx-a11y
이 도구는 JSX 코드에서 일부 접근성 문제를 발견할 수 있다.
예를 들어 다음과 같은 문제다.
- 이미지 Alt Text 누락
- Form Label 누락
- 내용이 없는 Link
- 이름이 없는 Button
하지만 CSS가 실제로 적용된 이후의 색상 대비를 JSX 코드만으로 정확하게 확인하기는 어렵다.
따라서 두 번째 단계에서는 실제 브라우저에서 검사를 수행한다.
대표적인 도구는 다음과 같다.
axe-core
예를 들어 페이지를 렌더링한 이후 텍스트와 배경의 대비를 확인할 수 있다.
하지만 자동화 검사를 통과했다는 사실만으로 모든 접근성 문제가 해결됐다고 판단해서는 안 된다.
실제 키보드 탐색과 스크린 리더 사용도 필요할 수 있다.
디자인 시스템의 접근성
저자는 디자인 Token을 이용해 대비 문제를 줄이는 방법도 설명한다.
예를 들어 다음 두 값을 생각할 수 있다.
- 일반 본문 텍스트의 색상
- 일반 페이지의 배경색
개발자가 모든 페이지에서 임의의 Hex 값을 사용한다면 각 화면의 대비를 개별적으로 검증해야 할 수 있다.
반면 공통 Token으로 색상 조합을 관리하면 새로운 컴포넌트에서도 검증된 조합을 재사용할 수 있다.
일반적인 본문 텍스트에는 WCAG AA 기준으로 4.5:1의 대비 비율이 요구되는 상황이 많다.
다만 실제 기준은 텍스트 크기와 사용 목적에 따라 달라질 수 있다.
따라서 모든 텍스트에 하나의 비율만 적용한다고 생각해서는 안 된다.
이번 분석의 한계
이번 글의 통계는 WebAIM이 수행한 기존 조사를 저자가 다시 분석한 것이다.
따라서 웹사이트 100만 개를 저자가 직접 새롭게 검사한 결과가 아니다.
또한 다음 한계가 존재한다.
- 홈페이지를 중심으로 조사
- 자동으로 탐지할 수 있는 오류만 포함
- 사이트의 실제 사용자 경험을 모두 평가하지 않음
- 프레임워크와 오류 사이의 인과관계를 증명하지 않음
- 실제 서비스의 접근성 준수 여부를 확정하지 않음
이번 글은 프론트엔드 접근성 문제가 최신 프레임워크의 부족한 기능 때문에만 발생하는 것이 아니라 공통 컴포넌트와 기본 HTML 구조에서 반복적으로 만들어질 수 있다는 점을 실제 조사 통계로 설명한다.
Stop Shipping Search Boxes With a Race Condition
- 저자: Parsa Jiravand
- 매체: bestpractic.org
- 게시일: 2026-11-07
- 기술: JavaScript, Fetch API, Debounce, AbortController, 비동기 상태 관리
- 원문: https://bestpractic.org/blog/fetch-race-condition-stale-responses
실제 상품 검색 인터페이스에서 사용자가 입력한 검색어와 다른 검색 결과가 일시적으로 표시되는 문제를 조사하고 해결한 기술 글이다.
이번 글의 핵심은 일반적인 Debounce만으로 모든 비동기 검색 문제를 해결할 수는 없다는 점이다.
저자는 실제 프로젝트에서 QA가 보고한 문제를 설명한다.
사용자는 검색창에 문자를 입력한다.
하지만 일부 상황에서 화면에는 이전 검색어의 결과가 표시된다.
새로운 결과가 도착하면 문제가 사라지기도 한다.
이러한 오류는 네트워크 상태에 따라 발생하므로 재현하기 어려울 수 있다.
실제 문제의 발생 과정
예를 들어 사용자가 검색창에 다음 단어를 입력했다고 가정해 보자.
react
애플리케이션은 검색 API를 호출한다.
이후 사용자가 기존 단어를 지우고 다음 단어를 입력한다.
vue
애플리케이션은 다시 검색 API를 호출한다.
두 요청은 서로 다른 시점에 시작됐다.
하지만 응답이 반드시 같은 순서로 도착한다는 보장은 없다.
예를 들어 다음 상황이 발생할 수 있다.
react검색 요청 시작vue검색 요청 시작vue검색 응답 도착- Vue 검색 결과를 화면에 표시
react검색 응답 도착- React 검색 결과로 화면을 다시 변경
사용자의 검색창에는 여전히 vue가 입력돼 있다.
하지만 실제 목록에는 React 관련 결과가 표시된다.
문제는 검색 API가 잘못된 결과를 반환한 것이 아니다.
서로 다른 비동기 요청의 응답 도착 순서를 현재 사용자가 원하는 검색어의 순서로 잘못 해석한 것이다.
Debounce를 적용해도 해결되지 않은 이유
저자는 처음에 Debounce를 적용했다.
입력 이후 다음 시간 동안 추가 입력이 없을 때만 검색을 실행했다.
250ms
이렇게 하면 사용자가 빠르게 글자를 입력하는 동안 불필요한 API 요청을 줄일 수 있다.
하지만 요청 수가 감소했다고 해서 이미 실행 중인 요청이 사라지는 것은 아니다.
예를 들어 다음과 같은 상황이다.
- 사용자가
react를 입력한다. - 250ms가 지나 첫 번째 요청이 시작된다.
- 사용자가 입력값을
vue로 변경한다. - 다시 250ms가 지나 두 번째 요청이 시작된다.
- 두 번째 요청이 먼저 완료된다.
- 첫 번째 요청이 나중에 완료된다.
이 경우 Debounce를 사용했더라도 여전히 잘못된 결과가 표시될 수 있다.
따라서 Debounce의 목적과 비동기 응답의 정확성을 유지하는 목적은 서로 다르다.
Debounce는 불필요한 요청을 줄인다.
하지만 어떤 요청의 결과를 화면에 반영할지 결정하지는 않는다.
latestRequestId
저자는 이를 해결하기 위해 요청마다 식별자를 부여했다.
대표적인 변수는 다음과 같다.
latestRequestId
새로운 검색을 시작할 때마다 이 값을 증가시킨다.
각 요청은 자신이 시작한 시점의 ID를 저장한다.
예를 들어 다음과 같은 상황이다.
- React 검색 요청 ID: 1
- Vue 검색 요청 ID: 2
Vue 요청의 결과가 도착했다.
현재 최신 ID는 2다.
따라서 Vue 결과를 화면에 반영할 수 있다.
이후 React 요청의 결과가 도착했다.
하지만 해당 요청의 ID는 1이다.
현재 최신 ID는 여전히 2다.
따라서 React 요청의 결과는 무시한다.
대표적인 조건은 다음과 같다.
requestId !== latestRequestId
이 조건이 참이라면 오래된 결과를 화면에 반영하지 않는다.
이러한 방식에서는 실제 네트워크 요청이 어떤 순서로 완료되더라도 현재 사용자가 마지막으로 요청한 결과만 표시하도록 제어할 수 있다.
요청의 실행과 결과의 적용을 분리
이번 해결 방법에서 중요한 부분은 비동기 작업을 무조건 취소할 필요가 없다는 점이다.
요청은 실제로 계속 실행될 수 있다.
하지만 결과가 도착한 이후 현재 화면에 적용해도 되는지 확인한다.
이러한 방식에서는 서버나 네트워크 요청을 직접 제어할 수 없는 상황에서도 오래된 결과가 UI를 변경하는 문제를 줄일 수 있다.
예를 들어 외부 라이브러리가 API 요청을 관리한다고 가정해 보자.
개발자는 해당 요청을 취소할 수 없을 수도 있다.
하지만 결과를 화면에 적용하는 단계에서 ID를 확인할 수 있다면 잘못된 결과 표시를 방지할 수 있다.
AbortController를 사용하는 방법
저자는 다른 방법도 설명한다.
대표적인 API는 다음과 같다.
AbortController
새로운 검색이 시작될 때 기존 요청을 중단하도록 신호를 보낼 수 있다.
예를 들어 다음 흐름이다.
- 첫 번째 검색 시작
- AbortController 생성
- 새로운 검색어 입력
- 기존 Controller의
abort()호출 - 두 번째 요청 시작
이렇게 하면 더 이상 필요하지 않은 요청의 네트워크 작업을 줄일 수 있다.
다만 AbortController가 모든 작업을 완전히 되돌리는 것은 아니다.
예를 들어 HTTP 요청이 실제 서버에 도착해 데이터베이스를 변경했다고 가정해 보자.
클라이언트에서 abort()를 호출하더라도 이미 처리된 서버의 작업이 자동으로 Rollback되지는 않는다.
따라서 읽기 요청과 쓰기 요청을 구분해야 한다.
AbortController와 Request ID의 차이
두 방식은 목적이 조금 다르다.
| 방식 | 주요 역할 |
|---|---|
| Debounce | 불필요한 요청의 시작 감소 |
| AbortController | 더 이상 필요하지 않은 실행 중 요청 취소 |
| Request ID 검사 | 오래된 요청의 결과가 현재 상태를 변경하지 못하도록 제한 |
실제 애플리케이션에서는 여러 방법을 함께 사용할 수 있다.
예를 들어 Debounce로 요청 수를 줄인다.
새로운 요청이 시작되면 이전 요청을 취소한다.
그리고 실제 결과가 도착했을 때도 Request ID를 확인한다.
이렇게 하면 불필요한 네트워크 작업과 잘못된 UI 갱신을 각각 관리할 수 있다.
다만 같은 목적의 처리를 여러 계층에 중복해서 구현하면 코드의 복잡성이 증가할 수 있다.
따라서 사용하는 데이터 조회 라이브러리가 이미 어떤 기능을 제공하는지도 확인해야 한다.
React Query와 SWR
저자는 다음 라이브러리도 소개한다.
- TanStack Query
- SWR
이러한 라이브러리는 Query Key와 Cache, 요청 상태를 관리한다.
예를 들어 검색어를 Query Key에 포함하면 서로 다른 검색어의 결과를 구분할 수 있다.
따라서 React 결과가 Vue 검색의 데이터로 잘못 사용되는 상황을 줄일 수 있다.
하지만 라이브러리를 사용한다는 사실만으로 모든 비동기 상태 문제가 자동으로 해결되는 것은 아니다.
실제로 Query Key를 어떻게 구성했는지와 기존 데이터를 표시하는 정책에 따라 화면의 동작이 달라질 수 있다.
Redux Toolkit의 Request ID
Redux Toolkit에서도 비슷한 개념을 활용할 수 있다.
대표적인 API는 다음과 같다.
createAsyncThunk
이 기능은 실행마다 고유한 Request ID를 제공한다.
대표적인 값은 다음과 같다.
meta.requestId
개발자는 Reducer에서 해당 ID를 확인할 수 있다.
이렇게 하면 별도의 Counter를 직접 구현하지 않고도 현재 요청과 이전 요청의 결과를 구분할 수 있다.
다만 제공된 ID를 실제 상태 관리에 반영하는 로직은 개발자가 작성해야 할 수 있다.
Autosave에서는 별도의 서버 검증이 필요
저자는 같은 종류의 문제가 검색창 밖에서도 발생한다고 설명한다.
대표적인 사례는 Autosave다.
예를 들어 사용자가 문서를 작성한다고 가정해 보자.
첫 번째 저장 요청에는 이전 내용이 포함돼 있다.
두 번째 저장 요청에는 새로운 내용이 포함돼 있다.
하지만 첫 번째 요청이 두 번째 요청보다 늦게 처리될 수 있다.
이 경우 서버에서 이전 데이터가 나중에 저장될 가능성이 있다.
중요한 점은 클라이언트의 Request ID 검사만으로 서버에 저장된 데이터의 순서까지 보장할 수는 없다는 사실이다.
UI에서는 오래된 응답을 무시할 수 있다.
하지만 이미 서버에 반영된 변경을 되돌리지는 못한다.
따라서 실제 저장 작업에는 다음 방식이 추가로 필요할 수 있다.
- 버전 번호 검사
- Optimistic Concurrency Control
- 조건부 Update
- 서버에서의 요청 순서 제어
- 작업별 Idempotency Key
이러한 방법은 읽기 요청에서 발생하는 UI 문제와 실제 데이터 변경의 일관성을 구분하는 데 필요하다.
실제 적용 범위와 한계
이번 글은 실제 검색 UI에서 발견한 문제를 바탕으로 작성됐다.
하지만 특정 네트워크 환경에서 오류가 얼마나 자주 발생했는지에 관한 정량적인 Benchmark는 제공하지 않았다.
또한 Request ID 검사가 실제 응답 시간을 줄인다고 주장하지 않는다.
요청의 완료 순서를 변경하는 기능도 아니다.
여러 비동기 작업이 동시에 진행되는 상황에서 어떤 결과가 현재 상태를 변경할 권한을 가지고 있는지 명확하게 관리하는 방법을 설명한 글이다.
Stop Writing JavaScript to Open Dialogs. commandfor Fixes It.
- 저자: Parsa Jiravand
- 매체: bestpractic.org
- 게시일: 2026-11-08
- 기술: HTML, JavaScript, Dialog API, Popover API, Invoker Commands API
- 원문: https://bestpractic.org/blog/commandfor-invoker-commands-api
기존에는 JavaScript로 작성하던 Dialog와 Popover의 열기·닫기 동작을 HTML의 command와 commandfor 속성으로 표현하는 방법을 설명한 글이다.
이번 글은 새로운 브라우저 API의 최초 출시 발표가 아니다.
이미 여러 브라우저에서 제공되는 Invoker Commands API를 실제 웹 애플리케이션에 적용하는 방법을 분석한 자료다.
특히 간단한 UI 상태 변경을 위해 반복해서 작성하는 JavaScript를 줄이는 데 초점을 맞춘다.
기존 Dialog의 구조
일반적인 웹 애플리케이션에는 확인 Dialog가 존재할 수 있다.
예를 들어 사용자가 계정을 삭제하려고 한다고 가정해 보자.
사용자는 삭제 버튼을 누른다.
애플리케이션은 확인 Dialog를 표시한다.
사용자는 취소하거나 삭제를 확정한다.
기존에는 다음 API를 사용할 수 있었다.
showModal()close()
개발자는 JavaScript에서 버튼을 찾는다.
그리고 클릭 이벤트를 등록한다.
이후 해당 이벤트에서 Dialog의 메서드를 호출한다.
이러한 방식은 잘못된 구현이 아니다.
하지만 여러 Dialog에서 같은 코드를 반복해서 작성할 수 있다.
예를 들어 설정 화면에 11개의 Dialog가 존재한다면 각 Dialog의 열기와 닫기에 관한 코드를 별도로 관리해야 할 수 있다.
저자는 이러한 반복 작업을 줄이는 방법을 설명한다.
command와 commandfor
새로운 방식에서는 Button에 다음 두 속성을 사용한다.
commandcommandfor
commandfor는 어떤 요소를 대상으로 동작할지 지정한다.
command는 해당 요소에 어떤 동작을 수행할지 지정한다.
예를 들어 Dialog의 ID가 다음과 같다고 가정해 보자.
confirm-dialog
해당 Dialog를 열기 위해 다음 값을 사용할 수 있다.
command="show-modal"
commandfor="confirm-dialog"
브라우저는 사용자가 Button을 활성화했을 때 해당 Dialog를 찾는다.
그리고 Modal Dialog를 표시하는 동작을 수행한다.
따라서 단순히 Dialog를 여는 작업을 위해 별도의 JavaScript Event Listener를 작성할 필요가 없다.
Dialog의 세 가지 Command
이번 글에서는 다음 Command를 설명한다.
| Command | 동작 |
|---|---|
show-modal | Dialog를 Modal 형태로 표시 |
close | Dialog 닫기 |
request-close | 닫기 요청을 전달 |
여기서 중요한 차이는 close와 request-close다.
close는 Dialog를 직접 닫는다.
반면 request-close는 Dialog에 닫기 요청을 전달한다.
이 경우 Dialog의 cancel 이벤트를 이용해 실제로 닫아도 되는지 결정할 수 있다.
예를 들어 사용자가 설정 Form을 작성했다고 가정해 보자.
아직 저장하지 않은 변경 사항이 존재한다.
이때 취소 버튼을 눌렀다고 해서 Form을 즉시 닫으면 입력한 내용이 사라질 수 있다.
따라서 애플리케이션은 닫기 요청을 받은 뒤 저장되지 않은 변경 사항이 존재하는지 확인할 수 있다.
이러한 상황에서는 request-close가 적합할 수 있다.
Popover에서도 사용 가능
Invoker Commands API는 Dialog에만 적용되지 않는다.
Popover에도 사용할 수 있다.
대표적인 Command는 다음과 같다.
show-popoverhide-popovertoggle-popover
예를 들어 사용자 메뉴가 있다고 가정해 보자.
메뉴를 열거나 닫는 Button에 Command를 지정할 수 있다.
하지만 실제 대상 요소에는 여전히 popover 속성이 필요하다.
즉 commandfor가 존재한다는 사실만으로 일반적인 div가 자동으로 Popover가 되는 것은 아니다.
실제 UI의 역할을 올바르게 정의해야 한다.
기존 popovertarget과의 차이
기존 Popover API에서도 다음 속성을 사용할 수 있었다.
popovertarget
이 속성은 특정 Popover를 제어하는 데 사용된다.
반면 Invoker Commands API는 Dialog와 Popover를 공통적인 형태로 표현한다.
따라서 여러 종류의 요소를 대상으로 비슷한 명령 구조를 사용할 수 있다.
하지만 기존 popovertarget을 반드시 제거해야 하는 것은 아니다.
실제로 필요한 기능과 브라우저 지원 범위에 따라 선택할 수 있다.
사용자 정의 Command
이번 API에서는 직접 정의한 Command도 사용할 수 있다.
사용자 정의 Command는 다음 Prefix를 사용한다.
--
예를 들어 알림 목록에서 특정 알림을 읽음 상태로 변경한다고 가정해 보자.
다음 Command를 사용할 수 있다.
--mark-read
그리고 해당 알림 요소의 ID를 commandfor에 지정한다.
브라우저는 대상 요소에 command 이벤트를 전달한다.
개발자는 이 이벤트에서 실제 동작을 구현할 수 있다.
이벤트에서는 다음 값을 확인할 수 있다.
event.commandevent.sourceevent.target
event.command는 어떤 Command가 실행됐는지 알려준다.
event.source는 실제 명령을 발생시킨 Button을 가리킨다.
event.target은 해당 Command를 전달받은 대상 요소다.
이러한 구조에서는 어떤 Button이 실제 동작을 발생시켰는지와 어떤 요소가 변경 대상인지 구분할 수 있다.
모든 JavaScript가 사라지는 것은 아님
이번 글의 제목은 JavaScript 없이 Dialog를 열 수 있다는 점을 강조한다.
하지만 모든 Dialog 관련 로직이 사라지는 것은 아니다.
예를 들어 사용자가 실제로 계정 삭제를 확정하는 상황을 생각해 보자.
계정 삭제를 수행하려면 실제 API를 호출해야 할 수 있다.
또한 사용자 인증과 삭제 권한도 검증해야 한다.
이번 API는 Dialog의 열기와 닫기 같은 기본적인 UI 동작을 HTML에서 선언할 수 있도록 한다.
실제 비즈니스 로직을 모두 대체하지는 않는다.
사용자 정의 Command 역시 JavaScript에서 이벤트를 처리해야 할 수 있다.
브라우저 지원과 Fallback
저자가 설명한 지원 버전은 다음과 같다.
| 브라우저 | 지원 버전 |
|---|---|
| Chrome·Edge | 135 이상 |
| Firefox | 144 이상 |
| Safari | 26.2 이상 |
저자는 2025년 12월부터 주요 브라우저 엔진에서 사용할 수 있게 됐다고 설명한다.
따라서 이번 글의 게시일인 2026년 11월 8일을 API의 최초 출시일로 해석해서는 안 된다.
실제 프로젝트에서 오래된 브라우저를 지원해야 한다면 Fallback이 필요할 수 있다.
대표적인 기능 감지 방식은 다음과 같다.
"command" in HTMLButtonElement.prototype
이 값을 이용해 브라우저의 지원 여부를 확인할 수 있다.
하지만 실제로 사용하는 Command의 종류와 대상 요소의 지원 범위도 함께 확인해야 한다.
Form 안의 Button
저자는 다음 설정도 강조한다.
type="button"
HTML에서 Button을 Form 안에 배치하면 기본적으로 Submit 동작과 연결될 수 있다.
예를 들어 사용자가 확인 Dialog를 열기 위해 버튼을 눌렀다고 가정해 보자.
하지만 해당 Button의 Type을 명시하지 않았다.
이 경우 의도하지 않게 Form Submit이 발생할 수 있다.
따라서 단순히 Dialog를 열기 위한 Button에는 명시적인 Type을 사용하는 편이 적절하다.
접근성과 한계
Invoker Commands API를 사용하면 일부 UI의 동작을 브라우저의 기본 기능에 맡길 수 있다.
하지만 이것만으로 전체 애플리케이션의 접근성이 자동으로 보장되지는 않는다.
예를 들어 다음 항목은 여전히 검토해야 한다.
- Dialog의 제목과 설명
- 실제 Focus 이동
- 닫기 이후 Focus 복원
- 키보드 접근성
- Dialog가 열려 있는 동안의 다른 UI 상태
특히 사용자 정의 Command에서는 실제 동작의 의미와 접근성을 개발자가 관리해야 한다.
이번 글에는 JavaScript 파일 크기가 몇 KB 감소하는지 또는 Dialog를 표시하는 속도가 몇 퍼센트 빨라지는지에 관한 Benchmark가 포함되지 않았다.
단순한 UI 동작을 JavaScript의 Event Listener 대신 HTML의 선언적인 속성으로 표현하면서 실제 비즈니스 로직과 브라우저 기본 동작의 책임을 분리하는 방법을 설명한 자료다.
I Summed 400,000 Rows on Every Refresh
- 저자: Sukhpinder Singh
- 매체: DEV Community
- 게시일: 2026-11-08
- 기술: ASP.NET Core, C#, Output Cache, HTTP API, 성능 측정
- 원문: https://dev.to/ssukhpinder/i-summed-400000-rows-on-every-refresh-4mnj
실제 대시보드에서 40만 개의 데이터를 반복해서 집계하는 API가 불필요하게 계속 실행되는 문제를 Output Cache로 해결하고, 변경 전후의 실행 횟수와 시간을 측정한 글이다.
이번 글은 특정 C# 문법을 설명하는 데 그치지 않는다.
저자는 같은 계산을 반복해서 실행하던 API에서 실제로 어떤 비용이 발생하는지 분석했다.
이후 계산 결과만 Cache에 저장하는 방식과 HTTP Response 전체를 Cache에 저장하는 방식의 차이를 설명한다.
특히 사용자의 요청을 받았을 때 Handler를 실행하지 않아도 되는지 판단하는 것이 단순히 계산 결과를 Memoization하는 것보다 더 큰 효과를 만들 수 있다는 점을 보여준다.
실제 문제
저자는 매출 정보를 표시하는 대시보드를 개발했다.
화면에는 지역별 매출 합계가 표시됐다.
데이터는 약 40만 개의 행으로 구성돼 있었다.
사용자는 대시보드를 열어 둔다.
애플리케이션은 몇 초마다 API를 다시 호출한다.
문제는 실제 매출 데이터가 자주 변경되지 않았다는 점이다.
저자에 따르면 실제 데이터는 약 한 시간에 한 번 변경됐다.
하지만 API는 화면이 새로고침될 때마다 같은 데이터를 다시 집계했다.
따라서 실제 결과가 달라지지 않았는데도 다음 작업이 반복됐다.
- API 요청 처리
- Query Parameter 읽기
- 40만 개 데이터의 집계
- 결과 객체 생성
- JSON 직렬화
- HTTP 응답 반환
이는 실제 계산 결과가 이미 존재하더라도 같은 작업을 반복하는 구조였다.
일반적인 메모리 Cache
이러한 문제를 해결하는 방법 가운데 하나는 계산 결과를 메모리에 저장하는 것이다.
ASP.NET Core에서는 다음 기능을 사용할 수 있다.
IMemoryCache
예를 들어 지역별 합계를 계산한 뒤 결과를 Cache에 저장할 수 있다.
이후 같은 지역을 다시 조회하면 계산한 값을 재사용한다.
하지만 이 방식에서는 여전히 API Handler가 실행된다.
예를 들어 다음 작업은 계속 발생할 수 있다.
- 요청의 Model Binding
- Handler 실행
- Dependency Injection
- 결과 객체 생성
- Response Serialization
저자는 이러한 비용까지 줄이고 싶었다.
따라서 계산 결과가 아니라 HTTP 응답 전체를 Cache에 저장하는 방식을 선택했다.
Output Cache
ASP.NET Core에는 Output Cache Middleware가 존재한다.
저자는 다음 기능을 사용했다.
AddOutputCache()
실제 Middleware는 다음과 같다.
UseOutputCache()
그리고 특정 Endpoint에 Cache 정책을 적용한다.
대표적인 API는 다음과 같다.
CacheOutput()
이렇게 하면 요청을 처리할 때 이전에 생성한 Response를 재사용할 수 있다.
Cache Hit가 발생하면 실제 Handler를 실행하지 않고 저장된 HTTP 응답을 반환할 수 있다.
따라서 단순히 집계 계산만 생략하는 방식보다 더 많은 작업을 줄일 수 있다.
Cache 만료 시간
저자는 다음 기간을 설정했다.
5분
실제 데이터는 약 한 시간에 한 번 변경됐다.
따라서 최대 몇 분 정도 이전의 합계를 표시하는 것을 허용할 수 있다고 판단했다.
하지만 이 선택은 모든 서비스에 적합하지 않다.
예를 들어 일반적인 매출 대시보드에서는 몇 분 전의 데이터를 표시할 수 있다.
반면 실제 계좌 잔액이나 결제 승인 결과에서는 같은 정책이 부적절할 수 있다.
따라서 Cache의 만료 시간은 실제 데이터의 의미와 요구되는 최신성에 따라 결정해야 한다.
Query Parameter별 Cache 분리
이번 구현에서 중요한 부분은 지역별 결과를 분리하는 것이다.
예를 들어 다음 두 요청을 생각할 수 있다.
/sales?region=west
/sales?region=east
두 요청은 같은 Endpoint를 사용한다.
하지만 실제 반환해야 하는 데이터는 다르다.
따라서 Cache Key에도 해당 Query Parameter를 반영해야 한다.
저자는 다음 설정을 사용했다.
SetVaryByQuery("region")
이렇게 하면 west와 east의 응답을 서로 다른 Cache Entry로 관리할 수 있다.
하지만 모든 종류의 데이터를 Query Parameter만으로 구분할 수 있는 것은 아니다.
예를 들어 로그인한 사용자의 권한에 따라 결과가 달라진다면 사용자별 권한도 고려해야 한다.
단순히 region만 구분해서는 충분하지 않을 수 있다.
실제 Benchmark
저자는 두 종류의 Endpoint를 만들었다.
첫 번째는 Cache를 사용하지 않는다.
두 번째는 Output Cache를 사용한다.
두 Endpoint는 동일한 집계 작업을 수행한다.
이후 각각 50번의 요청을 실행했다.
측정 전에 Warm-up도 한 번 수행했다.
실제 결과는 다음과 같다.
| 항목 | Cache 없음 | Output Cache |
|---|---|---|
| 측정 요청 | 50회 | 50회 |
| Handler 실행 횟수 | 51회 | 1회 |
| 총 실행 시간 | 112ms | 14ms |
| 관찰된 시간 차이 | 기준 | 약 8배 빠름 |
여기서 가장 신뢰할 수 있는 결과는 Handler의 실행 횟수다.
Cache를 사용하지 않은 Endpoint에서는 실제 집계 작업이 반복됐다.
반면 Output Cache를 사용한 Endpoint에서는 첫 번째 실행 결과가 저장됐다.
이후 요청에서는 저장된 Response를 재사용했다.
따라서 Handler의 실행 횟수가 크게 감소했다.
8배 빠르다는 결과의 한계
저자는 실행 시간이 매번 동일하지는 않았다고 설명한다.
일부 실행에서는 약 6.6배 차이가 발생했다.
다른 실행에서는 약 8배 차이가 나타났다.
또한 테스트 환경은 Container였다.
실제 시스템의 부하와 JIT, 메모리 상태에 따라 절대 실행 시간이 달라질 수 있다.
따라서 이번 결과를 모든 ASP.NET Core API가 Output Cache를 적용하면 8배 빨라진다는 성능 보장으로 사용해서는 안 된다.
이번 측정은 저자가 작성한 40만 행 집계 예제에서 반복 요청을 실행한 자체 실험 결과다.
독립적인 제3자가 같은 조건으로 다른 Cache 시스템과 비교한 Benchmark는 아니다.
Cache Key가 잘못되면 다른 사용자의 데이터가 노출될 수 있음
이번 글에서 가장 중요한 보안상 제한은 사용자별 데이터에 관한 것이다.
예를 들어 다음 상황을 생각해 보자.
사용자 A는 자신의 매출 정보를 조회한다.
서버는 해당 결과를 Cache에 저장한다.
이후 사용자 B가 같은 Endpoint에 접근한다.
만약 Cache Key가 실제 사용자별 권한을 구분하지 않는다면 사용자 B에게 사용자 A의 데이터를 반환할 위험이 있다.
따라서 개인화된 HTTP Response를 Cache에 저장할 때는 신중해야 한다.
ASP.NET Core의 기본 Output Cache 정책은 일부 위험을 줄이도록 설계됐다.
대표적으로 다음 상황에서는 기본적으로 Cache하지 않는다.
- Authorization Header가 포함된 요청
- Set-Cookie Header를 포함하는 응답
하지만 개발자가 이러한 정책을 변경한다면 실제 사용자별 Cache 분리를 직접 관리해야 한다.
저자는 개인화된 응답을 무분별하게 Output Cache에 저장하지 않는 편이 적절하다고 설명한다.
여러 서버 인스턴스의 Cache
이번 실험에서는 기본 In-memory Cache를 사용했다.
문제는 실제 운영 환경에 여러 서버 인스턴스가 존재할 수 있다는 점이다.
예를 들어 Load Balancer 뒤에 세 개의 애플리케이션 서버가 있다고 가정해 보자.
각 서버는 자신의 메모리에 Cache를 저장한다.
서버 A에는 이미 특정 응답이 저장돼 있다.
하지만 같은 요청이 서버 B로 전달됐다.
서버 B에는 해당 Cache Entry가 존재하지 않을 수 있다.
따라서 같은 계산을 다시 실행한다.
이러한 상황에서는 전체 요청의 Cache Hit Rate가 예상보다 낮을 수 있다.
ASP.NET Core는 다음 인터페이스를 통해 다른 Cache 저장소를 사용할 수 있도록 한다.
IOutputCacheStore
하지만 공유 Cache를 추가하면 실제 운영 비용과 네트워크 지연이 증가할 수 있다.
따라서 공유 저장소가 항상 더 적합하다고 판단해서는 안 된다.
실제 Cache Hit Rate와 데이터의 크기를 측정한 뒤 결정해야 한다.
Output Cache와 데이터의 최신성
이번 구현에서는 5분 동안 같은 응답을 반환한다.
하지만 실제 데이터가 변경됐다고 해서 Cache가 즉시 갱신되는 것은 아니다.
예를 들어 다음 상황이다.
- 오전 9시에 Cache 생성
- 오전 9시 1분에 매출 데이터 변경
- 오전 9시 2분에 API 요청
- 여전히 오전 9시의 Cache 반환
실제 데이터는 변경됐지만 사용자는 이전 합계를 볼 수 있다.
저자는 이 정도의 지연을 대시보드에서 허용할 수 있다고 판단했다.
하지만 실제 업무 규칙에 따라 Cache 무효화 정책이 필요할 수 있다.
예를 들어 데이터 변경 시 관련 Cache Entry를 직접 무효화하는 방식도 고려할 수 있다.
이 경우에는 Cache의 유지보수 비용이 증가한다.
이번 구현의 적용 범위
이번 글의 결과는 다음 조건에서 의미가 크다.
- 많은 사용자가 같은 결과를 조회
- 실제 데이터는 자주 변경되지 않음
- 일정 시간 이전의 데이터를 허용할 수 있음
- HTTP Response를 재사용할 수 있음
- 개인화된 인증 정보가 포함되지 않음
반면 다음 상황에서는 적합하지 않을 수 있다.
- 사용자마다 서로 다른 응답
- 매우 빠르게 변경되는 데이터
- 실시간 거래 및 결제 정보
- POST 등 실제 상태를 변경하는 작업
- 요청마다 새로운 결과가 필요한 기능
이번 글은 API 내부의 계산 결과를 Cache하는 것과 HTTP 응답 전체를 Cache하는 것이 실제로 어떤 실행 비용의 차이를 만드는지 직접 작성한 Benchmark로 보여준다.
특히 Handler 실행 횟수와 응답 지연, 데이터 최신성, 사용자별 Cache 분리라는 네 가지 문제를 함께 고려해야 한다는 점을 설명한다.