2022년 4월 4일 - 5 React Design Patterns

포스팅 읽기

5 Advanced React Patterns


React 패턴 등장 배경

  1. 재사용 가능한 컴포넌트
  2. 사용이 간단한 컴포넌트
  3. 확장이 용이한 컴포넌트

위 세가지를 충족하는 컴포넌트를 디자인하기 위한 노력들이 리액트 패턴이 탄생시켰다.


1. Compound Components Pattern

  • 불필요한 Prop drilling 없이 표현적이고 선언적인 컴포넌트 형태이다.
  • 더 나은 관심사의 분리와 이해가 쉬운 API를 사용하였다.
  • 컴포넌트 UI를 커스터마이징하기 좋게 만들 수 있다.
  • 개인적 의견: 멋지다..
import React from "react";
import { Counter } from "./Counter";

function Usage() {
  const handleChangeCounter = (count) => {
    console.log("count", count);
  };

  return (
    <Counter onChange={handleChangeCounter}>
      <Counter.Decrement icon="minus" />
      <Counter.Label>Counter</Counter.Label>
      <Counter.Count max={10} />
      <Counter.Increment icon="plus" />
    </Counter>
  );
}

export { Usage };

장점

  • API Complexity가 줄었다. (사용시 파악이 쉽다)
  • 모든 prop들을 전부 부모 컴포넌트에 넣고 자녀 컴포넌트로 drilling 해주지 않아도 된다. 각각의 prop들을 SubComponent에 직접 전달하여 prop들이 어디에서 사용되는지 이해가 쉽다.
  • UI 유연성이 좋다. SubComponent를 어떻게 지정, 배치하는냐에 따라 UI를 flexible하게 조합 할 수 있다.
  • 관심사의 분리: 대부분의 로직은 메인 컴포넌트인 Counter에 속한다. 자녀 컴포넌트들에게 states, handler를 공유하기 위해 React.context가 사용된다.

단점

  • 너무 유연한 UI, 자식 배치를 잘 못하는 등 원치 않는 동작을 유발시킬 가능성이 있다.
  • JSX 코드 줄을 늘리게 된다.

이 패턴을 사용하는 라이브러리

  • React Bootstrap
  • Reach UI

2. Control Props Pattern

  • 컴포넌트를 더 컨트롤 할 수 있다.
  • 외부 state가 'single source of truth'로 사용됨
  • 유저가 커스텀 로직을 추가 할 수 있음
import React, { useState } from "react";
import { Counter } from "./Counter";

function Usage() {
  const [count, setCount] = useState(0);

  const handleChangeCounter = (newCount) => {
    setCount(newCount);
  };
  return (
    <Counter value={count} onChange={handleChangeCounter}>
      <Counter.Decrement icon={"minus"} />
      <Counter.Label>Counter</Counter.Label>
      <Counter.Count max={10} />
      <Counter.Increment icon={"plus"} />
    </Counter>
  );
}

export { Usage };

장점

  • main state가 컴포넌트 외부에 있으므로 사용자가 컴포넌트에 대한 제어권을 더 가진다.

단점

  • JSX의 control prop으로 들어갈 state, handler 코드들이 나뉘어 분산된다.

이 패턴을 사용하는 라이브러리

  • Material UI

3. Custom Hook Pattern

  • 메인 로직이 custom hook으로 옮겨진다.
  • 사용자가 hook을 통해 컴포넌트를 더 제어한다.
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

function Usage() {
  const { count, handleIncrement, handleDecrement } = useCounter(0);
  const MAX_COUNT = 10;

  const handleClickIncrement = () => {
    //Put your custom logic
    if (count < MAX_COUNT) {
      handleIncrement();
    }
  };

  return (
    <>
      <Counter value={count}>
        <Counter.Decrement
          icon={"minus"}
          onClick={handleDecrement}
          disabled={count === 0}
        />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment
          icon={"plus"}
          onClick={handleClickIncrement}
          disabled={count === MAX_COUNT}
        />
      </Counter>
      <button onClick={handleClickIncrement} disabled={count === MAX_COUNT}>
        Custom increment btn 1
      </button>
    </>
  );
}

export { Usage };

장점

  • 사용자가 hook과 JSX 사이에 로직을 추가 할 수 있어 더 많이 컴포넌트를 제어 할 수 있다.

단점

  • 로직 파트가 렌더링 파트와 나누어져 사용자가 이들을 잘 연결시켜주어야 한다.
  • 컴포넌트를 올바르게 사용하기 위해서는 컴포넌트에 대해 잘 파악해야 한다.

이 패턴을 사용하는 라이브러리

  • React table
  • React hook form

4. Props Getters Pattern

  • Custom Hook pattern은 컴포넌트를 더 많이 제어 할 수 있지만 로직을 생성해야하므로 컴포넌트를 통합하기는 더 어렵게 된다. Props Getters Pattern은 이를 보완한다.
  • native props를 노출시키는 것이 아니라 props getters 리스트를 제공한다.
  • getter는 여러 prop들을 리턴하는 함수이고 사용자가 JSX element에 링크하기 자연스럽게 네이밍 된다.
  • 개인적 의견: 코드가 못생긴 느낌.. 익숙하지 않아서 일 수도 있다.
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

const MAX_COUNT = 10;

function Usage() {
  const {
    count,
    getCounterProps,
    getIncrementProps,
    getDecrementProps
  } = useCounter({
    initial: 0,
    max: MAX_COUNT
  });

  const handleBtn1Clicked = () => {
    console.log("btn 1 clicked");
  };

  return (
    <>
      <Counter {...getCounterProps()}>
        <Counter.Decrement icon={"minus"} {...getDecrementProps()} />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment icon={"plus"} {...getIncrementProps()} />
      </Counter>
      <button {...getIncrementProps({ onClick: handleBtn1Clicked })}>
        Custom increment btn 1
      </button>
      <button {...getIncrementProps({ disabled: count > MAX_COUNT - 2 })}>
        Custom increment btn 2
      </button>
    </>
  );
}

export { Usage };

장점

  • 복잡한 부분은 숩겨서 사용자가 사용 할 때는 JSX와 올바른 getter를 연결시키면 된다.
  • getters에 prop들을 overload시켜 유연하게 로직을 추가 시킬 수 있다.

단점

  • 사용자가 getter가 가진 prop list를 파악해야하고 이 때 가독성이 안좋다.

이 패턴을 사용하는 라이브러리

  • React table
  • Downshift (웹 접근성을 준수하는 콤보박스 구현을 도와주는 React 라이브러리)

5. State reducer pattern

  • Custom Hook Pattern과 유사하고 사용자가 hook으로 전달하는 reducer를 추가로 정의한다. reducer는 컴포넌트 내부 동작을 전달한다.
  • 아래 예제 코드에서는 Custom Hook pattern과 연결했지만 Compound components pattern과도 사용 가능하며 reducer를 메인 컴포넌트에 전달해주면 된다.
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

const MAX_COUNT = 10;
function Usage() {
  const reducer = (state, action) => {
    switch (action.type) {
      case "decrement":
        return {
          count: Math.max(0, state.count - 2) //The decrement delta was changed for 2 (Default is 1)
        };
      default:
        return useCounter.reducer(state, action);
    }
  };

  const { count, handleDecrement, handleIncrement } = useCounter(
    { initial: 0, max: 10 },
    reducer
  );

  return (
    <>
      <Counter value={count}>
        <Counter.Decrement icon={"minus"} onClick={handleDecrement} />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment icon={"plus"} onClick={handleIncrement} />
      </Counter>
      <button onClick={handleIncrement} disabled={count === MAX_COUNT}>
        Custom increment btn 1
      </button>
    </>
  );
}

export { Usage };

장점

  • state reducer를 사용하여 사용자가 컴포넌트 내부 동작을 컴포넌트 바깥에서 제어 할 수 있게 해준다.

단점

  • 구현 복잡도가 높다.
  • 컴포넌트 로직에 대한 이해가 필요하다.

이 패턴을 사용하는 라이브러리

  • Downshift

마무리

  • 컴포넌트에 대한 제어권을 더 많이 줄 수록 plug and play 마인드셋에서 멀어진다. 나는 이것을 중요시하는 편이다.
  • 블로그 포스트를 읽으면서 다양한 리액트 디자인패턴에 대해 알아보았고 특히 Compound components pattern에 대해 알 수 있어 좋은 공부가 되었다.

좋은 웹페이지 즐겨찾기