2021. 9. 18. 22:25ㆍFrontend
Overview
리액트를 사용해서 프론트엔드 웹 애플리케이션을 개발하는 경우, 많은 곳에서 Atomic Design Pattern을 사용하게 됩니다. 컴포넌트를 하나의 함수로 보고, 클린 아키텍쳐의 관점에서 재사용 가능한 컴포넌트들을 그 기능과 구성에 따라 구조화 하여 사용하는 이 패턴은 어느샌가 프론트엔드 아키텍쳐를 이루는 하나의 교과서처럼 되어 알려지고 있습니다.
하지만 이 Atomic Design Pattern을 실제 애플리케이션에 적용해서 개발하다보면 생각보다 많은 부분들이 "추상적"이고 "주관적"이라는 느낌을 받는 경우가 많습니다. 오히려 적용한 이후에 코드가 더 관리가 어려워지고 지저분해지는 경우도 많은데, 이는 Atomic Design Pattern이 처음 등장할 때 그 사용처가 "Design System"에 한정되어 있기도 했고, 디자인 패턴을 이루는 Atoms, Molecules, Organisms등의 개념 자체도 단순하며, 예시는 "Input", "Button", "Form"등의 단순한 구조들에 한정되어 있기 때문입니다.
이번 포스팅에서는 Atomic Design Pattern이 본질적으로 지향하는 바가 무엇인지, 그리고 이를 사용해서 현실적이고 잘 구조화된 프론트엔드 아키텍쳐를 설계한다는 것은 무엇인지 한번 고찰해보려 합니다.
Atomic Design Pattern
Atomic Design Pattern은 2013년 6월(생각보다 역사가 오래된 패턴입니다) Brad Frost에 의해 처음으로 제시된 디자인 패턴입니다. 리액트가 처음으로 오픈소스화 된 것이 2013년 5월 JSconf에서인 것으로 볼 때, 거의 리액트의 탄생과 궤를 같이 했다고도 볼 수 있습니다. 이는 프론트엔드 시스템을 구성하는 컴포넌트들을 논리 구조 (Building Blocks) 단위로 나누어 효율적으로 재사용하도록 만들어서, 불필요한 "코드 중복"을 제거하고 "클린"한 아키텍쳐를 유지할 수 있도록 합니다. (atoms, molecules, organisms, templates등에 대한 정의는 아래 Refernce에 있는 원본에 자세하게 나와있습니다.)
Similarly, interfaces are made up of smaller components. This means we can break entire interfaces down into fundamental building blocks and work up from there. That’s the basic gist of atomic design.
- bradfrost
이 패턴을 소개할때 저자는 한가지 중요한 사실을 서두에 드러냅니다. 바로 이 Atomic Design Pattern은 바로 "디자인 시스템을 위한 것"이라는 점입니다. 즉 애초에 "재사용 가능한 컴포넌트들의 집합"인 디자인 시스템에서 컴포넌트들을 효율적으로 구성하는 방식을 의미하는 것입니다.
그렇기 때문에 저자가 소개하는 예시들도 완전히 재사용가능한 단위인(즉, 디자인 시스템에서 사용하기에 적합한) Input, Button, Form, Template등에 집중하고 있으며, 이들을 사용해서 구현된 Real-Life Example들을 어디에 구성해야 하는지는 명확히 알려주지 않고 있는 것입니다. 바로 이 부분에서 오해가 생기며, Atomic Design Pattern을 실제 프로젝트에 적용했는데도 오히려 코드의 가독성이 떨어지고 수십~수백개의 molecules와 organisms들을 만들어 유지보수를 어렵게 하는, 다시말해 리팩터링에서 말하는 "악취가 나는" 아키텍쳐의 원인이 됩니다.
A lot has been said about creating design systems, and much of it focuses on establishing foundations for color, typography, grids, texture and the like. This type of thinking is certainly important, but I’m slightly less interested in these aspects of design because ultimately they are and will always be subjective. Lately I’ve been more interested in what our interfaces are comprised of and how we can construct design systems in a more methodical way.
- bradfrost
Problems
Atomic Design Pattern을 사용할 때에 오해가 생기기 쉬운 이유는 그 본질이 "Design System"을 위한 패턴이기 때문이라는 것을 위에서 설명드렸습니다. 그렇다면 디자인 패턴의 본질이 "Design System(이하 디자인 시스템)"을 위한 것이라는 건 어떤 것을 의미할까요? 이를 위해서는 디자인 시스템이 왜 필요하고, 어떻게 구성되는지를 알아야 할 필요가 있습니다.
컴포넌트로서의 디자인 시스템(디자이너의 입장이 아닌, 이를 구현하여 사용하는 개발자의 입장에서)은 말 그대로 특정한 디자인 가이드를 따르는(회사, 기관 등 공동체에서 약속한 디자인 가이드라인) Input, Button, Form등과 같은 "기본 구성 요소"들을 코드화하여 재사용성을 높인 컴포넌트들의 집합을 의미합니다. 즉 "우리 회사에서 버튼의 종류는 4가지가 있고, 메인 컬러는 주황색, 보더는 8px정도로 둥글게 되어 있어야 해"라는 약속들을 컴포넌트로 구현하여 이를 사용하는 프로젝트에서 단순히 이를 import하여 사용할 수 있도록 만든 것입니다.
하지만 Atomic Design Pattern을 제대로 이해하고 사용하기 위해서는 단순히 디자인 시스템의 구성 요소들을 "디자인이 잘 입혀진 DOM Element"정도로 생각해서는 안됩니다. 물론 디자인 시스템을 가지고 있는 회사나 기관의 정체성을 대변하는 Style이 들어가고(예를 들면 브랜드 컬러, shadow등등), 이를 재사용함으로써 불필요한 스타일링에 드는 리소스를 절약한다는 의미도 있지만(그리고 이것이 디자인 시스템의 존재 이유 중 하나이기도 하지만), Atomic Design Pattern이 디자인 시스템을 위한 좋은 디자인 패턴인 이유는 디자인 시스템의 본질이 "컴포넌트를 기능의 단위로 나눈다는 점"에 있습니다.
예를 들어,
논리적인 단위로서의 버튼(Atoms)은 "클릭하면 바인딩된 특정 액션을 실행한다"라는 하나의 기능만을 제공하므로 그것이 가지고 있는 style(hover, active....)과는 상관없이 하나의 기능을 수행하는 하나의 단위입니다
논리적인 단위로서의 검색바(Molecules)는 "클릭하면 특정 액션을 실행하는" 검색 버튼(Atom)과 "타이핑하면 글자가 입력되는" Input(Atom)으로 이루어져있지만, 이 둘이 합쳐져서 "타이핑해서 클릭하면 해당 글자로 검색을 수행하는" 하나의 기능을 수행하는 하나의 단위입니다.
물론 디자인 시스템에서 컴포넌트를 구성하는 "스타일"은 디자인 시스템에서 굉장히 중요한 요소입니다. 하지만 여기서의 논의는 "디자인 시스템을 위해 만들어진 Atomic Design Pattern을 실제로 디자인 시스템이 아닌 시스템에 적용하기 위한" 것이므로 "논리적인 단위"라는 생각이 중요합니다.
실제로 Atomic Design Pattern을 디자인 시스템이 아닌 일반 웹 프로젝트에 적용할때 atoms, molecules, organisms등의 요소들을 "기능의 단위"가 아닌 "뷰를 이루는 단위"로 나누는 경우가 많으며, 이 부분이 관리가 어려운 아키텍쳐를 만드는 주범이 됩니다. Atomic Design Pattern이 디자인 시스템을 위해 만들어졌기 때문에 각각의 요소들이 "View를 이루는", 즉 전체 화면을 쪼개는 비주얼적인 구성요소들로 나누어진 것처럼 보입니다. 예를 들어서 아래의 그림에서 전체 화면은 Header, Body, Footer로 쪼갤 수 있으므로 각각을 Organisms으로 두고, 그 안의 요소들을 쪼개서 Menubar, Logo, SearchBar등을 Molecules로 놓는 식입니다.
이렇게 코드를 구조화하면 한두개의 페이지를 만드는 경우에는 괜찮을 수 있으나, 페이지가 많아질수록, "화면에 보이는" 요소들이 많아질수록 그에 비례해서 organisms, molecules들도 늘어나게 됩니다. 결국, "기능"을 고려하지 않고 "View"를 기준으로 요소들을 나누게 되면서 재사용하기도 어렵고, 관리하기도 어려운 아키텍쳐로 변하게 되는 것입니다.
Atomic Design Pattern은 그 본질이 "기능의 단위"라는 점을 기억해야 합니다.
디자인 시스템을 위해 만들어졌기 때문에 "View" 단위로 나눈 것처럼 보이지만,
실제로 Atomic Design Pattern을 구성하는 Building Block은 "Functionality"입니다.
Atomic Design Pattern as an Architecture
이를 해결하기 위해서는 Atomic Design Pattern의 본질로 돌아가야 합니다. 즉, "기능"에 집중하는 것입니다. 일반적인 웹 프로젝트에서는 많은 부분들을 재사용하기가 어렵다는 것을 인정하고(유지보수를 거듭하고, 기능을 추가하면서 더 어려워지곤 합니다), 재사용 가능한 코드들은 "View" 보다 그 "기능"에 집중하여 구현해야 합니다.
기능을 사용하는 컴포넌트들에서는 Hooks, function등을 통해 필요한 Props들을 정의해서 넘기고, css-in-js라이브러리(styled-components, emotionjs)등에서 커스텀 스타일을 입혀서 사용하게 되면 atomic 컴포넌트들(atoms, molecules, organisms)의 스타일 구현을 위해 props로 css style들을 넘기는 작업들을 생략하여 atomic 컴포넌트들이 최대한 기능에만 집중할 수 있도록 구현해야 합니다.
Navigation Bar (Molecules) Example
export interface TabbarItemModel {
label: string | ReactNode;
onClick?: () => void;
}
export interface NavigationBarProps {
tabbarItems: TabbarItemModel[];
}
const NavigationBar: FC<NavigationBarProps> = ({ tabbarItems }) => {
const [activeIndex, setActiveIndex] = useState<number>(0);
const handleClick = useCallback((item: TabbarItemModel, index: number) => {
setActiveIndex(index);
item.onClick && item.onClick();
}, []);
return (
<StyledTabbar activeIndex={activeIndex}>
{tabbarItems.map((item, index) => (
<StyledTabbarItem key={index} onClick={() => handleClick(item, index)}>
{item.label}
</StyledTabbarItem>
))}
</StyledTabbar>
);
};
export default NavigationBar;
const HeaderNavigationBar = styled(NavigationBar)`
// here css styled applied
`;
const Header: FC = () => {
const tabbarItems = useNavigationBar();
return (
<Wrapper>
<HeaderNavigationBar tabbarItems={tabbarItems} />
</Wrapper>
);
};
const useNavigationBar = (): TabbarItemModel[] => {
const tabbarItems: TabbarItemModel[] = [
{
label: 'Menu1',
onClick: () => {},
},
{
label: 'Menu2',
onClick: () => {},
},
{
label: 'Menu3',
onClick: () => {},
},
];
return tabbarItems;
};
export default useNavigationBar;
마지막으로 "기능"이 아닌 "레이아웃"을 고려해야 하는 페이지들은 그 속성과 도메인에 따라 재량껏 적절한 디렉토리 구조로 옮기는 것이 좋습니다. /sections, /pages, /headers, /swiperpage등 각 페이지의 특성에 맞는 디렉토리를 정의하고 이 디렉토리 도메인 내에서만 사용되는 컴포넌트들을 이곳에 모아두는 것이 좋습니다. 이렇게하면 해당 도메인 내의 컴포넌트를 수정할때, 다른 도메인에는 영향을 주지 않는다는 것을 보장할 수 있으므로 유지보수와 SideEffect 대응에 효과적입니다. 또한 도메인의 경계를 명확하게 하고 가독성을 높여 주는 장점도 있습니다.
아래의 예시에서 Footer, Header를 구성하는 FooterCompanyInfo, FooterRoute, HeaderMenu등을 organisms나 molecules에 그냥 넣어둔다고 한다면(CompanyInfo, Route, Menu등으로 재사용가능해보이는 이름을 가지고) 실제로 Footer의 CompanyInfo를 수정하고 싶을때, 이 CompanyInfo를 Import하고 있는 다른 컴포넌드들은 없는지, 사이드이펙트를 고려하며 개발해야 하지만, 아래와 같이 도메인을 명확하게 분리하고, 기능과 상관이 없는 뷰들은 atomic components에 포함시키지 않는다면, 위와 같은 사항들을 고려할 필요가 없습니다.
Conclusion
코드를 build하고 나면 최신 Webpack과 같은 모듈 번들러가 모듈들을 적절히 번들링, 난독화를 해주기 때문에 atomic pattern을 적용하든 그렇지 않든 빌드 결과물은 큰 차이가 없을 가능성이 높습니다. 하지만, 코드를 유지보수하는 관점에서 atomic pattern을 잘 적용하는 것은 새로운 기능을 추가하거나, 기존 코드를 수정할 때 그렇지 않은 코드보다 많은 리소스를 절약해주고, 안전한 개발을 도와줄 것입니다.
Reference
https://bradfrost.com/blog/post/atomic-web-design/
'Frontend' 카테고리의 다른 글
[Web.dev] Web Security (1) (0) | 2022.02.10 |
---|---|
React Deep Dive - React Event System (2) (0) | 2021.12.31 |
React Deep Dive - React Event System (1) (0) | 2021.07.19 |
Async / Await Under the Hood (1) | 2021.06.29 |
[Webpack] Code Splitting (2) | 2021.06.17 |