---
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 |
