1. 리액트의 모든 훅 파헤치기
1.1 useState: 상태 관리의 비밀과 클로저
useState는 함수 컴포넌트에서 상태를 관리할 수 있게 해주는 가장 기본적인 훅입니다. 하지만 단순히 “값을 저장하는 변수”로 이해하면 리액트의 렌더링 모델을 오해하게 됩니다.
🧐 Q. 왜 일반 변수는 안 될까?
함수 컴포넌트는 렌더링이 발생할 때마다 함수 자체가 다시 실행됩니다. 즉, 함수 내부의 let, const 변수는 매번 초기화됩니다.
function Component() {
let state = 'hello'
// 렌더링이 다시 되면 state는 항상 'hello'
}
useState의 상태는 컴포넌트 함수 안에 저장되지 않고, 리액트 내부에 따로 보관됩니다.
컴포넌트 함수
├─ count: 현재 렌더에서 보여줄 값
└─ setCount: 리액트 내부 상태를 가리키는 함수
useState가 상태를 기억하는 방법
리액트는 클로저(Closure) 를 활용해 상태를 컴포넌트 외부(리액트 내부)에 저장합니다. 그리고 setState는 이 외부 저장소를 참조하는 함수로 만들어집니다. 덕분에 컴포넌트가 다시 실행되어도, 이전 상태를 “기억”할 수 있습니다. useState에 값이 아니라 함수를 넘기면, 그 함수는 최초 렌더링 시 단 한 번만 실행됩니다.
const [count, setCount] = useState(() => {
return Number(localStorage.getItem('count'))
})
1.2 useEffect: 생명주기가 아닌 부수 효과
useEffect는 “렌더링 결과를 기반으로 부수 효과를 만드는 도구”입니다.
핵심 개념: 렌더링 이후 실행 (즉, 렌더링 과정에 직접 개입하지 않습니다.)
- 렌더링 → 화면 반영
- 그 다음
useEffect실행
1.2.1 의존성 배열의 진짜 의미
의존성 배열은 “이 값들이 바뀌면, 이 effect는 더 이상 유효하지 않다”는 선언입니다.
useEffect(() => {
// side effect
}, [a, b])
리액트는 의존성 배열을 Object.is 기반의 얕은 비교로 검사합니다.
a또는b중 하나라도 이전과 다르면 effect 재실행- 객체, 배열은 내용이 아니라 참조로 비교
- 새 객체 → 값이 같아도 “변경됨”으로 판단
1.2.2 클린업(Cleanup) 함수의 정확한 역할
다음 effect가 실행되기 직전에, 이전 effect를 정리하는 함수
(1) 렌더링 A → effect A 실행
(2) 상태 변경 → 렌더링 B
(3) effect B 실행 전에 effect A의 cleanup 실행
(4) effect B 실행
1.3 useMemo와 useCallback: 메모이제이션의 본질
useMemo
- 계산 결과를 기억
- 의존성이 바뀌지 않으면 이전 결과 재사용
const value = useMemo(() => expensiveCalc(a, b), [a, b])
useCallback
- 함수 자체를 기억
- 자식 컴포넌트로 함수 전달 시 유용
const onClick = useCallback(() => {
doSomething()
}, [])
1.4 useRef: 렌더링과 무관한 값
useRef는 값이 바뀌어도 렌더링을 발생시키지 않습니다.
- DOM 직접 접근
- 이전 값 저장
- 타이머 ID, AbortController 등
const timerRef = useRef<number | null>(null)
// 상태(state)가 아니라 “그냥 보관용 상자”라고 생각하면 이해가 쉽습니다.
1.5 useContext: Prop Drilling 해결의 대가
(1) Prop Drilling 문제_ C에서 값을 쓰기 위해 A, B는 **필요도 없는 props를 전달만 함
App
└─ A
└─ B
└─ C (값 필요)
(2) useContext로 해결
// 대안 ① Context + React.memo
// 👉 “필요 없는 리렌더링이 더 내려가지 않게 막자”
const Profile = React.memo(function Profile({ user }) {
return <Avatar name={user.name} />
})
function Parent() {
const user = useContext(UserContext)
return <Profile user={user} />
}
// Parent는 Context 변경 때문에 다시 실행됨 ❗
// 하지만 user 참조가 같다면
// Profile은 렌더링 안 됨
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// 대안 ② Context를 역할별로 분리
// 👉 “한 값 바뀌었다고 전부 흔들지 말자”
<UserContext value={user}>
<ThemeContext value={theme}>
1.6 useReducer: 상태 로직 분리
useReducer는 상태를 어떻게 바꿀지에 대한 로직을 컴포넌트 밖으로 분리하는 훅이다.
❌ useState로 상태를 직접 관리할 때
const [state, setState] = useState({
loading: false,
data: null,
error: null,
})
setState({ ...state, loading: true })
setState({ loading: false, data, error: null })
setState({ loading: false, data: null, error })
✅ useReducer로 상태 로직을 분리했을 때
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true }
case 'FETCH_SUCCESS':
return { loading: false, data: action.data, error: null }
case 'FETCH_ERROR':
return { loading: false, data: null, error: action.error }
}
}
const [state, dispatch] = useReducer(reducer, initialState)
dispatch({ type: 'FETCH_START' })
dispatch({ type: 'FETCH_SUCCESS', data })
dispatch({ type: 'FETCH_ERROR', error })
1.7 기타 고급 훅
- useImperativeHandle → ref로 노출할 인터페이스 제어
- useLayoutEffect → DOM 변경 직후, 페인팅 이전 실행
- useDebugValue → Custom Hook 디버깅용
2. 사용자 정의 훅과 고차 컴포넌트
2.1 사용자 정의 훅 (Custom Hook)
“상태 + effect 로직의 재사용”
- 반드시
use로 시작 - 렌더링 결과에는 직접 영향 없음
- 로직 공유에 최적
✔️ 가장 권장되는 방식
2.2 고차 컴포넌트 (HOC)
“컴포넌트를 감싸서 새로운 컴포넌트 생성”
- 대표 예:
React.memo withSomething네이밍 관례- 렌더링 구조에 직접 개입