Chat Conversation

참여자: User, QueryPie

Message 1 from User

react 에서 gof 원칙을 지키는 예제를 보여줘

Message 2 from 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 애플리케이션을 더 유연하고 확장 가능하며 유지보수하기 쉽게 만들 수 있습니다.