고급 Hook 패턴
4. 고급 Hook 패턴
React Hooks를 활용한 고급 패턴들을 학습해보겠습니다. 이 패턴들을 통해 더 재사용 가능하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
커스텀 Hook 고급 패턴
1. useToggle Hook
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(prev => !prev), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, { toggle, setTrue, setFalse, setValue }];
}
// 사용 예제
function ToggleExample() {
const [isVisible, { toggle, setTrue, setFalse }] = useToggle(false);
const [isEnabled, { toggle: toggleEnabled }] = useToggle(true);
return (
<div>
<h2>Toggle Hook 예제</h2>
<div>
<button onClick={toggle}>
{isVisible ? '숨기기' : '보이기'}
</button>
<button onClick={setTrue}>항상 보이기</button>
<button onClick={setFalse}>항상 숨기기</button>
{isVisible && <p>토글로 제어되는 내용입니다.</p>}
</div>
<div>
<button onClick={toggleEnabled}>
기능 {isEnabled ? '비활성화' : '활성화'}
</button>
<input
disabled={!isEnabled}
placeholder="기능 상태에 따라 활성화됩니다"
/>
</div>
</div>
);
}
2. useLocalStorage Hook
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// localStorage에서 초기값 읽기
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`localStorage에서 ${key}를 읽는 중 오류 발생:`, error);
return initialValue;
}
});
// 값 설정 함수
const setValue = (value) => {
try {
// 함수일 경우 현재 값을 인자로 전달
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`localStorage에 ${key}를 저장하는 중 오류 발생:`, error);
}
};
return [storedValue, setValue];
}
// 사용 예제
function LocalStorageExample() {
const [name, setName] = useLocalStorage('userName', '');
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [settings, setSettings] = useLocalStorage('userSettings', {
notifications: true,
language: 'ko'
});
return (
<div>
<h2>LocalStorage Hook 예제</h2>
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="이름을 입력하세요"
/>
{name && <p>안녕하세요, {name}님!</p>}
</div>
<div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
테마 변경 (현재: {theme})
</button>
</div>
<div>
<label>
<input
type="checkbox"
checked={settings.notifications}
onChange={(e) => setSettings(prev => ({
...prev,
notifications: e.target.checked
}))}
/>
알림 받기
</label>
<select
value={settings.language}
onChange={(e) => setSettings(prev => ({
...prev,
language: e.target.value
}))}
>
<option value="ko">한국어</option>
<option value="en">English</option>
</select>
</div>
</div>
);
}
3. useFetch Hook
import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
// 실제 API 호출 대신 시뮬레이션
await new Promise(resolve => setTimeout(resolve, 1000));
// 가상의 데이터 생성
const mockData = {
users: [
{ id: 1, name: '김개발', email: 'kim@example.com' },
{ id: 2, name: '이디자인', email: 'lee@example.com' },
{ id: 3, name: '박기획', email: 'park@example.com' }
],
posts: [
{ id: 1, title: 'React Hook 패턴', content: 'Hook 패턴에 대한 설명...' },
{ id: 2, title: '성능 최적화', content: '성능 최적화 방법...' }
]
};
// URL에 따라 다른 데이터 반환
if (url.includes('users')) {
setData(mockData.users);
} else if (url.includes('posts')) {
setData(mockData.posts);
} else {
setData({ message: 'Hello from API' });
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// 사용 예제
function FetchExample() {
const [endpoint, setEndpoint] = useState('/api/users');
const { data, loading, error } = useFetch(endpoint);
return (
<div>
<h2>Fetch Hook 예제</h2>
<div>
<button onClick={() => setEndpoint('/api/users')}>
사용자 목록
</button>
<button onClick={() => setEndpoint('/api/posts')}>
게시글 목록
</button>
<button onClick={() => setEndpoint('/api/info')}>
기본 정보
</button>
</div>
{loading && <p>로딩 중...</p>}
{error && <p>오류 발생: {error}</p>}
{data && (
<div>
<h3>데이터:</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
</div>
);
}
컴파운드 컴포넌트 패턴
Modal 컴파운드 컴포넌트
import { createContext, useContext, useState } from 'react';
// Modal Context 생성
const ModalContext = createContext();
function useModal() {
const context = useContext(ModalContext);
if (!context) {
throw new Error('Modal 컴포넌트는 Modal.Provider 내부에서 사용해야 합니다');
}
return context;
}
// 메인 Modal 컴포넌트
function Modal({ children }) {
const [isOpen, setIsOpen] = useState(false);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
return (
<ModalContext.Provider value={{ isOpen, open, close }}>
{children}
</ModalContext.Provider>
);
}
// Modal 하위 컴포넌트들
Modal.Trigger = function ModalTrigger({ children }) {
const { open } = useModal();
return (
<button onClick={open}>
{children}
</button>
);
};
Modal.Content = function ModalContent({ children }) {
const { isOpen, close } = useModal();
if (!isOpen) return null;
return (
<div>
<div>
{children}
</div>
</div>
);
};
Modal.Header = function ModalHeader({ children }) {
return (
<div>
<h3>{children}</h3>
</div>
);
};
Modal.Body = function ModalBody({ children }) {
return <div>{children}</div>;
};
Modal.Footer = function ModalFooter({ children }) {
return (
<div>
{children}
</div>
);
};
Modal.CloseButton = function ModalCloseButton({ children = '닫기' }) {
const { close } = useModal();
return <button onClick={close}>{children}</button>;
};
// 사용 예제
function CompoundComponentExample() {
return (
<div>
<h2>컴파운드 컴포넌트 패턴</h2>
<Modal>
<Modal.Trigger>프로필 모달 열기</Modal.Trigger>
<Modal.Content>
<Modal.Header>사용자 프로필</Modal.Header>
<Modal.Body>
<p>이름: 김개발</p>
<p>이메일: kim@example.com</p>
<p>역할: React 개발자</p>
</Modal.Body>
<Modal.Footer>
<Modal.CloseButton>확인</Modal.CloseButton>
</Modal.Footer>
</Modal.Content>
</Modal>
<Modal>
<Modal.Trigger>설정 모달 열기</Modal.Trigger>
<Modal.Content>
<Modal.Header>애플리케이션 설정</Modal.Header>
<Modal.Body>
<label>
<input type="checkbox" /> 알림 받기
</label>
<br />
<label>
테마:
<select>
<option>라이트</option>
<option>다크</option>
</select>
</label>
</Modal.Body>
<Modal.Footer>
<button>저장</button>
<Modal.CloseButton>취소</Modal.CloseButton>
</Modal.Footer>
</Modal.Content>
</Modal>
</div>
);
}
Render Props 패턴을 Hook으로 변환
기존 Render Props vs Hook 패턴
import { useState, useEffect } from 'react';
// Hook 패턴으로 구현한 Mouse Tracker
function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return position;
}
// 윈도우 크기 추적 Hook
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return size;
}
// 온라인 상태 추적 Hook
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
// 사용 예제
function HookPatternExample() {
const mousePosition = useMouse();
const windowSize = useWindowSize();
const isOnline = useOnlineStatus();
return (
<div>
<h2>Hook 패턴 예제</h2>
<div>
<h3>마우스 위치</h3>
<p>X: {mousePosition.x}, Y: {mousePosition.y}</p>
</div>
<div>
<h3>윈도우 크기</h3>
<p>너비: {windowSize.width}px, 높이: {windowSize.height}px</p>
</div>
<div>
<h3>연결 상태</h3>
<p>{isOnline ? '온라인' : '오프라인'}</p>
</div>
</div>
);
}
고급 상태 관리 Hook
useReducer를 활용한 복잡한 상태 관리
import { useReducer, useContext, createContext } from 'react';
// 쇼핑카트 액션 타입
const CART_ACTIONS = {
ADD_ITEM: 'ADD_ITEM',
REMOVE_ITEM: 'REMOVE_ITEM',
UPDATE_QUANTITY: 'UPDATE_QUANTITY',
CLEAR_CART: 'CLEAR_CART',
APPLY_COUPON: 'APPLY_COUPON'
};
// 쇼핑카트 reducer
function cartReducer(state, action) {
switch (action.type) {
case CART_ACTIONS.ADD_ITEM:
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
case CART_ACTIONS.REMOVE_ITEM:
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case CART_ACTIONS.UPDATE_QUANTITY:
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
)
};
case CART_ACTIONS.CLEAR_CART:
return {
...state,
items: [],
coupon: null
};
case CART_ACTIONS.APPLY_COUPON:
return {
...state,
coupon: action.payload
};
default:
return state;
}
}
// 쇼핑카트 Hook
function useShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
coupon: null
});
const addItem = (item) => {
dispatch({ type: CART_ACTIONS.ADD_ITEM, payload: item });
};
const removeItem = (id) => {
dispatch({ type: CART_ACTIONS.REMOVE_ITEM, payload: id });
};
const updateQuantity = (id, quantity) => {
if (quantity <= 0) {
removeItem(id);
} else {
dispatch({ type: CART_ACTIONS.UPDATE_QUANTITY, payload: { id, quantity } });
}
};
const clearCart = () => {
dispatch({ type: CART_ACTIONS.CLEAR_CART });
};
const applyCoupon = (coupon) => {
dispatch({ type: CART_ACTIONS.APPLY_COUPON, payload: coupon });
};
const getTotalPrice = () => {
const subtotal = state.items.reduce((total, item) =>
total + (item.price * item.quantity), 0
);
if (state.coupon) {
return subtotal * (1 - state.coupon.discount / 100);
}
return subtotal;
};
const getTotalItems = () => {
return state.items.reduce((total, item) => total + item.quantity, 0);
};
return {
items: state.items,
coupon: state.coupon,
addItem,
removeItem,
updateQuantity,
clearCart,
applyCoupon,
totalPrice: getTotalPrice(),
totalItems: getTotalItems()
};
}
// 사용 예제
function ShoppingCartExample() {
const cart = useShoppingCart();
const products = [
{ id: 1, name: '노트북', price: 1500000 },
{ id: 2, name: '마우스', price: 50000 },
{ id: 3, name: '키보드', price: 100000 }
];
const coupons = [
{ id: 1, name: '10% 할인', discount: 10 },
{ id: 2, name: '20% 할인', discount: 20 }
];
return (
<div>
<h2>고급 상태 관리 예제</h2>
<div>
<h3>상품 목록</h3>
{products.map(product => (
<div key={product.id}>
<span>{product.name} - {product.price.toLocaleString()}원</span>
<button
onClick={() => cart.addItem(product)}
>
장바구니에 추가
</button>
</div>
))}
</div>
<div>
<h3>쿠폰</h3>
{coupons.map(coupon => (
<button
key={coupon.id}
onClick={() => cart.applyCoupon(coupon)}
>
{coupon.name}
</button>
))}
</div>
<div>
<h3>장바구니 ({cart.totalItems}개 아이템)</h3>
{cart.items.length === 0 ? (
<p>장바구니가 비어있습니다.</p>
) : (
<>
{cart.items.map(item => (
<div key={item.id}>
<span>
{item.name} - {item.price.toLocaleString()}원
</span>
<button onClick={() => cart.updateQuantity(item.id, item.quantity - 1)}>
-
</button>
<span>{item.quantity}</span>
<button onClick={() => cart.updateQuantity(item.id, item.quantity + 1)}>
+
</button>
<button
onClick={() => cart.removeItem(item.id)}
>
삭제
</button>
</div>
))}
<div>
{cart.coupon && (
<p>적용된 쿠폰: {cart.coupon.name}</p>
)}
<p>
총 금액: {Math.round(cart.totalPrice).toLocaleString()}원
</p>
<button onClick={cart.clearCart}>장바구니 비우기</button>
</div>
</>
)}
</div>
</div>
);
}
Hook 조합 패턴
여러 Hook을 조합한 복합 기능
import { useState, useEffect, useCallback } from 'react';
// 디바운스 Hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// API 검색 Hook
function useSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (!debouncedQuery) {
setResults([]);
return;
}
const searchUsers = async () => {
setLoading(true);
// 가상 검색 API 시뮬레이션
await new Promise(resolve => setTimeout(resolve, 500));
const mockUsers = [
{ id: 1, name: '김개발', email: 'kim@example.com' },
{ id: 2, name: '이디자인', email: 'lee@example.com' },
{ id: 3, name: '박기획', email: 'park@example.com' },
{ id: 4, name: '최마케팅', email: 'choi@example.com' },
{ id: 5, name: '정운영', email: 'jung@example.com' }
];
const filtered = mockUsers.filter(user =>
user.name.includes(debouncedQuery) || user.email.includes(debouncedQuery)
);
setResults(filtered);
setLoading(false);
};
searchUsers();
}, [debouncedQuery]);
return {
query,
setQuery,
results,
loading
};
}
// 검색 컴포넌트
function SearchExample() {
const { query, setQuery, results, loading } = useSearch();
const [selectedUser, setSelectedUser] = useState(null);
return (
<div>
<h2>Hook 조합 패턴 - 실시간 검색</h2>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="사용자 검색..."
/>
{loading && <p>검색 중...</p>}
{results.length > 0 && (
<div>
{results.map(user => (
<div
key={user.id}
onClick={() => setSelectedUser(user)}
>
<div>{user.name}</div>
<small>{user.email}</small>
</div>
))}
</div>
)}
{selectedUser && (
<div>
<h3>선택된 사용자</h3>
<p>이름: {selectedUser.name}</p>
<p>이메일: {selectedUser.email}</p>
<button onClick={() => setSelectedUser(null)}>선택 해제</button>
</div>
)}
</div>
);
}
정리
고급 Hook 패턴의 핵심 개념을 학습했습니다:
- 커스텀 Hook 패턴: useToggle, useLocalStorage, useFetch 등의 재사용 가능한 로직
- 컴파운드 컴포넌트: Context와 Hook을 조합한 유연한 컴포넌트 구조
- Render Props를 Hook으로 변환: 더 간결하고 재사용 가능한 코드
- 고급 상태 관리: useReducer를 활용한 복잡한 상태 로직
- Hook 조합: 여러 Hook을 조합하여 복합적인 기능 구현
- 성능 최적화: useCallback, useMemo와 함께 사용한 최적화 기법
- 실전 활용: 실제 프로젝트에서 사용할 수 있는 패턴들
이러한 고급 Hook 패턴들은 React 애플리케이션의 코드 재사용성을 높이고, 유지보수성을 향상시키며, 더 선언적인 코드 작성을 가능하게 합니다. 복잡한 로직을 Hook으로 분리하여 컴포넌트를 단순하게 유지하는 것이 중요합니다.
다음 장에서는 React의 테스팅에 대해 알아보겠습니다.