유용한 리액트 패턴 5가지

Bard·2021년 5월 25일
389
post-thumbnail

본 글은 Alexis Regnaud님의 5 Advanced React Patterns 를 참고한 포스팅입니다. 오역, 피드백 등은 답글을 달아주세요!

본론에 앞서...

다들 리액트 개발자라면 한번쯤 다음과 같은 질문을 자신에게 던져본 적이 있을 것이다.

  • 어떻게 하면 재사용가능한 컴포넌트를 각각 다른 use case들에 맞도록 만들 수 있을까?
  • 어떻게 하면 컴포넌트를 간단한 API로 쓰기 쉽게 만들 수 있을까?
  • 어떻게 하면 UI와 기능성의 측면에서 확장 가능한 컴포넌트를 만들 수 있을까?

본 포스팅에서는 5개의 다른 패턴을 살펴본다.
비교를 용이하게 하기 위해서 각각의 패턴에 대해 동일한 구조를 통해 알아보자.

  1. Counter 예제를 통한 실제 코드 예시
  2. 패턴의 장, 단점
  3. 두 개의 기준을 통한 비교
  • Inversion of Control(IoC): 컴포넌트를 사용하는 유저에게 주어지는 유연성(flexibility)와 제어(control)의 정도
  • Implementation complexity: 유저와 개발자 모두에 대해 그 패턴을 사용하는 난이도.
  1. 그 패턴을 사용해 배포한 라이브러리 예시

참고로 여기에서 유저는 이 컴포넌트를 사용하는 다른 개발자를 말하고, 개발자는 이 컴포넌트를 개발하는 리액트 개발자(바로 당신)을 말한다.


Compound Components Pattern

이 패턴은 불필요한 prop drilling 없이 Expressive하고 Declarative한 컴포넌트를 만들 수 있게 도와준다. 만약 좀 더 customizable하고 관심사를 분리하도록 하고 싶다면 이 패턴을 고려해보아야 한다.

prop drilling이란 App 컴포넌트에서 C컴포넌트에게 데이터를 주고 싶을 때, A와 B에는 필요없지만 C 컴포넌트에게 데이터를 주고 싶을 때, 상위 컴포넌트에서 프로퍼티를 아래로 내려 꽂듯이 데이터를 전달해주는 것을 말한다.

예제

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이 각각의 서브컴포넌트에 붙어있는 방법이 더 좋다.
// 이렇게 쓰는 것보다
return (
  <Counter
    label="label"
    max={10}
    iconDecrement="minus"
    iconIncrement="plus"
    onChange={handleChangeCounter}
  />
);
// 이렇게 쓰는게 낫다!
return (
  <Counter onChange={handleChangeCounter}>
    <Counter.Decrement icon={"minus"} />
    <Counter.Label>Counter</Counter.Label>
    <Counter.Count max={10} />
    <Counter.Increment icon={"plus"} />
  </Counter>
);
  • 유연한 마크업 구조

    이처럼 컴포넌트가 엄청난 자유도를 갖는다.
  • 관심사의 분리
    대부분의 로직은 Counter Component에 포함되며, Context API를 통해 states와 handlers를 Children 컴포넌트간에 공유한다.

단점

  • 너무 UI 자유도가 크다. 이렇게 큰 자유도는 예상치 못한 행동을 유발할 수도 있다. 유저가 어떻게 이 컴포넌트를 사용하는지에 의존한다면, 이렇게 큰 자유도를 주면 안된다.

  • JSX가 너무 무겁다. 이 패턴을 적용하게 되면 JSX의 열 개수를 너무 늘릴 수 있다. 특히 EsLint나 Prettier를 사용한다면 말이다.
    이건 작은 컴포넌트에서는 큰 문제가 아닌 것처럼 보이지만 큰 그림을 보게 된다면 엄청난 차이를 느낄 수 있을 것이다.

Criteria

  • Inversion of Control : ★☆☆☆
  • Integration complexity : ★☆☆☆

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


Control Props Pattern

이 패턴은 컴포넌트를 controlled component로 바꿔준다. 외부 상태는 single source of truth, SSOT로 사용되어 유저로 하여금 커스텀 로직을 삽입할 수 있게끔 한다.

  • Controlled component란 component의 상태를 제어할 수 있는 컴포넌트를 의미한다.
  • SSOT란 단일 진실 공급원이라고도 번역되는데, 이는 모든 데이터 요소를 한 곳에서만 제어, 편집하도록 하는 것이다.

예제

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

장점

  • 더 많은 통제권을 준다. 메인 state가 컴포넌트 바깥에 드러나있기 때문에 유저는 직접적으로 그 컴포넌트를 컨트롤할 수 있게 된다.

단점

  • 사용하는 것이 복잡하다. 이전에는 JSX에만 구현하는 것만으로도 컴포넌트를 동작하게끔 하는 것이 가능했지만, 이제는 JSX, useState, handleChange 세 곳 모두를 체크해야 한다.

Criteria

  • Inversion of Control: ★★☆☆
  • Integration complexity: ★☆☆☆

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


Custom Hook Pattern

좀 더 IoC에 집중해보자. 메인 로직은 이제 custom hook으로 들어간다. hook은 State, Handler와 같은 내부 로직들을 포함하며 유저에게 더 많은 통제권을 준다.

예제

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 사이에 자신만의 로직을 넣을 수 있다.

단점

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

Criteria

  • Inversion of Control: ★★☆☆
  • Integration complexity: ★★☆☆

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


Props Getters Pattern

Custom hook pattern이 엄청난 통제권을 주긴 하지만, 그만큼 컴포넌트를 이용하기 어렵게 만든다. Props Getters Pattern은 이런 복잡도를 감싸기 위해 시도한다. native props를 노출하는 대신 props getters의 목록을 제공한다. 이는 유저가 올바른 JSX요소에 접근할 수 있도록 의미있는 이름을 사용해야 한다.

예제

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를 오버로드할 수 있다.

단점

  • 잘 보이지가 않는다. 물론 getters를 통한 추상화는 컴포넌트를 사용하기 쉽게 만들어주지만 결국 더 불투명하고 마법처럼 만들어준다. 정확하게 오버라이드하기 위해서는 getters에 의해 제공된 prop 리스트와 하나가 바뀔 때 생기는 내부에서의 로직 변화를 알아야만 한다.

Criteria

  • Inversion of Control: ★★★☆
  • Integration complexity: ★★★☆

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


State reducer pattern

IoC에 있어서는 최고의 패턴이다. 이 패턴은 유저에게 컴포넌트를 내부적으로 제어할 수 있는 더 발전된 방법을 제시한다.
코드는 Custom Hook Pattern과 비슷해보이지만, 더 나아가 유저가 Hook을 통해 전달된 reducer를 정의한다. 이 reducer는 컴포넌트 내의 모든 action들을 오버로드한다.

예제

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>
    </>
  );

이 예에서는 Custom hook Pattern에 State reducer pattern을 적용했지만 우리는 이를 Compound components pattern에 적용하여 사용할 수도 있다.

장점

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

단점

  • 이 패턴은 확실히 유저와 개발자 모두에게 가장 복잡한 패턴이다.
  • reducer의 액션이 바뀔 수 있기 때문에, 컴포넌트 내부 로직에 대한 깊은 이해가 필요하다.

Criteria

  • Inversion of Control: ★★★★
  • Integration complexity: ★★★★

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


결론적으로,

이 5개의 패턴을 보면서 우리는 IoC를 조절하는 다양한 방법을 알 수 있다. 이들은 우리로 하여금 더 유연하고 융통성있는 컴포넌트들을 만드는 강력한 방법을 제공한다.

그러나 우리는 이 유명한 격언을 알고 있다.

큰 힘에는 큰 책임이 따른다

컴포넌트에게 더 많은 제어권을 주게 된다면 이는 컴포넌트가 plug and play라는 사고방식으로부터 멀어지게 만든다. 그렇기 때문에 올바른 패턴을 선택하는 것은 개발자로서의 책임이 된다.

이 문제에 도움이 되기 위해 각 패턴을 Integration complexity와 Inversion of control에 대해 정리한 그림을 첨부한다.

profile
The Wandering Caretaker

6개의 댓글

comment-user-thumbnail
2021년 5월 27일

너무 도움되는 자료 감사합니다 :()

답글 달기
comment-user-thumbnail
2021년 5월 30일

컴포넌트 구조 설계를 거지같이 못하고 있었는데 이걸 보고 가닥이라도 잡았습니다 감사합니다.

답글 달기
comment-user-thumbnail
2021년 5월 30일

오 미디엄글 번역해주셨네요.
저도 이거 보고 예제 작성하면서 했었는데, 이렇게 벨로그에 정리를 해주시다니!
쓰니님 노력에 감사함을!

답글 달기
comment-user-thumbnail
2021년 6월 2일

ㄷㄷ

답글 달기
comment-user-thumbnail
2021년 6월 3일

무쳤네

답글 달기
comment-user-thumbnail
2021년 6월 7일

도움받고 갑니다! 감사합니다 :)

답글 달기