React 16.8에서 도입된 Hooks는 함수형 컴포넌트에서 상태 관리와 생명주기를 다룰 수 있게 해주었습니다. 현재 React 개발의 표준이 된 Hooks 패턴을 체계적으로 알아봅니다.
Hooks, introduced in React 16.8, allow functional components to handle state management and lifecycle. Let's systematically explore the Hooks patterns that have become the standard in React development.
useState: 상태 관리의 기본 useState: Basics of State Management
컴포넌트 내에서 변하는 값(상태)을 관리합니다. 상태가 변경되면 컴포넌트가 다시 렌더링됩니다.
Manages changing values (state) within a component. When state changes, the component re-renders.
function Counter() {
// [현재값, 설정함수] = useState(초기값)
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
증가
</button>
</div>
);
}
function Counter() {
// [currentValue, setter] = useState(initialValue)
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
const [user, setUser] = useState({ name: '', age: 0 });
// ❌ 잘못된 방법 (원본 객체 직접 수정)
// user.name = 'Kim';
// ✅ 올바른 방법 (새 객체 생성)
setUser({ ...user, name: 'Kim' });
// 이전 상태 기반 업데이트 (함수형 업데이트)
setCount(prev => prev + 1);
const [user, setUser] = useState({ name: '', age: 0 });
// ❌ Wrong way (directly mutating original object)
// user.name = 'Kim';
// ✅ Correct way (create new object)
setUser({ ...user, name: 'Kim' });
// Update based on previous state (functional update)
setCount(prev => prev + 1);
useEffect: 부수 효과 처리 useEffect: Handling Side Effects
데이터 fetching, 구독, DOM 조작 등 렌더링 외의 작업을 처리합니다. 클래스 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount를 대체합니다.
Handles operations outside rendering such as data fetching, subscriptions, and DOM manipulation. Replaces componentDidMount, componentDidUpdate, and componentWillUnmount from class components.
useEffect 실행 시점 When useEffect Runs
useEffect(() => {}, [])- 마운트 시 1회 실행useEffect(() => {}, [dep])- dep 변경 시마다 실행useEffect(() => {})- 매 렌더링마다 실행 (주의!)useEffect(() => {}, [])- Runs once on mountuseEffect(() => {}, [dep])- Runs when dep changesuseEffect(() => {})- Runs on every render (caution!)
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 데이터 패칭
async function fetchUser() {
setLoading(true);
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
setUser(data);
setLoading(false);
}
fetchUser();
}, [userId]); // userId가 바뀔 때마다 실행
if (loading) return <p>Loading...</p>;
return <p>{user.name}</p>;
}
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Data fetching
async function fetchUser() {
setLoading(true);
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
setUser(data);
setLoading(false);
}
fetchUser();
}, [userId]); // Runs whenever userId changes
if (loading) return <p>Loading...</p>;
return <p>{user.name}</p>;
}
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
// 클린업 함수 반환
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
// Return cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
useRef: DOM 접근과 값 유지 useRef: DOM Access and Value Persistence
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</>
);
}
// 렌더링과 무관한 값 저장 (리렌더링 안 됨)
const countRef = useRef(0);
countRef.current += 1; // 렌더링 유발 안 함
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</>
);
}
// Store value unrelated to rendering (no re-render)
const countRef = useRef(0);
countRef.current += 1; // Doesn't trigger re-render
useMemo와 useCallback: 성능 최적화 useMemo and useCallback: Performance Optimization
// useMemo: 값의 메모이제이션
const expensiveValue = useMemo(() => {
return heavyCalculation(data);
}, [data]); // data가 바뀔 때만 재계산
// useCallback: 함수의 메모이제이션
const handleClick = useCallback(() => {
console.log('Clicked!', count);
}, [count]); // count가 바뀔 때만 함수 재생성
// useMemo: Value memoization
const expensiveValue = useMemo(() => {
return heavyCalculation(data);
}, [data]); // Recalculates only when data changes
// useCallback: Function memoization
const handleClick = useCallback(() => {
console.log('Clicked!', count);
}, [count]); // Recreates function only when count changes
⚠️ 최적화 남용 주의 ⚠️ Beware of Over-Optimization
useMemo와 useCallback은 비용이 있습니다.
모든 곳에 사용하지 말고, 실제 성능 문제가 있는 경우에만 적용하세요.
useMemo and useCallback have costs.
Don't use them everywhere—only apply when there are real performance issues.
Custom Hooks: 로직 재사용 Custom Hooks: Logic Reuse
반복되는 상태 로직을 커스텀 훅으로 추출하여 재사용할 수 있습니다.
훅 이름은 반드시 use로 시작해야 합니다.
You can extract repetitive state logic into custom hooks for reuse.
Hook names must start with use.
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// 사용
const [theme, setTheme] = useLocalStorage('theme', 'dark');
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'dark');
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
컴포넌트 패턴 Component Patterns
{isLoggedIn && <Dashboard />}
{isLoggedIn ? <Dashboard /> : <Login />}
// 리스트 렌더링
{items.map(item => (
<Item key={item.id} data={item} />
))}
// Props 전달
<Button
variant="primary"
onClick={handleClick}
disabled={loading}
>
Submit
</Button>
{isLoggedIn && <Dashboard />}
{isLoggedIn ? <Dashboard /> : <Login />}
// List rendering
{items.map(item => (
<Item key={item.id} data={item} />
))}
// Props passing
<Button
variant="primary"
onClick={handleClick}
disabled={loading}
>
Submit
</Button>
💡 왜 이 패턴들이 타자 연습에 포함되었는가? 💡 Why are these patterns included in typing practice?
useState, useEffect, 조건부 렌더링, 리스트 맵핑은 React 개발에서 가장 자주 작성하는 코드입니다. 이 패턴들을 손에 익히면 React 컴포넌트를 더 빠르게 작성할 수 있습니다.
useState, useEffect, conditional rendering, and list mapping are the most frequently written code in React development. Mastering these patterns helps you write React components faster.