Render Props 패턴

고차 컴포넌트 섹션에서 여러 컴포넌트가 동일한 데이터나 동일한 로직을 포함해야 할 때 컴포넌트의 로직을 재사용할 수 있게 되면 편해질 수 있다는 것을 알았다.

컴포넌트를 재사용 가능하게 할 수 있는 또 다른 방법으로, render prop 패턴을 사용하는 방법이 있다.

render prop은 컴포넌트의 prop으로 함수이며 JSX 엘리먼트를 리턴한다. 컴포넌트 자체는 아무런 것도 렌더링하지 않지만 render prop함수를 호출한다.

Title컴포넌트가 있다고 생각해 보자. Title컴포넌트는 prop으로 넘어온 함수를 호출하여 반환하는 것 외에는 아무런 동작을 하지 않는다. Title컴포넌트에 render prop을 아래와 같이 넣어 보자

<Title render={() => <h1>I am a render prop!</h1>} />

Title컴포넌트 내에서는 단순히 prop의 render함수를 호출하여 반환한다.

const Title = props => props.render()

컴포넌트 엘리먼트에 React엘리먼트를 반환하는 render라는 이름의 prop을 넘긴다.

render prop패턴의 장점은 prop을 받는 컴포넌트가 재사용성이 좋다는 점이다. Title컴포넌트는 이제 render prop만 바꿔가며 여러번 사용할 수 있다.

이 패턴의 이름이 render prop이지만 넘기는 prop의 이름을 꼭 render로 할 필요는 없다. JSX를 렌더하는 어떤 prop이던 render prop으로 볼 수 있다. 아래 예제에서는 이름을 변경하여 사용하고 있다.

위의 예제에서 render prop패턴을 사용하여 컴포넌트를 재사용 가능하게 만들었다. 하지만 이 패턴은 이보다 더 유용하게 쓰일 수 있다.

render prop을 받는 컴포넌트는 단순히 함수를 호출해 JSX엘리먼트를 렌더링하는 것 외에도 많은 동작을 할 수 있다. 단지 함수를 호출하는 것 대신에. render prop 함수를 호출할 때 인자를 전달할 수 있다.

function Component(props) {
  const data = { ... }

  return props.render(data)
}

위처럼 인자를 넘기게 구현하면 render prop은 이제 아래 코드와 같이 데이터를 인자로 받을 수 있다.

<Component render={data => <ChildComponent data={data} />} />

아래 예제는 텍스트박스에 섭씨 온도를 받아서 켈빈과 화씨 온도로 표현해주는 단순한 앱이다.

위의 예제를 보면 Input 컴포넌트는 값 입력을 받기 위해 state를 갖고 있는데. Fahrenheit컴포넌트와 Kelvin 컴포넌트는 이 state를 전달 받을 방법이 없다.


상태를 부모 컴포넌트로 올리기

Fahrenheit컴포넌트와 Kelvin 컴포넌트가 사용자가 입력한 값을 전달받기 위한 방법 중 하나는 상태를 부모 컴포넌트로 올려보내는 방법이 있다.

아래 예제에서 상태를 가지고 있는 Input 컴포넌트가 있지만 형제 컴포넌트인 Fahrenheit, Kelvin 컴포넌트도 이 값에 접근할 수 있어야 변환된 값을 보여줄 수 있다. 이 때 Input 자체가 상태를 갖는 것 대신 세 컴포넌트의 부모 컴포넌트로 상태를 올려보내는 것이다. 아래 예제에서는 App 컴포넌트가 될 것이다.

function Input({ value, handleChange }) {
  return <input value={value} onChange={e => handleChange(e.target.value)} />
}

export default function App() {
  const [value, setValue] = useState('')

  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input value={value} handleChange={setValue} />
      <Kelvin value={value} />
      <Fahrenheit value={value} />
    </div>
  )
}

이 방법도 유효하긴 하지만 규모가 큰 앱에서 컴포넌트가 여러 자식 컴포넌트를 가지고 있는 경우 이 작업을 하기란 까다로운 일이다. 상태의 변경은 모든 자식 컴포넌트의 리렌더링을 유발할 수 있고 이런 상황이 쌓이면 앱의 전체적인 성능을 떨어트릴 수 있다.


Render props

그 대신에 render props 패턴을 활용할 수 있다. Input 컴포넌트가 render prop을 받도록 리펙토링 해 보자.

function Input(props) {
  const [value, setValue] = useState('')

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Temp in °C"
      />
      {props.render(value)}
    </>
  )
}

export default function App() {
  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input
        render={value => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      />
    </div>
  )
}

이로써 KelvinFahrenheight 컴포넌트는 사용자의 입력 값을 받을 수 있게 되었다.


자식 컴포넌트를 함수로 받아보자

일반적인 JSX컴포넌트에 자식 엘리먼트로 React 엘리먼트를 반환하는 함수를 전달할 수 있다. 해당 컴포넌트에서 이 함수는 children prop으로 사용 가능하며 이것도 역시 render prop에 해당한다.

Input 컴포넌트에 명시적으로 render prop을 넘기는 대신 자식 컴포넌트를 함수로 넘기도록 수정해 보자.

export default function App() {
  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input>
        {value => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      </Input>
    </div>
  )
}

Input컴포넌트는 props.children을 통해 이 함수에 접글할 수 있다. props.render를 쓰는 대신에 props.children함수를 호출하며 인자를 넘기도록 수정한다.

function Input(props) {
  const [value, setValue] = useState('')

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Temp in °C"
      />
      {props.children(value)}
    </>
  )
}

이렇게 하여 render prop의 이름을 어떻게 지을까 고민하지 않고 KelvinFahrenheit 둘 다 사용자의 입력 값에 접근할 수 있다


Hooks

몇몇 상황에 render props 패턴은 hooks로 대체될 수 있다. Apollo Client가 좋은 예시이다.

아래 예제 코드를 이해하는데 Apollo Client에 대한 지식은 필요하지 않다.

Apollo Client를 사용하는 방법 중 하나는 MutationQuery 컴포넌트를 사용하는 것이다. 아래 예시는 HOC PatternInput컴포넌트 예시와 동일한데 graphql() HOC를 사용하는 대신 Mutation 컴포넌트가 render prop을 받는것을 알 수 있다.

Mutation컴포넌트가 자식 엘리먼트에게 데이터를 전달할 수 있도록 하기 위해 컴포넌트를 렌더하는 함수를 자식 요소로 제공했다. 이 함수에서 인자로 데이터를 받을 수 있다.

<Mutation mutation={...} variables={...}>
  {addMessage => <div className="input-row">...</div>}
</Mutation>

render prop 형태는 HOC에 비교하여 조금 더 선호되긴 하지만 단점이 존재한다.

첫번째 단점은 트리가 깊어진다는 것이다. 컴포넌트가 여러 개의 mutation을 사용해야 하는 경우 Mutation 컴포넌트나 Query 컴포넌트를 중첩해 사용해야 한다.

<Mutation mutation={FIRST_MUTATION}>
  {firstMutation => (
    <Mutation mutation={SECOND_MUTATION}>
      {secondMutation => (
        <Mutation mutation={THIRD_MUTATION}>
          {thirdMutation => (
            <Element
              firstMutation={firstMutation}
              secondMutation={secondMutation}
              thirdMutation={thirdMutation}
            />
          )}
        </Mutation>
      )}
    </Mutation>
  )}
</Mutation>

React에 훅이 추가되고 나서 Apollo에도 훅을 지원하기 시작했다. Mutation 혹은 Query 컴포넌트를 사용하는 대신 개발자는 훅을 사용하여 직접 필요한 값을 참조할 수 있게 되었다.

아래 예시에서는 Query 컴포넌트를 render prop과 함께 사용하는 대신 useQuery훅을 사용하고 있다.

useQuery훅을 사용하여 꽤 많은 양의 코드를 줄이면서 필요한 데이터를 사용할 수 있게 되었다.


장점

render prop을 사용하여 몇몇 컴포넌트간 데이터를 공유하는것은 간단하다. children prop을 활용하는 것으로 해당 컴포넌트를 재사용할 수 있게 된다. HOC패턴도 마찬가지로 재사용성과 데이터의 공유 부분에서 같은 이슈를 해결할 수 있다. render prop은 HOC를 사용할 때 마주칠 수 있는 몇 가지 이슈들을 해결할 수 있다.

props를 자동으로 머지하도록 구현하지 않기 때문에 HOC패턴을 사용할 때 prop이 어디서 만들어져 어디서 오는지 구별하기 힘들었던 이슈가 없다. 부모 컴포넌트로부터 받은 prop을 명시적으로 받아 처리하기 때문이다.

함수의 인자에서 명시적으로 prop이 전달되기 때문에 HOC를 사용할 때 prop이 모호한 문제가 해결된다. 이 때문에 prop이 어디로부터 오는지 확실히 알 수 있다.

render props를 활용하여 렌더링 컴포넌트와 앱의 로직을 분리할 수 있다. 상태를 가진 컴포넌트는 render prop을 받고. 상태가 없는 컴포넌트를 렌더할 수 있다.


단점

위에서 render props로 해결하려 한 문제는 React hooks로 대체되었다. Hooks는 컴포넌트에 재사용성과 데이터 공유를 위한 방법 자체를 바꿔놓았다. 대부분의 render props는 Hooks로 대체 가능하다.

render prop 내에서는 생명 주기 함수를 사용할 수 없기 때문에. render prop 패턴은 받은 데이터를 수정할 필요가 없는 컴포넌트들에 대하여 사용할 수 있다.

참조