Chat Conversation
참여자: User, QueryPie
Message 1 from User
User
react 에서 gof 원칙을 지키는 예제를 보여줘
Message 2 from QueryPie
QueryPie
React에서 GoF(Gang of Four) 디자인 패턴 원칙을 적용하는 실용적인 예제들을 보여드리겠습니다.
주요 GoF 패턴의 React 적용 예제
1. Observer 패턴 - 상태 관리
Context API와 Custom Hook을 사용한 Observer 패턴 구현:
// Observer 패턴을 활용한 테마 관리
import React, { createContext, useContext, useReducer } from 'react';
// Subject (Observable)
const ThemeContext = createContext();
const themeReducer = (state, action) => {
switch (action.type) {
case 'TOGGLE_THEME':
return { ...state, isDark: !state.isDark };
case 'SET_PRIMARY_COLOR':
return { ...state, primaryColor: action.payload };
default:
return state;
}
};
// Theme Provider (Subject)
export const ThemeProvider = ({ children }) => {
const [state, dispatch] = useReducer(themeReducer, {
isDark: false,
primaryColor: '#007bff'
});
return (
<ThemeContext.Provider value={{ state, dispatch }}>
{children}
</ThemeContext.Provider>
);
};
// Observer Hook
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
// Observer Components
const Header = () => {
const { state, dispatch } = useTheme();
return (
<header style={{
backgroundColor: state.isDark ? '#333' : '#fff',
color: state.isDark ? '#fff' : '#333'
}}>
<button onClick={() => dispatch({ type: 'TOGGLE_THEME' })}>
{state.isDark ? '🌞' : '🌙'}
</button>
</header>
);
};
const Sidebar = () => {
const { state } = useTheme();
return (
<aside style={{
backgroundColor: state.primaryColor,
opacity: state.isDark ? 0.8 : 1
}}>
사이드바
</aside>
);
};
2. Factory 패턴 - 컴포넌트 생성
동적 컴포넌트 생성을 위한 Factory 패턴:
// Factory 패턴을 사용한 폼 필드 생성
import React from 'react';
// 제품들 (Products)
const TextInput = ({ name, value, onChange, placeholder }) => (
<input
type="text"
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
className="form-input"
/>
);
const EmailInput = ({ name, value, onChange, placeholder }) => (
<input
type="email"
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
className="form-input email"
/>
);
const SelectInput = ({ name, value, onChange, options }) => (
<select name={name} value={value} onChange={onChange} className="form-select">
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
const TextareaInput = ({ name, value, onChange, placeholder, rows = 4 }) => (
<textarea
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
rows={rows}
className="form-textarea"
/>
);
// Factory (Creator)
class FormFieldFactory {
static createField(type, props) {
const fieldTypes = {
text: TextInput,
email: EmailInput,
select: SelectInput,
textarea: TextareaInput
};
const FieldComponent = fieldTypes[type];
if (!FieldComponent) {
throw new Error(`Field type "${type}" is not supported`);
}
return <FieldComponent {...props} />;
}
}
// Client 사용 예제
const DynamicForm = () => {
const [formData, setFormData] = React.useState({
name: '',
email: '',
category: '',
message: ''
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const formFields = [
{
type: 'text',
props: {
name: 'name',
value: formData.name,
onChange: handleChange,
placeholder: '이름을 입력하세요'
}
},
{
type: 'email',
props: {
name: 'email',
value: formData.email,
onChange: handleChange,
placeholder: '이메일을 입력하세요'
}
},
{
type: 'select',
props: {
name: 'category',
value: formData.category,
onChange: handleChange,
options: [
{ value: '', label: '카테고리 선택' },
{ value: 'general', label: '일반' },
{ value: 'support', label: '지원' },
{ value: 'feedback', label: '피드백' }
]
}
},
{
type: 'textarea',
props: {
name: 'message',
value: formData.message,
onChange: handleChange,
placeholder: '메시지를 입력하세요'
}
}
];
return (
<form className="dynamic-form">
{formFields.map((field, index) => (
<div key={index} className="form-group">
{FormFieldFactory.createField(field.type, field.props)}
</div>
))}
<button type="submit">전송</button>
</form>
);
};
3. Strategy 패턴 - 알고리즘 교체
정렬 전략을 동적으로 변경하는 패턴:
// Strategy 패턴을 사용한 데이터 정렬
import React, { useState, useMemo } from 'react';
// Strategy Interface (전략 인터페이스)
class SortStrategy {
sort(data) {
throw new Error('Sort method must be implemented');
}
}
// Concrete Strategies (구체적인 전략들)
class NameAscStrategy extends SortStrategy {
sort(data) {
return [...data].sort((a, b) => a.name.localeCompare(b.name));
}
}
class NameDescStrategy extends SortStrategy {
sort(data) {
return [...data].sort((a, b) => b.name.localeCompare(a.name));
}
}
class PriceAscStrategy extends SortStrategy {
sort(data) {
return [...data].sort((a, b) => a.price - b.price);
}
}
class PriceDescStrategy extends SortStrategy {
sort(data) {
return [...data].sort((a, b) => b.price - a.price);
}
}
class DateStrategy extends SortStrategy {
sort(data) {
return [...data].sort((a, b) => new Date(b.date) - new Date(a.date));
}
}
// Context (컨텍스트)
class DataSorter {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
sort(data) {
return this.strategy.sort(data);
}
}
// React 컴포넌트에서 사용
const ProductList = () => {
const [sortType, setSortType] = useState('nameAsc');
const [products] = useState([
{ id: 1, name: '노트북', price: 1200000, date: '2024-01-15' },
{ id: 2, name: '마우스', price: 50000, date: '2024-02-20' },
{ id: 3, name: '키보드', price: 150000, date: '2024-01-10' },
{ id: 4, name: '모니터', price: 400000, date: '2024-03-05' }
]);
// Strategy 팩토리
const getStrategy = (type) => {
const strategies = {
nameAsc: new NameAscStrategy(),
nameDesc: new NameDescStrategy(),
priceAsc: new PriceAscStrategy(),
priceDesc: new PriceDescStrategy(),
date: new DateStrategy()
};
return strategies[type] || new NameAscStrategy();
};
// 정렬된 데이터 계산 (useMemo로 최적화)
const sortedProducts = useMemo(() => {
const sorter = new DataSorter(getStrategy(sortType));
return sorter.sort(products);
}, [products, sortType]);
return (
<div className="product-list">
<div className="sort-controls">
<label htmlFor="sort-select">정렬 기준:</label>
<select
id="sort-select"
value={sortType}
onChange={(e) => setSortType(e.target.value)}
>
<option value="nameAsc">이름 ↑</option>
<option value="nameDesc">이름 ↓</option>
<option value="priceAsc">가격 ↑</option>
<option value="priceDesc">가격 ↓</option>
<option value="date">최신순</option>
</select>
</div>
<div className="products">
{sortedProducts.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>가격: {product.price.toLocaleString()}원</p>
<p>등록일: {product.date}</p>
</div>
))}
</div>
</div>
);
};
4. Decorator 패턴 - HOC 활용
Higher-Order Component를 사용한 Decorator 패턴:
// Decorator 패턴을 사용한 컴포넌트 기능 확장
import React, { useState, useEffect } from 'react';
// Base Component
const SimpleButton = ({ children, onClick, ...props }) => (
<button onClick={onClick} {...props}>
{children}
</button>
);
// Decorator: Loading 기능 추가
const withLoading = (WrappedComponent) => {
return ({ loading, ...props }) => {
if (loading) {
return (
<WrappedComponent {...props} disabled>
<span className="loading-spinner">⏳</span> 로딩중...
</WrappedComponent>
);
}
return <WrappedComponent {...props} />;
};
};
// Decorator: Analytics 추가
const withAnalytics = (WrappedComponent) => {
return ({ analyticsEvent, ...props }) => {
const handleClick = (e) => {
// Analytics 이벤트 전송
if (analyticsEvent) {
console.log(`Analytics: ${analyticsEvent} clicked`);
// 실제로는 GA, Mixpanel 등으로 전송
}
if (props.onClick) {
props.onClick(e);
}
};
return <WrappedComponent {...props} onClick={handleClick} />;
};
};
// Decorator: Tooltip 추가
const withTooltip = (WrappedComponent) => {
return ({ tooltip, tooltipPosition = 'top', ...props }) => {
const [showTooltip, setShowTooltip] = useState(false);
if (!tooltip) {
return <WrappedComponent {...props} />;
}
return (
<div
className="tooltip-container"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<WrappedComponent {...props} />
{showTooltip && (
<div className={`tooltip tooltip-${tooltipPosition}`}>
{tooltip}
</div>
)}
</div>
);
};
};
// Decorator: 권한 체크 추가
const withPermission = (WrappedComponent) => {
return ({ requiredPermission, userPermissions = [], fallback, ...props }) => {
const hasPermission = !requiredPermission ||
userPermissions.includes(requiredPermission);
if (!hasPermission) {
return fallback || <div>권한이 없습니다</div>;
}
return <WrappedComponent {...props} />;
};
};
// 여러 Decorator를 조합한 Enhanced Button
const EnhancedButton = withPermission(
withTooltip(
withAnalytics(
withLoading(SimpleButton)
)
)
);
// 사용 예제
const App = () => {
const [isLoading, setIsLoading] = useState(false);
const userPermissions = ['read', 'write'];
const handleSave = async () => {
setIsLoading(true);
// 비동기 작업 시뮬레이션
await new Promise(resolve => setTimeout(resolve, 2000));
setIsLoading(false);
alert('저장 완료!');
};
return (
<div className="app">
<h2>Decorator 패턴 예제</h2>
<div className="button-examples">
{/* 기본 버튼 */}
<SimpleButton onClick={() => alert('기본 클릭')}>
기본 버튼
</SimpleButton>
{/* Loading Decorator만 적용 */}
<EnhancedButton
loading={isLoading}
onClick={handleSave}
>
저장하기
</EnhancedButton>
{/* 모든 Decorator 적용 */}
<EnhancedButton
loading={isLoading}
tooltip="이 버튼은 데이터를 저장합니다"
tooltipPosition="bottom"
analyticsEvent="save_button_clicked"
requiredPermission="write"
userPermissions={userPermissions}
onClick={handleSave}
className="primary-button"
>
고급 저장 버튼
</EnhancedButton>
{/* 권한 없는 경우 */}
<EnhancedButton
requiredPermission="admin"
userPermissions={userPermissions}
fallback={<button disabled>관리자 전용</button>}
tooltip="관리자 권한이 필요합니다"
onClick={() => alert('관리자 작업')}
>
관리자 버튼
</EnhancedButton>
</div>
</div>
);
};
핵심 원칙과 장점
개방-폐쇄 원칙 (Open-Closed Principle)
- 기존 컴포넌트를 수정하지 않고 새로운 기능 추가
- HOC와 Composition을 통한 확장성
단일 책임 원칙 (Single Responsibility Principle)
- 각 컴포넌트와 Hook이 하나의 명확한 책임만 가짐
- 관심사의 분리를 통한 유지보수성 향상
의존성 역전 원칙 (Dependency Inversion Principle)
- Props와 Context를 통한 느슨한 결합
- 추상화에 의존하는 설계
이러한 패턴들을 활용하면 React 애플리케이션을 더 유연하고 확장 가능하며 유지보수하기 쉽게 만들 수 있습니다.