포스트 검색

포스트 제목으로 검색합니다

Vercel의 React Best Practices 톺아보기 [8]

ReactNext.jsReact Best PracticesOptimization

8. 고급 패턴 - 낮음 (LOW)

세심한 구현이 필요한 특정 사례에 대한 고급 패턴입니다.

8.1 Do Not Put Effect Events in Dependency Arrays (Effect Event를 의존성 배열에 넣지 마세요.)

  • 영향도: 낮음 (LOW / 불필요한 effect 재실행, 린트 오류 방지)

Effect Event 함수는 안정적인 참조가 아닙니다. 렌더마다 의도적으로 다른 함수로 취급됩니다. useEffectEvent가 돌려준 함수를 useEffect의 의존성 배열에 넣지 마세요. 반응해야 하는 실제 값(예: roomId)만 deps에 두고, Effect Event는 effect 본문이나 그 effect가 만든 구독 안에서만 호출하세요.

잘못된 예 (Effect Event를 의존성에 포함):

import { useEffect, useEffectEvent } from "react"; function ChatRoom({ roomId, onConnected, }: { roomId: string; onConnected: () => void; }) { const handleConnected = useEffectEvent(onConnected); useEffect(() => { const connection = createConnection(roomId); connection.on("connected", handleConnected); connection.connect(); return () => connection.disconnect(); }, [roomId, handleConnected]); // handleConnected는 매 렌더마다 달라짐 → effect가 매번 다시 실행됨 }

Effect Event를 deps에 넣으면 렌더마다 effect가 다시 돌고, React Hooks 린트 규칙에도 걸릴 수 있습니다.

올바른 예 (반응 값만 의존, Effect Event는 제외):

import { useEffect, useEffectEvent } from "react"; function ChatRoom({ roomId, onConnected, }: { roomId: string; onConnected: () => void; }) { const handleConnected = useEffectEvent(onConnected); useEffect(() => { const connection = createConnection(roomId); connection.on("connected", handleConnected); connection.connect(); return () => connection.disconnect(); }, [roomId]); }

참고: React useEffectEvent — Effect Event in deps


8.2 Initialize App Once, Not Per Mount (앱 초기화는 마운트마다가 아닌 한 번만 진행하세요.)

  • 영향도: 중하 (LOW-MEDIUM / 개발 과정에서 중복 초기화 방지)

앱 전체에서 딱 한 번만 돌아야 하는 초기화를 컴포넌트의 useEffect([])에만 두지 마세요. 컴포넌트는 언마운트 후 다시 마운트될 수 있고, effect는 다시 실행됩니다. 모듈 수준 플래그엔트리 모듈 최상단에서 한 번만 돌게 하세요.

잘못된 예 (개발 환경에선 두 번, 리마운트 시 또 실행):

function Comp() { useEffect(() => { loadFromStorage(); checkAuthToken(); }, []); // ... }

올바른 예 (앱 로드당 한 번에 가깝게):

let didInit = false; function Comp() { useEffect(() => { if (didInit) return; didInit = true; loadFromStorage(); checkAuthToken(); }, []); // ... }

참고: Initializing the application


8.3 Store Event Handlers in Refs (이벤트 핸들러는 ref에 저장하세요.)

  • 영향도: 낮음 (LOW / 안정적인 구독)

콜백 변경 시 재구독이 필요하지 않은 effect에서 콜백을 사용할 때는 콜백을 ref에 저장하세요.

잘못된 예 (handler가 바뀔 때마다 구독 해제, 재등록):

function useWindowEvent(event: string, handler: (e) => void) { useEffect(() => { window.addEventListener(event, handler); return () => window.removeEventListener(event, handler); }, [event, handler]); }

올바른 예 (event만 바뀔 때 구독 갱신):

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

대안: 최신 React에서는 useEffectEvent:

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

useEffectEvent는 같은 목적을 더 단순한 API로 제공합니다. 참조는 안정적이면서 항상 최신 handler를 호출합니다.


8.4 useEffectEvent for Stable Callback Refs (안정적인 콜백 참조를 위해 useEffectEvent를 사용하세요.)

  • 영향도: 낮음 (LOW / 불필요한 effect 재실행 방지)

콜백 안에서 최신 값을 쓰되, 그걸 의존성 배열에 넣지 않아도 됩니다. 오래된 클로저 생성은 피하고, 콜백이 바뀔 때마다 이펙트가 다시 도는 것도 막습니다.

잘못된 예 (onSearch가 바뀔 때마다 이펙트 재실행):

function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { const [query, setQuery] = useState(""); useEffect(() => { const timeout = setTimeout(() => onSearch(query), 300); return () => clearTimeout(timeout); }, [query, onSearch]); }

올바른 예 (React useEffectEvent):

import { useEffectEvent } from "react"; function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { const [query, setQuery] = useState(""); // 최신 onSearch를 호출하지만, 이펙트 deps에는 넣지 않음 const onSearchEvent = useEffectEvent(onSearch); useEffect(() => { const timeout = setTimeout(() => onSearchEvent(query), 300); return () => clearTimeout(timeout); }, [query]); }

마치며

Vercel의 React Best Practices를 1챕터부터 8챕터까지 읽으며 성능 최적화 패턴을 정리해 볼 수 있었습니다.

처음엔 숨은 비법이 많을 거라 기대했는데, 항목을 하나씩 읽다 보니 Vercel만의 대단하고 어마무시한 기법이라기보다는 React/Next.js 개발에서 흔히 접할 수 있는 안티 패턴과 그에 대한 대응이 많았다는 느낌이었고 가볍게 읽어 볼수 있었습니다.

앞으로 개발할 때 필요할 때마다 하나씩 꺼내 보며 리마인드할 수 있게 되어 기쁩니다.
(에이전트 전용 Skills용으로 만든 룰이라, 사실 AI에 학습시키면 그만이기도 합니다…ㅠㅠ)