본문으로 바로가기

이번에 링크드인 클론 프로젝트를 진행하면서 아토믹 컴포넌트 구조를 적용하게 되었는데, 아토믹 컴포넌트를 사용하면서 실제로 느꼈던 장점과 단점, 그리고 단점과 문제점들을 어떻게 개선했는지에 대해 애기해보고자 한다.

아토믹 컴포넌트(Atomic Component)란?

아토믹 컴포넌트는 brad frost가 고안한 디자인 시스템[1]으로, 화학적 관점에서 영감을 얻어 이를 컴포넌트 구조에 적용한 방식이다. 컴포넌트를 총 5가지 계층으로 나누게 되는데. atom(원자), molecule(분자), organism(유기체) 순으로 나누어진 컴포넌트를 template, page에서 사용하는 방식이다.

 

이 디자인 시스템의 장점은 매우 명확한데, 버려지는 컴포넌트 없이 모두 재활용할 수 있다는 것이다. 대부분의 서비스에서 컴포넌트는 특별한 계층으로 나누어 관리하지 않는다. 많은 서비스에서 컴포넌트를 share(공유하거나), page(페이지에 종속되거나)로 나누어 관리하고 있다.

 

이와 같이 컴포넌트를 관리할 경우 컴포넌트를 구분하기 쉽다는 장점이 있지만, 중복되거나 비슷한 구조의 컴포넌트를 여러개 만들어야 하는 상황이 생기곤 한다. 아토믹 컴포넌트는 분해할 수 없는 컴포넌트라는 개념을 도입하여 이러한 문제를 해결하고 있다.

 

label, input, button같은 컴포넌트는 최소 단위이기 때문에 분해될 수 없는 컴포넌트에 해당된다. 마찬가지로 텍스트를 작성할때 사용되는 p, span, h1등의 타이포그래피 컴포넌트도 최소 단위이다. 이러한 컴포넌트는 atom(원자)에 속하게 된다. 상위 계층에 속하는 molecule(분자), organism(유기체)에서 텍스트나 버튼을 사용하고자 하면 atom에서 가져와야만 한다.

 

위에서 말했던 label, input, button 을 조합하여 컴포넌트를 만들게 되면, 이것을 molecule(분자)로 판단한다. 용도에 따라 여러 칼리브레이션이 존재할 수 있는 계층으로, moleucle를 어떻게 관리하느냐에 따라 컴포넌트의 성격이 정의되기도 한다.

 

예를 들어서 label옆에 icon이라는 atom이 추가되어야 한다고 가정해보겠다. 이를 구현하는 방법으로는 1) icon이라는 prop을 추가한다 2) FormWithIcon이라는 컴포넌트를 만든다의 두가지 방법이 존재한다. 둘중 어느게 정답이라고 하기는 어렵고, 상황에 따라서 더 좋은 방법이 존재할 수 있을것이다.

 

이 외에도 organism, template, page같은 계층이 존재하고 각 계층마다 역할이 명확하게 주어져 있는 디자인 시스템이다.

아토믹 컴포넌트의 단점

중복이 없는 컴포넌트를 운용한다는 면에 있어서 아토믹 컴포넌트 방법론은 완벽에 가깝게 느껴질 수 있다. 하지만 이러한 디자인 시스템도 단점을 가지고 있다.

Molecule와 Organism의 경계가 불명확하다.

아토믹 컴포넌트에서는 기본적으로 atom이 모여서 molecule가 되고, molecule가 모여서 oraganism이 된다. 이렇게만 봤을때는 오해의 소지가 없어보이지만, 실제로 개발에 들어갔을때는 애기가 또 다르다. 다음의 예를 보도록 하자.

 


이와 같은 디자인을 컴포넌트로 구현할때, molecule는 무엇이 되어야 할까?

 


위와 같이 1개 이상의 atom을 조합한 컴포넌트를 molecule로 정한다고 해보자(빨간색이 molecule, 파란색이 atom이다). 이렇게 구현했을 경우 organism에서 atom을 불러와 직접 사용해야 하는 상황이 발생하게 된다.

 

이같은 방식은 어디서 어디까지가 molecule고, 어디까지가 organism인지 모호하게 만든다. 원하는대로 묶어버려도 되겠지만, 가장 큰 문제는 정확한 기준이 없다는 것이다. 필자는 이러한 문제를 해결하기 위해 다음과 같이 컴포넌트를 구성하였다.

 


빨간색은 molecule, 초록색은 organism이다. 사실상 molecule를 organism의 하위에 종속시키는 방식을 사용하였다. 아토믹 컴포넌트의 가장 중요한 개념은 모든 것을 재사용하는것이다. 모든 계층의 컴포넌트가 특정한 페이지나 개념에 종속되는걸 막고 있지는 않다.

 

물론 위처럼 구현하였을 경우 다른 page, organism에서 해당 컴포넌트를 불러와 사용하고자 하는 상황에서 문제가 생기지 않을까, 라고 생각할 수 있다. 하지만 data fetch은 organism에서만 사용하고 있고, 하위 계층인 atom, molecule는 prop으로만 작동하기 때문에 파일 명만 잘 정해두면 언제든지 불러와 사용할 수 있다.

 

보통 컴포넌트를 구현한다면 계층을 나누어서 폴더화 시키는게 일반적일 것이다. 아래와 같이 말이다.

  • profileSnb/
    • Header
    • profileSnbItems/
      • Connections
      • Viewer
      • Premium
      • MyItems
    • profileAdiitionals
      • List
      • More

그러나 아토믹 컴포넌트에서는 좀 다르게 접근해야 한다. 폴더화를 시키는 대신 파일명으로 수준을 표현하는 방식이다.

  • profileSnb/
    • ProfileSnbHeader
    • ProfileSnbConnections
    • ProfileSnbViewer
    • ProfileSnbPremium
    • ProfileSnbMyItems
    • ProfileSnbAdditionalList
    • ProfileSnbAdiitionalMore

이처럼 구현하면 다른 컴포넌트에서 해당 컴포넌트를 재사용하고자 할때, <Viewer/> 대신 <ProfileSnbViewer>로 사용할 수 있어 네이밍이 겹치지는 상황을 피할 수 있게된다.

template, page까지 사용하면서 생기는 props drilling

만약 아토믹 컴포넌트를 정석대로 사용하고자 하면 template과 page까지 사용해야 한다. page는 react에서 사용하는 페이지를 사용한다고 가정하고, template에 organism을 배치하고, page에 template를 배치하는 방식으로 구현하는것이 정석이다.

 

그러나 이와 같은 방식으로 구현할 경우 page에서 data fetching을 하게 되고, 무려 5계층을 내려가면서 prop을 전달해야 하기 때문에 props drilling 이슈가 발생하게 된다.

export interface Props {
  bannerImg: StaticImageData;
  avatarImg: StaticImageData;
  title: string;
  desc: string;
}

const SnbProfileHeader = ({
  bannerImg,
  avatarImg,
  title,
  desc,
  ...rest
}: Props) => {
  return (
    <SnbProfileHeaderStyled {...rest}>
      <BannerImageWrap>
        <BannerImage src={bannerImg} layout="fill" objectFit="cover" />
      </BannerImageWrap>

      <AvatarImageWrap>
        <AvatarImage src={avatarImg} layout="fill" objectFit="cover" />
      </AvatarImageWrap>

      <PLinkWrak>
        <P>{title}</P>
      </PLinkWrak>
      <PWrap>
        <P fontSize={12} color="grayPoint6">
          {desc}
        </P>
      </PWrap>
    </SnbProfileHeaderStyled>
  );
};
export default SnbProfileHeader;

코드를 보면 Props를 컴포넌트 함수의 타입으로 받아주고 있다. 만약 상위 계층에서 내려줘야 할 prop의 자료형이 변경된다면 관련된 모든 컴포넌트의 prop을 변경해줘야 하는 상황이 발생한다. 뿐만 아니라 prop을 사용해서 데이터를 주입받을 경우, 이를 추적하기가 어렵고 코드가 복잡해지게 된다.

 

이를 해결하기 위해 고민을 오랫동안 하였고, 결과적으로 아토믹 컴포넌트에서 template와 page는 사용하지 않도록 하였다. 이 말은 곧 data fetching등의 컨텍스트 관련 로직을 oragnism에서 실행한다는 의미이다. data fetching은 react-query를 사용하고, global state는 redux로 관리한 결과 page에서는 이미 컨텍스트 로직이 적용된 organism만 배치하여 사용하기만 하면 되는 방식으로 구현할 수 있었다.

하위 컴포넌트에서 오류 발생시 모든 상위 컴포넌트의 오류로 파생될 수 있음

이 문제는 atom이 여러 계층에서 재사용되며 생길 수 있는 이슈인데, atom에 의존하고 있는 컴포넌트들이 많다보니 atom 컴포넌트 한개만 오류가 발생해도 페이지, 서비스 전체에 영향을 줄 수 있는 이슈이다.

 

snapshot test는 이에 대한 좋은 해결책이 될 수 있다. 컴포넌트 함수가 변경되거나, 로직 함수가 변경되어 UI까지 영향을 끼칠 경우 기존의 컴포넌트와 비교했을때 UI가 깨지거나, 목적과 다르게 보이는 이슈가 발생하게 된다. 매번 테스트를 할때마다 snapshot test를 실행한다면 UI가 의도치 않게 변경됬을 경우 테스트가 실패하기 때문에 배포하기 전에 이슈를 처리할 수 있게된다.

같은 계층에 위치한 컴포넌트끼리 의존성을 가지는 경우가 발생함

atom과 molecule의 중간쯤에 위치한 컴포넌트도 있다. children을 가지는 컴포넌트들이 그 대표적 예이다.

export interface Props {
  active?: boolean;
  children: JSX.Element | undefined;
}

const GnbButton = ({ active, children, ...rest }: Props) => {
  return (
    <GnbButtonStyled active={active} {...rest}>
      {children}
    </GnbButtonStyled>
  );
};
export default GnbButton;

위의 코드는 chlidren을 받아서 버튼 안에 넣어주는 컴포넌트이다. 해당 컴포넌트는 atom을 불러와주고 있지는 않지만, chlidren prop으로 organism에 위치한 컴포넌트를 주입받아 배치시킨다. 만약 해당 컴포넌트를 organism으로 분류할 경우 같은 계층에서 의존성을 가지는 컴포넌트가 생기게 되고, 그렇다고 해서 atom으로 분류할 경우 최소 단위의 컴포넌트만 atom으로 정한다라는 규칙이 무너지게 된다. 이런 상황에서는 어떻게 해야 하는가?

 

필자는 의존성을 가지게 됨으로써 이슈가 발생할 수 있는 위험성보다 컴포넌트를 나누는 기준을 어기게 되는것이 더 나쁘다고 판단했다. 따라서 해당 컴포넌트는 organism에 소속시킨 상태이다. 이렇게 특정 계층에 속하지 않는 컴포넌트들도 생기게 되는 경우도 발생한다.

개인적 의견

코드의 기본은 중복을 없애는 것이다. 그리고 컴포넌트의 특성상 중복이 생기기 쉽다. 작은 컴포넌트, pspan 같은 기본적인 요소부터 컴포넌트화 시켜 중복을 최대한 없애는 아토믹 컴포넌트 방법론은 내게 있어 매력적으로 느껴졌고, 따라서 클론 프로젝트에 적용하게 되었다.

 

프론트엔드 개발자가 하는 고민중 큰 부분을 코드 재사용성이 차지하는 만큼, 아토믹 컴포넌트는 이를 보다 근본적인 차원에서 개선할 수 있는 방법론으로 여겨진다. 하지만 지금까지 알아본것처럼 여러 단점이 있으니, 디자인 시스템에서 취할점은 취하고 버릴건 버리며 더 좋은 컴포넌트를 만들 수 있게 되면 좋을것 같다.

레퍼런스

Atomic Design Pattern의 Best Practice 여정기 | 요즘IT
아토믹 디자인을 활용한 디자인 시스템 도입기 | 카카오엔터테인먼트 FE 기술블로그
JS 아토믹 컴포넌트 디자인 개발 패턴 — 와플공장
소프트웨어 디자인 관점에서 바라본 아토믹 디자인의 의미와 한계 | overthcode.io
React아토믹(Atomic) 컴포넌트 디자인 개발 패턴

Atomic Design Methodology | Atomic Design by Brad Frost

'개발 > 프론트엔드' 카테고리의 다른 글

웹팩(Webpack)이란?  (0) 2023.01.11
화살표 함수 사용하기  (0) 2022.12.30
[Vue] Mixin으로 반복되는 메소드 모듈화시키기  (0) 2021.04.14
Webpack/Babel이란?  (0) 2021.04.11
[ES6] Async와 Promise의 차이점  (0) 2021.04.11