useState: 값 저장 및 변경
useEffect: 부수효과 (API 등)
useContext: 전역 데이터 읽기
useRef: DOM 참조/기억
useMemo/useCallback: 성능 최적화
useReducer: 복잡한 상태변화
Hook 이름 | 사용 방법 | 주 사용 목적 | 동작원리 | 주요 사용 케이스 |
useState | const [state, setState] = useState(초깃값); | 간단 상태값/ 로직 | 상태값을 컴포넌트에 바인딩(초기값 등록). setter 호출 시 React가 상태 저장 및 리렌더링 | 숫자 카운터, 입력값 관리, 토글 등 간단한 상태 저장 |
useEffect | useEffect(() ⇒ { effect }, [의존성]) | 사이드이펙트(부수효과) 관리 | 렌더 후 사이드이펙트(부수효과) 실행. 의존성이 바뀌거나 mount/unmount 때 실행 | API 콜, 타이머, 이벤트 구독/해제 |
useContext | const value = useContext(MyContext); | 글로벌 상태, 전역 데이터 공급 | Context.Provider에서 제공한 데이터를 쉽게 읽음 | 전역 상태, 언어 전역 설정, 테마 관리 등 최상위 부터 하위까지 prop drilling 없이 자식 요소까지 값 공유 |
useRef | const ref = useRef(초깃값); | 리렌더 필요 없이 "값/ DOM" 저장 | 변수를 리렌더링 없이 “참조(ref) 공간”에 저장. .current로 접근 | DOM 요소 직접 조작, 이전 값 저장, setTimeout 등 외부 값 저장 |
useMemo | const value = useMemo(() ⇒ expensiveFn(), [의존성]); | '비싼 계산 결과' 캐싱 | 특정 연산 결과를 의존성 배열 기준으로 “메모이제이션”(캐싱) | 연산이 무거운 값, 불필요한 재계산 방지, 렌더 성능 최적화 필요할 때 |
useCallback | const fn = useCallback(() ⇒ { ... }, [의존성]); | '함수' 캐싱, 자식 렌더링 최적화 | 함수(콜백)를 “메모이제이션”(생성/바뀌는 타이밍 최소화) | 자식에게 함수를 prop으로 넘길 때 불필요한 재생성 방지, useMemo/ useEffect의 의존성 배열에 함수가 들어갈 때 렌더 최적화 (함수 생성 자체의 리소스 절약이 주목적이 아님) |
useReducer | const [state, dispatch] = useReducer(reducer, 초기값); | 복잡 로직, 다양한 액션/ 상태 | 다양한 action에 따른 상태 업데이트 로직을 한 곳에(리듀서로) 집중 | 복잡한 상태 관리, 여러 액션/상태가 얽힌 폼 등에서 사용 |
1. useState
심화 설명
- 컴포넌트 개별 상태값을 함수형 컴포넌트에서도 사용할 수 있게 하는 Hook
- setter 함수 호출 시 React는 해당 컴포넌트만 리렌더
- 상태값과 Setter는 컴포넌트마다 독립적으로 유지
코드 예제
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0); // count: 상태값, setCount: 상태 변경함수
return (
<div>
<button onClick={() => setCount(count - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
설명:
클릭할 때마다 상태(count) 값이 변하고, 변할 때마다 화면이 다시 그려집니다.
2. useEffect
심화 설명
- **렌더 후 수행되는 "부수 효과(side-effect)"**를 관리
- 컴포넌트가 마운트/업데이트/언마운트 되는 시점을 제어 가능
- 의존성 배열 값을 기준으로 언제 effect를 실행할지 설정
코드 예제
import React, { useState, useEffect } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => setSeconds(s => s + 1), 1000);
// 언마운트 시 타이머 제거
return () => clearInterval(timer);
}, []); // []: 최초 1회만 실행 (마운트/언마운트)
return <div>{seconds}초 지남</div>;
}
설명:
setInterval로 초를 증가시키되, 컴포넌트가 사라질 때(return) 타이머 삭제
3. useContext
심화 설명
- 컴포넌트 트리 전체에 데이터(전역 상태 등)를 prop drilling 없이 공급
- 값을 읽어올 때마다 최신 컨텍스트 값을 자동 반영
- 주로, 테마, 로그인 정보, 언어 설정 등에 사용
코드 예제
import React, { createContext, useContext } from "react";
const ThemeContext = createContext("light");
function ThemedButton() {
const theme = useContext(ThemeContext); // 컨텍스트 값 읽기
return <button style={{ background: theme === "dark" ? "#222" : "#eee" }}>Button</button>
}
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}
설명:
ThemedButton은 ThemeContext의 값을 받아 버튼의 스타일을 자동으로 변경합니다.
5. useMemo / useCallback
심화 설명 (둘의 차이)
- useMemo: 비싼 연산(무거운 계산 결과)를 캐시
- useCallback: 함수를 캐시 (같은 함수가 자식에 계속 전달될 때 불필요한 렌더 방지)
- props로 함수를 전달할 때 (특히 React.memo/자식컴포넌트에)
- 함수 주소가 바뀌지 않게 하려면 useCallback 필요
- 의존성 배열에 함수가 들어가는 useEffect/useMemo 사용할 때
- 많은 수의 컴포넌트 리렌더링/최적화가 중요한 경우
- 불필요하게 모든 함수에 useCallback을 남용하면, 메모리와 관리 비용이 오히려 늘 수 있음
- 부모의 state/props를 사용하는 함수일 경우, 그 값이 변하면 useCallback도 새 함수가 됨
(의존성 배열에 값을 꼭 명확히 넣어야 함)
- props로 함수를 전달할 때 (특히 React.memo/자식컴포넌트에)
useMemo 코드 예제
const expensiveValue = useMemo(() => {
return heavyComputation(num); // num 바뀔 때만 재계산
}, [num]);
설명:
num 값이 바뀔 때만 heavyComputation() 실행, 그 외 캐시 사용.
1. 대규모 리스트 필터링 & 소트 (비효율적 렌더링 방지)
대규모 데이터 배열에서 필터링과 정렬 같은 연산이 자주 일어날 때, 상태(state)가 변할 때마다 무조건 연산하면 성능에 치명적입니다.
이럴 때 useMemo로 의존성 값이 바뀔 때만 계산하도록 만들어 렌더 성능을 최적화합니다.
예시 코드
import React, { useState, useMemo } from "react";
// 수천 개의 유저 리스트가 있다고 가정
const userList = [...Array(10000)].map((_,i) => ({
id: i, name: `User ${i}`, age: Math.floor(Math.random() * 80)
}));
function UserListApp() {
const [keyword, setKeyword] = useState("");
const [sortByAge, setSortByAge] = useState(false);
// userList 필터링 & 정렬 연산을 useMemo로 캐싱
const filteredSortedUsers = useMemo(() => {
let list = userList.filter(user => user.name.toLowerCase().includes(keyword.toLowerCase()));
if (sortByAge) list = list.sort((a,b) => a.age - b.age);
console.log('expensive filtration!'); // 실제 렌더링 횟수 확인용
return list;
}, [keyword, sortByAge]); // keyword나 sortByAge가 바뀔 때만 재계산
return (
<div>
<input placeholder="Search user" value={keyword}
onChange={e => setKeyword(e.target.value)} />
<button onClick={() => setSortByAge(s => !s)}>
{sortByAge ? "나이순 정렬 해제" : "나이순 정렬"}
</button>
<ul>
{filteredSortedUsers.map(user =>
<li key={user.id}>{user.name} ({user.age})</li>
)}
</ul>
</div>
);
}
설명:
- 컴포넌트 리렌더: 모든 state 변경 시 항상 발생
- useMemo 계산: 오직 의존성 배열 항목(keyword, sortByAge)이 바뀔 때만 발생
- 나머지 경우: 아무리 컴포넌트가 리렌더돼도 useMemo 결과(비싼 연산)는 재사용됨
이렇게 불필요한 연산 제외로 대규모 데이터 처리 시 성능을 크게 향상시킬 수 있습니다.
useCallback 코드 예제
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 의존값이 없으면 최초 1회만 생성
// 자식에 handleClick 등 함수를 prop으로 줄 때, 불필요한 재생성 방지
- useEffect/useMemo의 의존성 배열 안에 함수가 들어가고, 그 함수가 리렌더마다 새로 만들어질 수 있다면 → useCallback으로 감싸주는 게 필수!
- 그렇지 않으면, 본래 의도와 다르게 useEffect/useMemo가 과도하게 실행됨
function Parent({ value }) {
// 매 렌더마다 handleChange는 새로운 함수임
const handleChange = (v) => {
console.log("changed:", v);
};
useEffect(() => {
// handleChange가 새 함수로 인식돼서, value와 상관없이 매번 실행됨
handleChange(value);
}, [value, handleChange]);
// 여기서 handleChange는 매번 새 함수라 useEffect가 매번 작동
return <div>{value}</div>;
}
- 문제점:
- value가 변하지 않아도, Parent가 리렌더될 때마다 handleChange가 바뀌므로
- useEffect가 쓸데없이 반복 실행됨
function Parent({ value }) {
// useCallback으로 함수 재사용 (value가 변할 때만 새로 만듦)
const handleChange = useCallback((v) => {
console.log("changed:", v);
}, []);
useEffect(() => {
handleChange(value);
}, [value, handleChange]);
// 이제 handleChange가 변하지 않으므로, useEffect는 value 변화시에만 실행
return <div>{value}</div>;
}
- 해결:
- handleChange가 의미 있게 바뀔 때만 새로 만들어짐
- useEffect가 불필요하게 반복 실행되지 않음
6. useReducer
심화 설명
- 복잡 상태(여러 값, 다양한 action, 상태 전이 필요 시) 사용
- reducer 함수 형태로 state, action에 따라 새 상태를 산출
- Redux 패턴의 간소화 버전
코드 예제
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
설명:
상태(state), action(무엇을 할지)에 따라 상태 관리. 복잡한 폼, 여러 입력값, 다양한 이벤트 트리거에 적