728x90
반응형
React 개발에서 자주 사용되는 디자인 패턴들을 알아보고, 언제 어떤 패턴을 사용해야 하는지 실용적인 예제와 함께 살펴보겠습니다.
1. 팩토리 패턴 (Factory Pattern)
데이터에 따라 적절한 컴포넌트를 선택합니다.
function ButtonFactory({ type, children, onClick }) {
if (type === 'primary') {
return <button className="bg-blue-500 text-white px-4 py-2" onClick={onClick}>{children}</button>;
}
if (type === 'danger') {
return <button className="bg-red-500 text-white px-4 py-2" onClick={onClick}>{children}</button>;
}
return <button className="bg-gray-300 px-4 py-2" onClick={onClick}>{children}</button>;
}
// 아이콘 팩토리
const icons = {
'home': () => lazy(() => import('./HomeIcon.tsx')),
}
function Icon({ name, size = 24 }) {
const iconFactory = icons[name];
const Component = iconFactory ? iconFactory() : null;
return (
<Component size={size}/>
);
}
// 사용 예시
function App() {
return (
<div>
<ButtonFactory type="primary">저장</ButtonFactory>
<ButtonFactory type="danger">삭제</ButtonFactory>
<Icon name="heart" size={32} />
<Icon name="star" />
</div>
);
}
2. 싱글톤 패턴 (Singleton Pattern)
앱 전체에서 공유하는 하나의 인스턴스를 만듭니다.
// 간단한 전역 스토어
let globalUser = null;
let listeners = [];
const userStore = {
getUser: () => globalUser,
setUser: (user) => {
globalUser = user;
listeners.forEach(callback => callback(user));
},
subscribe: (callback) => {
listeners.push(callback);
return () => {
listeners = listeners.filter(l => l !== callback);
};
}
};
// 스토어를 사용하는 Hook
function useUser() {
const [user, setUser] = useState(userStore.getUser());
useEffect(() => {
const unsubscribe = userStore.subscribe(setUser);
return unsubscribe;
}, []);
return {
user,
login: (newUser) => userStore.setUser(newUser),
logout: () => userStore.setUser(null)
};
}
// 같은 사용자 정보를 사용하는 컴포넌트들
function Header() {
const { user, logout } = useUser();
return (
<header>
{user ? (
<div>
안녕하세요, {user.name}님!
<button onClick={logout}>로그아웃</button>
</div>
) : (
<div>로그인해주세요</div>
)}
</header>
);
}
function Profile() {
const { user } = useUser();
if (!user) return <div>로그인되지 않음</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
3. 옵저버 패턴 (Observer Pattern)
변화를 감지하는 컴포넌트들을 만듭니다.
// 간단한 이벤트 시스템
const events = {
listeners: {},
on: (event, callback) => {
if (!events.listeners[event]) {
events.listeners[event] = [];
}
events.listeners[event].push(callback);
},
emit: (event, data) => {
if (events.listeners[event]) {
events.listeners[event].forEach(callback => callback(data));
}
}
};
// 다른 컴포넌트들이 감시할 수 있는 장바구니
function useCart() {
const [items, setItems] = useState([]);
const addItem = (item) => {
const newItems = [...items, item];
setItems(newItems);
events.emit('cart-changed', newItems);
};
return { items, addItem };
}
// 장바구니 변화를 감시하는 카운터
function CartCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
events.on('cart-changed', (items) => {
setCount(items.length);
});
}, []);
return <div>장바구니: {count}개 상품</div>;
}
// 장바구니에 상품을 추가하는 제품 컴포넌트
function Product({ product }) {
const { addItem } = useCart();
return (
<div>
<h3>{product.name}</h3>
<p>₩{product.price}</p>
<button onClick={() => addItem(product)}>
장바구니에 추가
</button>
</div>
);
}
4. 전략 패턴 (Strategy Pattern)
같은 작업을 다양한 방법으로 수행합니다.
const sortMethods = {
name: (items) => [...items].sort((a, b) => a.name.localeCompare(b.name)),
price: (items) => [...items].sort((a, b) => a.price - b.price),
newest: (items) => [...items].sort((a, b) => new Date(b.date) - new Date(a.date))
};
function ProductList({ products }) {
const [sortBy, setSortBy] = useState('name');
const sortedProducts = sortMethods[sortBy](products);
return (
<div>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">이름순</option>
<option value="price">가격순</option>
<option value="newest">최신순</option>
</select>
{sortedProducts.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
// 사용 예시 - 폼 검증
const validators = {
required: (value) => value ? null : '필수 입력 항목입니다',
email: (value) => /\S+@\S+\.\S+/.test(value) ? null : '올바른 이메일을 입력해주세요',
minLength: (length) => (value) => value.length >= length ? null : `최소 ${length}자 이상 입력해주세요`
};
function SignupForm() {
return (
<form>
<FormField
name="email"
type="email"
validation={['required', 'email']}
placeholder="이메일"
/>
<FormField
name="password"
type="password"
validation={['required', validators.minLength(8)]}
placeholder="비밀번호"
/>
</form>
);
}
5. 커맨드 패턴 (Command Pattern)
작업을 저장하여 실행 취소할 수 있게 만듭니다.
function useUndo() {
const [history, setHistory] = useState([]);
const [current, setCurrent] = useState(-1);
const execute = (action, undo) => {
const newHistory = history.slice(0, current + 1);
newHistory.push({ action, undo });
setHistory(newHistory);
setCurrent(newHistory.length - 1);
action();
};
const undo = () => {
if (current >= 0) {
history[current].undo();
setCurrent(current - 1);
}
};
const redo = () => {
if (current < history.length - 1) {
const next = current + 1;
history[next].action();
setCurrent(next);
}
};
return {
execute,
undo,
redo,
canUndo: current >= 0,
canRedo: current < history.length - 1
};
}
function DrawingApp() {
const [lines, setLines] = useState([]);
const { execute, undo, redo, canUndo, canRedo } = useUndo();
const addLine = (line) => {
execute(
() => setLines(prev => [...prev, line]),
() => setLines(prev => prev.slice(0, -1))
);
};
const clearAll = () => {
const oldLines = lines;
execute(
() => setLines([]),
() => setLines(oldLines)
);
};
return (
<div>
<button onClick={undo} disabled={!canUndo}>실행 취소</button>
<button onClick={redo} disabled={!canRedo}>다시 실행</button>
<button onClick={clearAll}>전체 삭제</button>
<div>
{lines.map((line, i) => (
<div key={i}>{line}</div>
))}
</div>
<button onClick={() => addLine(`라인 ${lines.length + 1}`)}>
라인 추가
</button>
</div>
);
}
6. 데코레이터 패턴 (Decorator Pattern)
컴포넌트에 추가 기능을 덧붙입니다.
function withLoading(Component) {
return function LoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>로딩 중...</div>;
}
return <Component {...props} />;
};
}
// 에러 처리 추가
function withError(Component) {
return function ErrorComponent({ error, ...props }) {
if (error) {
return <div style={{color: 'red'}}>에러: {error}</div>;
}
return <Component {...props} />;
};
}
// 원본 컴포넌트
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// 강화된 컴포넌트
const EnhancedUserList = withError(withLoading(UserList));
function App() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(setUsers)
.catch(setError)
.finally(() => setLoading(false));
}, []);
return (
<EnhancedUserList
users={users}
isLoading={loading}
error={error}
/>
);
}
7. 어댑터 패턴 (Adapter Pattern)
서로 다른 형태의 데이터를 같은 방식으로 동작하게 만듭니다.
// 서로 다른 데이터 형식
const oldApiUser = {
user_name: 'John',
user_email: 'john@email.com',
profile_pic: 'pic.jpg'
};
const newApiUser = {
name: 'John',
email: 'john@email.com',
avatar: 'pic.jpg'
};
// 어댑터 함수
function adaptUser(user, format) {
if (format === 'old') {
return {
name: user.user_name,
email: user.user_email,
avatar: user.profile_pic
};
}
return user; // 이미 새로운 형식
}
function UserCard({ user, format = 'new' }) {
const adaptedUser = adaptUser(user, format);
return (
<div>
<img src={adaptedUser.avatar} alt={adaptedUser.name} />
<h3>{adaptedUser.name}</h3>
<p>{adaptedUser.email}</p>
</div>
);
}
// 다른 데이터 형식과 함께 사용
function App() {
return (
<div>
<UserCard user={oldApiUser} format="old" />
<UserCard user={newApiUser} format="new" />
</div>
);
}
8. 컴파운드 컴포넌트 (Compound Components)
함께 동작하는 컴포넌트들을 만듭니다.
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
);
}
Modal.Header = function ModalHeader({ children }) {
return <div className="modal-header">{children}</div>;
};
Modal.Body = function ModalBody({ children }) {
return <div className="modal-body">{children}</div>;
};
Modal.Footer = function ModalFooter({ children }) {
return <div className="modal-footer">{children}</div>;
};
// 사용하기 쉬운 모달
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>모달 열기</button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<Modal.Header>
<h2>항목 삭제</h2>
</Modal.Header>
<Modal.Body>
<p>정말로 삭제하시겠습니까?</p>
</Modal.Body>
<Modal.Footer>
<button onClick={() => setShowModal(false)}>취소</button>
<button onClick={() => setShowModal(false)}>삭제</button>
</Modal.Footer>
</Modal>
</div>
);
}
9. 커스텀 훅 (Custom Hooks)
컴포넌트 간에 로직을 공유합니다.
// 간단한 데이터 페칭 훅
function useApi(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 UserProfile() {
const { data: user, loading, error } = useApi('/api/user');
if (loading) return <div>사용자 정보 로딩 중...</div>;
if (error) return <div>사용자 정보 로딩 실패</div>;
return <div>안녕하세요, {user.name}님!</div>;
}
function UserPosts() {
const { data: posts, loading, error } = useApi('/api/posts');
if (loading) return <div>게시글 로딩 중...</div>;
if (error) return <div>게시글 로딩 실패</div>;
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
);
}
언제 어떤 패턴을 사용할까?
📋 패턴 선택 가이드
- 팩토리 패턴: 데이터 타입에 따라 다른 컴포넌트가 필요할 때
- 싱글톤 패턴: 사용자 상태, 테마 같은 전역 데이터가 필요할 때
- 옵저버 패턴: 이벤트에 반응하는 컴포넌트들이 필요할 때
- 전략 패턴: 같은 작업을 다양한 방법으로 수행해야 할 때
- 커맨드 패턴: 실행 취소/다시 실행 기능이 필요할 때
- 데코레이터 패턴: 기존 컴포넌트를 수정하지 않고 기능을 추가할 때
- 어댑터 패턴: 서로 다른 데이터 형식을 처리해야 할 때
- 컴파운드 컴포넌트: 함께 사용되는 컴포넌트들을 만들 때
- 커스텀 훅: 컴포넌트 간에 로직을 공유해야 할 때
💡 핵심 원칙
디자인 패턴은 멋있어 보이기 때문에 사용하는 것이 아니라, 실제 문제를 해결할 때 사용해야 합니다.
단순함을 유지하고, 패턴이 코드를 더 복잡하게 만든다면 사용하지 마세요. 좋은 코드는 읽기 쉽고 이해하기 쉬운 코드입니다.
마무리
이러한 패턴들을 적절히 활용하면 더 유지보수하기 쉽고 확장 가능한 React 애플리케이션을 만들 수 있습니다. 각 패턴의 목적을 이해하고, 프로젝트의 요구사항에 맞는 패턴을 선택하는 것이 중요합니다.
패턴을 과도하게 사용하지 말고, 정말 필요한 곳에서만 사용하여 코드의 복잡성을 관리하세요!
728x90
반응형