Next.js 블로그 만들기 — 스크롤 프로그레스 바와 Canvas 렌더링 이슈 해결
스크롤 프로그레스 바
블로그 글이 길어지면 "지금 어디쯤 읽고 있는 거지?"라는 생각이 든다. 브라우저 기본 스크롤바로도 알 수 있지만, 화면 상단에 얇은 프로그레스 바가 있으면 직관적이다.
구현
생각보다 간단하다. 전체 문서 높이 대비 현재 스크롤 위치의 비율을 계산하면 된다.
'use client'
import { useEffect, useState } from 'react'
export default function ScrollProgressBar() {
const [progress, setProgress] = useState(0)
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY
const docHeight =
document.documentElement.scrollHeight - window.innerHeight
if (docHeight <= 0) {
setProgress(0)
return
}
setProgress(Math.min((scrollTop / docHeight) * 100, 100))
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [])
return (
<div className="fixed top-14 right-0 left-0 z-50
h-0.5 bg-transparent">
<div
className="h-full bg-text transition-[width]
duration-75 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
)
}핵심 포인트:
document.documentElement.scrollHeight - window.innerHeight— 실제 스크롤 가능한 거리{ passive: true }— 스크롤 이벤트 성능 최적화. 브라우저에게preventDefault()를 호출하지 않겠다고 알려줘서 스크롤을 차단하지 않음top-14— navbar(h-14) 바로 아래에 위치bg-text— 라이트 모드에서는 검은색, 다크 모드에서는 밝은 색으로 테마에 자동 대응transition-[width] duration-75— 너무 부드러우면 느려 보이고, 없으면 뚝뚝 끊겨 보인다. 75ms가 적당
레이아웃에 적용
루트 레이아웃의 Navbar 바로 아래에 배치한다:
<Navbar />
<ScrollProgressBar />
<main className="mt-14 flex-1">{children}</main>Canvas 커서 효과와 GNB 깜빡임
프로그레스 바를 추가하고 나서 테스트하는데, 마우스를 움직일 때마다 GNB가 깜빡거리는 현상을 발견했다. 프로그레스 바와는 무관하고, 이전에 추가한 커서 트레일 효과가 원인이었다.
원인 분석
문제의 구조를 정리하면 이렇다:
z-index 레이어 구조 (수정 전)
─────────────────────────
z-9999 Canvas (커서 효과) ← 매 프레임 clearRect + 다시 그리기
z-50 Navbar (backdrop-blur) ← blur 대상이 계속 바뀜
z-40 ScrollProgressBar
본문 콘텐츠
Canvas가 fixed inset-0 z-[9999]로 화면 전체를 덮고 있었다. requestAnimationFrame으로 매 프레임(약 16ms마다) clearRect → 원 그리기를 반복한다.
여기서 문제가 생긴다. Navbar의 backdrop-blur-md는 뒤에 있는 레이어의 픽셀을 블러 처리하는 CSS 필터다. Canvas가 navbar 위에서 매 프레임 다시 그려지면, 브라우저는 그때마다 backdrop-blur를 재계산해야 한다.
결과적으로:
- Canvas가
clearRect로 지워짐 → backdrop-blur 재계산 - Canvas에 원이 그려짐 → backdrop-blur 또 재계산
- 이게 초당 60번 반복 → GNB가 깜빡거림
해결
두 가지를 수정했다:
1. Canvas를 navbar 아래로 이동
// Before
<canvas className="pointer-events-none fixed inset-0 z-[9999]" />
// After
<canvas className="pointer-events-none fixed top-14 right-0 bottom-0 left-0 z-40" />inset-0→top-14: navbar 영역(56px)을 제외z-[9999]→z-40: navbar(z-50)보다 아래로
이렇게 하면 Canvas가 navbar와 겹치지 않으므로 backdrop-blur 재계산이 발생하지 않는다.
2. 마우스 좌표 보정
Canvas의 시작점이 바뀌었으니, 마우스 Y좌표도 navbar 높이만큼 빼줘야 한다:
const NAV_HEIGHT = 56
const handleResize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight - NAV_HEIGHT
}
const handleMouseMove = (e: MouseEvent) => {
mouseX = e.clientX
mouseY = e.clientY - NAV_HEIGHT // navbar 높이만큼 보정
}수정 후 레이어 구조
z-index 레이어 구조 (수정 후)
─────────────────────────
z-50 Navbar (backdrop-blur) ← Canvas와 겹치지 않음
z-50 ScrollProgressBar
z-40 Canvas (커서 효과) ← navbar 아래 영역만 커버
본문 콘텐츠
Canvas가 navbar 영역을 침범하지 않으므로, backdrop-blur가 매 프레임 재계산될 이유가 없다. 깜빡임 해결.
교훈
이번 이슈에서 배운 점:
backdrop-blur는 비싼 연산이다 — 뒤에 있는 레이어가 자주 바뀌면 매번 재계산된다. 가능하면 blur 뒤에서 애니메이션을 돌리지 않는 게 좋다.- z-index는 신중하게 — "일단 9999"로 올려놓으면 의도치 않은 레이어 충돌이 생긴다. 필요한 만큼만 사용하자.
- Canvas와 CSS 필터의 조합은 주의 — 둘 다 GPU를 사용하지만, 상호작용 방식에 따라 성능 문제가 발생할 수 있다.
사소해 보이는 깜빡임이지만, 사용자 경험에 직접적인 영향을 주는 문제였다. 레이어 구조를 정리하는 것만으로 깔끔하게 해결된 케이스.
관심 있을 만한 포스트
Next.js 블로그 만들기 — TOC와 커서 효과로 디테일 살리기
IntersectionObserver 기반 TOC(Table of Contents)와 Canvas 커서 트레일 효과 구현기. 스크롤 하이라이팅, fixed 레이아웃 처리까지.
Next.js 블로그 만들기 — 정적 블로그에 맞춤 추천 포스트 기능 추가
localStorage에 조회 이력을 저장하고, 태그 가중치 스코어링으로 정적 블로그에서도 개인화 추천을 구현하는 방법.
Next.js 블로그 만들기 — 정적 블로그에 검색 기능 추가
빌드 타임 검색 인덱스 생성과 클라이언트 사이드 필터링으로 정적 블로그에 검색 기능을 구현하기. Cmd+K 단축키, 오버레이 UI까지.
Bun vs Node.js vs Deno — 뭐가 다른지, 그래서 뭘 쓰면 좋은지 (2026 기준)
런타임 3대장 비교: 호환성(Node), 속도/번들(Bun), 올인원/보안(Deno). 팀/프로덕트 상황별 선택 기준과 체크리스트까지 정리.
번들러(Bundle)란 뭐고, 왜 필요할까? — 요즘 번들러/빌드 툴 비교 가이드
번들러의 역할(모듈/의존성/트랜스파일/최적화)을 쉽게 설명하고, Vite·Rollup·esbuild·Webpack·Rspack·Turbopack 같은 도구를 상황별로 비교합니다.
Next.js 블로그 만들기 — 검색엔진 등록 4종 완전 정복
Google Search Console, Bing Webmaster Tools, 네이버 서치어드바이저, 다음 검색등록까지. Next.js 블로그를 검색엔진에 노출시키는 전체 과정을 정리했다.
Next.js 블로그 만들기 — GitHub Pages에서 Cloudflare Pages로 이전하기
GitHub Pages의 한계를 넘어 Cloudflare Pages로 블로그를 이전한 과정. 비교, 설정, SEO까지 한 번에 정리.
Claude Code 에이전트 팀 — 여러 AI가 협업하는 새로운 방식
Claude Code의 실험적 기능인 에이전트 팀을 정리했다. 여러 Claude 인스턴스가 리더-팀원 구조로 작업을 분담하고, 서로 메시지를 주고받으며 병렬로 협업하는 구조다.