Atomic Design은 컴포넌트를 원자부터 페이지까지 계층적으로 구성해 재사용성과 일관성을 높이는 설계 방법입니다. 실무에서는 디자인 시스템 구축, 대규모 UI 유지보수, 팀 협업에서 특히 효과적입니다. 이 글은 React 앱에 Atomic Design을 적용하는 실전 가이드를 제공합니다.
1. Atomic Design 5단계 요약
Atoms: 버튼, 입력, 아이콘처럼 더 쪼갤 수 없는 UI 단위입니다. 스타일 토큰과 접근성 속성을 포함하되 비즈니스 로직은 없습니다.
Molecules: 여러 Atom을 조합한 기능 단위입니다. 예: 라벨+인풋+에러 메시지 필드, 버튼 그룹 등입니다.
Organisms: 헤더, 카드 리스트, 검색 헤더처럼 의미 있는 영역을 구성합니다. 상태는 UI 영역 내부 동작 정도로 제한합니다.
Templates: 레이아웃과 배치만 정의합니다. 실제 데이터 없이 슬롯(children) 중심으로 구성합니다.
Pages: 라우팅, 데이터 로딩, 의존성 주입(서비스 호출) 등 화면 완성 단계입니다.
2. 폴더 구조 예시
src/
components/
atoms/
Button/
Button.jsx
Button.css
index.js
Input/
Input.jsx
Input.css
index.js
molecules/
LoginForm/
LoginForm.jsx
index.js
organisms/
Header/
Header.jsx
index.js
templates/
AuthTemplate/
AuthTemplate.jsx
index.js
pages/
LoginPage.jsx
app/
router.jsx
index.jsx레이어 간 의존성은 아래 방향(높은 추상화 → 낮은 추상화)으로만 허용합니다: Pages → Templates → Organisms → Molecules → Atoms.
3. Atom: Button 구현
// src/components/atoms/Button/Button.jsx
import React from 'react';
const VARIANTS = { primary: 'btn btn--primary', secondary: 'btn btn--secondary', text: 'btn btn--text' };
const SIZES = { sm: 'btn--sm', md: 'btn--md', lg: 'btn--lg' };
export default function Button({ type = 'button', variant = 'primary', size = 'md', disabled = false, onClick, children, ...rest }) {
const className = [VARIANTS[variant], SIZES[size]].filter(Boolean).join(' ');
return (
<button type={type} disabled={disabled} onClick={onClick} className={className} {...rest}>
{children}
</button>
);
}// src/components/atoms/Button/index.js
export { default } from './Button';가이드: Atom은 단일 책임, 접근성(aria-*), 디자인 토큰(variant/size)만 처리합니다. API 호출, 상태 관리 등 비즈니스 로직은 금지합니다.
4. Atom: Input 구현
// src/components/atoms/Input/Input.jsx
import React from 'react';
export default function Input({ id, label, error, ...rest }) {
return (
<div className='field'>
{label && <label htmlFor={id}>{label}</label>}
<input id={id} aria-invalid={!!error} aria-describedby={error ? id + '-error' : undefined} {...rest} />
{error && (
<span id={id + '-error'} role='alert' className='field__error'>{error}</span>
)}
</div>
);
}5. Molecule: LoginForm 구현
// src/components/molecules/LoginForm/LoginForm.jsx
import React, { useState } from 'react';
import Input from '@atoms/Input';
import Button from '@atoms/Button';
export default function LoginForm({ onSubmit, loading = false }) {
const [values, setValues] = useState({ email: '', password: '' });
function handleChange(e) {
setValues(v => ({ ...v, [e.target.name]: e.target.value }));
}
function handleSubmit(e) {
e.preventDefault();
onSubmit?.(values);
}
return (
<form onSubmit={handleSubmit} noValidate>
<Input id='email' name='email' type='email' label='이메일' onChange={handleChange} value={values.email} required />
<Input id='password' name='password' type='password' label='비밀번호' onChange={handleChange} value={values.password} required />
<Button type='submit' disabled={loading}>{loading ? '로그인 중...' : '로그인'}</Button>
</form>
);
}6. Organism & Template
// src/components/organisms/Header/Header.jsx
import React from 'react';
export default function Header({ rightSlot }) {
return (
<header className='site-header'>
<a href='/' className='logo'>MyApp</a>
<nav className='nav'>
<a href='/features'>기능</a>
<a href='/pricing'>요금</a>
</nav>
<div className='header-right'>{rightSlot}</div>
</header>
);
}// src/components/templates/AuthTemplate/AuthTemplate.jsx
import React from 'react';
export default function AuthTemplate({ title, children }) {
return (
<main className='auth'>
<section className='auth__panel'>
<h1>{title}</h1>
{children}
</section>
<aside className='auth__aside'>{/* 비주얼/카피 */}</aside>
</main>
);
}7. Page: 데이터와 라우팅
// src/pages/LoginPage.jsx
import React, { useState } from 'react';
import AuthTemplate from '@templates/AuthTemplate';
import LoginForm from '@molecules/LoginForm';
import Header from '@organisms/Header';
async function fakeApiLogin(values) {
return new Promise(res => setTimeout(res, 600));
}
export default function LoginPage() {
const [loading, setLoading] = useState(false);
async function handleLogin(values) {
try {
setLoading(true);
await fakeApiLogin(values);
// 라우팅, 토스트 등 처리
} finally {
setLoading(false);
}
}
return (
<>
<Header rightSlot={<a href='/signup'>회원가입</a>} />
<AuthTemplate title='로그인'>
<LoginForm onSubmit={handleLogin} loading={loading} />
</AuthTemplate>
</>
);
}8. 경로 별칭과 배럴(Barrel) 내보내기
// jsconfig.json
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@atoms/*": ["components/atoms/*"],
"@molecules/*": ["components/molecules/*"],
"@organisms/*": ["components/organisms/*"],
"@templates/*": ["components/templates/*"]
}
}
}각 컴포넌트 폴더에 index.js를 두어 기본 내보내기를 제공하면 import가 간결해집니다. 레이어 루트에 index.js를 만들어 대량 재노출도 가능하나, 불필요한 의존성 확장을 막기 위해 폴더 단위 import를 추천합니다.
9. Storybook으로 문서화
// src/components/atoms/Button/Button.stories.jsx
import Button from './Button';
export default {
title: 'Atoms/Button',
component: Button,
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'text'] },
size: { control: 'select', options: ['sm', 'md', 'lg'] }
}
};
const Template = (args) => <Button {...args}>Button</Button>;
export const Primary = Template.bind({});
Primary.args = { variant: 'primary', size: 'md' };원자와 분자 단계부터 문서화/시각테스트를 시작하면 UI 변동 비용을 크게 줄일 수 있습니다.
10. 테스트 전략 (Jest + RTL)
// src/components/atoms/Button/Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
test('클릭 핸들러 호출', () => {
const fn = jest.fn();
render(<Button onClick={fn}>OK</Button>);
fireEvent.click(screen.getByRole('button', { name: 'OK' }));
expect(fn).toHaveBeenCalled();
});원자에서 상호작용과 접근성 롤(role)을 검증하고, 분자/유기체에서 조합 동작을 테스트합니다. 페이지는 데이터 흐름과 라우팅을 통합 테스트합니다.
11. 의존성 경계 ESLint 규칙
// .eslintrc.js (요약)
module.exports = {
rules: {
'import/no-restricted-paths': [2, {
zones: [
// atoms가 상위 레이어를 import 금지
{ target: './src/components/molecules', from: './src/components/atoms' },
{ target: './src/components/organisms', from: './src/components/atoms' },
{ target: './src/components/templates', from: './src/components/atoms' },
// molecules가 organisms/templates import 금지
{ target: './src/components/organisms', from: './src/components/molecules' },
{ target: './src/components/templates', from: './src/components/molecules' }
]
}]
}
};원자 → 분자 방향으로만 의존하도록 자동 검사합니다. 사이클이 생기지 않도록 import/no-cycle 규칙도 함께 권장합니다.
12. 스타일 가이드
디자인 토큰(색상, 간격, 그림자)은 CSS 변수나 Tailwind/Styled System으로 통일합니다. Atom에는 토큰 기반 variant/size만 두고, 레이아웃/배치는 상위(Template/Organism)에서 책임지게 합니다. 클래스 이름은 레이어에 독립적이어야 하며, 공용 유틸 클래스(예: .visually-hidden)는 atoms에서 바로 사용합니다.
13. 성능과 접근성 체크
성능: 반복 렌더가 잦은 Organism에는 React.memo, list에는 key 안정성, 핸들러는 useCallback으로 메모이제이션합니다. 과도한 memo는 피하고, 프로파일링으로 병목을 확인합니다.
접근성: Input/Label 연결, 포커스 트랩(모달), 키보드 탐색, role/aria-속성, 대비 비율을 원자 단계에서 보장하고 Storybook에서 a11y 애드온으로 확인합니다.
14. 점진적 도입 전략
신규 컴포넌트부터 Atomic 구조로 작성합니다. 공통 버튼/입력부터 추출해 변형을 variant/size로 통합합니다. 기존 화면은 페이지 레벨에서 새 Molecule/Organism으로 교체하고, 사용처가 3회 이상 반복되는 UI부터 승격(분자→유기체)합니다.
15. 흔한 함정과 회피 팁
레이어 과분화: 꼭 5단계를 강요하지 말고, 프로젝트 규모에 따라 Atoms/Molecules/Organisms 3단계만 우선 적용해도 충분합니다.
비즈니스 로직 누수: 폼 검증, API 호출은 Page/컨테이너에서 수행하고 하위에는 콜백만 전달합니다.
Variant 폭증: variant 조합이 6가지를 넘기면 스타일 토큰/조합을 재검토하고 Storybook에서 사용성 점검합니다.
16. 적용 체크리스트
1) Atoms는 순수 UI이며 접근성 보장됨 2) 상향 의존성 금지 린트 적용 3) 경로 별칭과 배럴 내보내기 정리 4) Storybook/테스트로 회귀 방지 5) Templates는 레이아웃만, Pages는 데이터/라우팅만 담당합니다.
Atomic Design을 React에 적용하면 UI의 일관성과 재사용이 극대화됩니다. 위의 구조/규칙/예제를 팀 컨벤션으로 명문화하고 Storybook과 ESLint로 자동화하면 유지보수 효율이 크게 향상됩니다.
'React' 카테고리의 다른 글
| React에서 Lottie 애니메이션 통합하기 (0) | 2026.05.15 |
|---|---|
| React로 마이크로 프론트엔드 구현하기 (0) | 2026.05.14 |
| React에서 IndexedDB를 이용한 오프라인 데이터 저장 (1) | 2026.05.13 |
| React에서 컴포넌트 성능 측정 및 분석하기 (1) | 2026.05.13 |
| React 앱 CI/CD 파이프라인 구축하기 (0) | 2026.05.12 |