아토믹 패턴 디자인은 시스템을 만드는 하나의 방법론으로 총 5가지의 구분 단계가 있다.
- Atoms(원자)
- Molecules(분자)
- Organisms(유기체)
- Templates
- Pages
뷰를 Atoms(원자) -> Molecules(분자) -> Organisms(유기체) -> Templates -> Pages 순으로 작은것들을 만들고,
결합해 좀 더 큰 단위의 뷰를 만들어 나가는 디자인 시스템이다. 웹앱은 여러 페이지 단위 이루이지고 페이지는
Input, button, form 등의 태그들로 이루어져 있다. 이를 원자, 분자, 유기체같은 생물학적인 개념으로
접근한 것이다.
장점
1. 재사용 가능한 설계 시스템을 제공합니다.
컴포넌트들을 혼합해 일관성 있고 재사용의 효율을 높이는 디자인을 할 수 있다.
2. 디자인을 쉽게 수정할 수 있다.
컴포넌트가 단위별로 이루어져 큰 컴포넌트에서 작은 컴포넌트를 삭제, 추가, 수정하는 것으로 쉽게
수정할 수 있다.
3. 레이아웃을 이해하기 쉬워진다.
페이지를 처음부터 설계하는 시도가 있어, 페이지의 레이아웃의 이해가 오래가고, 팀 프로젝트 시
제 멋대로가 되는 스타일 가이드를 최소화시킨다.
단점
1. 오랜 기간의 디자인 설계
설계의 개념은 이상적이나 설계의 힘을 써야하는 장점이 오히려 단점이 된다.
2. 일관성이 떨어지는 결과 발생 위험성
잘못된 디자인으로 컴포넌트들을 합치고 나눌 시, 기술 부채와 개발 기간이 증대해 결국에는
일관성이 떨어지는 디자인의 결과가 발생할 것이다.
3. 원자, 분자보다 익숙한 유기체
보통 사람들은 큰 단위를 정하고 그 내용물로 작은 단위를 만드는 top-down 방식에 익숙하다.
그래서 원자, 분자부터 만드는 bottom-up이 익숙치 않아 학습과 훈련이 필요하다는 것.
구성 요소
Atoms
- 해당 설계의 최소 단위
- form, input, button 같은 HTML의 태그나 최소의 기능을 가진 기능의 커스텀 태그 컴포넌트
- 설계에 따라 속성에 따른 스타일 주입이 들어갈 수 있다.
- Card System에서 제목, 내용, footer 들이 각각 이에 해당합니다.
Molecules
- Atom들을 최소의 역할을 수행할 수 있게 합한 그룹
- 입력을 받기 위한 form + label + input이 해당된다.
- Card System에서 제목 + 내용 + footer들이 합쳐진 하나의 Card가 이에 해당된다.
Organims
- 배치를 위한 layout 단위로 하나의 인터페이스를 형성하는 그룹
- header, navigation 등이 이에 해당된다.
- Card System에서 Card들이 Grid layout으로 형성된 집합이 이에 해당된다.
Templates
- 실제 Organisms들을 레이아웃이나 데이터 흐름을 연결한다.
- 클래스 시스템의 클래스로, 객체의 설계도, 페이지의 설계도이다.
Pages
- 정의된 Template에 데이터를 넣어 뷰를 완성시키는 단계이다.
- 클래스 시스템의 인스턴스, 객체의 구현체, 페이지 설계도로 그린 페이지 그 자체이다.
React를 사용하면서 자체적으로 컴포넌트의 단위로 뷰를 이루고, 상태에 따라 다른 뷰를 보여주기 때문에,
Automic Design이론만 보아도 React와 잘 맞을 것이다.
직접 구현
반응형 웹
우선 Atomic Design을 들어가기전에 반응형 웹을 고려해보자.
Content만 보면 테블릿 너비없이 모바일, PC 딱 두가지 상태만 봐도 된다.
Navigation
네비게이션바를 보면 왼쪽 텍스트는 title, 오른쪽 텍스트들은 category button들일 것이다. 모바일 상태에서는
다음과 같이 width를 100%로 줄 것이다.
Content
Movie List에서는 딱히 너비에 따른 수정이 필요 없어보인다.
하지만 Todo List에선 Item Content와 수정/삭제 버튼이 함께 하나의 block 단위로 되 있는데 모바일에서는 다음과 같이 각각 한 block을 차지하는 형태로 만들 것이다.
Atomic Design 설계
Atomic 구성
Atom의 구성 요소로는 Button, Span, Input 3개로 도출했다.
Span
- 파란색
- 텍스트 정보를 나타냄
- main-title, Content-title, todo-item, movie-item에 사용됨
Button
- 빨간색
- 컴포넌트를 클릭하고 이벤트같은 무언가를 발생시킨다.
- category-button, todo-button에 사용된다.
Input
- 분홍색
- 무언가를 입력하고 제출 시, 상태나 외부 변화를 준다.
- todo-input-create, todo-input-update에 사용된다.
Form
- 노란색
- 폼에 제출하게 도와줌
- todo-form 단 한 번 사용됨
Molecules 구성
Title
- 빨간색
- 무언가를 강하게 나타내는 컴포넌트
- 텍스트가 가운데 정렬
- app-title, todo-title에 사용
ListItem
- 보라색
- List에서 여러개로 존재하는 요소로, 모두 같은 크기를 가짐
- todo-list-item, movie-list-item에 사용된다.
ButtonList
- 파란색
- 버튼들이 가로로 나열된 형태
- todo-buttons, category-buttons 에 사용됨
List
- 회색
- 세로 방향으로 요소들이 결합된 상태
- todo-list, movie-list에 사용됨
Organisms 구성
Navigation
- 파란색
- 네비게이션 역할을 담당
TodoContent
- 빨간색
- todo list 콘텐츠를 제공
MovieContent
- 초록색
- movie list 콘텐츠를 제공
Templates && Page 구성
App.tsx 에 속한 컴포넌트
- Navigation (Organisms)
- Route
- Todo(Page)
- Movie(Page)
Content(template)는 Page의 구성요소로, Page내에서 구성될 때 속성을 받아서 Organisms들을 결합한다.
Atomic Design 구현
프로젝트 구성
├── components # atomic design을 위한 atoms, molecules, organisms
│ ├── atoms # atoms 컴포넌트
│ ├── molecules # molecules 컴포넌트
│ └── organisms # organisms 컴포넌트
└── pages # atomic design을 위한 templates, pages
├── Movie
│ ├── templates
| └── index.tsx
└── Todo
├── templates
└── index.tsx
atoms, molecules, organism 가지 3개의 단계의 단위는 components에 디렉토리 단위로 분리한다.
template와 pages단계는 pages에 page단위로 분리했다. page마다 props 안에 디렉토리를 구성해 index.tsx를
컴포넌트로 두었다. templates는 page의 레이아웃을 구성하는 내부적인 단계라 생각해 pages/{page}/templates
디렉토리를 만들어서 page를 구성하는 템플릿 파일들을 두었다.
Atoms 구성
프로젝트가 작다보니 시작 단위인 atom을 필요한 원시적 html태그를 두었다. 이번 atomic design에서 atom 컴포넌트에 넘겨주는 속성을 어떻게 어떤 기준으로 설정할지 고민해볼 수 있다. 그래서 필요해 보이는 width, height,flex 등등의
속성을 하나씩 생각해 적용해 나갔다.
<Button width="20px" height="20px" flex flexDirectoin="row"> Btn </Button>
예를 들어, Button이란 Atom 컴포넌트를 저렇게 사용하게 처음에 설계해 두었다. 하지만 필요로 하는 속성의 수가
많아져서 장황해지기 떄문에 보기 좋지 않다.
그래서 생각한 것은 컴포넌트의 크기는 클래스 단위로 나누고 필요한 속성만 추가하는 것이였다.
왠만한 크기의 범주는 class 이름인 'small', 'normal', 'big' 을 두고, Span의 경우 molecules의 title을 고려해 title이란
범주를 추가했다, 프로젝트 프로토타입을 보면서 이 범주들을 class 단위로 보면 적당한 font size, padding, line-height
등을 적용했다, Button을 예로 들면
const StyledButton = styled.button<ButtonProps>`
display: flex;
justify-content: center;
align-items: stretch;
border-radius: 3.7px;
cursor: pointer;
outline: none;
&.small {
padding: 7px 7px;
font-size: 1rem;
}
&.normal {
padding: 10px 10px;
font-size: 1.2rem;
}
&.big {
padding: 14px 14px;
font-size: 1.4rem;
}
`;
Stlyed-components로 컴포넌트에 스타일을 적용하고, StyledButton 자체가 하나의 button태그이며, 스타일을
적용하기 위한 컴포넌트 변수이다. 미리 flex를 설정해 horizontal적으로 가운데 정렬을 사전에 정하는 등
default 스타일 속성을 정의했다. 그리고 위에서 언급한 size를 class 단위로 설정했다.
const StyledButton = styled.button<ButtonProps>`
...
flex: ${(props: ButtonProps) => props.flex};
border: ${(props: ButtonProps) => (props.outline === 'none' ? 'none' : `0.7px solid ${props.outline}`)};
background: ${(props: ButtonProps) => (props.transparent ? 'transparent' : props.bgColor)};
color: ${(props: ButtonProps) => props.color};
...
`
그 다음, 해당 특서엥 따라 필요한 스타일 속성에 맞게 props에 들어갈 수 있는 것들을 추가한다. 예를 들어 Button은
글자 색상(string), outline(string), transparent(boolean) 속성을, Span은 가운데 정렬인 textAlign(string), 취소선의
유무인 del(boolean)을 추가했다.
const Button = ({
children,
flex = 'auto',
color = 'black',
outline = 'black',
bgColor = 'white',
transparent = false,
size = 'normal',
type = 'button',
url,
className,
onClick
}: ButtonProps) => {
const classCandidate = [size, className];
const commonProps = {
flex,
color,
size,
outline,
bgColor,
transparent
};
return (
<StyledButton {...commonProps}
className={cn(classCandidate)}
onClick={onClick}
>
{children}
</StyledButton>
);
위의 코드를 기준으로 설명하자면 atom 컴포넌트를 사용하는 상위 컴포넌트에서 className을 지정해 스타일링
하거나 onClick같은 이벤트를 적용하기 위해 기본 html 태그에 상속되게 구현을 하고 children의 경우도 태그들 사이에
그대로 넘겨주었다. commonProps는 스타일을 위해 설정한 outline,color 등의 props들을
spread opreator(스프레드 연산자)로 한번에 넘겨주기 위한 오브젝트이다.
classCandidate는 다수의 클래스들을 classnames란 라이브러리를 사용해 적용하기 위한 배열이다. 예를 들어
size란 props는 small이면, 이 배열에 small이란 string값이 들어간 뒤, classnames가 적용되어 jsx의
className에 'small'이란 클래스가 적용된다. 결과적으로 Button이란 atom 컴포넌트는 아래와 같이 사용할 수 있다.
<Button
color="blue"
outline="none"
transparent
onClick={() => console.log("clicked!")}
>
텍스트
</Button>
특별 case인 Input에 대해 설명하자면 Input은 React에서 보통 OnChange 이벤트를 이용해 state값을
e.target.value로 지정하는 것이다. 하지만 다른 이벤트를 지정해줄 수도 있다. 그래서 생각한 방식이
state,setState함수를 직접, props로 받아서 onChange 속성을 props로 받지 않으면 e.target.value를 setState로
넘겨주고 onChange 속성을 props로 받으면 사전에 부모 컴포넌트에서 저으이한 이벤트를 적용시킨다.
const Input = ({
value = '',
setValue,
onChange,
}: InputProps) => {
const onChangeInput =
onChange ||
useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue!(e.target.value);
}, []);
...
return <StyledInput onChange={onChangeInput} />
};
Molecules 구성
molecules는 atom 보다 어떻게 할지 더 명확해 보인다. atom이 뷰의 기능을 수행할수 있도록 설계한다면,
molecules는 atom을 상징할 수 있는 단위로 만드는 것이다. 그래서 props로 받는 것에 따라서 필요한 atom과
atom에 정의된 속성을 주입해서 불러오는 것 뿐이었다. 게다가 프로젝트에 사용되는 뷰가 적어 moleclues에 받는
props에 양이 많이 한정적이라 설계하는데 시간이 많이 걸리지는 않는다.
Title 컴포넌트를 예로 들어보자.
const Title = ({ children, color = 'inherit', className }: TitleProps) => {
const needProps = {
color
};
const classCandidate = [className];
return (
<StyledTitle {...needProps} className={cn(classCandidate)}>
<Span width="100%" textAlign="center" size="title">
{children}
</Span>
</StyledTitle>
);
};
정의된 props가 color, className 밖에 없는데 프로젝트가 더 커지면 정의된 props가 더 많을테지만 이 프로젝트에서는
필요한 props는 이것밖에 없다. 무언가의 내용을 대표하는 것으로 예를 들어 App의 title,Content의 title에 사용되는
목적으로 만들었다. 그래서 Navigation의 왼쪽 Text, Todos, Movie List란 텍스트들을 타겟으로 정의한 것이였다.
이것들의 공통점은 가운데 정렬이고, width가 wrapper나 속한 부모 컴포넌트에서 100%의 width를 가질 것이라고
판단했다.또한 big보다 큰 title범주의 크기를 가진 Span의 Size를 정의했기 때문에 size="title"을 주었다.
const StyledTitle = styled.div`
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
padding: 1em 0;
color: ${props => props.color};
`;
위 코드처럼 입맛대로 스타일링을 커스터마이징 하면 된다. 적용해보면 justify-content, align-items는 Span을
vertical,horizontal적으로 가운데 정렬을 하려고 설정한 것인데 span은 크기를 padding으로 설정후 하나의
span만 들어가기 떄문에 의미가 없고 span의 padding도 height로 쳐주기 위해 display:flex자체만 필요했다.
그리고 button-list와 list 둘다 여러개 요소를 그룹회 시키는 molecules로 direction prop을 추가하는
등의 작업으로 하나로 통일할지 그대로 두개로 나눌지 선택할수 있다.
하지만 List안에 Button들을 listItem으로 한번 더 묶는 것이 싫었고 역할을 세분화하는 것이 좋다고 판단해
Button의 그룹화를 따로 두었다.
Organisms 구성
이 프로젝트의 아쉬움 점이 있다면 설계시 Organisms이 위에 같이 한정적으로 나와 templates와의 경계가 모호해진
부분이다.
그래서 결론은 Organisms는 기능을 구현한 디테일한 내용을 templates는 페이지란틀에서 레이아웃을 잡기 위한
스타일 속성만을 담당하는 것으로 하자는 것이였다. 그래서 이 프로젝트의 React 비즈니스 로직은
Organisms에 거의 작성되었다. TodoContent 컴포넌트를 예시로 들어보자. 아무래도 TodoContent란 컴포넌트 하나밖에 사용이 되지 않아 templates에서 넘겨주는 props가 하나도 없다.
그래서 Todo List 라는 서비스를 만들기 위한 상태와 이벤트 함수들을 모두 Organisms 단계에서 모두 작성되었다.
const todoItems = useMemo(
() =>
todos.map(v => (
<ListItem key={v.id} hr className="todo-list-item">
{v.writeMode ? (
<Input
className="todo-description todo-update-input"
onChange={onChangeInput(v.id)}
onKeyDown={onEnter(v.id, updateInputValues[v.id], v.done)}
value={updateInputValues[v.id]}
size="big"
/>
) : (
<>
<Span className="todo-description" del={v.done} onClick={onToggleDone(v)}>
{v.text}
</Span>
<ButtonList className="todo-buttons">
<Button color="blue" outline="none" transparent onClick={onChangeToInput(v.id, v.text)}>
수정
</Button>
<Button color="#FDA7DF" outline="none" transparent onClick={onDeleteItem(v.id)}>
삭제
</Button>
</ButtonList>
</>
)}
</ListItem>
)),
[todos, updateInputValues]
);
<StyledTodoContent>
<Title color="#FDA7DF">Todos</Title>
<Form flexDirection="column" className="todo-form" onSubmit={onSubmitForm}>
<Input
placeholder="무엇을 해야하나요?"
name="todo-create-input"
value={insertInputValue}
setValue={setInsertInputValue}
/>
</Form>
{loading ? <Modal dialog={<Span size="title">Loading...</Span>} /> : null}
<List white listHeight="66px">
{todoItems}
</List>
</StyledTodoContent>
위 코드는 뷰에 필요한 부분만을 보여주는 상태 정의문 과 함수 정의문을 제외하고 return 해주는 jsx부분만을
가져온 것이다. Title, List 같이 molecules, Form 같은 Atoms, Modal 같은 Organisms등 이전의 모든 단계에
정의하는 것들이 사용된다. 이 단계에서 이전 단계의 컴포넌트에 필요한 속성들을 주면서 사용하되
내부적인 layout나 media query등을 적용하기 위해 className을 사용한 컴포넌트에 적용해 스타일을
적용하였다. 그래서 atoms, molecules에 필요한 것들이 사용이 많이 되는 속서들만 남겨두는 장황한 props 설정을
피할 수 있었다.
Templates && Pages 구성
templates는 organisms에서 컴포넌트들을 가져와 적용하고, 때에 따라 props를 설정해 레이아웃을 조정하는
설계를 하였다. pages는 각 pages 내부에서 정의한 Templates 파일들을 가져와 사용하고, 각 templates 컴포넌트들에
Props를 넘겨 설정하게 하는 컴포넌트로 정의하였다.
하지만 이 프로젝트에서 templates에 필요한 layout은 하나의 컴포넌트를 가운데 정렬하는 것 밖에 없고
뷰의 내용이 극히 제한되 Pages 컴포넌트에서도 Templates의 파일 하나만을 가져와 정의하는 것밖에 없다.
Todo Page를 예로 들어보자.
└── pages
└── Todo
└── templates
│ └── index.tsx
└── index.tsx
프로젝트에서 pages와 templates는 이런 구성이다.
// Pages/Todo/index.tsx
import React from 'react';
import Template from './templates';
const TodoPage = () => {
return <Template />;
};
export default TodoPage;
Templates의 파일을 가져와 사용하기만 하면된다.
// pages/Todo/templates/index.tsx
import React from 'react';
import styled from 'styled-components';
import TodoContent from '../../../components/organisms/TodoContent';
const StyledTemplate = styled.div`
display: flex;
justify-content: center;
`;
const Template = () => {
return (
<StyledTemplate>
<TodoContent />
</StyledTemplate>
);
};
export default Template;
templates 또한, organisms의 Content 하나를 가져와 사용하고 wrapper를 씌어 가운데 정렬한 것이 다이다.
organisms까지 구성하니 templates 부터는 할것이 별로 없었다. 원래 Atomic Design이 이런 것이 아닌
소규모의 프로젝트인 만큼 뷰의 구성 요소가 적어 Organisms 단계에서 구현이 거의 되기 떄문에 생긴 현상,
그렇기에 이 프로젝트에 Atomic Design을 적용한 것은 Overfetching인 것 같다.
정리
- Atoms는 설계가 가장 중요
- 좀 더 복잡한 프로젝트에 적용해보면서 연습
- Bottom-up의 뷰 설계는 어렵다.
- 그래도 React의 렌더링 최적화에 효과적인 설계이다.
참고 링크
'프론트 엔드 > React' 카테고리의 다른 글
[React] 클래스형 컴포넌트와 함수형 컴포넌트의 차이 (0) | 2022.01.27 |
---|---|
[React] Component와 props, state (0) | 2022.01.18 |
[React] Container-Presenter 패턴 (0) | 2022.01.18 |