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

기준 시점: 2026-11-08 06:00 KST
조사 범위: 전일 2026-11-07 06:00 ~ 당일 2026-11-08 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
React, Next.js, Vercel, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro, Remix의 공식 릴리스와 관련 발표를 조사했다.
이번 24시간에는 새로운 주요 안정 버전이나 중요한 사용자 API의 정식 출시를 확인하지 못했다.
Next.js 16.5 Canary의 변경 사항
조사 과정에서 Next.js 16.5 계열의 Canary 업데이트를 확인했다.
- 발표일: 2026-11-07 KST
- 버전: Next.js 16.5.0-canary.43
- 공개 시각: 2026-11-06 23:41 UTC, 한국 시간 2026-11-07 08:41
- 안정화 단계: Canary Pre-release
- 원문: https://github.com/vercel/next.js/releases/tag/v16.5.0-canary.43
이번 릴리스에는 다음과 같은 내부 수정이 포함됐다.
- Prerender된
LinkHeader가 Revalidation 과정에서 누적되는 문제 수정 - Node.js 응답 객체의 Header 할당 비용 감소
eslint-config-next의 TypeScript Peer Dependency 조정- Image Optimizer의 실험적 API 위치 정리
- Request Insights의 Context Menu와 Trace 상호작용 개선
이 가운데 Prerendered Link Header 수정은 서버 렌더링과 캐시 갱신의 정확성에 관한 변경이다.
HTTP Link Header는 다른 리소스와의 관계를 표시하는 데 사용된다. 예를 들어 리소스의 Preload나 Preconnect에 관한 정보를 전달할 수 있다.
하지만 페이지를 다시 검증하는 과정에서 이전 Header가 불필요하게 누적된다면 실제 응답에 의도하지 않은 정보가 포함될 수 있다.
이번 수정은 이러한 누적을 방지한다.
다만 Canary 개발 버전의 내부 수정이며, 공식 릴리스에 구체적인 성능 수치나 광범위한 사용자 영향에 관한 설명은 포함되지 않았다.
따라서 중요한 정식 기능 발표로 선정하지 않았다.
별도로 확인한 후속 Canary의 주요 변경 역시 문서와 개발 환경의 내부 구성 정리에 집중돼 있어 새로운 주요 뉴스로 포함하지 않았다.
이번 추천 글에서는 프론트엔드에서 실제로 발생하는 문제를 다룬다. 특히 5만 행을 표시하는 Vue 컴포넌트의 Virtualization 구현과 CSS·JavaScript 사이의 상태 관리, AI 채팅 화면의 접근성 문제를 자세히 설명한다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome Developers·Chromium, Mozilla·Firefox, WebKit·Safari의 공개 자료와 JavaScript·TypeScript·CSS, 웹 성능 및 접근성 관련 발표를 조사했다.
이번 기간에 새롭게 정식 채택된 주요 웹 표준이나 안정화된 브라우저 API의 출시 발표는 확인하지 못했다.
WHATWG HTML Living Standard에서는 2026년 11월 6일의 문서 갱신을 확인했다.
하지만 Living Standard의 갱신일만으로 특정 브라우저 기능이 정식 출시됐다고 판단할 수는 없다.
이번 조사에서는 개별 변경의 최초 발표 시각과 적용 범위를 확인할 수 있는 중요한 신규 항목을 찾지 못해 포함하지 않았다.
최근 공개된 Chrome 158 Beta와 Firefox 159 정식 릴리스도 이전 브리핑에서 다뤘으므로 반복하지 않았다.
이번 추천 글에는 이미 사용할 수 있는 웹 플랫폼 기능을 실제 애플리케이션에서 어떻게 활용해야 하는지 분석한 글을 포함했다.
특히 CSS Container Query와 JavaScript ResizeObserver를 동시에 사용하면서 발생하는 상태 불일치, aria-live를 이용한 AI 채팅 응답의 접근성 문제를 살펴볼 수 있다.
3️⃣ 백엔드·인프라
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
Cloudflare, AWS, Google Cloud, Microsoft Azure의 주요 공식 발표와 서버리스·엣지 플랫폼, 데이터베이스 및 API 관련 정보를 조사했다.
Cloudflare의 공식 Changelog에서 확인한 최근 발표는 11월 6일에 집중돼 있었다.
- AI Gateway의 Provider Key 사용 기록 확인
- Cloudflare Access의 Passkey 로그인 지원
- User API Token 생성 화면 개편
- Cloudflare One의 Fast Flux 탐지 기능
이러한 항목은 이번 조사 범위의 시작 시점인 11월 7일 오전 6시 이전에 공개된 것으로 보이는 자료이므로 새 소식으로 포함하지 않았다.
게시 날짜가 11월 6일이라는 사실만으로 정확한 한국 시간 게시 시각을 확정할 수는 없지만, 이번 조사 범위에 포함된다는 근거 역시 확인하지 못했다.
- Cloudflare 공식 Changelog: https://developers.cloudflare.com/changelog/
AWS와 Google Cloud, Azure에서도 조사 범위에 해당하는 중요한 신규 발표를 공식 원문으로 확인하지 못했다.
최근 발표된 Cloudflare Durable Objects의 자동 재시도, 새로운 Module Registry 기본 적용 예고, Vercel Schedules, Google Cloud Application Load Balancer의 분산 추적 기능은 이전 브리핑에서 이미 다뤘으므로 반복하지 않았다.
이번 기간에는 새로운 항목을 억지로 추가하는 대신 발표일과 적용 시점을 확인할 수 없는 내용을 제외했다.
4️⃣ 개발 도구 및 보안
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
Node.js, Deno, Bun, Vite, webpack, Rollup, esbuild, Babel, Biome, ESLint, Prettier, npm, pnpm, Yarn, GitHub 및 GitLab의 주요 릴리스와 변경 기록을 조사했다.
조사 과정에서 확인한 개발 중 변경 사항
Bun의 공식 저장소에서는 11월 7일에 작성된 Commit을 확인했다.
해당 Commit 가운데 하나는 Windows 관련 아카이브의 SHA-256을 검증하고, NUL 문자가 포함된 경로를 처리하는 변경이다.
그러나 개발 브랜치의 Commit이 존재한다는 사실만으로 정식 Runtime의 새로운 보안 수정 버전이 출시됐다고 판단할 수는 없다.
이번 변경이 실제 사용자가 설치하는 정식 버전에 포함됐는지 별도로 확인되지 않아 주요 신규 발표에서 제외했다.
pnpm 저장소에서도 11월 7일의 변경을 확인했다.
대표적인 작업은 다음과 같다.
-
npm Package 응답에서 불필요한 저장소 Metadata 제거
-
실행 파일의 Symbolic Link 처리 정리
-
Workspace의
modulesDir처리 수정 -
패키지 게시 및 삭제 오류 메시지 개선
이러한 변경은 개발 브랜치의 유지보수 작업이다.
실제 신규 정식 릴리스의 중요한 변경 사항으로 확인되지 않아 주요 소식으로 포함하지 않았다.
GitHub 공식 Changelog에서는 11월 6일 공개된 Copilot Code Review 관련 개선과 사용량 Dashboard 변경을 확인했다.
그러나 이번 조사 범위에 포함되는 새로운 발표로 확인되지 않아 반복하거나 날짜를 변경해 소개하지 않았다.
보안 관련 공식 자료에서도 조사 기간에 새롭게 공개된 중요한 취약점과 그에 대한 수정 사항을 함께 검증할 수 있는 항목을 확인하지 못했다.
기존 취약점의 재소개나 일반적인 탐지 규칙 개선을 새로운 긴급 보안 사고로 취급하지 않았다.
📚 추천 글
Making Immersive LLM Chat Experiences More Accessible
- 저자: Eric Bailey
- 매체: Smashing Magazine
- 게시일: 2026-11-05
- 기술: HTML, ARIA, JavaScript, 접근성, 키보드 Navigation, Screen Reader
- 원문: https://www.smashingmagazine.com/2026/11/making-immersive-llm-chat-experiences-more-accessible/
AI 채팅 인터페이스를 개발할 때 발생하는 접근성 문제를 실제 사용자 경험과 브라우저 기술의 관점에서 분석한 글이다.
저자는 여러 AI 채팅 인터페이스의 접근성을 개선한 경험을 바탕으로 일반적인 웹사이트와 달리 AI 채팅 화면에서 특히 주의해야 하는 상호작용 문제를 설명한다.
이번 글은 단순히 ARIA 속성을 추가하는 방법을 소개하지 않는다.
실제로 스크린 리더 사용자가 대화 내용을 이해하고 이전 메시지로 이동하며 새로운 응답을 확인하는 과정까지 고려한다.
AI 채팅 화면의 기본 구조
일반적인 AI 채팅 애플리케이션에는 다음 영역이 존재한다.
- 대화 목록
- 사용자 입력창
- 모델이 생성하는 응답
- 이전 대화 선택
- 파일 및 코드 미리보기
- 설정과 기타 도구
문제는 이러한 영역이 동적으로 변경된다는 점이다.
예를 들어 사용자가 질문을 전송했다고 가정해 보자.
모델은 답변을 생성하기 시작한다.
답변은 여러 Token으로 나뉘어 도착한다.
사용자는 화면에서 텍스트가 점진적으로 나타나는 것을 확인할 수 있다.
하지만 스크린 리더를 사용하는 사용자에게는 이러한 변화가 자동으로 적절하게 전달되지 않을 수 있다.
따라서 단순히 화면에 텍스트가 표시된다는 사실만으로 모든 사용자가 해당 정보를 인지할 수 있다고 볼 수 없다.
aria-live의 잘못된 사용
AI 채팅 화면에서는 새로운 응답이 생성됐다는 사실을 알려줘야 한다.
이를 위해 다음 속성을 사용할 수 있다.
aria-live
Live Region은 화면의 특정 영역에 변경이 발생했을 때 보조 기술이 해당 변경을 사용자에게 알릴 수 있도록 한다.
하지만 저자는 흔히 사용하는 구현 방식에 문제가 있다고 설명한다.
예를 들어 모델이 생성하는 각각의 응답 메시지에 다음 속성을 추가했다고 가정해 보자.
aria-live="polite"
이 경우 모델이 새로운 Token을 생성할 때마다 해당 영역의 내용이 변경될 수 있다.
스크린 리더는 이러한 변경을 계속 알리려고 시도할 수 있다.
그 결과 사용자는 다음 문제를 경험할 수 있다.
- 문장이 완성되기 전에 반복해서 읽힘
- 이전에 읽던 내용이 중단됨
- 같은 텍스트가 여러 번 전달됨
- 실제 응답이 어디까지 생성됐는지 이해하기 어려움
저자는 이를 해결하기 위해 실제 대화 메시지와 별도의 전용 Live Region을 만들도록 권장한다.
이렇게 하면 대화 메시지는 일반적인 콘텐츠로 유지할 수 있다.
반면 새로운 응답에 관한 알림은 하나의 전용 영역에서 처리할 수 있다.
스트리밍 중에는 완료된 문장을 기다리기
이번 글에서 가장 중요한 설계 선택은 응답을 언제 알릴지 결정하는 것이다.
저자는 모델이 Token을 생성할 때마다 알리는 방식을 권장하지 않는다.
대신 응답이 완성된 이후 해당 내용을 하나의 메시지로 전달하는 방식을 설명한다.
예를 들어 모델이 다음 문장을 생성한다고 가정해 보자.
The answer is 42.
실제 스트리밍에서는 다음처럼 여러 조각으로 전달될 수 있다.
Theansweris42.
각 조각을 스크린 리더에 별도로 전달하면 자연스러운 문장으로 듣기 어렵다.
따라서 실제 화면에는 스트리밍을 유지하더라도 보조 기술에는 완료된 메시지를 전달하는 편이 더 적합할 수 있다.
하지만 응답 완료까지 아무런 정보를 전달하지 않는 것도 문제가 된다.
모델이 오래 작업하는 경우 사용자는 애플리케이션이 멈췄다고 생각할 수 있기 때문이다.
처리 중 상태 알림
저자는 모델이 작업을 수행하는 동안 처리 상태도 전달하도록 권장한다.
예를 들어 다음 상황이다.
- 질문을 전송함
- 모델이 답변을 준비 중
- 도구를 실행 중
- 답변 생성 완료
실제 화면에서는 Spinner나 Loading Animation을 표시할 수 있다.
하지만 이러한 시각적 요소만으로는 스크린 리더 사용자가 현재 상태를 알기 어렵다.
따라서 시각적인 상태 메시지와 보조 기술의 알림을 함께 제공해야 한다.
예를 들어 실제 화면에 다음 문구가 표시된다면 해당 상태를 Live Region에도 전달할 수 있다.
답변을 생성하고 있습니다.
다만 처리 중 상태와 완료된 답변을 무분별하게 반복해서 알리면 오히려 사용하기 어려워질 수 있다.
따라서 알림의 시점과 빈도를 명확하게 관리해야 한다.
ariaNotify()와 실제 브라우저 지원
저자는 새로운 ariaNotify()도 검토한다.
이 API는 기존 Live Region을 직접 관리하는 복잡성을 줄이려는 기능이다.
하지만 저자는 새로운 API가 브라우저에서 지원된다는 사실만으로 즉시 모든 기존 접근성 구현을 교체하지 않도록 권장한다.
여기에는 중요한 이유가 있다.
스크린 리더 사용자는 브라우저를 항상 최신 버전으로 유지하지 않을 수 있다.
또한 보조 기술과 브라우저의 조합에 따라 실제 동작이 달라질 수 있다.
따라서 새로운 API의 존재 여부뿐 아니라 실제 사용자의 환경에서 안정적으로 동작하는지 확인해야 한다.
저자는 현재 시점에서는 검증된 전용 Live Region 방식이 더 적합할 수 있다고 설명한다.
Semantic HTML과 Landmark
AI 채팅 화면에는 여러 종류의 영역이 존재한다.
저자는 다음 Semantic HTML을 활용하도록 설명한다.
mainformasidearticle
예를 들어 대화 메시지는 main 안에 배치할 수 있다.
사용자의 입력 영역은 form으로 표현할 수 있다.
별도의 코드 미리보기는 aside로 표현할 수 있다.
이렇게 하면 스크린 리더 사용자가 페이지의 전체 구조를 더 쉽게 이해할 수 있다.
하지만 모든 메시지를 독립적인 Landmark로 만들어서는 안 된다.
예를 들어 대화 메시지가 100개 존재한다고 가정해 보자.
각 메시지를 모두 section Landmark로 지정하면 스크린 리더의 Landmark 목록이 지나치게 길어질 수 있다.
따라서 저자는 메시지에 article과 시각적으로 숨긴 제목을 사용하는 단순한 구조를 제안한다.
대화 목록의 키보드 Navigation
사용자는 이전 대화를 선택해야 할 수 있다.
하지만 마우스를 사용할 수 없는 사용자도 존재한다.
저자는 다음 키보드 동작을 제안한다.
| 키 | 동작 |
|---|---|
| 위쪽 화살표 | 이전 대화 |
| 아래쪽 화살표 | 다음 대화 |
| Home | 첫 번째 대화 |
| End | 마지막 대화 |
| 문자 입력 | 해당 문자로 시작하는 대화 제목 검색 |
이러한 구조는 사용자가 반복해서 Tab 키를 누르지 않고도 많은 대화 가운데 원하는 항목을 찾는 데 도움이 된다.
하지만 모든 대화 목록이 이러한 키보드 패턴을 반드시 사용해야 한다는 의미는 아니다.
실제 컴포넌트의 역할과 ARIA 패턴에 맞게 키보드 동작을 설계해야 한다.
Control + F6 Navigation
저자는 데스크톱 애플리케이션에서 사용하던 F6 기반 Navigation을 웹 AI 채팅 화면에도 적용하는 방법을 설명한다.
대표적인 동작은 다음과 같다.
Control + F6: 다음 주요 영역으로 이동Control + Shift + F6: 이전 주요 영역으로 이동
이러한 기능을 사용하면 사용자가 대화 목록과 입력창, 응답 영역 사이를 빠르게 이동할 수 있다.
다만 일반적인 브라우저에서는 F6 키 자체에 이미 다른 Navigation 기능이 존재할 수 있다.
따라서 웹에서는 Control 키를 조합한다.
반면 Electron과 같은 설치형 애플리케이션에서는 다른 키보드 규칙을 적용할 수 있다.
실제 구현에서의 한계
이번 글은 실제 AI 채팅 인터페이스의 접근성을 개선한 경험을 바탕으로 작성됐다.
하지만 모든 스크린 리더와 브라우저 조합에 대한 정량적인 성공률을 제공하는 Benchmark는 아니다.
또한 접근성은 ARIA 속성의 존재 여부만으로 평가할 수 없다.
실제 키보드 동작과 포커스 이동, 화면 확대, 보조 기술 사용 경험을 함께 검증해야 한다.
이번 글은 AI 채팅 UI를 구현할 때 실제 화면에 표시되는 콘텐츠와 스크린 리더에 전달되는 정보를 분리하고, 대화의 상태와 키보드 탐색 구조를 명확하게 설계하는 방법을 설명하는 자료다.
Modern CSS Might Make Your Front-End More Complex
- 저자: Gabriel Shoyombo
- 매체: Smashing Magazine
- 게시일: 2026-11-03
- 기술: CSS Container Queries, ResizeObserver, CSS Cascade, JavaScript, 프론트엔드 아키텍처
- 원문: https://www.smashingmagazine.com/2026/11/modern-css-might-make-your-front-end-more-complex/
최신 CSS 기능을 도입하면 JavaScript 코드를 줄일 수 있다.
예를 들어 기존에는 JavaScript로 화면의 크기와 상태를 계산해야 했던 작업을 CSS Container Query나 :has()로 처리할 수 있다.
하지만 저자는 이러한 변화가 항상 전체 애플리케이션의 복잡성을 줄이는 것은 아니라고 설명한다.
이번 글의 핵심은 개별 파일의 코드가 간단해지는 것과 전체 시스템에서 상태를 이해하고 변경하기 쉬워지는 것은 다른 문제라는 점이다.
JavaScript에서 CSS로 옮긴 반응형 카드
저자는 다음과 같은 카드 컴포넌트를 예로 든다.
카드는 자신의 너비가 480px보다 넓어지면 두 개의 열로 콘텐츠를 표시한다.
기존 구현에서는 JavaScript의 ResizeObserver를 사용했다.
ResizeObserver는 요소의 크기가 변경되는 것을 관찰할 수 있다.
카드의 너비가 480px을 초과하면 다음 Class를 추가한다.
card--wide
이후 CSS에서는 해당 Class를 기준으로 레이아웃을 변경한다.
예를 들어 다음 요소를 두 개의 열로 표시한다.
card__body
이러한 구조에서는 JavaScript가 카드의 크기를 확인한다.
그리고 CSS가 실제 스타일을 적용한다.
Container Query로 변경
개발자는 이 기능을 CSS Container Query로 변경할 수 있다.
대표적인 조건은 다음과 같다.
@container (width > 480px)
이렇게 하면 JavaScript의 ResizeObserver 없이도 카드의 실제 컨테이너 너비에 따라 레이아웃을 변경할 수 있다.
겉으로 보기에는 좋은 리팩터링이다.
JavaScript 코드가 줄어든다.
불필요한 Event Listener도 제거할 수 있다.
그리고 UI 상태를 CSS 안에서 처리할 수 있다.
하지만 실제 프로젝트에서는 일부 기존 코드가 남을 수 있다.
두 곳에서 같은 상태를 관리하는 문제
저자는 다음 상황을 제시한다.
개발자가 card__body의 Layout을 Container Query로 변경했다.
하지만 기존 ResizeObserver는 다른 파일에 등록돼 있었기 때문에 제거되지 않았다.
또한 다음 스타일도 남아 있었다.
card--wide .card__title
따라서 하나의 카드에 두 종류의 상태 판단이 존재하게 된다.
첫 번째는 CSS Container Query다.
두 번째는 JavaScript ResizeObserver가 추가하는 Class다.
처음에는 두 조건 모두 480px을 사용했다.
따라서 문제가 나타나지 않았다.
하지만 나중에 디자이너가 Layout의 Breakpoint를 600px로 변경하도록 요청한다.
개발자는 Container Query를 600px로 변경한다.
하지만 ResizeObserver는 여전히 480px을 사용한다.
이제 카드의 너비가 500px이라면 다음 상태가 발생한다.
- Container Query는 아직 좁은 Layout을 적용
- ResizeObserver는
card--wideClass를 추가 - 카드 제목은 넓은 Layout의 스타일을 적용
결과적으로 본문은 한 개의 열로 표시되지만 제목은 넓은 카드의 스타일을 사용한다.
이러한 상태는 원래 개발자가 의도하지 않았다.
문제는 동일한 UI 상태를 CSS와 JavaScript가 각각 관리하게 됐다는 사실이다.
코드 수 감소와 복잡성 감소의 차이
저자는 다음 두 개념을 구분한다.
| 구분 | 의미 |
|---|---|
| Local Simplicity | 하나의 파일 또는 기능이 얼마나 단순한가 |
| System Complexity | 전체 시스템의 관계와 동작을 이해하는 데 필요한 복잡성 |
예를 들어 JavaScript 50줄을 CSS 10줄로 변경했다고 가정해 보자.
개별 파일의 코드 수는 감소했다.
하지만 기존 JavaScript가 남아 있고 다른 CSS 규칙에도 영향을 준다면 전체 시스템을 이해하기는 오히려 더 어려워질 수 있다.
따라서 단순히 코드 줄 수가 감소했다는 사실만으로 리팩터링이 성공했다고 판단해서는 안 된다.
CSS :has()와 JavaScript 상태
비슷한 문제는 :has()에서도 발생할 수 있다.
예를 들어 사용자가 특정 입력 필드를 선택했다고 가정해 보자.
JavaScript에서는 다음 Class를 추가할 수 있다.
is-active
반면 CSS에서는 다음과 같은 조건을 사용해 상태를 계산할 수 있다.
:has(input:checked)
두 방식이 동시에 존재하면 문제가 발생할 수 있다.
JavaScript가 관리하는 is-active 상태와 실제 Input의 checked 상태가 서로 다를 수 있기 때문이다.
예를 들어 브라우저의 Back Navigation으로 Form 상태가 복원됐다고 가정해 보자.
실제 Input은 Checked 상태가 됐다.
하지만 JavaScript가 관리하는 Class는 이전 상태를 유지할 수 있다.
따라서 CSS와 JavaScript가 서로 다른 결과를 적용할 수 있다.
이러한 상황에서는 어떤 상태를 실제 진실의 원천으로 사용할지 결정해야 한다.
CSS가 상태를 소유할 때의 장점
저자는 최신 CSS 기능을 사용하지 말라고 주장하지 않는다.
오히려 적합한 경우에는 CSS가 더 단순한 해결책이 될 수 있다고 설명한다.
예를 들어 다음 조건은 CSS에서 직접 처리하는 편이 적합할 수 있다.
- 컨테이너의 너비
- Pointer의 Hover 상태
- 요소의 Focus 상태
- 사용자의 움직임 감소 설정
- 현재 DOM 안의 선택 상태
이러한 정보는 브라우저가 이미 알고 있다.
따라서 JavaScript에서 다시 계산하고 Class를 추가할 필요가 없을 수 있다.
하지만 실제 비즈니스 데이터나 서버 상태처럼 CSS가 직접 이해하지 못하는 정보까지 CSS로 관리하려고 하면 구조가 복잡해질 수 있다.
실제 마이그레이션 과정
저자는 기존 JavaScript 기반 동작을 새로운 CSS 기능으로 변경할 때 다음 관계를 확인해야 한다고 설명한다.
첫 번째는 기존 이벤트 처리 코드다.
예를 들어 ResizeObserver와 관련 Event Listener가 남아 있는지 확인해야 한다.
두 번째는 기존 Class다.
새로운 CSS 기능으로 상태를 판단한다면 기존에 상태를 표현하던 Class가 여전히 필요한지 확인해야 한다.
세 번째는 연결된 스타일이다.
특정 Class를 사용하는 CSS 규칙이 다른 파일에 남아 있을 수 있다.
네 번째는 브라우저 지원 범위다.
새로운 CSS 기능이 실제 지원 대상 브라우저에서 동작하는지 확인해야 한다.
다섯 번째는 Fallback이다.
기존 JavaScript를 Fallback으로 유지한다면 어떤 조건에서 실제로 실행되는지 명확하게 정의해야 한다.
코드의 책임 경계
이번 글에서 반복적으로 등장하는 주제는 책임 경계다.
예를 들어 다음 두 작업을 생각할 수 있다.
- CSS: 컨테이너 너비에 따라 Layout 결정
- JavaScript: 실제 사용자 데이터를 가져오고 상태 변경
이러한 구분은 비교적 명확하다.
반면 동일한 상태를 JavaScript와 CSS에서 독립적으로 계산하면 두 구현이 어긋날 가능성이 있다.
따라서 새로운 CSS 기능을 도입할 때는 기존 JavaScript 코드가 완전히 제거됐는지 확인해야 한다.
또는 두 구현이 서로 다른 역할을 수행한다는 사실을 명확하게 정의해야 한다.
적용 범위와 한계
이번 글은 구체적인 CSS 및 JavaScript 예제와 CodePen Demo를 제공한다.
하지만 새로운 CSS 기능으로 변경했을 때 실제 Bundle 크기나 CPU 사용량이 얼마나 감소하는지에 관한 독립적인 Benchmark를 제공하지 않는다.
또한 모든 CSS 기반 구현이 JavaScript 구현보다 복잡하다고 주장하지 않는다.
실제 복잡성은 기존 코드와 새로운 기능의 관계에 영향을 받는다.
이번 글은 최신 CSS 기능을 도입할 때 기존 JavaScript 코드의 일부만 제거하면 동일한 UI 상태를 두 곳에서 관리하게 될 수 있으며, 이로 인해 전체 시스템의 유지보수 비용이 증가할 수 있다는 점을 실제 코드 예제로 설명한다.
Rendering 50,000 Gantt rows in Vue without a virtual list library
- 저자: Timur Kutumbayev
- 매체: DEV Community
- 게시일: 2026-11-05
- 기술: Vue 3, TypeScript, Virtualization, Computed Stability, CSS Sticky, 시간대 처리
- 원문: https://dev.to/ktimur91/rendering-50000-gantt-rows-in-vue-without-a-virtual-list-library-4f15
- 프로젝트: https://github.com/ktimur91/ganttkit
Vue 3로 5만 개의 행을 가진 Gantt Chart를 구현하면서 별도의 Virtual List 라이브러리를 사용하지 않고 스크롤 성능을 관리한 방법을 설명한 기술 글이다.
저자는 실제 관리자 애플리케이션에서 Gantt Chart가 필요했다.
하지만 기존 라이브러리는 자신의 프로젝트 구성에 적합하지 않았다.
일부는 별도의 UI 라이브러리를 함께 요구했다.
다른 라이브러리는 특정 기능을 유료로 제공했다.
또한 기존 shadcn-vue와 Tailwind CSS에 맞춰 수정하기 어려운 경우도 있었다.
따라서 저자는 ganttkit이라는 프로젝트를 만들었다.
이번 글에서는 실제 구현의 성능과 관련된 다섯 가지 설계 선택을 설명한다.
Gantt Chart의 기본 구조
Gantt Chart는 작업과 시간을 함께 표시하는 UI다.
예를 들어 다음 정보가 존재한다.
- 작업 이름
- 작업 시작일
- 작업 종료일
- 담당자
- 작업 상태
- 작업 사이의 의존 관계
화면 왼쪽에는 작업 목록이 존재한다.
오른쪽에는 날짜별 Timeline이 존재한다.
사용자는 세로로 스크롤해 많은 작업을 확인할 수 있다.
또한 가로로 스크롤해 다른 날짜를 확인할 수 있다.
이러한 구조에서는 두 종류의 위치가 중요하다.
- 세로 스크롤 위치
- 가로 스크롤 위치
특히 작업 이름과 Timeline Bar는 항상 같은 행에 있어야 한다.
하지만 일반적인 구현에서는 이 부분에서 문제가 발생할 수 있다.
첫 번째 설계: 하나의 Scroll Container
기존 Gantt Chart 구현에서는 왼쪽 작업 목록과 오른쪽 Timeline을 서로 다른 Scroll Container에 배치하는 방식이 사용될 수 있다.
예를 들어 왼쪽 영역을 스크롤하면 JavaScript에서 해당 값을 읽는다.
이후 오른쪽 영역의 scrollTop을 같은 값으로 설정한다.
하지만 이러한 구조에서는 두 영역의 스크롤 위치가 완전히 같은 시점에 변경되지 않을 수 있다.
특히 Trackpad의 Momentum Scrolling에서는 작은 지연이 눈에 띌 수 있다.
저자는 이를 해결하기 위해 하나의 Scroll Container만 사용했다.
그리고 왼쪽 작업 목록과 상단 Timeline Header에는 다음 CSS를 사용했다.
position: sticky
이렇게 하면 실제 스크롤은 브라우저가 하나의 Container에서 처리한다.
따라서 JavaScript에서 두 Scroll Container의 위치를 계속 동기화할 필요가 없다.
이러한 구조는 Scroll Event 처리 코드도 줄인다.
두 번째 설계: 화면에 보이는 행만 렌더링
5만 개의 행을 모두 DOM으로 생성하면 많은 리소스가 필요할 수 있다.
예를 들어 각 행이 다음 요소를 포함한다고 가정해 보자.
- 작업 이름
- 시작일
- 종료일
- Timeline Bar
- Tooltip
- 선택 상태
행 하나에 DOM Node가 10개만 존재하더라도 전체 5만 행에는 매우 많은 DOM Node가 생성된다.
이러한 구조는 브라우저의 Layout과 Style 계산, 메모리 사용량에 영향을 줄 수 있다.
저자는 이를 해결하기 위해 Virtualization을 사용했다.
Virtualization은 실제로 화면에 필요한 항목만 DOM에 생성하는 방식이다.
예를 들어 현재 화면에 20개 행만 보인다고 가정해 보자.
이 경우 전체 5만 개의 데이터는 메모리에 유지한다.
하지만 DOM에는 실제 화면에 표시할 행과 주변의 일부 행만 생성한다.
이렇게 하면 전체 데이터 크기와 실제 DOM의 크기를 분리할 수 있다.
일반적인 Virtualization의 문제
기본적인 Virtualization에서는 Scroll Position을 기준으로 현재 보이는 행을 계산한다.
예를 들어 행의 높이가 40px이라고 가정해 보자.
현재 Scroll Position이 400px라면 대략 열 번째 행부터 화면에 표시될 수 있다.
이러한 계산은 비교적 간단하다.
문제는 스크롤 이벤트가 매우 자주 발생한다는 점이다.
사용자가 천천히 스크롤하더라도 Scroll Position은 여러 번 변경된다.
그때마다 Vue의 Computed Value가 새로운 행 목록을 반환하면 불필요한 렌더링 작업이 발생할 수 있다.
저자는 이를 해결하기 위해 화면에 표시할 행을 Chunk 단위로 관리했다.
Chunk 기반 Virtualization
저자는 행을 10개씩 묶었다.
예를 들어 다음과 같은 Chunk가 존재한다.
- Chunk 0: 0~9행
- Chunk 1: 10~19행
- Chunk 2: 20~29행
사용자가 몇 Pixel 정도 스크롤하더라도 현재 필요한 Chunk가 변경되지 않았다면 실제 렌더링 대상은 유지할 수 있다.
반면 새로운 Chunk가 화면에 들어오면 해당 Chunk를 추가한다.
이때 현재 화면의 위쪽과 아래쪽에 여분의 Chunk도 유지한다.
따라서 사용자가 빠르게 스크롤하더라도 즉시 필요한 행이 준비돼 있을 가능성을 높인다.
이러한 방식에서는 대부분의 Scroll Event가 새로운 행 배열을 생성하지 않아도 된다.
Vue의 Computed Stability
저자는 Vue 3.4부터 사용할 수 있는 Computed의 이전 값 전달 기능을 활용했다.
일반적인 Computed에서는 계산 결과가 변경되면 해당 값을 사용하는 다른 Computed나 Component에도 영향을 줄 수 있다.
하지만 실제로 같은 범위를 표현하는 새로운 객체를 생성했다면 값의 의미는 같더라도 객체 참조는 달라질 수 있다.
예를 들어 다음 두 객체를 생각할 수 있다.
{ start: 10, end: 30 }
{ start: 10, end: 30 }
두 객체는 같은 정보를 표현한다.
하지만 JavaScript에서 서로 다른 객체다.
따라서 단순한 참조 비교에서는 서로 다르다고 판단된다.
저자는 새로운 계산 결과와 이전 값을 비교했다.
실제로 같은 범위라면 이전 객체를 그대로 반환했다.
이렇게 하면 Vue가 관련 Computed를 불필요하게 다시 계산하는 상황을 줄일 수 있다.
동일한 Bar 객체 재사용
비슷한 문제는 Timeline Bar에서도 발생했다.
예를 들어 화면에 100개의 Bar가 존재한다고 가정해 보자.
스크롤 위치가 변경됐지만 실제 Bar의 위치와 길이는 달라지지 않았다.
그런데 계산 함수가 매번 새로운 Bar 객체를 생성한다면 Vue는 관련 Props가 변경됐다고 판단할 수 있다.
저자는 이전에 생성한 Bar를 ID 기준으로 저장했다.
새롭게 계산한 Bar와 이전 Bar의 실제 필드가 같다면 기존 객체를 재사용했다.
이렇게 하면 컴포넌트에 전달하는 객체 참조를 유지할 수 있다.
저자가 공개한 자체 측정에서는 1만 명을 표시하는 Resource View의 Scroll Step이 다음과 같이 개선됐다.
- 변경 전: 11.1ms
- 변경 후: 8.6ms
하지만 이 수치는 저자의 특정 머신과 구현에서 측정한 결과다.
모든 Vue 애플리케이션에서 같은 개선이 발생한다는 의미는 아니다.
실제 Benchmark
저자는 Headless Chromium에서 실제 Gantt Chart를 렌더링했다.
테스트 환경은 다음과 같다.
- Apple M1 Pro
- Headless Chromium
- 300번의 Scroll Step
- Step마다 3개 행 이동
결과는 다음과 같았다.
| 전체 행 수 | DOM Node 수 | 중앙값 | P95 |
|---|---|---|---|
| 1,000 | 891 | 0.2ms | 7.4ms |
| 10,000 | 891 | 0.2ms | 6.9ms |
| 50,000 | 891 | 0.2ms | 6.8ms |
이 결과에서 중요한 부분은 전체 데이터가 50배 증가해도 실제 DOM Node 수가 증가하지 않았다는 점이다.
또한 대부분의 Scroll Step은 기존 Chunk 안에서 처리됐다.
따라서 실제로 새로운 행을 렌더링하지 않아도 됐다.
하지만 P95에는 새로운 Chunk로 이동해 일부 DOM을 갱신하는 작업이 포함될 수 있다.
즉 중앙값 0.2ms가 모든 Scroll Event의 처리 시간이라는 의미는 아니다.
또한 이번 수치는 저자가 직접 수행한 Benchmark다.
독립적인 제3자가 다른 UI 라이브러리와 같은 조건에서 비교한 결과는 아니다.
높이가 다른 행의 처리
저자는 Resource View에서 높이가 서로 다른 행도 지원한다.
예를 들어 한 사람의 예약이 여러 개 겹쳐 있다고 가정해 보자.
같은 행 안에서 겹친 예약을 여러 Lane으로 표시할 수 있다.
그러면 해당 행의 높이가 증가한다.
이 경우 모든 행의 높이가 같다고 가정할 수 없다.
따라서 단순히 다음 수식으로 현재 행을 계산하기 어렵다.
scrollTop / rowHeight
저자는 이를 해결하기 위해 각 행의 시작 위치를 누적 합으로 계산했다.
그리고 해당 정보를 Float64Array에 저장했다.
실제 Scroll Position에 해당하는 행은 Binary Search로 찾는다.
이렇게 하면 행 높이가 서로 다른 상황에서도 Virtualization을 사용할 수 있다.
날짜 계산과 Timezone
Gantt Chart에서는 날짜와 실제 시각을 구분해야 한다.
예를 들어 작업을 하루 뒤로 이동한다고 가정해 보자.
단순히 다음 값을 더할 수 있다.
86_400_000
이는 24시간의 Millisecond 수다.
하지만 모든 지역에서 달력의 하루가 항상 24시간인 것은 아니다.
Daylight Saving Time이 변경되는 시점에는 하루가 23시간이나 25시간일 수 있다.
따라서 하루 뒤의 날짜를 계산할 때 단순히 24시간을 더하면 잘못된 시각이 생성될 수 있다.
저자는 이를 해결하기 위해 다음 라이브러리를 사용했다.
@internationalized/date
실제 Gantt Chart의 Timezone을 기준으로 날짜를 계산한다.
그리고 날짜 단위의 이동과 실제 시간 단위의 이동을 구분한다.
이러한 방식은 여러 국가의 사용자가 같은 프로젝트 일정을 관리하는 경우 중요하다.
입력 데이터의 형식 유지
저자는 데이터의 원래 형식도 유지하도록 했다.
예를 들어 API에서 다음 날짜를 전달했다고 가정해 보자.
2026-03-02
사용자가 해당 작업을 다른 날짜로 이동한다.
그러면 변경 이벤트에서도 같은 형식의 날짜 문자열을 반환한다.
반면 원래 값이 JavaScript의 Date 객체였다면 변경 이후에도 Date 객체를 반환할 수 있다.
이렇게 하면 UI 라이브러리가 사용하는 날짜 형식 때문에 애플리케이션의 API 데이터를 다시 변환해야 하는 부담을 줄일 수 있다.
Canvas 대신 DOM을 선택한 이유
저자는 Canvas 기반 구현도 검토했다.
Canvas는 많은 그래픽 요소를 직접 그릴 수 있다.
따라서 복잡한 Chart에서 성능상의 장점이 존재할 수 있다.
하지만 저자는 DOM을 선택했다.
이유는 다음과 같다.
- 기존 Tailwind CSS 사용
- shadcn-vue 컴포넌트와 통합
- Slot을 이용한 사용자 정의 UI
- Focus 관리
- Screen Reader 접근성
Canvas를 사용하면 이러한 기능을 별도로 구현해야 할 수 있다.
저자는 Chunk 기반 Virtualization으로 충분한 성능을 얻을 수 있다고 판단했다.
따라서 최대 처리량만을 기준으로 Canvas를 선택하지 않았다.
구현의 한계
이번 글은 실제 프로젝트의 구조와 성능 측정 코드를 공개한다.
하지만 테스트 결과는 Headless Chromium과 M1 Pro에서 측정한 것이다.
다음 환경에서는 결과가 달라질 수 있다.
- 저사양 모바일 기기
- 실제 브라우저의 여러 확장 프로그램
- 복잡한 Custom Cell
- 많은 Tooltip과 Popup
- 높은 빈도의 데이터 변경
- 실제 사용자가 빠르게 Scroll하는 상황
또한 DOM Node 수를 줄였다는 사실만으로 모든 Gantt Chart 기능의 실행 비용이 줄어드는 것은 아니다.
예를 들어 전체 5만 개의 작업에 대한 데이터 정렬과 의존성 계산은 별도의 비용을 발생시킬 수 있다.
이번 글은 Vue에서 대량의 데이터를 표시할 때 Virtualization 자체뿐 아니라 Computed의 객체 참조와 렌더링 대상의 변경 빈도를 관리하는 것이 중요하다는 점을 실제 코드와 Benchmark로 설명한다.