| title | impact | impactDescription | tags |
|---|---|---|---|
| 자동 Deduplication을 위해 SWR 사용 | MEDIUM-HIGH | 자동 deduplication | client, swr, deduplication, data-fetching |
자동 Deduplication을 위해 SWR 사용
SWR은 component instances 간 request deduplication, caching, revalidation을 활성화합니다.
Incorrect (deduplication 없음, 각 instance가 fetch):
import { useState, useEffect } from "react";
type User = {
id: string;
name: string;
};
function UserList() {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetch("/api/users")
.then(r => r.json())
.then(setUsers);
}, []);
return (
<ul>
{users.map(u => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}Correct (여러 instances가 하나의 request를 공유):
import useSWR from "swr";
type User = {
id: string;
name: string;
};
const fetcher = (url: string) => fetch(url).then(r => r.json());
function UserList() {
const { data: users } = useSWR<User[]>("/api/users", fetcher);
if (!users) return <div>Loading...</div>;
return (
<ul>
{users.map(u => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}immutable data의 경우:
import { useImmutableSWR } from "@/lib/swr";
type Config = {
apiUrl: string;
features: string[];
};
function StaticContent() {
const { data } = useImmutableSWR<Config>("/api/config", fetcher);
return <div>{data?.apiUrl}</div>;
}mutations의 경우:
import { useSWRMutation } from "swr/mutation";
async function updateUser(url: string, { arg }: { arg: { name: string } }) {
return fetch(url, {
method: "PUT",
body: JSON.stringify(arg),
}).then(r => r.json());
}
function UpdateButton() {
const { trigger, isMutating } = useSWRMutation("/api/user", updateUser);
return (
<button onClick={() => trigger({ name: "New Name" })} disabled={isMutating}>
Update
</button>
);
}Reference: SWR Documentation
| title | impact | impactDescription | tags |
|---|---|---|---|
| localStorage 데이터 버전 관리 및 최소화 | MEDIUM | schema 충돌 방지, storage size 감소 | client, localStorage, storage, versioning, data-minimization |
localStorage 데이터 버전 관리 및 최소화
keys에 version prefix를 추가하고 필요한 fields만 저장하세요. Schema 충돌과 민감한 데이터의 실수로 인한 저장을 방지합니다.
Incorrect:
// version 없음, 모든 것 저장, error handling 없음
localStorage.setItem("userConfig", JSON.stringify(fullUserObject));
const data = localStorage.getItem("userConfig");Correct:
const VERSION = "v2";
type UserConfig = {
theme: string;
language: string;
};
function saveConfig(config: UserConfig): void {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
} catch {
// incognito/private browsing, quota 초과, 또는 비활성화 시 throw됨
}
}
function loadConfig(): UserConfig | null {
try {
const data = localStorage.getItem(`userConfig:${VERSION}`);
return data ? JSON.parse(data) : null;
} catch {
return null;
}
}
// v1에서 v2로 Migration
function migrate(): void {
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 {
// Migration 실패 시 조용히 무시
}
}server responses에서 minimal fields만 저장:
type FullUser = {
id: string;
name: string;
email: string;
preferences: {
theme: string;
notifications: boolean;
language: string;
// ... 추가 필드들
};
// ... 20개 이상의 필드
};
type UserPreferences = {
theme: string;
notifications: boolean;
};
// User object가 20개 이상 fields를 가지지만, UI에 필요한 것만 저장
function cachePrefs(user: FullUser): void {
try {
const prefs: UserPreferences = {
theme: user.preferences.theme,
notifications: user.preferences.notifications,
};
localStorage.setItem("prefs:v1", JSON.stringify(prefs));
} catch {
// Storage 실패 시 조용히 무시
}
}항상 try-catch로 감싸기: getItem()과 setItem()은 incognito/private browsing (Safari, Firefox), quota 초과, 또는 비활성화 시 throw됩니다.
Benefits: versioning을 통한 schema evolution, 감소된 storage size, tokens/PII/internal flags 저장 방지.
| title | impact | impactDescription | tags |
|---|---|---|---|
| Scrolling 성능을 위한 Passive Event Listeners 사용 | MEDIUM | event listeners로 인한 scroll delay 제거 | client, event-listeners, scrolling, performance, touch, wheel |
Scrolling 성능을 위한 Passive Event Listeners 사용
즉각적인 scrolling을 활성화하기 위해 touch와 wheel event listeners에 { passive: true }를 추가하세요. Browsers는 일반적으로 preventDefault()가 호출되는지 확인하기 위해 listeners가 끝날 때까지 기다리며, 이것이 scroll delay를 유발합니다.
Incorrect:
import { useEffect } from "react";
function ScrollTracker() {
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);
};
}, []);
return <div>Scroll Tracker</div>;
}Correct:
import { useEffect } from "react";
function ScrollTracker() {
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);
};
}, []);
return <div>Scroll Tracker</div>;
}passive를 사용할 때: tracking/analytics, logging, preventDefault()를 호출하지 않는 모든 listener.
passive를 사용하지 않을 때: custom swipe gestures 구현, custom zoom controls, 또는 preventDefault()가 필요한 모든 listener.
| title | impact | impactDescription | tags |
|---|---|---|---|
| Global Event Listeners 중복 제거 | LOW | N개 components에 단일 listener | client, swr, event-listeners, subscription |
Global Event Listeners 중복 제거
component instances 간에 global event listeners를 공유하기 위해 useSWRSubscription()을 사용하세요.
Incorrect (N개 instances = N개 listeners):
import { useEffect } from "react";
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]);
}useKeyboardShortcut hook을 여러 번 사용하면, 각 instance가 새로운 listener를 등록합니다.
Correct (N개 instances = 1개 listener):
import { useEffect } from "react";
import useSWRSubscription from "swr/subscription";
// key별 callbacks를 추적하기 위한 Module-level Map
const keyCallbacks = new Map<string, Set<() => void>>();
function useKeyboardShortcut(key: string, callback: () => void) {
// 이 callback을 Map에 등록
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() {
// 여러 shortcuts가 동일한 listener를 공유
useKeyboardShortcut("p", () => {
console.log("Profile shortcut");
});
useKeyboardShortcut("k", () => {
console.log("Keyboard shortcut");
});
return <div>Profile Page</div>;
}