f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
222 lines
6.8 KiB
Markdown
222 lines
6.8 KiB
Markdown
---
|
|
id: wiki-2026-0508-반응형-윈도우-리사이즈-resize-이벤트-처리
|
|
title: 반응형 윈도우 리사이즈(Resize) 이벤트 처리
|
|
category: 10_Wiki/Topics
|
|
status: verified
|
|
canonical_id: self
|
|
aliases: [Resize Event, ResizeObserver, window resize, 윈도우 리사이즈]
|
|
duplicate_of: none
|
|
source_trust_level: A
|
|
confidence_score: 0.9
|
|
verification_status: applied
|
|
tags: [frontend, browser-events, performance, react]
|
|
raw_sources: []
|
|
last_reinforced: 2026-05-10
|
|
github_commit: pending
|
|
tech_stack:
|
|
language: typescript
|
|
framework: react
|
|
---
|
|
|
|
# 반응형 윈도우 리사이즈(Resize) 이벤트 처리
|
|
|
|
## 매 한 줄
|
|
> **"매 resize 의 fire 매 cheap 매 listener 매 expensive — debounce 또는 ResizeObserver"**. window resize 매 60fps+ 의 fire — 매 naive listener 매 layout thrash. 매 modern (2026) 의 ResizeObserver 의 element-level + native browser-throttled API.
|
|
|
|
## 매 핵심
|
|
|
|
### 매 두 종류 의 API
|
|
- **`window.addEventListener('resize', ...)`** — viewport-level, fires 매 every pixel change.
|
|
- **`ResizeObserver`** — element-level, browser-throttled, observes contentBox/borderBox.
|
|
|
|
### 매 throttle vs debounce
|
|
- **Debounce** — fire only N ms 후 last event (e.g., resize stops).
|
|
- **Throttle** — fire at most every N ms (continuous fire).
|
|
- **Resize**: 보통 debounce (final size 만 needed) 또는 throttle (live preview).
|
|
|
|
### 매 응용
|
|
1. **Responsive breakpoint** — JS-driven layout.
|
|
2. **Canvas/Chart resize** — re-draw on size change.
|
|
3. **Virtualized list** — recalculate row count.
|
|
4. **Modal positioning** — re-center.
|
|
|
|
## 💻 패턴
|
|
|
|
### 1. ResizeObserver (modern, preferred)
|
|
```typescript
|
|
const ro = new ResizeObserver((entries) => {
|
|
for (const entry of entries) {
|
|
const { width, height } = entry.contentRect;
|
|
console.log('resized:', width, height);
|
|
}
|
|
});
|
|
|
|
const el = document.querySelector('.box')!;
|
|
ro.observe(el);
|
|
// cleanup
|
|
// ro.unobserve(el); ro.disconnect();
|
|
```
|
|
|
|
### 2. React hook: useResizeObserver
|
|
```typescript
|
|
import { useEffect, useState, useRef } from 'react';
|
|
|
|
export function useResizeObserver<T extends HTMLElement>() {
|
|
const ref = useRef<T>(null);
|
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
|
|
useEffect(() => {
|
|
if (!ref.current) return;
|
|
const ro = new ResizeObserver(([entry]) => {
|
|
const { width, height } = entry.contentRect;
|
|
setSize({ width, height });
|
|
});
|
|
ro.observe(ref.current);
|
|
return () => ro.disconnect();
|
|
}, []);
|
|
|
|
return [ref, size] as const;
|
|
}
|
|
|
|
// usage
|
|
function Card() {
|
|
const [ref, { width }] = useResizeObserver<HTMLDivElement>();
|
|
return <div ref={ref}>{width < 400 ? <Compact /> : <Full />}</div>;
|
|
}
|
|
```
|
|
|
|
### 3. Window resize with debounce
|
|
```typescript
|
|
function debounce<T extends (...args: any[]) => any>(fn: T, ms = 200): T {
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
return ((...args: Parameters<T>) => {
|
|
if (timer) clearTimeout(timer);
|
|
timer = setTimeout(() => fn(...args), ms);
|
|
}) as T;
|
|
}
|
|
|
|
const onResize = debounce(() => {
|
|
console.log('done resizing:', window.innerWidth);
|
|
}, 200);
|
|
|
|
window.addEventListener('resize', onResize);
|
|
```
|
|
|
|
### 4. React useWindowSize hook
|
|
```typescript
|
|
import { useEffect, useState } from 'react';
|
|
|
|
export function useWindowSize() {
|
|
const [size, setSize] = useState({
|
|
width: typeof window === 'undefined' ? 0 : window.innerWidth,
|
|
height: typeof window === 'undefined' ? 0 : window.innerHeight,
|
|
});
|
|
|
|
useEffect(() => {
|
|
let raf = 0;
|
|
const onResize = () => {
|
|
cancelAnimationFrame(raf);
|
|
raf = requestAnimationFrame(() => {
|
|
setSize({ width: window.innerWidth, height: window.innerHeight });
|
|
});
|
|
};
|
|
window.addEventListener('resize', onResize, { passive: true });
|
|
return () => {
|
|
window.removeEventListener('resize', onResize);
|
|
cancelAnimationFrame(raf);
|
|
};
|
|
}, []);
|
|
|
|
return size;
|
|
}
|
|
```
|
|
|
|
### 5. Throttle with rAF
|
|
```typescript
|
|
function rafThrottle<T extends (...args: any[]) => any>(fn: T): T {
|
|
let raf = 0;
|
|
let lastArgs: Parameters<T>;
|
|
return ((...args: Parameters<T>) => {
|
|
lastArgs = args;
|
|
if (raf) return;
|
|
raf = requestAnimationFrame(() => {
|
|
fn(...lastArgs);
|
|
raf = 0;
|
|
});
|
|
}) as T;
|
|
}
|
|
|
|
window.addEventListener('resize', rafThrottle(() => {
|
|
// runs at most once per frame
|
|
redrawCanvas();
|
|
}));
|
|
```
|
|
|
|
### 6. matchMedia (breakpoint detection)
|
|
```typescript
|
|
const mql = window.matchMedia('(min-width: 768px)');
|
|
|
|
function onChange(e: MediaQueryListEvent) {
|
|
console.log('is desktop:', e.matches);
|
|
}
|
|
mql.addEventListener('change', onChange);
|
|
console.log('initial desktop:', mql.matches);
|
|
```
|
|
|
|
### 7. Canvas resize (HiDPI handling)
|
|
```typescript
|
|
function fitCanvasToDPR(canvas: HTMLCanvasElement) {
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const rect = canvas.getBoundingClientRect();
|
|
canvas.width = rect.width * dpr;
|
|
canvas.height = rect.height * dpr;
|
|
const ctx = canvas.getContext('2d')!;
|
|
ctx.scale(dpr, dpr);
|
|
return ctx;
|
|
}
|
|
|
|
const ro = new ResizeObserver(([entry]) => {
|
|
const canvas = entry.target as HTMLCanvasElement;
|
|
fitCanvasToDPR(canvas);
|
|
draw(canvas.getContext('2d')!);
|
|
});
|
|
ro.observe(canvas);
|
|
```
|
|
|
|
## 매 결정 기준
|
|
| 상황 | Approach |
|
|
|---|---|
|
|
| Element-level size watch | `ResizeObserver` (browser-throttled, no manual debounce). |
|
|
| Window-level (viewport) | `window.addEventListener('resize')` + debounce. |
|
|
| Smooth visual feedback during drag | `requestAnimationFrame` throttle. |
|
|
| Breakpoint change only | `matchMedia` (fires on threshold cross only). |
|
|
| Canvas / chart redraw | `ResizeObserver` + DPR scaling. |
|
|
|
|
**기본값**: `ResizeObserver` 의 element-level. `matchMedia` 의 breakpoint. window resize 의 only viewport-wide concerns + debounce 200ms.
|
|
|
|
## 🔗 Graph
|
|
- 변형: [[ResizeObserver]] · [[IntersectionObserver]]
|
|
- 응용: [[가상 스크롤]]
|
|
- Adjacent: [[debounce / throttle]] · [[requestAnimationFrame]] · [[컨테이너 쿼리 (Container Queries)|Container Queries]]
|
|
|
|
## 🤖 LLM 활용
|
|
**언제**: dynamic layout, canvas/chart, virtualized lists, modal repositioning, JS-driven breakpoint.
|
|
**언제 X**: pure CSS responsive 매 sufficient — JS resize handler 매 unneeded.
|
|
|
|
## ❌ 안티패턴
|
|
- **No debounce/throttle on window.resize**: 60+ fires/sec 매 layout thrash.
|
|
- **`offsetWidth` in resize handler without rAF**: forced sync layout.
|
|
- **Forgetting cleanup**: memory leak (especially in React without `removeEventListener`).
|
|
- **Polling `window.innerWidth`**: use event-driven approach.
|
|
- **Heavy work synchronously**: canvas full redraw 매 each event — defer/batch.
|
|
|
|
## 🧪 검증 / 중복
|
|
- Verified (MDN ResizeObserver 2026, web.dev resize patterns, React hooks docs).
|
|
- 신뢰도 A.
|
|
|
|
## 🕓 Changelog
|
|
| 날짜 | 변경 |
|
|
|---|---|
|
|
| 2026-05-08 | Phase 1 |
|
|
| 2026-05-10 | Manual cleanup — full content with ResizeObserver/debounce/rAF/matchMedia patterns |
|