React Hooks 사용법: 예제로 배우는 완벽 가이드
React Hooks 사용법: 예제로 배우는 완벽 가이드
2019년 초 React 16.8이 Hooks를 도입하면서, 함수형 컴포넌트가 클래스 컴포넌트 없이도 상태, 사이드 이펙트, 컨텍스트를 관리할 수 있게 되어 개발자들이 React 애플리케이션을 구축하는 방식이 근본적으로 바뀌었습니다. 현대 React 개발자라면 React Hooks를 효과적으로 사용하는 방법을 이해하는 것이 필수적이며, Hooks는 이제 "대부분의 React 코드가 작성되는 유일한 방식"이 되었습니다. 이 가이드에서는 필수 내장 Hooks를 살펴보고, 이를 규율하는 규칙을 설명하며, 바로 사용할 수 있는 실용적인 예제를 제공합니다.
배우게 될 내용
이 가이드를 마치면 useState, useEffect, useContext, useRef, useReducer, useMemo, useCallback 같은 React의 핵심 Hooks를 자신의 프로젝트에서 자신 있게 사용할 수 있습니다. 어떤 Hook을 언제 선택해야 하는지, 불필요한 리렌더링 같은 흔한 실수를 피하는 방법, 재사용 가능한 로직을 커스텀 Hooks로 추출하는 방법을 이해하게 됩니다. 가장 중요한 점은 Hooks는 단지 함수일 뿐이며, 이들을 조합하는 방법을 배우는 것이 React 마스터의 핵심이라는 것입니다.
Hooks 규칙 이해하기
개별 Hooks를 살펴보기 전에 eslint-plugin-react-hooks 패키지가 적용하는 두 가지 절대 규칙을 이해해야 합니다:
Hooks는 최상위 레벨에서만 호출하세요. 루프, 조건문, 중첩 함수 안에서 Hooks를 절대 호출하지 마세요. React는 Hooks가 호출되는 순서에 의존하여 렌더링 간 상태를 유지합니다. 순서가 바뀌면 React가 상태를 해당 Hook과 올바르게 연결할 수 없습니다.
Hooks는 React 함수에서만 호출하세요. Hooks는 함수형 컴포넌트나 다른 커스텀 Hooks에서만 호출할 수 있습니다. 일반 JavaScript 함수는
useState나useEffect를 호출할 수 없습니다.Google AdInline article slot
두 번째 규칙에서 관례가 따릅니다: Hooks를 호출하는 모든 함수는 use로 시작해야 합니다(예: useCustomHook). 린터는 이 접두사를 사용하여 규칙을 올바르게 적용합니다.
매일 사용하게 될 가장 필수적인 Hooks
useState: 컴포넌트 상태 관리하기
useState Hook은 가장 기본적인 Hook으로, 함수형 컴포넌트에 상태를 추가할 수 있게 해줍니다. 현재 상태 값과 이를 업데이트하는 함수라는 두 가지 요소를 가진 배열을 반환합니다.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 초기 상태는 0
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
이전 값을 기반으로 상태를 업데이트할 때는 함수형 업데이트 형식을 사용하여 경쟁 상태를 피하세요, 특히 React의 자동 배치 처리에서 중요합니다:
// 안전: 이전 상태 값을 사용
setCount(prevCount => prevCount + 1);
// 객체에도 안전: 이전 상태를 펼침
setUser(prevUser => ({ ...prevUser, name: 'Jane' }));
초기 상태 계산이 비용이 많이 드는 경우, 값을 전달하는 대신 함수를 전달하세요. React는 초기 렌더링에서만 이를 실행합니다:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
useEffect: 사이드 이펙트 처리하기
useEffect Hook을 사용하면 함수형 컴포넌트에서 데이터 가져오기, 구독 설정, DOM 수동 변경과 같은 사이드 이펙트를 수행할 수 있습니다. 기본적으로 이펙트는 모든 완료된 렌더링 후에 실행되지만, 의존성 배열을 사용하여 실행 시점을 제어할 수 있습니다.
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 이 이펙트는 컴포넌트가 마운트된 후와 userId가 변경될 때 실행됨
fetchUser(userId).then(data => setUser(data));
// 선택적 정리 함수는 다음 이펙트 전과 언마운트 시 실행됨
return () => {
// 보류 중인 요청 취소 또는 구독 정리
};
}, [userId]); // 의존성 배열: userId가 변경될 때만 다시 실행
return <div>{user?.name}</div>;
}
useEffect의 주요 패턴:
- 빈 의존성 배열
[]: 이펙트는 초기 렌더링(마운트) 후 한 번만 실행됩니다. - 의존성 배열 없음: 이펙트는 모든 렌더링 후에 실행됩니다(거의 원하는 동작이 아님).
- 정리 함수: 구독, 타이머, 보류 중인 네트워크 요청을 정리하여 메모리 누수를 방지합니다.
중요 경고: 모든 상태 변경에 이펙트가 필요한 것은 아닙니다. 렌더링 중에 값을 계산할 수 있다면 useEffect 대신 직접 계산하세요:
// ❌ 불필요한 이펙트
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ 렌더링 중 직접 계산
const fullName = `${firstName} ${lastName}`;
useContext: Prop Drilling 없이 컨텍스트 사용하기
useContext Hook을 사용하면 Consumer 컴포넌트로 감싸지 않고 React Context에서 값을 읽을 수 있습니다:
import { useContext } from 'react';
// 컨텍스트 생성
const ThemeContext = React.createContext('light');
function ThemedButton() {
// 컨텍스트 값을 직접 읽음
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
컨텍스트 제공자의 값이 변경되면 useContext를 사용하는 모든 컴포넌트가 자동으로 리렌더링됩니다.
useRef: 리렌더링을 트리거하지 않는 값 참조하기
useRef Hook은 .current 속성이 렌더링 간에 유지되는 변경 가능한 ref 객체를 생성합니다. 상태와 달리 ref를 업데이트해도 리렌더링이 발생하지 않습니다. 따라서 ref는 다음에 이상적입니다:
- DOM 요소 참조 저장
- 타임아웃 ID 추적
- UI 업데이트를 트리거하지 않아야 하는 값 유지
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
// 컴포넌트 마운트 후 입력 필드에 포커스
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
useReducer: 복잡한 상태 로직 관리하기
여러 하위 값이나 복잡한 전환이 포함된 상태의 경우 useReducer가 useState보다 더 적합한 경우가 많습니다. 리듀서 함수와 초기 상태를 받아 현재 상태와 디스패치 함수를 반환합니다:
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
</>
);
}
성능 최적화 Hooks
useMemo: 비용이 많이 드는 계산 캐싱하기
useMemo는 계산 결과를 캐시하고 의존성이 변경될 때만 다시 계산합니다:
import { useMemo } from 'react';
function TodoList({ todos, filter }) {
// todos나 filter가 변경될 때만 다시 계산
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.status === filter);
}, [todos, filter]);
return (
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
useCallback: 함수 참조 메모이제이션하기
useCallback은 의존성이 변경될 때만 변경되는 콜백 함수의 메모이제이션된 버전을 반환합니다. 이는 참조 동등성에 의존하여 불필요한 리렌더링을 방지하는 최적화된 자식 컴포넌트에 콜백을 전달할 때 특히 유용합니다:
import { useCallback } from 'react';
function ParentComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // 함수 참조가 렌더링 간에 안정적으로 유지됨
return <ChildButton onClick={handleClick} />;
}
커스텀 Hooks: 컴포넌트 간 로직 재사용하기
Hooks의 진정한 힘은 상태 저장 로직을 추출하고 재사용하기 위해 커스텀 Hooks를 만들 때 나타납니다. 커스텀 Hook은 이름이 use로 시작하고 다른 Hooks를 호출할 수 있는 JavaScript 함수일 뿐입니다.
예제: useLocalStorage
이 커스텀 Hook은 상태를 localStorage와 동기화하여 페이지 새로고침 간 데이터를 유지합니다:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
if (typeof window === 'undefined') return initialValue;
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// 사용법
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
);
}
예제: useMediaQuery
이 Hook은 CSS 미디어 쿼리(예: 사용자의 동작 감소 선호도)를 기반으로 조건부로 컴포넌트를 렌더링할 수 있게 해줍니다:
import { useState, useEffect } from 'react';
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia(query).matches;
});
useEffect(() => {
const mediaQueryList = window.matchMedia(query);
const onChange = (event) => setMatches(event.matches);
mediaQueryList.addEventListener('change', onChange);
return () => mediaQueryList.removeEventListener('change', onChange);
}, [query]);
return matches;
}
// 사용법
function MotionAwareComponent() {
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
return prefersReducedMotion ? <StaticHero /> : <AnimatedHero />;
}
React 19: 폼과 비동기 데이터를 위한 새로운 Hooks
React 19(2024년 12월 출시)는 폼 처리와 비동기 작업을 간소화하기 위해 여러 새로운 Hooks를 도입했습니다.
useActionState: 폼 제출 간소화하기
useActionState는 폼 제출 상태를 관리하며, 최신 결과, 액션 함수, 보류 중 플래그를 반환합니다:
import { useActionState } from 'react';
async function subscribeAction(prevState, formData) {
const email = formData.get('email');
try {
await subscribe(email);
return { ok: true };
} catch (error) {
return { ok: false, error: error.message };
}
}
function NewsletterForm() {
const [state, formAction, isPending] = useActionState(
subscribeAction,
{ ok: false }
);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Subscribing...' : 'Subscribe'}
</button>
{state.ok && <p>You're subscribed!</p>}
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
</form>
);
}
useFormStatus: 부모 폼 상태 접근하기
useFormStatus는 중첩된 컴포넌트가 부모 폼이 제출 중인지 알 수 있게 해줍니다:
import { useFormStatus } from 'react';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
);
}
Redux와 통합: useSelector Hook
React와 함께 Redux를 사용할 때 useSelector Hook은 Redux 스토어에서 상태에 접근하는 현대적이고 권장되는 방법입니다. 이는 기존의 connect 고차 컴포넌트를 대체하며 TypeScript와 더 잘 작동합니다.
import { useSelector, useDispatch } from 'react-redux';
function CounterComponent() {
// Redux 스토어에서 특정 데이터 추출
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>Increment</button>
</div>
);
}
중요: useSelector는 기본적으로 엄격한 === 참조 동등성을 사용합니다. 매번 새 객체를 반환하면 리렌더링이 강제됩니다. 성능을 최적화하려면 메모이제이션된 셀렉터를 사용하거나 개별 필드에 대해 useSelector를 여러 번 호출하세요:
// ❌ 모든 액션에서 리렌더링 발생
const { count, user } = useSelector((state) => ({
count: state.count,
user: state.user
}));
// ✅ 각 필드에 대해 useSelector 호출
const count = useSelector((state) => state.count);
const user = useSelector((state) => state.user);
출처
- React 공식 문서: 내장 React Hooks
- React 공식 문서: Hooks API 참조
- React Redux 문서: Hooks API
- Patterns.dev: Hooks 패턴
- React 공식 문서: State Hook 사용하기
— Editorial Team
아직 댓글이 없습니다.