$ yh.log
[톺아보기] Vercel의 react-best-practices #10 - Advanced Patterns

[톺아보기] Vercel의 react-best-practices #10 - Advanced Patterns

reactadvancedhooksuseRefuseEffectEventoptimization

작성자 : 오예환 | 작성일 : 2026-03-21 | 수정일 : 2026-03-21 | 조회수 :

titleimpactimpactDescriptiontags
Event Handlers를 Refs에 저장LOWstable subscriptionsadvanced, hooks, refs, event-handlers, optimization

Event Handlers를 Refs에 저장

이 패턴은 React hooks에서 흔한 성능 문제를 해결합니다. event handlers가 dependency arrays에 포함되면 매 render마다 불필요한 re-subscriptions가 발생합니다.

Incorrect (handler 변경마다 재구독):

function useWindowEvent(event: string, handler: (e: Event) => void) {
  useEffect(() => {
    window.addEventListener(event, handler);
    return () => window.removeEventListener(event, handler);
  }, [event, handler]); // handler가 변경될 때마다 effect 재실행
}

이로 인해 handler가 변경될 때마다 effect가 재실행되어 반복적인 add/remove 사이클이 발생합니다.

Correct (Refs 사용):

handler를 ref에 저장하고 별도로 업데이트하여 subscription effect를 stable하게 유지:

function useWindowEvent(event: string, handler: (e: Event) => void) {
  const handlerRef = useRef(handler);
 
  useEffect(() => {
    handlerRef.current = handler;
  }, [handler]);
 
  useEffect(() => {
    const listener = (e: Event) => handlerRef.current(e);
    window.addEventListener(event, listener);
    return () => window.removeEventListener(event, listener);
  }, [event]); // handler가 dependency가 아님
}

Correct (useEffectEvent 사용):

React의 최신 hook은 handler의 latest version을 항상 호출하는 stable function reference를 생성하는 더 깔끔한 대안을 제공합니다:

import { useEffectEvent } from "react";
 
function useWindowEvent(event: string, handler: (e: Event) => void) {
  const onEvent = useEffectEvent(handler);
 
  useEffect(() => {
    window.addEventListener(event, onEvent);
    return () => window.removeEventListener(event, onEvent);
  }, [event]); // onEvent는 stable reference
}


titleimpactimpactDescriptiontags
Mount마다가 아닌 App을 한 번만 초기화LOW-MEDIUMdevelopment에서 중복 초기화 방지initialization, useEffect, app-startup, side-effects

Mount마다가 아닌 App을 한 번만 초기화

애플리케이션 전체 초기화 코드는 components 내 useEffect([]) hooks에 배치해서는 안 됩니다. components가 development 중 remount될 수 있어 effects가 예상치 못하게 재실행될 수 있기 때문입니다.

문제점:

초기화 logic이 빈 dependency array를 가진 component의 effect에서 실행되면 여러 번 실행될 수 있습니다:

  • Development에서 두 번 (React의 Strict Mode)
  • Component가 remount되면 다시 실행

Incorrect (Strict Mode에서 두 번 실행):

function App() {
  useEffect(() => {
    // development에서 두 번 실행됨!
    loadFromStorage();
    checkAuthToken();
    initializeAnalytics();
  }, []);
 
  return <Main />;
}

Correct (module-level guard):

초기화가 정확히 한 번 실행되도록 module-level guard 변수를 구현하세요:

let didInit = false;
 
function App() {
  useEffect(() => {
    if (didInit) return;
    didInit = true;
 
    loadFromStorage();
    checkAuthToken();
    initializeAnalytics();
  }, []);
 
  return <Main />;
}

대안: Entry Module에서 Top-level 초기화:

// index.tsx 또는 main.tsx
loadFromStorage();
checkAuthToken();
initializeAnalytics();
 
const root = createRoot(document.getElementById("root")!);
root.render(<App />);

이 접근법은 React의 application initialization patterns에 대한 공식 guidance와 일치합니다.

Reference: React - Initializing the application



titleimpactimpactDescriptiontags
Stable Callback Refs에 useEffectEvent 사용LOWeffect 재실행 방지advanced, hooks, useEffectEvent, refs, optimization

Stable Callback Refs에 useEffectEvent 사용

dependency arrays에 추가하지 않고 callbacks에서 latest values에 접근하세요.

문제점:

callbacks가 dependency arrays에 포함되면 effects가 불필요하게 재실행됩니다:

function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
  const [query, setQuery] = useState("");
 
  useEffect(() => {
    const timeout = setTimeout(() => onSearch(query), 300);
    return () => clearTimeout(timeout);
  }, [query, onSearch]); // onSearch가 dependency로 불필요한 재실행 유발
 
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

onSearch를 dependency로 추가하면 callback reference가 변경될 때마다 effect가 반복적으로 실행됩니다.

Correct (useEffectEvent로 stable reference 생성):

React의 useEffectEvent hook은 callback에 stable reference를 제공합니다:

import { useEffectEvent } from "react";
 
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
  const [query, setQuery] = useState("");
  const onSearchEvent = useEffectEvent(onSearch);
 
  useEffect(() => {
    const timeout = setTimeout(() => onSearchEvent(query), 300);
    return () => clearTimeout(timeout);
  }, [query]); // onSearchEvent가 dependency로 필요하지 않음
 
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

이 접근법은 latest callback에 대한 접근을 유지하고 stale closures를 방지하면서 불필요한 재실행을 제거합니다.

useEffectEvent 사용 시점:

  • Subscriptions (window events, WebSocket, etc.)
  • Debounced/throttled callbacks
  • Effect 내에서 변경되는 props/state를 읽어야 하지만 재실행을 trigger하고 싶지 않을 때

Reference: React useEffectEvent (experimental)