Atomic 디자인
안녕하세요!
구글 캘린더를 클론 코딩하면서 일관된 UI를 유지하기 위해 다양한 컴포넌트와 스타일 시스템을 설계하며 구현해보았습니다.
서론
UI 통일성
UI 컴포넌트 각각의 디자인이 더 나은 사용성을 가지고 매력적인 디자인으로 보이는 것도 중요하지만, 앱의 화면이 같은 브랜드로 느껴지도록 통일성을 유지하는 것도 중요합니다.
구글 캘린더 역시 UI 컴포넌트들이 통일된 디자인을 유지하고 있었습니다.
(그림 - 구글 캘린더)
(통일된 UI)
구글 캘린더의 버튼들은 동일한 UI 구조를 가지고 있고,
Content만 다를 뿐 전반적으로 통일감 있는 색상과 스타일이 적용되어 있었습니다.
Atomic design
Atomic 디자인은 UI를 작은 구성 요소부터 큰 화면까지 계층적으로 나누어 설계하는 방법론입니다.
가장 작은 단위인 원자(Atoms)부터 시작해, 분자(Molecules), 유기체(Organisms), 템플릿(Templates), 페이지(Pages)
순으로 점점 복잡한 컴포넌트를 만듭니다.
이렇게 하면 재사용성과 유지보수가 쉬워지고, 일관된 디자인 시스템 구축이 가능합니다. UI 개발을 체계적으로 관리할 때 많이 활용됩니다.
Atomic 디자인에서 Button, Text, Input 같은 기본 UI 요소들은 가장 작은 단위인 Atom으로 분리됩니다.
이러한 Atom 컴포넌트를 기반으로 더 큰 컴포넌트를 만들고, 이를 모아 페이지를 구성하면 일관된 UI가 자연스럽게 설계됩니다.
(Atom - Button)
이 컴포넌트는 엄밀히 따지면 분자(Molecules) 라고 할 수 있습니다.
Button + Text 두 개의 Atom이 합쳐졌기 때문입니다.
하지만 Atom이라 가정하겠습니다.
( Molecules - Button)
이 컴포넌트는 분자(Molecules) 입니다.
Button 2개 + Text 2개 즉 다양한 Atom이 합쳐졌기 때문입니다.
( Organisms - Header)
Atomic 디자인에서는 유기체(Organisms)와 템플릿(Templates)을 구분하지만,
두 단계를 합쳐서 사용하기도 합니다.
저는 합쳐서 설명하겠습니다.
이러한 Button Atom + Text Atom + Molecules 들이 합쳐 하나의 Organisms를 만들고 있습니다.
( Pages - Calendar)
앞서 만든 Atom + Molecules + Organisms 이루어진 Pages를 만들고 있습니다.
이렇게 하여, 일관된 UI를 만들 수 있습니다.
How?
이러한 Atomic한 컴포넌트 설계를 어떻게 하면 될까요?
먼저 코드부터 살펴봅시다.
import classNames from "classnames";
import type { CSSProperties, PropsWithChildren } from "react";
type Props = PropsWithChildren<{
type?: "primary" | "light" | "dark" | "none";
style?: "outline" | "flat";
className?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
disabled?: boolean;
css? : CSSProperties
size? : "xs" | "sm" | "md" | "lg" | "xl"
}>;
function Button(props: Props) {
const {
type,
style,
size,
disabled,
className,
children,
css,
...rest
} = props;
return (
<button
className={classNames(
"button",
{
[`button--type-${type}`]: type,
[`button--style-${style}`]: style,
[`button--size-${size}`]: size,
},
{ disabled: disabled },
className
)}
style={css}
{...rest}
>
{children}
</button>
);
}
export default Button;
- Props 기반 설계: type, style, size, disabled 등을 prop으로 받아 버튼의 상태와 스타일을 유연하게 설정할 수 있습니다.
- classNames 라이브러리 활용: 조건부로 클래스를 적용해, CSS 클래스 관리가 깔끔하고 확장성 있게 구성됩니다.
- 재사용성: 다양한 버튼 변형(Primary, Outline, Flat 등)을 하나의 컴포넌트로 커버할 수 있습니다.
먼저 classNames 라이브러리는 조건에 따라 CSS 클래스를 동적으로 적용할 수 있게 해주는 작은 유틸리티 라이브러리입니다.
만약 이 코드를 classNames 라이브러리를 사용하지 않았더라면
const btnClass =
"button" +
(isPrimary ? " button--primary" : "") +
(isDisabled ? " button--disabled" : "");
조건마다 문자열을 덧붙여야 해서 코드가 점점 복잡해지고 가독성이 떨어집니다. 조건이 많아질수록 더욱 심해지겠죠
하지만 classNames 라이브러리를 사용하면
className={classNames(
"button",
{
[`button--type-${type}`]: type,
[`button--style-${style}`]: style,
[`button--size-${size}`]: size,
},
{ disabled: disabled },
className
)}
이렇게 classNames 문법을 간단하게 만들 수 있습니다.
사용법
(outline Button)
<Button
type="none"
style="outline"
size: "md"
className="rounded-[30px]"
onClick={resetToday}
>
<Text size="sm">오늘</Text>
</Button>
props로 outline, type, size를 넘겨 해당 이미지와 같은 버튼을 만들 수 있습니다.
덕분에 UI의 일관성을 유지하면서도 확장성과 재사용성을 모두 확보할 수 있습니다.
(outline Button)
<Button
size="xs"
type="primary"
className="rounded-full"
>
7
</Button>
마찬가지로 다음과 같이 props를 넘겨 일관된 UI를 만들 수 있습니다.
결론
Button처럼 Text, Input 등도 props 기반의 재사용 가능한 Atomic 컴포넌트로 설계하면, 일관된 UI를 만들 수 있습니다.