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 메모이즈와 분리 설계로 전역 상태 전파 렌더를 억제하면 효율적인 앱을 만들 수 있습니다.
'프론트엔드' 카테고리의 다른 글
Tailwind CSS or ShadCN UI 가이드 (1) | 2025.08.26 |
---|---|
Next.js 프레임워크 SSR vs SSG 차이 완벽 정리 (2) | 2025.08.24 |
Next.js로 블로그 만들기 (2) | 2025.08.23 |
React 상태 관리 비교: Context, Redux, React Query (2) | 2025.08.22 |
React 입문 가이드 (1) | 2025.05.23 |