4.1 Deduplicate Global Event Listeners
- 핵심: 전역 이벤트 리스너를 훅 인스턴스 간 공유한다.
- 왜 중요한가: 리스너 폭증으로 인한 메모리/CPU 낭비를 막는다.
- 코드리뷰 신호: 같은 훅 사용 수만큼 window 리스너가 늘어난다.
같은 훅 사용 수만큼 window 리스너가 늘어난다 코드는 보통 구현 편의 때문에 생기지만, 트래픽이 늘면 병목으로 확대된다. 이 항목의 핵심은 문법 변경이 아니라 실행/렌더 흐름을 정리해 전역 이벤트 리스너를 훅 인스턴스 간 공유한다를 기본값으로 만드는 데 있다.
변경 뒤에는 요청 중복 여부, stale 데이터 노출 시간, 재시도/취소 동작와 회귀 테스트를 같이 보면서 리스너 폭증으로 인한 메모리/CPU 낭비를 막는다 결과가 재현되는지 점검한다.
예시 (Bad) ▼
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [key, callback])
}
예시 (Good) ▼
import useSWRSubscription from 'swr/subscription'
// 키별 콜백을 추적하는 모듈 레벨 맵
const keyCallbacks = new Map<string, Set<() => void>>()
function useKeyboardShortcut(key: string, callback: () => void) {
// 이 콜백을 맵에 등록
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set())
}
keyCallbacks.get(key)!.add(callback)
return () => {
const set = keyCallbacks.get(key)
if (set) {
set.delete(callback)
if (set.size === 0) {
keyCallbacks.delete(key)
}
}
}
}, [key, callback])
useSWRSubscription('global-keydown', () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
}
function Profile() {
// 여러 단축키가 동일 리스너를 공유
useKeyboardShortcut('p', () => { /* ... */ })
useKeyboardShortcut('k', () => { /* ... */ })
// ...
}
4.2 Use Passive Event Listeners for Scrolling Performance
- 핵심: scroll/touch 계열 이벤트는 passive 옵션을 기본 적용한다.
- 왜 중요한가: 메인 스레드 블로킹을 줄여 스크롤 성능을 개선한다.
- 코드리뷰 신호: 스크롤/터치 이벤트에 passive 설정이 없다.
코드리뷰에서 스크롤/터치 이벤트에 passive 설정이 없다. 신호가 보이면 먼저 책임 경계를 다시 나누는 것이 우선이다.
책임이 정리되면 요청 상태와 화면 상태를 분리하고 dedup, 취소, 재검증 정책을 명확히 둔다. 이 원칙을 자연스럽게 적용할 수 있고, scroll/touch 계열 이벤트는 passive 옵션을 기본 적용한다. 규칙도 팀 기준으로 고정하기 쉬워진다.
마지막으로 요청 중복 여부, stale 데이터 노출 시간, 재시도/취소 동작를 기준으로 메인 스레드 블로킹을 줄여 스크롤 성능을 개선한다 개선이 체감과 일치하는지 확인한다.
예시 (Bad) ▼
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
document.addEventListener('touchstart', handleTouch)
document.addEventListener('wheel', handleWheel)
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
예시 (Good) ▼
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
document.addEventListener('touchstart', handleTouch, { passive: true })
document.addEventListener('wheel', handleWheel, { passive: true })
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
4.3 Use SWR for Automatic Deduplication
- 핵심: 동일 키 요청은 SWR의 dedup/revalidate 전략을 사용한다.
- 왜 중요한가: 중복 네트워크 요청을 줄이고 데이터 일관성을 높인다.
- 코드리뷰 신호: 같은 endpoint를 여러 컴포넌트가 각각 useEffect(fetch)로 호출한다.
같은 endpoint를 여러 컴포넌트가 각각 useEffect(fetch)로 호출한다 징후는 기능을 빠르게 추가할 때 쉽게 누적된다. 먼저 요청 상태와 화면 상태를 분리하고 dedup, 취소, 재검증 정책을 명확히 둔다. 이렇게 구조를 잡아두면 동일 키 요청은 SWR의 dedup/revalidate 전략을 사용한다 원칙을 예외 없이 유지하기가 훨씬 쉬워진다.
적용 후에는 요청 중복 여부, stale 데이터 노출 시간, 재시도/취소 동작를 확인해 중복 네트워크 요청을 줄이고 데이터 일관성을 높인다 효과가 실제로 나타나는지 검증한다.
예시 (Bad)
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
}
예시 (Good)
import useSWR from 'swr'
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
4.4 Version and Minimize localStorage Data
- 핵심: localStorage 데이터는 스키마 버전과 최소 필드로 관리한다.
- 왜 중요한가: 마이그레이션 실패와 파싱 비용을 줄일 수 있다.
- 코드리뷰 신호: 버전 없는 대형 객체를 그대로 저장한다.
실무에서 버전 없는 대형 객체를 그대로 저장한다 부분만 단발성으로 고치면 같은 문제가 다시 나오기 쉽다. 작은 단위(한 페이지, 한 훅, 한 API)부터 localStorage 데이터는 스키마 버전과 최소 필드로 관리한다 패턴을 적용하고, 경계 케이스를 함께 정리해야 회귀를 줄일 수 있다.
이후 요청 중복 여부, stale 데이터 노출 시간, 재시도/취소 동작를 배포 전후로 비교해 마이그레이션 실패와 파싱 비용을 줄일 수 있다 개선이 지속되는지 확인한다.
예시 (Bad) ▼
// 버전 없음, 모든 데이터 저장, 에러 처리 없음
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
const data = localStorage.getItem('userConfig')
예시 (Good) ▼
const VERSION = 'v2'
function saveConfig(config: { theme: string; language: string }) {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
} catch {
// 시크릿 모드/저장소 제한 초과/비활성화 환경에서 예외 발생
}
}
function loadConfig() {
try {
const data = localStorage.getItem(`userConfig:${VERSION}`)
return data ? JSON.parse(data) : null
} catch {
return null
}
}
// v1에서 v2로 마이그레이션
function migrate() {
try {
const v1 = localStorage.getItem('userConfig:v1')
if (v1) {
const old = JSON.parse(v1)
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
localStorage.removeItem('userConfig:v1')
}
} catch {}
}
'Develop > Web' 카테고리의 다른 글
| [React Best Practices] 3. 번들 크기 최적화 (0) | 2026.04.27 |
|---|---|
| [React Best Practices] 2. 번들 크기 최적화 (0) | 2026.04.17 |
| [React Best Practices] 1. 워터폴 제거 (0) | 2026.03.29 |
| [Web][React] 디자인 시스템 JSDoc에 스크린샷 자동 주입하기 (1) | 2026.01.13 |
| [Web] JSDoc과 Storybook으로 컴포넌트 문서화 (0) | 2025.12.25 |