프론트엔드

React 기본 Hook 성능까지 끌어올리는 사용법

지식소 채움이 2025. 8. 22. 08:39

React의 기본 Hook(useState, useEffect, useRef, useContext)은 “상태를 쓴다/부수효과를 관리한다”로 끝나지 않습니다. 언제, 어떻게 쓰느냐에 따라 렌더 횟수, 불필요한 네트워크 호출, 이벤트 리스너 누수까지 좌우됩니다. 아래는 개념 + 실전 성능 팁을 한 번에 정리한 가이드입니다.


1) useState — 초기화·업데이트만 잘해도 빨라진다

개념 요약
컴포넌트 로컬 상태를 선언하고 업데이트합니다. 업데이트가 일어나면 컴포넌트는 다시 렌더됩니다.

성능 팁 1: Lazy 초기화로 무거운 계산 1회로 제한

// ❌ 매 렌더마다 heavy() 실행됨
const [list, setList] = useState(heavy());

// ✅ 최초 마운트 시에만 heavy() 실행
const [list, setList] = useState(() => heavy());

초기값 계산이 비싼 경우 함수 형태 초기화를 쓰면 렌더마다 재계산을 피합니다.

성능 팁 2: 함수형 업데이트로 안정적 누적

// 이벤트 루프 안에서 여러 번 호출해도 안전
setCount(prev => prev + 1);

비동기/연속 업데이트 시 이전 값 기반으로 갱신하면 불필요한 렌더나 경합을 줄입니다.

성능 팁 3: 파생 값은 상태로 두지 말 것
리스트 정렬/필터처럼 파생되는 값은 상태가 아니라 계산 결과로 두고, 필요 시 useMemo로 메모이즈하세요(아래에 예시).


2) useEffect — “언제 실행되는가”를 설계하라

개념 요약
DOM 반영 이후 실행되는 부수효과(데이터 요청, 구독, 타이머 등)를 선언합니다. 의존성 배열(deps)로 실행 타이밍을 제어합니다.

성능 팁 1: 의존성 최소화로 불필요한 호출 차단

// ❌ query가 바뀔 때마다 네트워크 레이스가 발생할 수 있음
useEffect(() => {
  fetch(`/api/search?q=${query}`).then(r => r.json()).then(setData);
}, [query]);

// ✅ 디바운스 + AbortController로 중복 요청 줄이기
const timerRef = useRef(null);
useEffect(() => {
  if (!query) return;
  const controller = new AbortController();
  clearTimeout(timerRef.current);
  timerRef.current = setTimeout(async () => {
    try {
      const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal });
      setData(await res.json());
    } catch (_) {/* 취소 시 무시 */}
  }, 300);

  return () => {
    controller.abort();
    clearTimeout(timerRef.current);
  };
}, [query]);

짧은 타이핑 동안 발생하는 중복 요청을 디바운스하고, 이전 요청은 abort하여 브라우저·서버 부담을 낮춥니다.

성능 팁 2: 이벤트 리스너는 한 번만 등록

// ❌ scrollY가 바뀔 때마다 리스너 재등록
useEffect(() => {
  const onScroll = () => setScrollY(window.scrollY);
  window.addEventListener('scroll', onScroll);
  return () => window.removeEventListener('scroll', onScroll);
}, [scrollY]);

// ✅ 리스너는 1회 등록, 내부에서 상태 갱신
useEffect(() => {
  const onScroll = () => setScrollY(window.scrollY);
  window.addEventListener('scroll', onScroll);
  return () => window.removeEventListener('scroll', onScroll);
}, []);

의존성에 상태를 넣어 리스너를 재등록하는 실수를 줄이면 CPU 스파이크를 예방합니다.


3) useRef — “렌더 없이 바뀌는 값”의 집

개념 요약
렌더 간에 값을 기억하지만, 값이 바뀌어도 렌더를 유발하지 않는 저장소입니다. DOM 노드 접근에도 사용합니다.

성능 팁 1: 고빈도 이벤트(스크롤/리사이즈)에서 쓰로틀

const ticking = useRef(false);
useEffect(() => {
  const onScroll = () => {
    if (ticking.current) return;
    ticking.current = true;
    requestAnimationFrame(() => {
      setScrollY(window.scrollY); // 프레임당 1번만 갱신
      ticking.current = false;
    });
  };
  window.addEventListener('scroll', onScroll);
  return () => window.removeEventListener('scroll', onScroll);
}, []);

useRef로 작업 중 플래그를 관리하면 이벤트 폭주 시 렌더 빈도를 억제할 수 있습니다.

성능 팁 2: “상태가 아닌” 캐시 보관
API 응답 캐시, IntersectionObserver 인스턴스, 타이머 ID처럼 렌더가 필요 없는 값을 ref에 넣어 재생성·재등록을 막습니다.


4) useMemo / useCallback — “계산/함수의 안정성” 확보

개념 요약

  • useMemo: 비싼 계산 결과를 메모이즈
  • useCallback: 함수를 참조 동일성으로 고정

성능 팁 1: 무거운 파생값 메모이즈

const sorted = useMemo(
  () => items.slice().sort((a, b) => a.score - b.score),
  [items]
);

큰 배열 정렬·필터링을 렌더마다 반복하는 비용을 줄입니다.

성능 팁 2: 메모된 자식에게 안정적인 핸들러 전달

const List = React.memo(({ items, onSelect }) => /* ... */);

function Parent({ items }) {
  const onSelect = useCallback((id) => { /* ... */ }, []);
  return <List items={items} onSelect={onSelect} />;
}

React.memo된 자식에게 매 렌더마다 새 함수를 넘기면 메모이제이션이 무의미해집니다. useCallback으로 불필요한 자식 재렌더를 차단하세요.

과용 금지: 값이 작거나 계산이 싸다면 useMemo/useCallback이 오히려 오버헤드가 될 수 있으니 Profiler로 측정 후 적용합니다.


5) useContext — 전역 상태의 “파급 렌더”를 통제하라

개념 요약
트리 하위로 값을 공급합니다. 단, Provider의 value가 바뀌면 모든 소비자가 다시 렌더됩니다.

성능 팁 1: value 객체를 메모이즈

// ❌ 매 렌더마다 새 객체 → 모든 소비자 렌더
<UserContext.Provider value={{ user, setUser }}>

// ✅ 값 메모이즈로 불필요한 전파 억제
const ctxValue = useMemo(() => ({ user, setUser }), [user]);
<UserContext.Provider value={ctxValue}>

성능 팁 2: 컨텍스트 분리
상태 값과 액션(디스패치)을 서로 다른 컨텍스트로 쪼개면, 값이 바뀌지 않는 소비자는 렌더를 피할 수 있습니다.

 

 


6) 마무리  

React 기본 Hook(useState, useEffect, useRef, useMemo, useContext)은 단순히 개념을 아는 것보다 성능까지 고려해 사용하는 게 핵심입니다.
useState는 lazy 초기화·함수형 업데이트, useEffect는 의존성 최소화와 cleanup으로 불필요한 연산을 줄일 수 있습니다.
useRef는 렌더 없이 값 캐싱, useMemo/useCallback은 무거운 계산·핸들러를 안정화해 불필요한 렌더링을 줄입니다.
마지막으로 useContext는 value 메모이즈와 분리 설계로 전역 상태 전파 렌더를 억제하면 효율적인 앱을 만들 수 있습니다.