제어권 역전 기법 찍먹 후기 (a.k.a. 의존성 주입)

Einere·2022년 12월 30일
0
post-thumbnail

발단

회사에서 A님과 PR에 대해 얘기하던 도중, A님이 "제어권 역전 기법을 사용하려다, 적용하기 힘들어서 컨텍스트 API를 적용했다"는 얘기를 해주셨다. 제어권 역전 기법이 뭔지 궁금해서 더 물어봤고, 설명을 들었다. 이후, 이 기법을 내 코드에도 적용할 수 있을 것 같아서 시도해봤다.

현재 상황

기존 코드는 다음과 같은 구조를 가지고 있었다.

function DataProviderRegisterPage() {
  // react-hook-form 을 이용해 데이터 제공자 양식 상태를 관리하는 녀석. 
  const { register, formState, setValue, watch, handleSubmit, control } =
    useForm(...);
  useFieldArray(...);

  // tanstack query를 이용해 API를 요청하는 녀석
  const { mutate, isLoading } = useMutation(...);
  
  return (
      <form
        // submit 이벤트 시, 데이터 제공자를 등록한다.
        onSubmit={handleSubmit((data) => {...})}
      >
        <DataProviderForm
          // useForm 반환값을 prop으로 받고 있다.
          register={register}
          setValue={setValue}
          watch={watch}
          errors={errors}
          // ...
        />
        <DataProviderContractForm
          // useForm 반환값을 prop으로 받고 있다.
          register={register}
          setValue={setValue}
          watch={watch}
          errors={errors}
          control={control}
          // ...
        />
        <button>Submit</button>
      </form>
  );
}

DataProviderRegisterPage 컴포넌트의 useForm 훅의 반환값을 DataProviderFormDataProviderContractForm 컴포넌트가 동일하게 받고 있다.

적용해보자

우선, form 태그를 DataProviderFormContainer 컴포넌트로 추상화했다.

function DataProviderFormContainer(props: DataProviderFormContainerProps) {
  const { onSubmit, children } = props;

  // 끌어내려진 상태들
  const { register, formState, setValue, watch, handleSubmit, control } =
    useForm(...);
  useFieldArray(...);

  const childrenList = Array.isArray(children) ? children : [children];

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {childrenList.map((child) => {
        if (typeof child === "function") {
          return child({
            register,
            formState,
            setValue,
            watch,
            handleSubmit,
            control,
          });
        }

        return child;
      })}
    </form>
  );
}

기존 코드에서 <form/> 관련 코드만 따로 떼어내서 새로운 추상화 컴포넌트인 DataProviderFormContainer 로 만들었다. 자연스럽게 DataProviderRegisterPage 컴포넌트에 있던 양식 관련 상태를 이 컴포넌트로 끌어내렸고, 고립시켰다. 그리고 만약 렌더링 가능한 자식 컴포넌트가 있다면, useForm 관련 값들을 prop 으로 주입해준다.

function DataProviderRegisterPage() {
 
  const { mutate, isLoading } = useMutation(...);
  function registerDataProvider(data) { ... }

  return (
      /* onSubmit 핸들러 함수를 주입해준다. */
      <DataProviderFormContainer onSubmit={registerDataProvider}>
      {/* 자식 컴포넌트도 외부에서 주입해주며, 추상화된 컨테이너는 어떤 자식이 올 지 신경쓰지 않는다. */}
        {(childProps) => (
          /* 익명함수 형태의 HOC를 이용해, 컨테이너로부터 여러가지 상태와 값들을 받아온다. */
          <DataProviderForm
            {...childProps}
            key="data-provider-form"
            defaultValues={defaultValuesForDataProvider}
          />
        )}
        {(childProps) => (
          <DataProviderContractForm
            {...childProps}
            key="data-provider-contract-form"
            isRegisterMode={true}
          />
        )}

        {/* 만약 컨테이너의 상태가 필요없다면, 컴포넌트를 바로 넣어준다. */}
        <button>Submit</button>
      </DataProviderFormContainer>
  );
}

<form/> 요소를 추상화한 <DataProviderFormContainer/> 로 교체해 준 뒤, 필요한 prop을 주입해준다.
그리고 <form/> 요소의 자식들을 HOC 기법을 이용해 렌더링해준다.

적용하고 나서 보니..

하지만 DataProviderForm, DataProviderContractForm 을 쓰는 다른 화면은 DOM 트리 구조가 매우 복잡해서 제어권 역전 기법을 동일하게 적용하기는 무리가 있었다. 😞

내가 생각하는 이 기법의 장단점은 다음과 같다.

장점

  1. 상태 끌어내리기 (상태의 적용 범위가 줄어드므로, 성능 개선의 여지가 있다.)
  2. 상태 고립 (상태와 관련없는 컴포넌트는 분리해낼 수 있으므로, 부작용의 위험이 줄어든다.)
  3. 일관성 (데이터 제공자 양식을 써야 하면 무조건 컨테이너를 갖다 쓰면 된다.)
  4. 결합도 낮추기, 일반화, 재사용성 증가 (컨테이너는 자식 컴포넌트가 무엇인지 알 지 모르며, 신경쓰지도 않는다.)

단점

  1. 현재 내 코드는 제어권 역전 기법으로 일반화의 효과를 크게 누리지 못한다. (자식 컴포넌트로 넣을 수 있는게 고만고만해서..)

  2. children 에 익명함수 형태의 HOC가 포함되므로, Children API를 사용할 수 없다. (함수 형태의 자식은 Children API 내부에서 자동으로 걸러져서, UI가 렌더링되지 않는다.)

  3. Children API를 사용하지 못하므로, 자식에게 일일이 key 속성을 할당해줘야 한다. (Children API 가 내부적으로 key 를 설정해준다.)

    <Container>
      {(childProps) => <A key="A" {...childProps} />}
      {(childProps) => <B key="B" {...childProps} />}
      {(childProps) => <C key="C" {...childProps} />}
    </Container>
  4. 컨테이너의 “자식”들만 제어권 역전 및 의존성 주입이 가능하다. (깊이가 깊은 자식 컴포넌트가 있다면, 결국 그 자식 컴포넌트 내부에서 자손 컴포넌트까지 props drilling 이 발생할 수 있다.) (대체제로는 useRef, Context API 등이 있다.)

    <Container>
      {(childProps) => <A {...childProps} />}
    </Container>
    
    function A(props) {
      return (
        <B {...props} />
      );
    }
    
    function B(props) {
      return (
        <C {...props} />
      );
    }

참고

Headless UI

Compound Components In React - Smashing Magazine

Typescript(ing) React.cloneElement or how to type a child element with props injected by the parent

[React] 5. 컴포넌트 디자인하기

React Children 과 친해지기

React 최상위 API - React

profile
지속가능한 웹 개발자를 지향합니다. 경험의 공유를 통해 타인에게 도움이 되는 것을 좋아합니다. 사용자에게 가치를 제공하는 것에 기쁨을 느낍니다.

0개의 댓글