반응형

props drilling을 줄이고 사용하는 입장에서 '조합'해서 사용할 수 있도록 하는 패턴 

 

예를 들어 

아래와 같은 코드가 있고, 내부적으로 2~4단계의 트리 depth단계로 구성된 컴포넌트가 있다면

<Counter text="example" ex1={<div></div>} onchange{()=>{ console.log(change)}} onClick={() => console.log('test1')}>

 

사용하는 입장에서는 내부 구성을 정확하게 알 수 없을것이다.

그래서 사용자 입장에서 컴포넌트를 직접 조합해서 구현하고 props를 직접 내려주는 방식이다.

 

코드 예시)

export default function AppEx() {
  return (
    <div>
      <Counter onClick={() => console.log('test1')}>
        <Counter.Increment />
        <Counter.Decrement />
        <Counter.Label />
      </Counter>

      <Counter onClick={() => console.log('test2')}>
        <Counter.Increment />
        <Counter.Label color="blue" />
        <Counter.Decrement />
      </Counter>

      <Counter onClick={() => console.log('test3')}>
        <Counter.Increment />
        <Counter.Label color="red" />
      </Counter>
    </div>
  );
}

 

구현된 화면 예시

 

장점 

 

매우 높은 유연성

선언적으로 구성 -> 가독성이 좋음 

하나의 giant component를 쪼개 여러개로 분리함으로써 복잡성이 줄어듬 

 

단점

 

매우 높은 유연성(실수 혹은 의도치 않는 코드 삽입 가능성 증가)

코드가 좀 복잡해짐

JSX 크기 증가

 

코드 예시)

import React, { useEffect, useState, Children } from 'react';
import styled from 'styled-components';

import { Decrement, Increment, Label, Reset } from './modules';

const StyledDiv = styled.div`
  display: inline-flex;
  flex-direction: row;
  border: 0.5px solid;
  gap: 0.5px;

  margin: 30px;
`;

function Counter({ children, onClick }) {
  const [counter, setCounter] = useState(0);
  const resetCounter = () => setCounter(0);

  return (
    <StyledDiv onClick={onClick}>
      {Children.map(children, (child) => {
        return React.cloneElement(child, {
          counter,
          resetCounter, // 그냥 첨가 가능하다는 예시용
          setCounter,
        });
      })}
    </StyledDiv>
  );
}

Counter.Reset = Reset;
Counter.Increment = Increment;
Counter.Decrement = Decrement;
Counter.Label = Label;

export default function AppEx() {
  return (
    <div>
      <Counter onClick={() => console.log('test1')}>
        <Counter.Increment />
        <Counter.Decrement />
        <Counter.Label />
      </Counter>

      <Counter onClick={() => console.log('test2')}>
        <Counter.Increment />
        <Counter.Label color="blue" />
        <Counter.Decrement />
      </Counter>

      <Counter onClick={() => console.log('test3')}>
        <Counter.Increment />
        <Counter.Label color="red" />
      </Counter>
    </div>
  );
}

 

구현하는 방식은 Context Api를 사용하는 방식과 Children와 cloneElement를 사용하는 방식 2가지를 발견했는데

 

ex) children & cloneElement 이용

function Counter({ children, onClick }) {
  const [counter, setCounter] = useState(0);
  const resetCounter = () => setCounter(0);

  return (
    <StyledDiv onClick={onClick}>
      {Children.map(children, (child) => {
        return React.cloneElement(child, {
          counter,
          resetCounter, // 그냥 첨가 가능하다는 예시용
          setCounter,
        });
      })}
    </StyledDiv>
  );
}

ex) context api 이용

const CounterContext = createContext(undefined);

function CounterProvider({ children, value }: any) {
  return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>;
}

function useCounterContext() {
  const context = useContext(CounterContext);
  if (context === undefined) {
    throw new Error('useCounterContext must be used within a CounterProvider');
  }
  return context;
}

function Counter2({ children, onClick }) {
  const [counter, setCounter] = useState(0);
  const resetCounter = () => setCounter(0);

  const handleIncrement = () => {
    setCounter(counter + 1);
  };

  const handleDecrement = () => {
    setCounter(counter - 1);
  };

  return (
    <CounterProvider value={{ counter, handleIncrement, handleDecrement }}>
      <StyledDiv onClick={onClick}>{children}</StyledDiv>
    </CounterProvider>
  );
}

children & cloneElement 방식의 경우 cloneElement라길래 복사하는 비용이 크지 않을까? 검색해봤는데 

 

stackoverflow에 따르면 

어차피 JSX가 React.createElement(Object)로 전환되는 과정에서 cloneElement를 쓰면 똑같이 Object로 전환되면서 props만 전달해서 바꿔주는 식으로 작동해서 성능 이슈는 큰 문제가 아니라고 한다.

 

https://stackoverflow.com/questions/54922160/react-cloneelement-in-list-performance

 

React.cloneElement in List performance

I have doubts about React.cloneElement in List. Is that something that we should avoid doing or not, if we have lots of elements in list? Does React.cloneElement makes unnecessary re-renders that c...

stackoverflow.com

 

cloneElement 방식의 경우 depth가 깊어지면 다루기 좀 힘들어질꺼 같지만 간편하고

Context API 방식은 boilerplate?를 까는게 번거롭긴 하지만 depth에 상관없이 사용 가능할꺼 같다.

 

headlessui 란 오픈소스는 context 방식을 사용한것으로 보인다.

 

그외 비슷한 패턴들

 

쓸만한게 있을까 빠르게 훑고 넘어가자

 

Control Props Pattern

 

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

Example

 

Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/control-props

 

컴포넌트를 제어 컴포넌트로 사용 

하나의 'single source of truth' 단일 상태 사용 -> base component에 custom logic과 상태 부여

 

장점 

직접적 제어권 부여

 

단점

(compound 패턴보다) 구현의 복잡성 -> state, function, component 제어가 필요

 

 

Custom Hook Pattern

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 }

Example

Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/custom-hooks

 

장점
제어의 역전 -> 메인 로직이 custom hook으로 전환

 

단점

 

로직과 렌더링UI의 분리 -> (남이 보기에) 가독성 하락

 

 

Props Getters Pattern

 

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를 통한 쉬운 사용 및 통합

복잡성을 getter에 감춤으로써 복잡성을 숨김

 

단점

 

Lack of visibility ->  getter가 어떻게 구현되어 있는지 모르는 사람들에게는 가독성이 하락한다

  //props getter for 'Counter'
  const getCounterProps = ({ ...otherProps } = {}) => ({
    value: count,
    "aria-valuemax": max,
    "aria-valuemin": 0,
    "aria-valuenow": count,
    ...otherProps
  });

getter는 이런식으로 구성되어있는데 spread 연산자로 뿌려주는 식이다.

 

State reducer pattern

 

custom hook 패턴에 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 };

Example

Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/state-reducer

 

가장 파워풀하고 가장 Lack of visibility한 패턴이지 싶다.

 

느낀점?

 

협업시 가독성 문제 때문에 느낌상 1 ~ 3번 패턴이 젤 무난하지 않을까 싶다.

그 외로는 공통으로 사용할 수 있는 로직을 가지고(추상화) UI만 갈아끼우는 식으로 사용 가능할꺼 같다

 

reference 

 

https://javascript.plainenglish.io/5-advanced-react-patterns-a6b7624267a6

 

5 Advanced React Patterns

An overview of 5 modern advanced React patterns, including integration codes, pros and cons, and concrete usage within public libraries.

javascript.plainenglish.io

 

반응형

'소프트웨어공학 > 디자인패턴' 카테고리의 다른 글

메모  (0) 2022.03.25
프론트엔드에서의 레포지토리 패턴?  (0) 2022.03.18
[행위] 책임 연쇄 패턴  (0) 2022.03.13
[행위] 전략 패턴 & 커맨드 패턴  (0) 2022.03.07
[행동] 중재자 패턴  (1) 2022.03.01

+ Recent posts