Atomic Design으로 Todo 만들기

thsoon·2020년 3월 2일
60
post-thumbnail

안녕하세요 이번 포스팅은 개발 환경도 세팅했겠다.. 이제 뷰를 작성할 생각입니다.
그래서 관심있었던 Atomic design을 공부하면서 프로젝트를 구성할 생각입니다. 우선 디자인이란 것은 정답이 없다는 전제하에, 딱딱한 이론을 따라가지 않고 공부한 내용들을 토대로 제가 생각하고 결론낸 Atomic Design이므로 주관적인 내용이 많습니다.

우선 Atomic design이 무엇인지 알아볼까요?

Atomic Design이란?

개념

뷰를 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가 이에 해당됩니다.

Organisms

  • 배치를 위한 layout 단위로 하나의 인터페스를 형성하는 그룹
  • header, navigation 등이 이에 해당됩니다.
  • Card System에서 Card들이 Grid layout으로 형성된 집합이 이에 해당됩니다.

Templates

  • 실제 Organisms들을 레이아웃이나 데이터 흐름을 연결합니다.
  • 클래스 시스템의 클래스로, 객체의 설계도, 페이지의 설계도입니다.

Pages

  • 정의된 Template에 데이터를 넣어 뷰를 완성시키는 단계입니다.
  • 클래스 시스템의 인스턴스, 객체의 구현체, 페이지 설계도로 그린 페이지 그 자체입니다.

개인적으로 React를 사용하면서 자체적으로 컴포넌트 단위로 뷰를 이루고, 상태에 따라 다른 뷰를 보여주기 때문에, Atomic Design 이론만 보아도 React와 잘 맞을 것 같습니다.

이제 숙제가 원하는 뷰를 기반으로 Atomic Design을 한 번 시도해볼까요?

숙제

반응형 웹

우선 Atomic Design을 들어가기전에 반응형 웹을 고려해봅시다.
Content만 보면 태블릿 너비없이 모바일, PC 딱 두가지 상태만 봐도 될 것 같습니다.

네비게이션바를 보면 왼쪽 텍스트는 title, 오른쪽 텍스트들은 category button들일 것입니다. 모바일 상태에선 다음과 같이 width를 100%로 줄 것입니다.

Content

Movie List에선 딱히 너비에 따른 수정이 필요가 없어보입니다.
하지만 Todo List에선 Item Content와 수정/삭제 버튼이 함께 하나의 block 단위로 되어 있는데 모바일에선 다음과 같이 각각 한 block을 차지하는 형태로 만들 것입니다.

Atomic Design 설계

Atom 구성

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 구성

  • 파란색
  • 네비게이션 역할을 담당

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, organisms까지 3개의 단계의 단위는 components에 디렉토리 단위로 분리를 했습니다.
template와 pages 단계는 pages에 page 단위로 분리를 했습니다. page마다 pages 안에 디렉토리를 구성해 index.tsx를 page 컴포넌트로 두었습니다. templates는 page의 레이아웃을 구성하는 내부적인 단계라 생각했기 때문에 pages/{page}/templates 디렉토리를 만들어 page를 구성하는 템플릿 파일들을 두었습니다.

프로젝트 github url을 첨부하겠습니다.
https://github.com/dlatns0201/prography_6th_react
컴포넌트 하나하나를 설명하면 양이 길어질 것 같아 구성 단계 중 하나만을 예로 들면서 설명할 것입니다.

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;
	}
`;

저는 styled-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), tranparent(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 등의 prop들을 spread operator로 한번에 넘겨주기 위한 Object입니다.
classCandidate는 다수의 클래스들을 classnames란 라이브러리를 사용해 적용하기 위한 배열입니다. 예를 들어 size란 prop은 '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에 정의된 속성을 주입해 불러오는 것뿐이였습니다. 게다가 프로젝트에 사용되는 뷰가 적어서 molecules에서 받는 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와의 경계가 모호해졌다는 것입니다. 그래서 처음으로 Atomic Design을 하면서, 개념 정리하는데 시간이 오래 걸린 장애 요소였습니다.
그래서 결론낸 것은 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 등 이전의 모든 단계에서 정의한 것들이 사용됩니다.
또, 이 단계에서 이전 단계의 컴포넌트들에 필요한 속성들을 주면서 사용하되, 내부적인 layoutmedia 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인 것 같습니다.

정리

  1. Atoms 설계가 가장 중요하다.
    • 프로젝트를 진행하면서 Atoms의 속성을 수도 없이 바꿨습니다.
    • 그 결과, Atoms의 설계, 구현이 Atomic design 과정에서 50%의 시간을 차지한 것 같습니다.
    • 원시적인 html 태그를 기준으로 할지, 어느정도의 기능을 가진 이름의 컴포넌트로 구성할지 프로젝트에 따라 잘 설정하는 것이 중요한 것 같습니다.
  2. 좀 더 복잡한 프로젝트에 적용해보면서 연습하고 싶다.
    • 뷰의 요소가 제한적이어서 templates의 개념이 모호했습니다.
    • templates와 organisms에 props를 적용하지 못해, organisms의 재사용성이 부족했고 templates의 존재 의의를 찾지 못했습니다.
    • 네이버 메인 홈페이지 정도의 규모에 적용해보는 것이 좋을 것 같습니다.
  3. Bottom-up의 뷰 설계는 어렵다.
    • 기존엔 레이아웃을 지정하고, 레이아웃 내용물을 구현한 뒤, 컴포넌트로 쪼개는 top-down 형식으로 설계를 했습니다.
    • 그래서 output을 보기전에 필요한 기능들을 미리 작은 단위에 설계해나가는 bottom-up 설계가 익숙치 않았습니다.
  4. 그래도 React의 렌더링 최적화에 효과적인 설계이다.
    • 상태를 분산해 적용하기 때문에 상태 변화에 따른 렌더링이 작은 범위로 이루어져 성능을 최적화시키기 좋았습니다.
    • 덕분에 렌더링 최적화와 더불어 효율적인 컴포넌트 분할의 필요성을 자각하게 되었습니다.

설계 분야일 뿐 아니라 정답이 없는 이런 분야는 혼자하는 것보다 스터디를 하면서 타인과 의견 공유를 하면서 개념을 정립하는 것이 좋은 것 같습니다 ㅠㅠ

profile
바닥부터 쌓아가는 FE 개발자

6개의 댓글

comment-user-thumbnail
2020년 9월 1일

좋은 글 정말 잘 읽었습니다. 프레임워크와 사용하면 어떤식으로 될지 그림이 그려져서 더 보기 좋았습니다.

1개의 답글
comment-user-thumbnail
2020년 11월 3일

좋은글 감사합니다. 아토믹디자인 처음 적용하는데 애를 많이 먹고있는데, 많은 도움이 될것 같네요. 🙂

답글 달기
comment-user-thumbnail
2021년 5월 25일

좋은 글 감사합니다.
궁금한 점이 있는데 Page에서 api통신을 하여 받아온 값을 Atom의 버튼 또는 span태그에 렌더링하려고 할 경우
Page -> Template -> Organisms -> Molecules -> Atom으로 쭉쭉 props를 내려주는
비효율적으로 보이는 props 할당이 발생하는데 이 부분에 대해서는 어떻게 생각하시나요?

저는 해당 부분에서 스트레스를 많이 받고있어 의견이 너무 궁금합니다 ㅠㅠ

1개의 답글
comment-user-thumbnail
2023년 7월 29일

글 너무 잘 읽었습니다!!!
프로그래밍 하면서 직관적인게 매우 중요하단 생각을 매일하는데
너무 직관적이어서 이해하기 너무 편했습니다ㅎㅎ

답글 달기