$ yh.log
[톺아보기] Vercel의 react-best-practices #6 - Client-Side Optimization

[톺아보기] Vercel의 react-best-practices #6 - Client-Side Optimization

reactclient-sideswrperformanceevent-listenerslocalStorage

작성자 : 오예환 | 작성일 : 2026-01-29 | 수정일 : 2026-01-29 | 조회수 :

titleimpactimpactDescriptiontags
자동 Deduplication을 위해 SWR 사용MEDIUM-HIGH자동 deduplicationclient, 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



titleimpactimpactDescriptiontags
localStorage 데이터 버전 관리 및 최소화MEDIUMschema 충돌 방지, 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 저장 방지.



titleimpactimpactDescriptiontags
Scrolling 성능을 위한 Passive Event Listeners 사용MEDIUMevent 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.



titleimpactimpactDescriptiontags
Global Event Listeners 중복 제거LOWN개 components에 단일 listenerclient, 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>;
}