본문 바로가기
DEV/FE

[FE] 아토믹 디자인 패턴에서 탈출: 컴포넌트 디자인 패턴, 방법론 찾아보기

by krokerdile 2024. 10. 7.

개요

프론트엔드 개발자를 희망하는 사람으로서 다양한 프로젝트를 진행하면서, 저는 몇 가지 디자인 패턴과 방법론을 경험해 왔습니다. 특히 React를 사용한 프로젝트에서는 아토믹 디자인 패턴이 큰 유행을 끌었고, 저 역시 이를 적용해 보았습니다. 그 과정에서 유행을 따라가다 보니 오히려 다른 디자인 패턴과 방법론을 많이 못 사용해본 문제도 생겼던 것 같습니다.

하지만 시간이 지나면서 아토믹 디자인 패턴이 모든 상황에 적합한 것은 아니라는 점을 깨달았습니다. 작은 규모의 프로젝트에서는 오히려 과도한 구조화로 인해 복잡성만 증가하는 경우가 있었습니다. 또한, 프로젝트의 특성에 따라 다른 패턴이나 방법론이 더 효과적일 수 있다는 것을 알게 되었습니다.

이러한 경험을 바탕으로, 이번에 새로운 React 프로젝트를 시작하기에 앞서 다양한 컴포넌트 디자인 패턴과 프론트엔드 디자인 방법론을 살펴보고 정리해보고자 합니다. 이를 통해 프로젝트의 규모와 요구사항에 가장 적합한 접근 방식을 선택할 수 있기를 기대합니다.

이 글에서는 주요 React 컴포넌트 패턴, 프론트엔드 디자인 방법론, 그리고 CSS 방법론에 대해 살펴볼 것입니다. 각 패턴과 방법론의 장단점을 분석하고, 실제 코드 예시를 통해 이해도를 올리면서 저도 정리를 해보려고 합니다.

컴포넌트 기반 디자인 패턴

컴포넌트 기반 아키텍처

컴포넌트 기반 아키텍처는 UI를 재사용 가능한 독립적인 조각으로 나누는 패턴입니다.

장점:

  • 재사용성과 유지보수성 향상
  • 개발 프로세스 간소화
  • 테스트 용이성

단점:

  • 초기 설계에 시간 소요
  • 과도한 분리로 인한 복잡성 증가 가능성

예시:

// Button 컴포넌트
const Button = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

// 사용 예
const App = () => (
  <div>
    <Button label="Click me" onClick={() => console.log('Clicked!')} />
  </div>
);

컨테이너/프레젠테이션 패턴

이 패턴은 컴포넌트를 로직 처리용 컨테이너와 UI 표시용 프레젠테이션 컴포넌트로 분리합니다.

장점:

  • 관심사 분리
  • 재사용성 향상
  • 테스트 용이성

단점:

  • 파일 수 증가
  • 간단한 컴포넌트에 과도할 수 있음

예시:

// 프레젠테이션 컴포넌트
const UserInfo = ({ name, email }) => (
  <div>
    <h2>{name}</h2>
    <p>{email}</p>
  </div>
);

// 컨테이너 컴포넌트
const UserInfoContainer = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser().then(setUser);
  }, []);

  if (!user) return <div>Loading...</div>;
  return <UserInfo name={user.name} email={user.email} />;
};

복합 컴포넌트 패턴

복합 컴포넌트 패턴은 관련 컴포넌트들을 그룹화하여 더 복잡한 UI를 구성합니다.

장점:

  • 유연성과 재사용성
  • 관련 컴포넌트 간 상태 공유 용이
  • 직관적인 API

단점:

  • 초기 학습 곡선
  • 과도한 사용 시 복잡성 증가

예시:

const Toggle = ({ children }) => {
  const [on, setOn] = useState(false);
  return Children.map(children, child =>
    cloneElement(child, { on, toggle: () => setOn(!on) })
  );
};

Toggle.On = ({ on, children }) => (on ? children : null);
Toggle.Off = ({ on, children }) => (on ? null : children);
Toggle.Button = ({ on, toggle }) => (
  <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
);

// 사용 예
const App = () => (
  <Toggle>
    <Toggle.On>The toggle is on</Toggle.On>
    <Toggle.Off>The toggle is off</Toggle.Off>
    <Toggle.Button />
  </Toggle>
);

고차 컴포넌트 (HOC) 패턴

HOC는 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수입니다.

장점:

  • 로직 재사용
  • 컴포넌트 기능 확장
  • 관심사 분리

단점:

  • 래퍼 지옥 가능성
  • 디버깅 어려움
  • prop 충돌 가능성

예시:

const withLoader = (WrappedComponent) => {
  return ({ isLoading, ...props }) => {
    if (isLoading) return <div>Loading...</div>;
    return <WrappedComponent {...props} />;
  };
};

const MyComponent = ({ data }) => <div>{data}</div>;
const EnhancedComponent = withLoader(MyComponent);

// 사용 예
const App = () => (
  <EnhancedComponent isLoading={false} data="Hello, World!" />
);

 

Render/Props 패턴

Render/Props 패턴은 컴포넌트 간에 값을 공유하는 유연한 방법을 제공합니다.

장점:

  • 코드 재사용성
  • 컴포넌트 구성의 유연성
  • HOC의 단점 일부 해결

단점:

  • 콜백 지옥 가능성
  • 성능 이슈 가능성

예시:

const Mouse = ({ render }) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({ x: event.clientX, y: event.clientY });
  };

  return (
    <div onMouseMove={handleMouseMove}>
      {render(position)}
    </div>
  );
};

// 사용 예
const App = () => (
  <Mouse render={({ x, y }) => (
    <h1>The mouse position is ({x}, {y})</h1>
  )} />
);

디자인 방법론

1. 아토믹 디자인 (Atomic Design)

설명: UI를 원자(Atoms), 분자(Molecules), 유기체(Organisms), 템플릿(Templates), 페이지(Pages)의 5단계로 구분하여 구성하는 방법론입니다.

장점:

  • 재사용성과 일관성 향상
  • 유지보수 용이성
  • 디자인 시스템 구축에 효과적

단점:

  • 초기 설정에 시간 소요
  • 작은 프로젝트에는 과도할 수 있음

코드 예시:

// Atom: Button
const Button = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

// Molecule: SearchBar
const SearchBar = ({ onSearch }) => (
  <div>
    <input type="text" placeholder="Search..." />
    <Button label="Search" onClick={onSearch} />
  </div>
);

// Organism: Header
const Header = () => (
  <header>
    <Logo />
    <Navigation />
    <SearchBar onSearch={() => console.log('Searching...')} />
  </header>
);

실제로 적용한다면?(리디의 경우)

저도 사용을 하면서 경험한 부분이지만 아토믹 디자인 패턴은 이론적으로는 명확해 보입니다. 하지만 실제 프로젝트에 적용할 때는 여러 가지 고려사항과 도전 과제가 있습니다. 이런 고민에 대해서 찾아보았을 때도 많은 개발자분들이 이 패턴을 프로젝트에 어떻게 효과적으로 녹여낼 수 있을지 고민하는 내용을 많이 읽었습니다. 이러한 과정에서 가장 많이 참고한 것이 바로 리디(Ridibooks)의 아토믹 디자인 패턴 적용 사례입니다.

리디가 전통적인 아토믹 디자인의 5단계 대신 3단계(Atoms, Blocks, Pages) 구조를 채택한 이유는 다음과 같습니다:

  1. 단순화와 효율성: 5단계 구조는 때로 불필요하게 복잡할 수 있습니다. 3단계 구조는 개발 프로세스를 단순화하고 효율성을 높입니다.
  2. 프로젝트 특성 반영: 리디의 서비스 특성상, 중간 단계(Molecules, Organisms)의 구분이 모호한 경우가 많았을 것입니다. Blocks라는 중간 단계로 통합함으로써 이러한 모호성을 해결했습니다.
  3. 유지보수 용이성: 더 적은 단계는 코드베이스의 구조를 더 명확하게 만들어, 유지보수와 새로운 팀원의 온보딩을 용이하게 합니다.
  4. 재사용성 강화: Blocks 단계는 여러 Atoms를 조합한 더 큰 단위의 재사용 가능한 컴포넌트를 만들 수 있게 해줍니다. 이는 개발 속도를 높이고 일관성을 유지하는 데 도움이 됩니다.
  5. 실용적 접근: 이론적인 구조보다는 실제 개발 과정에서 더 실용적이고 적용하기 쉬운 구조를 선택했습니다.

이러한 접근 방식은 아토믹 디자인의 핵심 원칙을 유지하면서도, 리디의 특정 요구사항과 개발 문화에 맞게 조정된 것입니다. 이는 디자인 시스템을 도입할 때 반드시 모든 이론적 단계를 그대로 따를 필요는 없으며, 각 회사나 프로젝트의 특성에 맞게 유연하게 적용할 수 있음을 보여줍니다.

리디의 아토믹 디자인 패턴 특징

1. 프로젝트 특성에 맞는 구조 설정

리디의 경우, 전통적인 아토믹 디자인의 5단계(Atoms, Molecules, Organisms, Templates, Pages) 대신 다음과 같은 구조를 채택했습니다.

  • Atoms: 기본 UI 요소
  • Blocks: 재사용 가능한 복합 컴포넌트
  • Pages: 실제 페이지 구현

2. 컴포넌트 분류 기준 설정

컴포넌트를 분류할 때 다음과 같은 기준을 고려할 수 있습니다:

  • 재사용성: 여러 페이지에서 사용되는 컴포넌트는 Atoms나 Blocks로 분류
  • 복잡성: 단순한 UI 요소는 Atoms로, 복잡한 구조는 Blocks로 분류
  • 의존성: 다른 컴포넌트에 의존하지 않는 독립적인 요소는 Atoms로 분류

3. 네이밍 규칙 수립

일관된 네이밍 규칙을 통해 컴포넌트의 역할과 위치를 쉽게 파악할 수 있습니다

`jsx*// Atoms*
Button.jsx
Input.jsx

*// Blocks*
ProductCard.jsx
SearchBar.jsx

*// Pages*
HomePage.jsx
ProductDetailPage.jsx`

4. 상태 관리 전략

컴포넌트의 상태 관리 방식을 결정합니다:

  • Atoms: 대부분 상태를 갖지 않는 순수 함수형 컴포넌트로 구현
  • Blocks: 필요한 경우 내부 상태를 가질 수 있음
  • Pages: 전역 상태나 서버 데이터를 관리하고 하위 컴포넌트에 전달

2. Feature-Sliced Design (FSD)

설명: 프로젝트를 기능(feature) 단위로 구조화하는 아키텍처 방법론입니다.

장점:

  • 확장성과 유지보수성 향상
  • 기능 단위의 독립성 보장
  • 팀 간 협업 용이성

단점:

  • 학습 곡선이 있음
  • 작은 프로젝트에는 과도할 수 있음

코드 예시 (디렉토리 구조):

src/
  shared/
    api/
    ui/
  entities/
    user/
    product/
  features/
    auth/
    cart/
  widgets/
    header/
    footer/
  pages/
    home/
    profile/
  app/

3. Component-Driven Development (CDD)

설명: UI를 독립적이고 재사용 가능한 컴포넌트로 분해하여 개발하는 방법론입니다.

장점:

  • 재사용성
  • 일관성
  • 개발 효율성

단점:

  • 초기 설계에 시간 소요
  • 과도한 분리로 인한 복잡성 증가 가능성

코드 예시:

// Button 컴포넌트
const Button = ({ label, onClick, variant = 'primary' }) => (
  <button className={`btn btn-${variant}`} onClick={onClick}>
    {label}
  </button>
);

// Card 컴포넌트
const Card = ({ title, content, actionLabel, onAction }) => (
  <div className="card">
    <h2 className="card-title">{title}</h2>
    <p className="card-content">{content}</p>
    <Button label={actionLabel} onClick={onAction} />
  </div>
);

// 사용 예
const App = () => (
  <div>
    <Card
      title="Welcome"
      content="This is a sample card."
      actionLabel="Learn More"
      onAction={() => console.log('Action clicked')}
    />
  </div>
);

4. Layered Architecture

설명: 애플리케이션을 여러 계층으로 나누어 구성하는 패턴입니다. 일반적으로 프레젠테이션, 비즈니스 로직, 데이터 액세스 계층으로 나눕니다.

장점:

  • 관심사 분리
  • 유지보수성 향상
  • 테스트 용이성
  • 확장성

단점:

  • 복잡성 증가
  • 계층 간 의존성 관리 필요
  • 작은 프로젝트에는 과도할 수 있음

코드 예시:

// Presentation Layer
const UserList = ({ users }) => (
  <ul>
    {users.map(user => <li key={user.id}>{user.name}</li>)}
  </ul>
);

// Business Logic Layer
const useUsers = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    UserService.getUsers().then(setUsers);
  }, []);

  return users;
};

// Data Access Layer
const UserService = {
  getUsers: async () => {
    const response = await fetch('/api/users');
    return response.json();
  }
};

// App Component (combines layers)
const App = () => {
  const users = useUsers();
  return <UserList users={users} />;
};

5. Micro Frontends

설명: 웹 애플리케이션을 더 작은, 독립적으로 개발 및 배포 가능한 프론트엔드 애플리케이션으로 분할하는 아키텍처 스타일입니다.

장점:

  • 독립적인 개발과 배포
  • 기술 스택의 유연성
  • 확장성과 유지보수성 향상
  • 팀 간 독립성

단점:

  • 초기 설정의 복잡성
  • 성능 오버헤드 가능성
  • 일관성 유지의 어려움
  • 테스트와 디버깅의 복잡성

코드 예시 (간단한 구현):

<!-- index.html -->
<html>
  <body>
    <div id="header"></div>
    <div id="product-list"></div>
    <div id="cart"></div>

    <script src="header.js"></script>
    <script src="product-list.js"></script>
    <script src="cart.js"></script>
  </body>
</html>
// header.js
class Header extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<header>My E-commerce Store</header>`;
  }
}
customElements.define('micro-header', Header);

document.getElementById('header').innerHTML = '<micro-header></micro-header>';

// product-list.js
class ProductList extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<ul><li>Product 1</li><li>Product 2</li></ul>`;
  }
}
customElements.define('micro-product-list', ProductList);

document.getElementById('product-list').innerHTML = '<micro-product-list></micro-product-list>';

// cart.js
class Cart extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<div>Cart (0 items)</div>`;
  }
}
customElements.define('micro-cart', Cart);

document.getElementById('cart').innerHTML = '<micro-cart></micro-cart>';

CSS 방법론

1. ITCSS (Inverted Triangle CSS)

설명: CSS 구조화를 위한 방법론으로, 특정성과 범위에 따라 스타일을 계층화합니다.

장점:

  • CSS 특정성 충돌 감소
  • 유지보수성 향상
  • 확장성 제공

단점:

  • 초기 설정에 시간 소요
  • 팀 전체의 이해와 동의 필요

코드 예시:

// Settings
$primary-color: #007bff;

// Tools
@mixin center-flex {
  display: flex;
  justify-content: center;
  align-items: center;
}

// Generic
* {
  box-sizing: border-box;
}

// Elements
body {
  font-family: Arial, sans-serif;
}

// Objects
.container {
  max-width: 1200px;
  margin: 0 auto;
}

// Components
.btn {
  padding: 10px 15px;
  background-color: $primary-color;
  color: white;
}

// Utilities
.text-center {
  text-align: center;
}

2. BEM (Block Element Modifier)

설명: CSS 클래스 명명 규칙을 제공하는 방법론입니다.

장점:

  • 명확한 구조
  • 재사용성
  • 모듈화

단점:

  • 클래스 이름이 길어질 수 있음
  • 과도한 중첩 가능성

코드 예시:

<div class="card">
  <div class="card__header">
    <h2 class="card__title">Title</h2>
  </div>
  <div class="card__body">
    <p class="card__text">Content</p>
  </div>
  <button class="card__button card__button--primary">Action</button>
</div>
.card { }
.card__header { }
.card__title { }
.card__body { }
.card__text { }
.card__button { }
.card__button--primary { }

결론

지금까지 다양한 컴포넌트 디자인 패턴, 프론트엔드 디자인 방법론, 그리고 CSS 방법론에 대해 살펴보았습니다. 이 과정에서 제가 깨달은 가장 중요한 점은 '정답'은 없다는 것입니다. 각 패턴과 방법론은 저마다의 장단점을 가지고 있으며, 우리가 해야 할 일은 프로젝트의 특성에 맞는 최선의 선택을 하는 것입니다.

제 경험상, 작은 프로젝트에서는 과도한 구조화가 오히려 독이 될 수 있습니다. 실제로 소규모의 프로젝트에서 아토믹 디자인 패턴을 적용했다가 컴포넌트가 필요에 비해 너무 많이 생성 되는 문제가 생긴적도 있습니다. 오히려 간단한 컴포넌트 기반 아키텍처나 BEM과 같은 CSS 방법론만으로도 충분히 깔끔하고 유지보수 가능한 코드를 작성할 수 있었습니다. 반면, 대규모 프로젝트를 진행할 때는 아토믹 디자인 패턴과 같은 더 체계적인 접근 방식이 큰 도움이 되었습니다.

하지만 어떤 방법론을 선택하든, 가장 중요한 것은 팀의 합의라고 생각합니다. 아무리 좋은 방법론이라도 팀원들이 이해하고 따르지 않는다면 무용지물이 됩니다. 저는 새로운 방법론을 도입할 때마다 팀 전체가 이를 이해하고 동의하는 과정을 거쳤고, 이는 프로젝트를 빠르게 진행하는데 도움이 되었습니다. 또한, 선택한 방법론에 얽매이지 않는 것도 중요하다고 생각합니다. 프로젝트가 진행되면서 처음 선택한 방법론이 적합하지 않다고 느껴질 때가 있었습니다. 당시에는 바로 방법론을 변경하지 못했지만, 리디의 경우와 같이 방법론을 프로젝트에 맞게 변경하는 모습을 보면서 응용이 필요함을 배울 수 있었습니다.

마지막으로 강조하고 싶은 점은, 이러한 패턴과 방법론은 결국 도구일 뿐이라는 것입니다. 도구에 집착하기보다는 그 도구를 어떻게 효과적으로 사용할지에 집중해야 된다고 생각합니다. 때로는 여러 방법론의 장점을 조합해 사용하는 것이 가장 효과적일 수도 있습니다.
프론트엔드 개발 분야는 정말 빠르게 변화하고 있습니다. 매년 컨퍼런스를 참석하고 영상을 보면서도 새로운 프레임워크, 라이브러리, 방법론이 계속해서 등장하고 사라지는 것이 보이는 것 같습니다. 이런 환경에서 결국 중요한 것은 끊임없이 학습하고 실험하는 자세가 아닐까 싶습니다. 물론 저도 아직 취업 준비를 하고 있는 개발자 0년차지만 말이죠.

참조

https://brunch.co.kr/@bf6b5403fa344c4/11
https://ghost4551.tistory.com/255
https://sumini.dev/til/007-design-system-references/
https://ridicorp.com/story/react-native-manta-app/