Files
2nd/10_Wiki/Topics/Frontend/반응형 윈도우 리사이즈(Resize) 이벤트 처리.md
T
2026-05-10 22:08:15 +09:00

223 lines
6.9 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
- 부모: [[브라우저 이벤트 모델]] · [[반응형 디자인 (Responsive Design)]]
- 변형: [[ResizeObserver]] · [[IntersectionObserver]] · [[matchMedia]]
- 응용: [[Canvas 렌더링]] · [[가상 스크롤]] · [[차트 라이브러리]]
- Adjacent: [[debounce / throttle]] · [[requestAnimationFrame]] · [[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 |