카테고리 없음

react로 알아보는 디자인 패턴

긍정왕_JERRY 2025. 8. 10. 10:20
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
반응형