여러가지 React 디자인 패턴

Jemin·2023년 7월 27일
0

프론트엔드

목록 보기
30/51
post-thumbnail

Compound Components 패턴

Compound Components 패턴은 컴포넌트의 구성 요소를 조합하여 더 복잡한 기능을 만드는 방법이다.

이 패턴은 하나의 상위 수준 컴포넌트가 여러 개의 하위 컴포넌트로 구성되어 있는 경우에 적합하다. 각 하위 컴포넌트들은 서로 간의 관계를 공유하며, 부모 컴포넌트의 상태와 데이터에 접근할 수 있다. 이 패턴은 불필요한 Props Drilling 없이 표현적이고 선언적인 컴포넌트를 만들 수 있다.

Compound Components 패턴의 주요 특징은 다음과 같다.

  1. 고립된 구성: 각 하위 컴포넌트는 독립적으로 작동하며, 부모 컴포넌트의 직접적인 관계를 가지지 않는다. 이로 인해 컴포넌트 간의 결합도가 낮아지고, 재사용성과 유지보수성이 향상된다.

  2. 공유 상태: 하위 컴포넌트들은 상위 수준 컴포넌트의 상태와 데이터에 접근할 수 있다. 이를 통해 다양한 하위 컴포넌트들이 동일한 상태를 활용하며, 같은 상태에 대해 반응적으로 동작할 수 있다.

  3. 유연한 구성: 하위 컴포넌트들은 여러 개의 컴포넌트로 조합될 수 있다. 이로 인해 사용자는 원하는 형태로 컴포넌트들을 구성하여 사용할 수 있다.

예를 들어, 토글 버튼과 해당 버튼을 활성화시키는 컨텐츠 패널이 서로 연결되는 기능을 구현할 때 컴파운트 컴포넌트 패턴을 사용할 수 있다. 이때, 토글 버튼과 컨텐츠 패널은 각각 독립적인 컴포넌트로 구성되지만, 부모 컴포넌트에서 이 두 개의 하위 컴포넌트를 함께 그룹화하여 사용할 수 있다. 또한, 토글 버튼이 클릭되었을 때 컨텐츠 패널의 표시 여부를 조절할 수 있게 된다.

예시 코드

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 복잡성 감소: 하나의 거대한 부모 컴포넌트에 모든 prop을 넣고 자식 컴포넌트에 전달해주는 방법이 아닌 각각의 prop이 각각의 서브 컴포넌트에 붙어있는 방법이다.

  • 유연한 마크업 구조: 다양한 케이스 생성이 가능하여 뛰어난 UI 유연성을 제공한다. 예를 들어 개발자는 하위 컴포넌트 순서를 쉽게 변경하거나 표시할 항목을 정의할 수 있다.

  • 관심사 분리: 대부분의 로직은 Counter 컴포넌트에 포함되며, Context API를 통해 상태와 핸들러를 자식 컴포넌트간에 공유한다. 이로 인해 컴포넌트들 간의 책임을 명확하게 나눌 수 있다.

단점

  • 과도한 UI 유연성: 높은 수준의 유연성을 가지면 예상하지 못한 상황이 발생할 수 있다.(원하지 않는 코드, 잘못된 자식 컴포넌트의 순서, 필수 자식 누락 등)

  • 무거운 JSX: 이 패턴은 ESLint나 Prettier를 사용한다면 JSX 코드의 양을 증가시킬 수 있다. 작은 컴포넌트에서는 큰 문제가 아닌 것처럼 보이지만, 큰 그림을 보면 확실히 큰 차이가 있을 수 있다.

다른 패턴들과 비교

  • Inversion of Control: 1/4

  • 구현 복잡도: 1/4

React BootstrapReach UI 라이브러리에서 이 패턴을 사용한다.

Inversion of Control이란?
일반적으로 프로그램에서는 제어 흐름이 애플리케이션 코드에서 결정된다. 하지만 IoC를 적용하면 이러한 제어 흐름의 결정 권한이 프레임워크나 컨테이너에게 넘어가게 된다. 즉, 개발자가 프레임워크나 컨테이너에게 어떤 객체를 사용하고자 하는지만 알려주면, 프레임워크나 컨테이너가 객체의 생성과 의존성 주입을 담당하여 애플리케이션의 제어 흐름을 결정하게 된다.

IoC는 애플리케이션의 유연성, 재사용성, 테스트 용이성을 향상시키는데 도움이 되며, 모듈 간의 결합도를 낮추고 유지 보수를 용이하게 하는데 기여한다.

Control Props 패턴

Control Props 패턴은 React 컴포넌트에서 자식 컴포넌트에게 제어를 위임하는 디자인 패턴이다.

이 패턴을 사용하면 부모 컴포넌트가 자식 컴포넌트의 동작을 컨트롤하고 상태를 변경할 수 있다.

Control Props 패턴의 핵심 아이디어는 자식 컴포넌트가 동작하는 방식을 완전히 부모 컴포넌트에게 위임하는 것이다. 자식 컴포넌트는 부모로부터 함수(props)를 받아와서 이를 이벤트 핸들러 또는 콜백으로 사용한다. 이렇게 하면 자식 컴포넌트가 어떤 동작을 수행하거나 상태를 변경할 때 부모 컴포넌트가 제어할 수 있다.

이러한 패턴은 특히 양식(form) 컴포넌트나 모달 컴포넌트와 같이 부모 컴포넌트가 자식 컴포넌트를 제어해야 하는 상황에서 유용하게 사용된다. 부모 컴포넌트는 자식 컴포넌트의 상태를 추적하거나 자식 컴포넌트의 동작을 트리거하는데 사용될 수 있다.

Control Props 패턴은 컴포넌트 간 협력과 상호작용을 가능하게 하며, 제어 가능성과 재사용성을 제공한다. 그러나 구조가 복잡해지고 결합도가 증가할 수 있다는 단점도 있다.

예시 코드

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

장점

  • 더 많은 제어 제공: 개발자가 기본 상태를 제어하므로 Counter 동작에 직접 영향을 미칠 수 있다.

단점

  • 구조 복잡성: 이전에는 단일 통합(JSX)로 컴포넌트를 작동시키기에 충분했다. 그러나, 이제는 JSX, useState, handleChange 세 곳 모두를 확인해야 한다.

  • 지나친 제어: 너무 많은 제어를 부모 컴포넌트로부터 받는 경우, 자식 컴포넌트가 너무 의존적이 되어 코드의 재사용성이 떨어질 수 있다. 이 경우, 컴포넌트 간의 협력이 잘못 설계되었을 수 있으므로 주의해야 한다.

  • 컴포넌트 간 결합도 증가: 컴포넌트 간의 결합도가 증가할 수 있다. 이는 컴포넌트의 재사용성과 유지보수성을 저하시킨다.

다른 패턴들과 비교

  • Inversion of Control: 2/4

  • 구현 복잡도: 1/4

Material UI 라이브러리에서 해당 패턴을 사용한다.

Custom Hook 패턴

Custom Hook 패턴은 특정 기능을 수행하는 로직을 함수로 묶어서 재사용할 수 있게 만드는 것을 의미한다.

Custom Hook 작성 규칙

  1. 함수 이름은 "use"로 시작해야 한다. 이렇게 함으로써 React는 해당 함수가 Custom Hook이라는 것을 인식하고, 특정 규칙에 따라 Hook을 사용할 수 있게 된다.

  2. Custom Hook 내에서는 다른 Hook을 사용할 수 있다. 즉, 상태 관리를 위해 useState, useEffect와 같은 기본 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 = () => {
    // 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 사이에 자신만의 로직을 삽입해 Counter의 기본 동작을 수정할 수 있다.

  • 로직 재사용: 로직을 추상화하고, 다른 컴포넌트에서도 동일한 로직을 재사용할 수 있다.

  • 코드 간소화: 컴포넌트 내부에서 로직을 묶어서 사용하므로, 컴포넌트 자체는 간결해지고 목적에 집중할 수 있다.

단점

  • 구현 복잡성: 사용하는 것이 더 복잡하다. 로직이 렌더링하는 부분과 분리되어 있으며, 유저는 둘을 이어줘야 한다. 올바르게 사용하기 위해서는 컴포넌트가 어떻게 동작하는지 알아야할 필요가 있다.

  • 이름 충돌: Custom Hook을 여러 곳에서 사용하면 이름 충돌이 발생할 수 있으므로 함수 이름을 유의하여 지정해야 한다.

  • Hook 규칙: 일반적인 함수와 다르게 Hook 규칙을 따라야 하므로, 이를 준수하는 것이 중요하다.

다른 패턴들과 비교

  • Inversion of Control: 2/4

  • 구현 복잡도: 2/4

React tableReact hook form 라이브러리에서 이 패턴을 사용한다.

Props Getters 패턴

Props Getters 패턴은 컴포넌트의 props를 조작하거나 확장하기 위해 사용된다.

이 패턴은 주로 고차 컴포넌트(Higher-Order Component, HOC)와 함께 사용되며, 컴포넌트 속성(props)를 조작하는데 유용하다. 엄청난 통제권을 주긴 하지만, 그만큼 컴포넌트를 이용하기 어렵게 만든다.

이런 복잡도를 감추기 위해 props getters의 목록을 제공한다. 이는 유저가 올바른 JSX요소에 접근할 수 있도록 의미있는 이름을 사용해야 한다.

Props Getters 패턴 작동 방식

  1. 부모 컴포넌트에서 자식 컴포넌트로 props를 전달한다.

  2. 고차 컴포넌트를 사용하여 자식 컴포넌트의 props를 가로채고 필요에 따라 조작한다.

  3. 가로챈 props와 함께 원래의 props를 자식 컴포넌트로 전달한다.

예시 코드

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

장점

  • 사용 용이성: 컴포넌트를 사용하는 쉬운 방법을 제공하면서 복잡한 부분은 가려져 있다. 사용자는 올바른 getter를 그에 맞는 JSX 요소에 사용하기만 하면 된다.

  • 유연성: 사용자는 원한다면 props를 오버로드할 수 있다.

  • 컴포넌트 분리: 로직을 고차 컴포넌트에 분리함으로써 자식 컴포넌트는 보다 간결하고 목적에 집중한 형태로 유지될 수 있다.

단점

  • 가시성 부족: getter는 컴포넌트를 더 쉽게 통합할 수 있도록 추상화를 제공하지만, 동시에 더 불투명하고 "마법"같은 요소를 만들기도 한다. 개발자들은 노출된 getter props와 해당 내부 로직을 올바르게 오버라이드하기 위해 충분한 이해력을 갖추어야 한다.(이 부분에 TypeScript가 도움이 될 수 있다.)

  • 복잡성: 컴포넌트의 구조가 더 복잡해질 수 있다. 여러 고차 컴포넌트가 사용되거나 여러 단계의 props 전달이 필요할 수 있다.

  • 이해 어려움: 개발자들이 고차 컴포넌트와 Props Getters 패턴에 익숙하지 않은 경우 이해하기 어려울 수 있다.

다른 패턴들과 비교

  • Inversion of Control: 3/4

  • 구현 복잡도: 3/4

React tableDownshift 라이브러리에서 이 패턴을 사용한다.

State Recuder 패턴

State Recuder 패턴은 상태 관리를 효과적으로 구현하는 디자인 패턴 중 하나다.
IoC 측면에서 가장 진보된 패턴이다. 이 패턴은 유저에게 컴포넌트를 내부적으로 제어할 수 있는 더 발전된 방법을 제시한다.

이 패턴은 상태 변경 로직을 재사용 가능한 함수로 분리하여 컴포넌트 내부에서 관리하는 것보다 더 유연하고 관리하기 쉬운 방법을 제공한다.

일반적으로 React에서 상태는 useState 훅이나 클래스 컴포넌트의 state를 사용하여 관리된다. 하지만 상태 변경 로직이 컴포넌트 내부에 밀접하게 결합되어 있으면 해당 컴포넌트를 재사용하기 어렵고, 상태 변경 로직을 공유하거나 테스트하기도 어렵게 된다.

state Recuder 패턴은 이러한 문제를 해결하기 위해 사용된다. 이 패턴에서는 상태 변경 로직을 별도의 리듀서 함수로 분리하고, 해당 함수를 컴포넌트에 전달하여 상태를 관리한다. 이 리듀서 함수는 현재 상태와 액션을 인자로 받아 새로운 상태를 반환한다.

예시 코드

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

이 예시는 Custom hook 패턴에 State reducer 패턴을 적용했지만, Compound components 패턴에 적용하여 사용할 수도 있다.

장점

  • 더 많은 제어 권한 제공: 아주 복잡한 경우에 state reducer를 사용하는 것은 사용자에게 통제권을 남겨주는 최고의 방법이다. 모든 내부 action들은 이제 외부에서 접근가능하며 오버라이드할 수 있다.

단점

  • 구현 복잡성: 이 패턴은 개발자에게 가장 복잡한 패턴이다.

  • 가시성 부족: 모든 리듀서의 동작은 변경될 수 있으므로 구성 요소의 내부 논리에 대한 충분한 이해가 필요하다.

다른 패턴들과 비교

  • Inversion of Control: 4/4

  • 구현 복잡도: 4/4

Downshift 라이브러리가 이 패턴을 사용한다.

결론

이 5가지 React 패턴을 통해 우리는 "Inversion of Control"이라는 개념을 활용하는 다양한 방법을 알아보았다.

이들은 유연하고 적응 가능한 컴포넌트를 만들기 위한 강력한 방법을 제공한다. 하지만 개발자들에게 더 많은 제어 권한을 주면 컴포넌트가 "plug and play"적인 마인드에서 멀어지게 된다. 따라서 적절한 패턴을 절적한 필요에 맞게 선택해야 한다.

plug and play란?
plug and play는 컴퓨터 용어로, 장치나 소프트웨어가 추가적인 설정 없이 자동으로 작동되는 기능을 의미한다. 예를 들어, 새로운 키보드를 컴퓨터에 연결하면 컴퓨터가 자동으로 키보드를 인식하고 사용할 수 있도록 설정해 주는 것이 plug and play다. plug and play 기능은 사용자에게 편의성과 간편함을 제공한다.

다음 그림은 모든 패턴을 "통합 복잡성(Integration complexity)"과 "제어 역전(Inversion of control)"에 따라 분류한다.

참고
유용한 리액트 패턴 5가지
5가지 고급 반응 패턴
[리액트 디자인 패턴] Compound Component Pattern (합성 컴포넌트 패턴) 알아보기
[React] 02. 리액트 디자인 패턴 (Control Props Pattern)

profile
꾸준하게

0개의 댓글