[Design Patterns] Render Props 패턴

·2024년 2월 16일
0

patterns

목록 보기
11/11
post-thumbnail

Render Props 패턴

컴포넌트를 재사용 가능하게 할 수 있는 방법으로 HOC 패턴을 알아보았다.
컴포넌트를 재사용 할 수 있는 또 다른 방법으로 Render Props 패턴을 사용할 수 있다.

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

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

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

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

const Title = props => props.render()

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

import { render } from "react-dom";

import "./styles.css";

const Title = (props) => props.render();

render(
  <div className="App">
    <Title
      render={() => (
        <h1>
          <span role="img" aria-label="emoji"></span>
          I am a render prop!{" "}
          <span role="img" aria-label="emoji"></span>
        </h1>
      )}
    />
  </div>,
  document.getElementById("root")
);

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

import { render } from "react-dom";
import "./styles.css";

const Title = (props) => props.render();

render(
  <div className="App">
    <Title render={() => <h1>✨ First render prop!</h1>} />
    <Title render={() => <h2>🔥 Second render prop! 🔥</h2>} />
    <Title render={() => <h3>🚀 Third render prop! 🚀</h3>} />
  </div>,
  document.getElementById("root")
);

이 패턴의 이름이 render prop이지만, 넘기는 prop의 이름을 꼭 render라고 할 필요는 없다. JSX를 렌더하는 어떤 prop이던 render prop으로 볼 수 있다.

import { render } from "react-dom";
import "./styles.css";

const Title = (props) => (
  <>
    {props.renderFirstComponent()}
    {props.renderSecondComponent()}
    {props.renderThirdComponent()}
  </>
);

render(
  <div className="App">
    <Title
      renderFirstComponent={() => <h1>✨ First render prop!</h1>}
      renderSecondComponent={() => <h2>🔥 Second render prop! 🔥</h2>}
      renderThirdComponent={() => <h3>🚀 Third render prop! 🚀</h3>}
    />
  </div>,
  document.getElementById("root")
);

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

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

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

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

예제

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

import { useState } from "react";
import "./styles.css";

function Input() {
  const [value, setValue] = useState("");

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

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

function Kelvin({ value = 0 }) {
  return <div className="temp">{value + 273.15}K</div>;
}

function Fahrenheit({ value = 0 }) {
  return <div className="temp">{(value * 9) / 5 + 32}°F</div>;
}

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

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

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

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

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

Hooks

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

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

import "./styles.css";

import { Mutation } from "react-apollo";
import { ADD_MESSAGE } from "./resolvers";

export default class Input extends React.Component {
  constructor() {
    super();
    this.state = { message: "" };
  }

  handleChange = (e) => {
    this.setState({ message: e.target.value });
  };

  render() {
    return (
      <Mutation
        mutation={ADD_MESSAGE}
        variables={{ message: this.state.message }}
        onCompleted={() =>
          console.log(`Added with render prop: ${this.state.message} `)
        }
      >
        {(addMessage) => (
          <div className="input-row">
            <input
              onChange={this.handleChange}
              type="text"
              placeholder="Type something..."
            />
            <button onClick={addMessage}>Add</button>
          </div>
        )}
      </Mutation>
    );
  }
}

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 훅을 사용하고 있다.

import { useState } from "react";
import "./styles.css";

import { useMutation } from "@apollo/react-hooks";
import { ADD_MESSAGE } from "./resolvers";

export default function Input() {
  const [message, setMessage] = useState("");
  const [addMessage] = useMutation(ADD_MESSAGE, {
    variables: { message }
  });

  return (
    <div className="input-row">
      <input
        onChange={(e) => setMessage(e.target.value)}
        type="text"
        placeholder="Type something..."
      />
      <button onClick={addMessage}>Add</button>
    </div>
  );
}

장점, 단점

장점

render prop을 사용하여 몇몇 컴포넌트 간 데이터를 공유하는 것은 간단하다. children 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 패턴은 받은 데이터를 수정할 필요가 없는 컴포넌트들에 대하여 사용할 수 있다.

< 출처 : https://patterns-dev-kr.github.io/design-patterns/render-props-pattern/ >

profile
개발을 개발새발 열심히➰🐶

0개의 댓글