React Hook — 함수 컴포넌트 이해하기
함수 컴포넌트는 렌더링할 때마다 처음부터 다시 실행되는 순수 함수예요. 그런데 상태를 기억하고, 부수 효과를 관리하고, 생명주기를 다뤄야 한다면? 그 다리 역할을 하는 게 Hook이에요. "왜 이렇게 동작하는가"를 중심으로 짚어볼게요.
함수 컴포넌트는 원래 기억력이 없다
함수 컴포넌트를 이해하는 가장 빠른 방법은, 매 렌더링마다 그 함수 전체가 처음부터 다시 호출된다는 사실을 받아들이는 거예요.
일반 자바스크립트 함수라면 지역 변수는 함수가 끝나는 순간 사라져요.
컴포넌트도 마찬가지예요. function Counter() {"{"}
안에서 let count = 0을 선언해봤자, 버튼을 클릭해서
리렌더링이 일어나는 순간 그 값은 다시 0으로 초기화돼요. React 16.8 이전에는
이 문제 때문에 상태가 필요한 컴포넌트는 무조건 클래스로 작성해야 했어요.
this.state가 인스턴스에 붙어있어서 렌더링 사이에도
살아남았거든요.
Hook은 "함수가 다시 호출돼도 특정 값만은 기억하게 해주는" 메커니즘이에요.
그 기억을 관리하는 방식이 클래스의 this 대신
React 내부의 별도 저장 공간(fiber)에 값을 붙여두는 방식이라, 함수형을 유지하면서도
상태와 생명주기를 쓸 수 있게 된 거예요.
import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) // count는 매 렌더링마다 새 변수처럼 보이지만, // 실제 값은 React가 컴포넌트 밖에서 들고 있다가 돌려준다 return ( <button onClick={() => setCount(count + 1)}> {count} </button> ) }
useEffect — 렌더링 결과물 밖의 일을 처리한다
컴포넌트 함수 자체는 "이 상태로는 화면이 이렇게 생겼다"를 계산하는 순수한
역할만 맡아야 해요. 그런데 API 호출, 구독 등록, 타이머 설정처럼 화면
계산과 무관한 부수 효과(side effect)는 렌더링 도중이 아니라
"렌더링이 끝나고 화면에 반영된 뒤"에 실행돼야 안전해요. 그 시점을 담당하는 게
useEffect예요.
useEffect(() => { const id = setInterval(() => tick(), 1000) // 컴포넌트가 사라지거나 다음 effect가 실행되기 전에 호출됨 return () => clearInterval(id) }, []) // 의존성 배열이 비어있으면 마운트 시 한 번만 실행
두 번째 인자인 의존성 배열이 핵심이에요. 배열 안의 값 중 하나라도 이전 렌더링과 달라지면 effect가 다시 실행돼요. 배열을 아예 생략하면 매 렌더링마다, 빈 배열이면 최초 한 번만, 특정 값을 넣으면 그 값이 바뀔 때만 실행됩니다.
effect나 이벤트 핸들러 안의 함수는 "그 함수가 만들어진 시점"의 상태값을
기억해요. 의존성 배열에 값을 빠뜨리면, setInterval 콜백이 계속 처음
렌더링 때의 count(예: 0)만 참조하는 식의 버그가 생겨요. React 19부터는
이런 이벤트성 로직을 위해 useEffectEvent가
도입되어, 최신 값을 항상 읽으면서도 의존성 배열에는 넣지 않아도 되는
패턴을 지원해요.
자주 쓰는 기본 Hook들
useRef — 리렌더링을 일으키지 않는 값
useState처럼 렌더링 사이에 값을 기억하지만,
값이 바뀌어도 컴포넌트를 다시 렌더링시키지 않아요. DOM 노드에 직접 접근하거나,
화면에 보여줄 필요 없는 값(이전 값 저장, 타이머 ID 등)을 들고 있을 때 씁니다.
useContext — props를 안 거치고 값 꺼내기
테마, 로그인 사용자 정보처럼 트리 전역에서 필요한 값을 여러 단계 props로 내려보내지 않고 바로 구독할 수 있게 해줘요. 다만 context 값이 바뀌면 그 값을 구독하는 모든 컴포넌트가 리렌더링되니, 자주 바뀌는 값을 담기엔 적합하지 않아요.
useMemo / useCallback — 계산과 함수를 기억하기
매 렌더링마다 새로 계산하기 비싼 값은 useMemo로,
자식 컴포넌트에 넘기는 함수가 매번 새로 생성되는 걸 막고 싶을 때는
useCallback으로 감싸요. 다만 React 19부터는
React Compiler가 빌드 시점에 이런 메모이제이션을 자동으로
처리해주기 때문에, 컴파일러를 쓰는 프로젝트라면 손으로 감싸는 코드는 점점
줄어드는 추세예요.
커스텀 Hook — 로직을 함수로 뽑아내기
Hook의 진짜 힘은 여기서 나와요. use로 시작하는
함수를 만들고 그 안에서 다른 Hook을 자유롭게 조합하면, 여러 컴포넌트가
공유하는 상태 로직을 하나의 함수로 뽑아낼 수 있어요. 클래스 시절의
mixin이나 고차 컴포넌트보다 훨씬 읽기 쉬운 방식이에요.
function useToggle(initial = false) { const [value, setValue] = useState(initial) const toggle = useCallback(() => setValue(v => !v), []) return [value, toggle] } // 사용하는 쪽 const [isOpen, toggleOpen] = useToggle()
지켜야 하는 두 가지 규칙
Hook은 순서에 의존해요. React는 Hook 이름이 아니라 호출된 순서로 어떤 상태가 어떤 useState에 대응하는지 찾기 때문에, 규칙을 깨면 상태가 엉뚱한 곳에 섞여 들어가요.
최상위에서만 호출한다
if문, for문, 중첩 함수 안에서 Hook을 호출하지 마세요. 렌더링마다 호출 순서가 같아야 React가 어떤 상태가 어떤 것인지 추적할 수 있어요.
React 함수 안에서만 호출한다
일반 자바스크립트 함수가 아니라 함수 컴포넌트나 커스텀 Hook 안에서만 호출하세요. 이벤트 핸들러나 setTimeout 콜백 안에서 직접 호출하면 안 돼요.
예외가 하나 생겼는데, React 19의 use()는 이 규칙에서
자유로워요. 조건문이나 반복문 안에서도 호출할 수 있도록 설계됐어요. 다만 이건
Promise나 Context를 "읽는" 특수한 API라서, 기존 useState/useEffect의 규칙과는
별개로 이해하는 게 맞아요.
클래스 컴포넌트와 비교하면
| 개념 | 클래스 컴포넌트 | 함수 컴포넌트 + Hook |
|---|---|---|
| 상태 저장 | this.state, this.setState() | useState() |
| 마운트 시 실행 | componentDidMount | useEffect(fn, []) |
| 업데이트 시 실행 | componentDidUpdate | useEffect(fn, [deps]) |
| 정리(cleanup) | componentWillUnmount | useEffect의 반환 함수 |
| 로직 재사용 | 고차 컴포넌트, render props | 커스텀 Hook |
| this 바인딩 문제 | 발생함 (bind, 화살표 함수 필요) | 없음 |
React 19 — 상태 관리를 넘어선 Hook들
React 19부터는 Hook의 역할이 상태·생명주기를 넘어 비동기 데이터와 폼 처리까지 확장됐어요. 대표적으로 네 가지가 새로 추가됐어요.
use()— Promise나 Context 값을 컴포넌트 렌더링 중에 바로 읽고, Suspense와 함께 로딩 상태를 처리useActionState— 폼 제출 같은 액션의 대기 상태와 결과값을 한 번에 관리useFormStatus— 부모 form의 제출 상태를 props 없이 자식 컴포넌트에서 바로 구독useOptimistic— 서버 응답을 기다리기 전에 낙관적으로 UI를 먼저 업데이트
이 흐름 때문에 데이터 페칭에 useEffect를 쓰던 예전
패턴은 점점 use() + Suspense, 혹은 TanStack Query
같은 라이브러리로 옮겨가는 중이에요. useEffect가 사라지는 건 아니지만,
"데이터를 가져오는 용도"로 쓰는 비중은 확실히 줄고 있어요.
정리
함수 컴포넌트는 매번 처음부터 다시 실행되는 함수라는 전제를 기억하면, Hook이 왜 필요한지가 자연스럽게 이해돼요. useState는 그 함수가 잊어버리는 값을 대신 기억해주고, useEffect는 렌더링 결과와 무관한 부수 효과를 안전한 시점에 실행해줘요. 여기에 커스텀 Hook으로 로직을 조합하는 능력이 더해지면서, 함수 컴포넌트는 클래스보다 더 유연한 재사용 단위가 됐어요.
'Frontend' 카테고리의 다른 글
| [Frontend] Framwork 정리 (0) | 2026.07.03 |
|---|---|
| [Frontend] 콜백 지옥에서 벗어나기 (0) | 2026.07.03 |
| [Frontend] Reflow는 왜 느릴까? (0) | 2026.07.03 |
| [Frontend] npm, Yarn, pnpm 정리 (0) | 2026.07.03 |
| [Frontend] Redux와 Recoil의 차이 (0) | 2026.07.03 |