본문 바로가기

Front-end

컴포넌트 디자인 패턴

디자인 패턴


정의

소프트웨어를 만드는데 있어 공통적으로 발생하는 문제에 대해 재사용 가능한 해결책

종류

  • 소프트웨어 디자인 패턴
    • 생성패턴 - 의존성 주입, 프로토타입, 싱글톤 …
    • 구조패턴 - 모듈, 프록시, 어댑터 …
    • 행동패턴 - 옵저버, 템플릿 메소드, 상태 …
  • 어플리케이션 디자인 패턴
    • MVC
    • MVVM
    • MVP
    • Flux
  • 컴포넌트 디자인 패턴
    • Presentational and Container Component Pattern
    • Atomic Design Pattern
    • VAC 패턴

Presentational and Container Component Pattern


정의

데이터 처리와 데이터 출력을 분리하는 패턴

 

Container Components

  • 주로 fetch 가 이루어짐
  • 연관있는 서브 컴포넌트 렌더링
  • Markup 이나 style 이 없음
  • 다른 컴포넌트에 callback 함수나 데이터를 전달해 줄 수 있음
  • stateful 한 경향을 가지고 있는 컴포넌트

Presentational Components

  • 화면에 보여지는 것만을 담당하는 Components
  • Markup 과 style 을 포함
  • props를 통해 데이터나 callback 을 받을 수 있음
  • 뷰에 필요한 state를 가지고 있을 수 있음
  • stateless한 경향을 가지는 컴포넌트

예시

Container Component

// CommentListContainer.js
import React from "react";
import CommentList from "./CommentList";

class CommentListContainer extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] };
  }

  componentDidMount() {
    fetch("/my-comments.json")
      .then(res => res.json())
      .then(comments => this.setState({ comments }));
  }

  render() {
    return <CommentList comments={this.state.comments} />;
  }
}

Presenter Component

// CommentListPresenter.js
import React from "react";

const Commentlist = comments => (
  <ul>
    {comments.map(({ body, author }) => (
      <li>
        {body}-{author}
      </li>
    ))}
  </ul>

장점

  • 관심사의 분리가 더 분명해짐
    • 기존 관심사 분리 + UI & state 분리
  • 재사용성을 높일 수 있음
    • 여러 가지 state를 prop으로 받더라도 같은 presentational component를 사용할 수 있음
  • Markup 작업이 편함
    • 이 패턴을 위해 Layout Component를 별도로 추출하게 되면 여러 Container를 작성하는 작업을 피할 수 있음

한계

  • 현재 hook으로 인해 이미 stateful 한 로직을 분리해주고 있어 해당패턴을 강제할 필요는 없음

Atomic Design Pattern


정의

디자인 요소들을 나누어 파악하고 이 요소들이 조합되는 과정을 통해서 디자인을 구성하는 방식

단계

  • Atoms
    • 하나의 구성 요소. 본인 자체의 스타일만 가지고 있으며 다른 곳에 영향을 미치는 스타일은 적용되지 않아야 합니다. 원자는 form labels, inputs, buttons와 같은 basic hmtl elements를 포함합니다.

  • Molecules
    • Atoms가 모여서 만들어지는 하나의 구성 요소
    • Atom 단위인 input label, input, buttoms를 합쳐 새로운 의미있는 단위를 만들 수 있습니다. 실제로 무언가 동작을 할 수 있게 됩니다.

  • Organisms
    • 서로 동일하거나 다른 molecules로 구성될 수 있음
    • 유기체는 로고, 메인 내비게이션, 검색, 소셜 미디어 채널리스트와 같은 다양한 컴포넌트(molecules)로 구성될 수 있습니다.

  • Templates
    • 유기체들을 모아 템플릿으로 생성, 스타일링에 집중한 단위
    • Templates의 중요한 특성은 페이지의 최종 내용보다는 페이지의 기본 내용 구조에 초점을 맞춘다는 것입니다.

  • Pages
    • 페이지는 실제 대표적인 콘텐츠가 배치된 UI의 모습을 보여주는 템플릿의 특정 인스턴스입니다.
    • Pages 단위에서 어플리케이션 상태 관리(리덕스, 모벡스 등등)가 이루어져야 합니다.
    • 하지만 분자, 유기체, 템플릿 단위에서 컴포넌틑를 동작시키기 위한 상태를 관리하는건 괜찮습니다. (input과 onChange를 useState로 관리하는등의 상태관리)

단점, 한계

  • 프로젝트 설계 시간이 오래 소요됨
  • 최소단위인 atmos 구성 단위에 따라 효율성이 달라짐
  • 최상위 구조인 pages에서 상태관리 로직을 관리한다면 props를 통해 하위 구조에 계속 내려줘야하기때문에 prop drilling으로 인해 복잡해지고 유지보수가 어려워질 수 있음

VAC


정의

View Asset Component
VAC 패턴은 View 컴포넌트에서 JSX 영역을 Props Object로 추상화하고, JSX를 VAC로 분리해서 개발하는 설계 방법입니다.

 

VAC 특징

  • 반복이나 조건부 노출, 스타일 제어와 같은 렌더링과 관련된 처리만을 수행합니다.
  • 오직 props를 통해서만 제어되며 스스로의 상태를 관리하거나 변경하지 않는 stateless 컴포넌트입니다.
  • 이벤트에 함수를 바인딩할 때 어떠한 추가 처리도 하지 않습니다

VAC는 state를 가질 수 없지만 state를 가진 컴포넌트를 자식으로 가지는 것은 가능합니다. 이 경우 VAC는 부모 컴포넌트와 자식 컴포넌트 중간에서 개입하지 않고 단순히 props를 전달하는 역할만 합니다.

예제

일반적인 설계

const SpinBox = () => {
  const [value, setValue] = useState(0);

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

props object 정의

  • View 컴포넌트에서 JSX(vue - template) 를 추상화한 props object를 생성하고 사용할 상태정보나 이벤트 핸들러 정의
const SpinBox = () => {
  const [value, setValue] = useState(0);

  // JSX를 추상화한 Props Object
  const props = {
    value,
    onDecrease: () => setValue(value - 1),
    onIncrease: () => setValue(value + 1),
  };

  // JSX의 유무는 중요하지 않음
  return <div></div>;
};

jsx를 VAC로 분리

  • JSX 영역을 분리하여 VAC로 만듦
  • props object 속성을 참고하여 VAC의 props를 정의
// VAC
const SpinBoxView = ({ value, onIncrease, onDecrease }) => (
  <div>
    <button onClick={onDecrease}>-</button>
    <span>{value}</span>
    <button onClick={onIncrease}>+</button>
  </div>
);
// View Component
const SpinBox = () => {
  const [value, setValue] = useState(0);

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

  // JSX를 VAC로 교체
  return <SpinBoxView {...props} />;
};

props object를 사용하는 이유

  • props object와 VAC를 사용하지 않고 직접 선언하여 UI 기능 의존성을 줄이는것도 가능
  • 하지만, 변수나 함수들이 많아지면 어떤것을 JSX에서 사용하는지 파악이 어려워 디버깅도 어려워짐
  • 또한, view 컴포넌트 내에서 JSX를 관리하고 있어 간단한 상태처리의 경우 무의식중에 바로 JSX 에서 적용할 수 있음
// View Component
const SpinBox = () => {
  const [value, setValue] = useState(0);

  // JSX에서 사용할 값을 미리 선언하여 JSX에 적용
  const onDecrease = () => setValue(value - 1);
  const onIncrease = () => setValue(value + 1);

  return (
    <div>
      <button onClick={onDecrease}>-</button>
      <span>{value}</span>
      <button onClick={onIncrease}>+</button>
    </div>
  );
};

presentational 컴포넌트와 VAC 차이

  • VAC 패턴은 Container 컴포넌트에 로직을 위임하는 설계 방식을 따르기 때문에 Presentational과 Container 컴포넌트 패턴의 한 종류라고 볼 수 있음
  • 두 컴포넌트의 근본적인 차이는 컴포넌트가 View 로직(UI 기능, 상태 관리)을 가질수 있는지 여부
  • Presentational 컴포넌트는 상황에 따라 View와 관련된 state를 가지고 스스로 상태를 제어하는 것을 허용하지만, VAC는 stateless 컴포넌트로 스스로의 상태를 제어하지 않고 항상 부모 컴포넌트에서 Props Object를 통해 관리합니다

presentationalVAC

목적 View 로직(UI 기능, 상태 관리)과 렌더링(JSX)의 관심사 분리 비즈니스 로직과 View의 관심사 분리가 목적
부모 컴포넌트 Container 컴포넌트
→ 비즈니스 로직을 관리하고 Presentational 컴포넌트를 제어
view 컴포넌트
→ VAC의 Container 컴포넌트 역할을 하며 JSX를 추상화한 Props Object를 관리하여 VAC를 제어
자식 컴포넌트 Presentational 컴포넌트
-> View 로직(UI 기능, 상태 관리)과 렌더링을 담당
VAC
→ JSX, Style을 관리하여 렌더링 처리

현재 사용중인 구조에서 개선점

  1. composables 의 오용
    1. composable은 stateful 한 로직중 재사용 가능한 로직을 캡슐화 시킨 함수를 가리킴
    2. 현재는 stateful 한 로직이 포함된것은 맞지만 재사용 가능성이 전혀 없는 로직들이 대부분
  2. composables을 props object나 container 형태로 대체
  3. stateful 한 코드와 UI 코드의 분리
    1. 의존성이 낮아져서 재사용이 가능함
    2. 보다 분명한 의도를 가진 구조로 처음 보아도 구조 파악이 쉬움

참고문서

'Front-end' 카테고리의 다른 글

turborepo  (0) 2024.07.18
Intersection Observer API  (0) 2023.07.18
Popover api  (1) 2023.06.13
testing  (0) 2022.12.12