[React] render prop pattern

ClayChanWoo·2024년 3월 1일

React

목록 보기
1/1
post-thumbnail

childrenprops 를 주입할 수 있나 ?

최근에 children 을 넘겨받는 재사용 가능한 컴포넌트를 만든적이 있습니다.
흔히 React 개발 상황에서 많이 마주하는 상황일 것 같아요.

이미지 출처 - https://www.patterns.dev/react/render-props-pattern/


예를 들어, 이런 모습입니다. 익숙하죠 ?

const RenewableComponent = ({ children }: { children: ReactNode }) => {
  // 공통으로 사용되는 state, logic
  return (
    <>
      {/* 공통적인 UI */}
      {children}
    </>
  );
};

컴포넌트를 사용하는 곳은 이렇게 될 수 있겠죠.

export const TextPage = () => {
  return (
    <RenewableComponent>
      <div>내부 컨텐츠</div>
    </RenewableComponent>
  );
};

기가 막힙니다. 통일성있는 UI에 원하는 컴포넌트를 삽입하다니, children 기능은 멋집니다.


그러나, RenewableComponent 컴포넌트에서 선언한 state 를 children으로 넘기고 싶어질 수 있지 않을까요 ??

그렇다면 예시를 좀 바꿔야겠습니다.
재사용할 컴포넌트인 RenewableComponentNameInputComponent 로 이름을 바꾸고, 기능도 추가했습니다. 사용자의 이름을 받는 컴포넌트네요.

const NameInputComponent = ({ children }: { children: JSX.Element }) => {
  const [name, setName] = useState('');

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {children}
    </div>
  );
};

사용할 때는 아래와 같습니다. 재사용 컴포넌트에서 입력한 name 을 사용자에게 표출하고 싶는데 값을 부모 컴포넌트로 받아올 방법이 없습니다.

export const MyPage = () => {
  return (
    <NameInputComponent>
      <div>{name} 안녕하세요 !</div> // name을 NameInputComponent에서 받아오고 싶다.
    </NameInputComponent>
  );
};

childrenprops를 넘긴다면 참 좋을텐데요.

const NameInputComponent = ({ children }: { children: JSX.Element }) => {
  const [name, setName] = useState('');

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {children} // 이곳에 name을 prop로 넘기면 좋을텐데,,,
    </div>
  );
};

어떻게 해야할까요 ?

  • 주로 이런 상황을 옛날에는 고차 컴포넌트(HOC), 요즘은 Hook 으로 대체해서 사용한다고 합니다.
  • 단순히 children에 props를 전달하는 건 React.cloneElement 를 사용하는 방법도 있다고 합니다.
  • 추천하는 대안은 3개정도가 있다고 나옵니다.
    • Render Props Custom Hook
    • Context API
    • Render Props

Render Props Pattern

Render Props Pattern으로 구현해보도록 하겠습니다.

재사용할 컴포넌트를 먼저 고치겠습니다.
propschildren 을 받지 않고, render 라는 이름으로 함수 하나를 받겠습니다. 이 render 함수에 name을 넘겨주면 다른 컴포넌트에서도 사용할 수 있을겁니다.

type TProps = {
  render: ({ name }: { name: string }) => JSX.Element;
};

const NameInputComponent = ({ render }: TProps) => {
  const [name, setName] = useState('');

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {render({ name })}
    </div>
  );
};

사용할 컴포넌트에서는 아래처럼 선언합니다.
render 라는 propsJSX 를 반환하는 함수를 선언해주면, render 함수의 매개변수를 JSX에 사용할 수 있습니다 !

export const MyPage = () => {
  return (
    <NameInputComponent
      render={({ name }) => <div>{name} 님 안녕하세요 !</div>}
    />
  );
};

훌륭하네요. 제가 원하는 재사용 컴포넌트에서 사용되는 값을 받아서 사용할 수 있게 되었습니다 !

재사용 컴포넌트를 이렇게 수정할 수 있습니다.
render 함수를 JSX 이름으로 재할당 받아서 JSX 컴포넌트 처럼 사용하는 것 입니다. (이건 취향 차이라고 생각합니다.)

type TProps = {
  render: ({ name }: { name: string }) => JSX.Element;
};

// RenderItem 으로 재할당
const NameInputComponent = ({ render: RenderItem  }: TProps) => {
  const [name, setName] = useState('');

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {/* JSX 로 사용 */}
      <RenderItem name={name} />
    </div>
  );
};

수정하고 보니 느낀게, 많이 본 패턴 같습니다.
react hook form을 사용해보신 분들은 <Controller /> 컴포넌트와 비슷한듯 합니다.

아래의 코드가 공식 문서에 있는 Controller 사용 예제입니다.
Controllerpropsrender 함수가 있고, 그 함수의 매개변수를 임의의 컴포넌트에서 사용할 수 있는 구조네요 !
위에서 저희가 구현한 코드와 같습니다 !

function App() {
  const { control } = useForm<FormValues>()

  return (
    <form>
      <Controller
        control={control}
        name="ReactDatepicker"
        render={({ field: { onChange, onBlur, value, ref } }) => (
          <ReactDatePicker
            onChange={onChange}  
            onBlur={onBlur}  
            selected={value}
          />
        )}
      />

      <input type="submit" />
    </form>
  )

react hook form 깃허브 controller 구현 코드 를 살펴보니, 역시 render props 패턴을 사용하고 있군요 !

const Controller = <
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
  props: ControllerProps<TFieldValues, TName>,
) => props.render(useController<TFieldValues, TName>(props));

export { Controller };

조금 다른 건 render 함수에 hook 을 반환하는군요. 그래서 ContolleruseController 는 같은 기능을 구현하나봅니다. 역시 아는만큼 보인다는 사실을 또 느낍니다.


이렇게 유용한 패턴하나를 익힌 것 같아 기분이 좋습니다.
어찌됐든 Reactjs, ts 의 도구일 뿐이고, 컴포넌트를 구현하는 방식도 다양합니다. 상황에 맞는 적절한 방식으로 개발하면 좋을 것 같습니다.

profile
자바스크립트는 우주와 같습니다.

0개의 댓글