이 글은 누구를 위한 것인가
- 컴포넌트를 만들었는데 비슷한 변형마다 새 컴포넌트를 만드는 팀
- 아토믹 디자인을 들어봤는데 실제로 어떻게 적용할지 모르는 팀
- Figma 컴포넌트 구조와 코드 컴포넌트를 맞추고 싶은 디자이너·개발자
들어가며
아토믹 디자인은 레이아웃 계층이 아니라 재사용 계층이다. Button이 Atom이고, InputWithLabel이 Molecule, SearchBar가 Organism이다. 계층 구조를 이해하면 "이 컴포넌트를 어느 레벨에서 만들어야 하나"가 명확해진다.
이 글은 bluefoxdev.kr의 컴포넌트 아키텍처 가이드 를 참고하여 작성했습니다.
1. 아토믹 계층 분류
[아토믹 디자인 계층]
Atoms (원자) - 더 이상 나눌 수 없는 단위:
Button, Input, Label, Icon, Badge, Avatar
Checkbox, Radio, Toggle, Spinner
규칙: 비즈니스 로직 없음, props로만 제어
Molecules (분자) - Atom 2개 이상 결합:
InputWithLabel (Input + Label)
SearchInput (Input + Icon + Button)
FormField (Label + Input + ErrorMessage)
AvatarWithName (Avatar + Text)
규칙: 하나의 목적을 가진 단위
Organisms (유기체) - 복잡한 UI 섹션:
SearchBar (SearchInput + Suggestions Dropdown)
ProductCard (Image + Title + Price + Button)
NavigationHeader (Logo + Navigation + UserMenu)
규칙: 실제 데이터와 연결 가능
Templates - 레이아웃 구조:
ProductListTemplate (Grid + Sidebar + Pagination)
Pages - 실제 데이터가 채워진 완성본:
ProductListPage
2. Compound Component 패턴
// Select 컴포넌트를 Compound Pattern으로 설계
import { createContext, useContext, useState } from "react";
interface SelectContextValue {
value: string;
onChange: (value: string) => void;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}
const SelectContext = createContext<SelectContextValue | null>(null);
function Select({
value,
onChange,
children,
}: {
value: string;
onChange: (value: string) => void;
children: React.ReactNode;
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<SelectContext.Provider value={{ value, onChange, isOpen, setIsOpen }}>
<div className="relative">{children}</div>
</SelectContext.Provider>
);
}
function SelectTrigger({ children }: { children: React.ReactNode }) {
const ctx = useContext(SelectContext)!;
return (
<button
onClick={() => ctx.setIsOpen(!ctx.isOpen)}
className="flex items-center justify-between w-full px-4 py-2 border rounded-lg"
>
{children}
<span>{ctx.isOpen ? "▲" : "▼"}</span>
</button>
);
}
function SelectContent({ children }: { children: React.ReactNode }) {
const ctx = useContext(SelectContext)!;
if (!ctx.isOpen) return null;
return (
<div className="absolute top-full left-0 right-0 bg-white border rounded-lg shadow-lg z-10 mt-1">
{children}
</div>
);
}
function SelectItem({ value, children }: { value: string; children: React.ReactNode }) {
const ctx = useContext(SelectContext)!;
return (
<button
onClick={() => {
ctx.onChange(value);
ctx.setIsOpen(false);
}}
className={`w-full text-left px-4 py-2 hover:bg-gray-50 ${
ctx.value === value ? "bg-blue-50 text-blue-600 font-medium" : ""
}`}
>
{children}
</button>
);
}
// 사용 예시 - 합성 가능하고 유연한 API
Select.Trigger = SelectTrigger;
Select.Content = SelectContent;
Select.Item = SelectItem;
function SortSelector() {
const [sort, setSort] = useState("newest");
return (
<Select value={sort} onChange={setSort}>
<Select.Trigger>
{sort === "newest" ? "최신순" : "인기순"}
</Select.Trigger>
<Select.Content>
<Select.Item value="newest">최신순</Select.Item>
<Select.Item value="popular">인기순</Select.Item>
<Select.Item value="price_asc">낮은 가격순</Select.Item>
</Select.Content>
</Select>
);
}
3. 슬롯 기반 Card 설계
// 슬롯 기반 설계: 내부 구조는 고정, 내용은 교체 가능
interface CardProps {
header?: React.ReactNode;
media?: React.ReactNode;
body: React.ReactNode;
footer?: React.ReactNode;
variant?: "default" | "compact" | "featured";
}
function Card({ header, media, body, footer, variant = "default" }: CardProps) {
return (
<div className={`card card--${variant} rounded-xl border overflow-hidden`}>
{media && <div className="card__media">{media}</div>}
{header && <div className="card__header px-4 pt-4">{header}</div>}
<div className="card__body px-4 py-3">{body}</div>
{footer && <div className="card__footer px-4 pb-4 border-t">{footer}</div>}
</div>
);
}
// 동일한 Card 컴포넌트로 다양한 카드 구현
function ProductCard({ product }: { product: Product }) {
return (
<Card
media={<img src={product.imageUrl} alt={product.name} className="w-full aspect-square object-cover" />}
body={
<>
<h3 className="font-medium">{product.name}</h3>
<p className="text-lg font-bold mt-1">{product.price.toLocaleString()}원</p>
</>
}
footer={<AddToCartButton productId={product.id} />}
/>
);
}
마무리
아토믹 디자인의 함정은 레벨 분류에 집착하는 것이다. 핵심은 "이 컴포넌트가 어디서든 재사용될 수 있는가?"다. 비즈니스 컨텍스트에 종속된 컴포넌트(예: 특정 페이지의 섹션)는 Organism이나 Template 레벨이다. Compound Component 패턴은 내부는 강하게 결합되지만, 외부에서는 유연하게 합성 가능한 API를 만들 때 최적이다.