React Design Pattern

aborile·2023년 4월 14일
7

React / Next

목록 보기
2/4
post-thumbnail

리액트 디자인 패턴

리액트 디자인 패턴에 관한 글을 여럿 찾아 보면, 조금 더 구조 설계(특히 폴더 구조 관리)와 business logic - view logic의 관리 부분에 초점이 맞춰진 느낌이다.

Presentational & Container Component Pattern

2015년 Dan Abramov가 처음 소개한 패턴으로, 가장 기본적이고 유명한 패턴이다.

  • Presentational Component
    • 화면에 표시하는 것만 담당. props를 통해서 데이터나 콜백을 받아옴
    • UI와 관련된 상태만 가짐 (대표 예시: dropdown 열림 여부)
  • Container Component
    • 동작, data fetch 등의 business logic만을 담당
    • Presentational Component에 보여줄 데이터를 가져오거나, 변화시키거나, 행동/동작 등을 정의
    • DOM Markup이나 스타일(css) 없이, 연관이 있는 서브 컴포넌트를 렌더링
    • stateful한 경향

예시

Before

function TotalComponent() {
  const [titleState, setTitleState] = useState("some title");
  const data = fetchSomething();
  const onClose = () => {
    // do something
  }
  // ...
  return (
    <div>
      <div>** This component only awares about UI</div>
      <div>{titleState}</div>
      <div>{data.description}</div>
      <button onClick={onClose}>Close</button>
      {* ... *}
    </div>
  )
}

After

function PresentationalComponent({ title, description, onClose, ...props }) {
  return (
    <div>
      <div>** This component only awares about UI</div>
      <div>{title}</div>
      <div>{description}</div>
      <button onClick={onClose}>Close</button>
      {* ... *}
    </div>
  )
}
function ContainerComponent() {
  const [titleState, setTitleState] = useState("some title");
  const data = fetchSomething();
  const onClose = () => {
    // do something
  }
  // ...
  return (
    <PresentationalComponent
      title={titleState}
      description={data.description}
      onClose={onClose}
      // other props
      />
  )
}

보편적인 폴더 구조는 다음과 같은 식이다.

components
├── Login
│   ├── LoginContainer.tsx
│   ├── LoginPresentation.tsx
│   └── Login.style.ts
└── User
    ├── UserContainer.tsx
    ├── UserPresentation.tsx
    └── User.style.ts
...

장점

  • 기능과 UI를 명확하게 분리하므로, 코드가 복잡할수록 가독성이 개선되고 유지보수가 용이해짐
  • UI 컴포넌트가 추출되므로, 마크업이 편리, UI 재사용이 가능함

단점

  • hooks의 도입 이후, Dan Abramov는 이 패턴을 더 이상 추천하지 않음
    • 불필요하게 맹목적으로 패턴을 강제하게 됨
    • hooks의 도입으로 로직 분리가 쉬워지고, 로직 또한 재사용할 수 있게 됨

Component - Custom Hook

hooks의 도입 이후, Dan Abramov가 새롭게 제안한 방식이다.

예시

Before

function TotalComponent() {
  const [titleState, setTitleState] = useState("some title");
  const data = fetchSomething();
  const onClose = () => {
    // do something
  }
  // ...
  return (
    <div>
      <div>** This component only awares about UI</div>
      <div>{titleState}</div>
      <div>{data.description}</div>
      <button onClick={onClose}>Close</button>
      {* ... *}
    </div>
  )
}

After

function useTitle() {
  const [title, setTitle] = useState("some title");
  const data = fetchSomething();
  const onClose = () => {
    // do something
  };
  // ...

  return { title, setTitle, data, onClose };
}

function TotalComponent() {
  const { title, setTitle, data, onClose } = useTitle();

  return (
    <div>
      <div>** This component only awares about UI</div>
      <div>{titleState}</div>
      <div>{data.description}</div>
      <button onClick={onClose}>Close</button>
      {* ... *}
    </div>
  )
}

장점

  • UI에서 기능을 명확하게 분리함
  • 연관된 기능 단위로 묶어서 분리할 수 있으므로, 로직을 재사용할 수 있음

Atomic Design Pattern

2013년 Brad Frost에 의해 처음으로 제시되었다. 원래는 디자인 시스템을 위한 패턴이다. (디자인 시스템에서 컴포넌트를 효율적으로 구성하는 방식에 대한 내용이다.)

  • Atoms
    • 가장 작은 단위의 구성 요소. 본인 자체의 스타일만 가지고 있어야 함.
    • 예: input, label, button 등
  • Molecules
    • atoms가 모여서 만들어지는 하나의 구성 요소
    • 실제로 무언가 동작을 할 수 있도록 만든 의미있는 단위.
    • 예: label과 input 2개를 합쳐 아이디와 패스워드를 입력할 수 있는 LoginInput을 만듦
  • Organisms
    • 서로 동일하거나 다른 molecules, atoms로 구성한 비교적 복잡한 구조
    • 예: LoginInput, LoginStatusToggle, button을 합쳐 LoginComponent를 만듦
  • Templates
    • 페이지의 기본 구조 및 스타일링에 집중한 단위
    • 예: Header - Title Logo - Component - Advertise Banner - Footer 구조로 LoginTemplate을 만듦
  • Pages
    • template을 사용하여 실제 페이지를 구성
    • 어플리케이션의 상태 관리가 이루어져야 함.
      • 컴포넌트를 동작시키기 위한 상태 관리는 atom, molecule 등의 하위 단계에서 이루어져도 됨
      • 예: input의 onChange를 위한 상태 등은 input atom에서
      • 예: 실제로 input을 통해 로그인하는 action은 page에서

보편적인 폴더 구조는 다음과 같은 식이다.

components
├── ui
│   ├── atoms // 가장 작은 단위의 컴포넌트
│   │   ├── Input
│   │   └── Checkbox
│   ├── molecules // atom을 여러 개 조합한 컴포넌트 
│   │   └── LoginInputs
│   └── organisms // molecule과 atom을 조합하여 만든 컴포넌트
│       └── LoginComponent
├── templates // 컴포넌트를 넣어 사용할 레이아웃
│   ├── LoginTemplate
│   ├── UserTemplate
│   └── BookTemplate
└── pages // 가장 큰 단위의, templates에 atom, molecule, organism 등을 주입한 컴포넌트
    └── Login

장점

  • 컴포넌트를 기능의 단위로 나누므로 UI 재사용성이 뛰어남
  • application과 분리하여 컴포넌트를 개발하고 테스트할 수 있음. 스타일 가이드 도구 사용이 용이.
  • 통합 개발 시 백엔드 로직에 의존하지 않음
  • 디자인을 일관성 있게 통일하기에 용이 (디자인 시스템 구축에 용이한 구조)

단점

  • 디자인 시스템을 구축하기 위한 초기 비용이 필요
    • 어디까지를 어느 단위로 볼 것인가에 대한 논의
    • 최소 단위를 재사용하기 위한 디자인 시스템
  • 로직과 상태를 낮은 단위까지 공유해야 하기 때문에 props drilling issue 발생할 수 있음
  • 컴포넌트가 명확하게 분리되어 있으므로, 상위 컨테이너 컴포넌트의 사이즈를 모를 때 미디어쿼리를 사용하기 어려울 수 있음.

예시

리디에서는 Atomic Design Pattern을 도입하여 프로젝트를 구성하였는데, 5단계는 너무 많다는 생각이 들어 Atom - Block - Page의 3단계로만 구분하여 사용하였다고 한다.

components // UI모듈을 구조화해서 관리 
├── atoms // 버튼, 체크박스 등 가장 작은 기능을 담당하는 컴포넌트
│   ├── Button
│   │   └── index.tsx
│   ├── CheckBox
│   └── ...
├── blocks // 페이지보다 작은 단위면서 공통적으로 사용할 수 있는 컴포넌트
└── pages // 가장 큰 단위의 페이지
    ├── Home // 라우트 단위 directory
    │   ├── index.tsx // 최상위 코드, container 형태의 로직만 담음
    │   ├── Home.tsx // UI 모듈이 필요할 시 별도로 분리
    │   ├── OtherChild.tsx
    │   └── styles.ts // style 관련 코드
    └── ...
ducks // redux 모듈을 관리. ducks패턴을 사용하여 reducer&action을 같이 관리
├── auth.ts
├── home.ts
├── index.ts
├── series.ts
└── ...
hocs // common hocs
hooks // custom hooks

VAC Pattern

View Asset Component.

기존의 View 컴포넌트에서 jsx와 관련된 영역을 props object로 추상화한 뒤, VAC로 분리해서 개발하는 방식이다. 비즈니스 로직 뿐 아니라 UI 로직에서도 렌더링 관심사를 명확하게 분리하는 것이 목적이다.

예시

Before

function NumberBox() {
  const [value, setValue] = useState(0);

  return (
    <div>
      <button disabled={value < 1} onClick={() => setValue(value - 1)}>-</button>
      <span>{value}</span>
      <button disabled={value > 9} onClick={() => setValue(value + 1)}>+</button>
    </div>
  );
}

After

function NumberBox() {
  const [value, setValue] = useState(0);

  const props = {
    value,
    disabledDecrease: value < 1,
    disabledIncrease: value > 9,
    onDecrease: () => setValue(value - 1),
    onIncrease: () => setValue(value + 1),
  };

  // JSX 대신 VAC
  return <NumberBoxView {...props} />;
}

function NumberBoxView= ({ value, disabledDecrease, disabledIncrease, onIncrease, onDecrease }) => (
  <div>
    <button disabled={disabledDecrease} onClick={onDecrease}>-</button>
    <span>{value}</span>
    <button disabled={disabledIncrease} onClick={onIncrease}>+</button>
  </div>
);

VAC의 특징

  • 반복, 조건부 렌더링, 스타일 제어 등 렌더링과 관련된 처리만을 수행
  • props를 통해서만 제어되며, 스스로의 상태를 관리하거나 변경하지 않음 (stateless component)
  • state를 가질 수는 없지만, state를 가진 컴포넌트를 자식으로 가지는 것은 가능. 이 경우, 중간 개입 없이 부모에서 자식으로 props를 전달하는 역할만 해야 함.
  • Presentational & Container 패턴의 한 종류.
    • 근본적인 차이는 View를 담당하는 컴포넌트가 상태를 가질 수 있는지 여부.
    • Presentational 컴포넌트는 상황에 따라 View와 관련된 state를 가지고 스스로 상태를 제어. (비즈니스 로직과 View 로직의 분리가 최우선 목적.)
    • VAC는 stateless 컴포넌트. 항상 부모 컴포넌트에서 props를 통해 관리해야 함. (비즈니스, View 로직과 렌더링 분리가 최우선 목적.)

장점

  • UI 개발자와 로직 개발자가 다를 경우, UI 개발자는 VAC 부분만, 로직 개발자는 props를 전달하는 부분만 개발하면 되므로 충돌을 최소화할 수 있음.

단점

  • props가 지나치게 많아지는 등 코드가 복잡해질 수 있음.

References

이외에도 이 글을 작성하는 데 직접적으로 참고하진 않았지만, 관련해서 다양한 글을 읽어보며 흥미롭게 읽었던 글 몇 개를 같이 남긴다.

profile
기록하고 싶은 것을 기록하는 저장소

0개의 댓글