본문 바로가기
JavaScript

[JavaScript] Render Props Pattern 알아보기

by 배잼 2023. 8. 1.

자바스크립트 패턴 스터디에서 설명한 render props pattern 관련 글이다.

 

요약:

컴포넌트에 JSX를 리턴하는 함수 props를 제공한다.

render prop를 받는 컴포넌트는 단순히 props로 넘겨받은 함수를 호출해서 JSX를 렌더링할수도 있지만, 함수에 인자도 전달할 수 있다.


Basic - Counter 예제

 

+를 누르면 1 증가, -를 누르면 1 감소

위에 있는 숫자는 현재 수, 아래에는 해당 수의 제곱을 보여줍니다.

Basic : App으로 상태를 끌어 올림

function Counter({ value }) {
  return <div>{value}</div>;
}

function CounterPower({ value }) {
  return <div>{value * value}</div>;
}

function Button({ onClick, buttonText }) {
  return <button onClick={onClick}>{buttonText}</button>;
}

function App() {
  const [number, setNumber] = useState(0);

  return (
    <div className="App">
      <Counter value={number} />
      <CounterPower value={number} />
      <Button onClick={() => setNumber((num) => num + 1)} buttonText="+" />
      <Button onClick={() => setNumber((num) => num - 1)} buttonText="-" />
    </div>
  );
}

export default App;

→ 만약 앱의 규모가 더 커진다면? 우리는 App에서 모든 걸 관리 가능할까?

Render props Pattern: 상태를 Counter가 가지고 있다.

useState 훅이 App에서 Counter로 내려간 코드다.

function Counter(props) {
  const [number, setNumber] = useState(0);
  return <>{props.render(number, setNumber)}</>;
}

function CounterValue({ value }) {
  return <div>{value}</div>;
}

function CounterPower({ value }) {
  return <div>{value * value}</div>;
}

function Button({ onClick, buttonText }) {
  return <button onClick={onClick}>{buttonText}</button>;
}

function App() {
  return (
    <div className="App">
      <Counter
        render={(value, setValue) => (
          <>
            <CounterValue value={value} />
            <CounterPower value={value} />
            <Button onClick={() => setValue((num) => num + 1)} buttonText="+" />
            <Button onClick={() => setValue((num) => num - 1)} buttonText="-" />
          </>
        )}
      />
    </div>
  );
}

export default App;

질문: 꼭 props.render여야 할까? → render 대신에 이름을 마음대로 바꿔도 된다. 개수도 늘릴 수 있다.

renderView, renderButton으로 할 수 도 있다. 

function Counter(props) {
  const [number, setNumber] = useState(0);
  return (
    <>
      {props.renderView(number)}
      {props.renderButton(setNumber)}
    </>
  );
}

function CounterValue({ value }) {
  return <div>{value}</div>;
}

function CounterPower({ value }) {
  return <div>{value * value}</div>;
}

function Button({ onClick, buttonText }) {
  return <button onClick={onClick}>{buttonText}</button>;
}

function App() {
  return (
    <div className="App">
      <Counter
        renderView={(value) => (
          <>
            <CounterValue value={value} />
            <CounterPower value={value} />
          </>
        )}
        renderButton={(setValue) => (
          <>
            <Button onClick={() => setValue((num) => num + 1)} buttonText="+" />
            <Button onClick={() => setValue((num) => num - 1)} buttonText="-" />
          </>
        )}
      />
    </div>
  );
}

export default App;

children도 props이므로, 이를 활용하면 이름 짓기 문제가 어느정도 해결되어 보인다.

function Counter(props) {
  const [number, setNumber] = useState(0);
  return (
    <>
      {props.children(number)}
      {props.renderButton(setNumber)}
    </>
  );
}

function CounterValue({ value }) {
  return <div>{value}</div>;
}

function CounterPower({ value }) {
  return <div>{value * value}</div>;
}

function Button({ onClick, buttonText }) {
  return <button onClick={onClick}>{buttonText}</button>;
}

function App() {
  return (
    <div className="App">
      <Counter
        renderButton={(setValue) => (
          <>
            <Button onClick={() => setValue((num) => num + 1)} buttonText="+" />
            <Button onClick={() => setValue((num) => num - 1)} buttonText="-" />
          </>
        )}
      >
        {(value) => (
          <>
            <CounterValue value={value} />
            <CounterPower value={value} />
          </>
        )}
      </Counter>
    </div>
  );
}

export default App;

Hooks도 쓸 수 있을까?

Render Props의 단점 : Callback Hell처럼, 점점 깊어질 수 있다. 아래와 같은 예제를 보자.

function App() {
  return (
    <div className="App">
      <Counter>
        {(value) => (
          <Element>
            {(value) => (
              <SecondElement>
                {(value) => (
                <ThirdElement /> //더 깊어진다면?
                )}
                </SecondElement>
            )}
          </Element>
        )}
      </Counter>
    </div>
  );
}

export default App;

 

이 단점은 라이브러리에서 제공하는 hooks를 활용해서 개선할 수 있다고 한다. 아래 사진을 보면 한눈에 비교된다.

출처 : https://javascript.plainenglish.io/render-props-vs-hooks-a73ec72180ed

Pros & Cons

장점

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

- HOC패턴을 사용할 때 prop이 어디서 만들어져 어디서 오는지 구별하기 힘들었던 이슈가 없다. 함수의 인자에서 명시적으로 prop이 전달되기 때문에 HOC를 사용할 때 prop이 모호한 문제가 해결된다. 이 때문에 prop이 어디로부터 오는지 확실히 알 수 있다.

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

 

단점

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

 

ref

https://patterns-dev-kr.github.io/design-patterns/render-props-pattern/

https://www.patterns.dev/posts/render-props-pattern

https://velog.io/@jaewoogwak/Render-Props와-상태-끌어올리기에-관한-고찰

https://javascript.plainenglish.io/render-props-vs-hooks-a73ec72180ed

 

댓글