[React] Render Props Pattern
Render Props패턴이란 JSX를 반환하는 함수 prop을 사용하여 React 구성 요소 간의 코드를 공유하는 패턴이다. HOC와 같이 컴포넌트를 재사용하기 위해서 즉, 여러 컴포넌트가 공통된 논리/데이터를 포함할 때 Render Props 패턴을 사용할 수 있다. Render Props 패턴이 어떤 것인지 알아보자.
1. Simple Example
const Title = props => props.render(); // 2. render prop을 호출하여 렌더링
<Title render={() => <h1>I am a render prop!</h1>} /> // 1. render prop을 내린다.
아주 간단한 render props 패턴이다. 먼저 두 번째 줄을 보자. render라는 이름의 prop으로 Title에 함수를 내려주고 있다. 이것이 바로 render prop이다. 이 render prop의 특징은 JSX를 리턴하는 함수라는 것이다. 그 후 첫 번째 줄인 Title의 정의를 보면, 받은 render prop을 받아 UI를 렌더링 하는 것을 볼 수 있다. 즉, 상위에 렌더링 책임이 있고, 하위에선 받은 UI를 아래에서 렌더링 하기만 하는 것. 이것이 바로 render props 패턴이다.
2. Data Share
간단한 온도 단위 변환 프로그램을 만들어보자. 사용자가 섭씨 온도를 입력하면, 켈빈, 화씨로 변환하여 보여주는 리액트 프로그램이다.
- App: 최상위 컴포넌트
- Input: 사용자 입력을 상태로 관리하는 컴포넌트
- Kelbin: 사용자 입력을 켈빈으로 변환하여 보여주는 컴포넌트
- Fahrenheit: 사용자 입력을 화씨로 변환하여 보여주는 컴포넌트
나는 이 프로그램의 컴포넌트 구조를 위와 같이 짜기로 했다. 최상위에 App이 있고, Input, 켈빈, 화씨 컴포넌트가 하위에 존재하는 구조이다. Input은 사용자의 입력을 받아 이를 상태로 관리하는 컴포넌트이다. 이때 켈빈과 화씨 컴포넌트가 본인의 변환 결과를 렌더링 하기 위해선 사용자 입력이 필요하다. 켈빈과 화씨는 어떻게 사용자 입력을 가져올 수 있을까?
2-1. 상태 끌어올리기(State Lifting Up)
상태를 공유하는 일반적인 방법은 상태 끌어올리기이다. App에서 handleChange라는 함수 prop을 Input에게 내려주고, Input은 사용자 입력이 발생하면 handleChange를 사용자 입력과 함께 호출을 하여 App에 사용자 입력을 전달하는 것이다. App에 도달한 사용자 입력 상태는 켈빈과 화씨 컴포넌트로 전달된다. 코드는 다음과 같다.
function Input({value, handleChange}) {
return (<input value={value} onChange={(e) => handleChange(e.target.value}} />);
};
function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>Temperature Converter</h1>
<Input value={value} handleChange={setValue}/>
<Kelvin value={value} />
<Fahreheit value={value} />
</div>
);
}
상태 끌어올리기는 가장 간단하게 데이터를 공유할 수 있는 방법이다. 그러나, 최상위 컴포넌트인 App이 사용자 입력과 관련 없는 컴포넌트를 하위 컴포넌트로 갖고 있다면, 사용자 입력과 관련 없는 컴포넌트의 렌더링에도 영향을 줄 수 있다는 단점이 있다.
2-2. Render Props Pattern
Render Props 패턴으로 구현한 데이터 공유의 구조는 위와 같다. 간단히 설명하면 render로 렌더링 로직을 내려주고, Input에서 이를 호출하는 구조이다. 코드로 살펴보자.
function Input(props){
const [value, setValue] = useState("");
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Temp in ℃"
/>
{props.render(value)}
</>
)
}
먼저 Input에서는 사용자 입력을 관리하고, 내려온 render prop에 자신의 사용자 입력 상태를 인자로 전달하여 호출한다.
function App(){
return (
<div className="App">
<h1>Temperature Converter</h1>
<input
render={(value) => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
/>
</div>
)
}
App은 Input에게 render prop 함수를 내려준다. 해당 내용은 value를 받으면 이를 이용하여 켈빈, 화씨 컴포넌트를 렌더링하는 것이다. 이때 render 함수의 value라는 파라미터는 Input이 render 함수를 호출하며 전달된 사용자 입력 값이 된다.
정리하면 render props는 무엇을 렌더링할 것인지를 렌더링 로직을 상위 컴포넌트가 갖고 있고, 하위 컴포넌트인 Input은 자신이 관리하고 있는 상태를 인자로 내려온 render prop을 호출하여 특정 데이터/로직이 필요한 컴포넌트들이 데이터/로직을 공유하게 하는 패턴인 것이다.
3. vs HOC
컴포넌트를 재사용하기 위한 또다른 패턴은 HOC가 있다. HOC와의 차이는 무엇일까? 위 코드는 withData1과 withData2라는 HOC 2개를 중첩하여 렌더링 하는 코드이다. 여기서 주목할 점은 withData1과 withData2라는 두 HOC 모두 data라는 이름으로 데이터를 전달하고 있다는 점이다.
HOC로 전달된 prop은 일반적으로 자동으로 병합된다. 따라서 마지막에 전달된 data인 1이 최종적으로 덮어쓰게 되어, withData2가 전달한 값인 2는 잃어버리게 된다.
function WithData2(props) {
return props.render(2);
}
function WithData1(props) {
return props.render(1);
}
const Div = ({ data }) => <div>{data}</div>;
class App extends React.Component {
render() {
return (
<div className="App">
<WithData2
render={(data2) => {
return (
<WithData1 render={(data1) => Div({ data: data1 + data2 })} />
);
}}
/>
</div>
);
}
}
같은 내용을 Render Props 패턴으로 구현해보자. Render Props의 특징은 상위 요소(App)에서 결정된 렌더링 로직을 하위 요소(WithData2)에게 전달하는 함수를 작성해야 한다. 이때, 하위 요소에서 결정된 상태 데이터는 render 함수의 인자(argument)로 사용되므로, 함수를 작성하며 자연스럽게 이름(parameter)이 정의된다.
이름 중첩 해결을 구체적으로 보면 위와 같다. withData2에서 전달된 2라는 데이터는 render 함수의 data2라는 parameter에 해당되므로, 내부 HOC(withData1)에는 data2라는 새로운 이름으로 전달되는 것이다.
function WithData2(props) {
return props.render(2);
}
function WithData1(props) {
return props.render(1);
}
const Div = ({ data }) => <div>{data}</div>;
class App extends React.Component {
render() {
return (
<div className="App">
<WithData2 render={this.renderData2} />
</div>
);
}
renderData2 = (data2) => {
return <WithData1 render={(data1) => this.renderData1(data1 + data2)} />;
};
renderData1 = (data1) => {
return Div({ data: data1 });
};
}
Render Props 패턴은 콜백 헬에 빠지기 쉽다는 단점이 있다. 이 경우 위와 같이 각 render 함수를 컴포넌트 클래스의 메서드로 정의하여 사용하는 코딩 패턴을 사용하여 해결한다. 또한 이러한 코딩 방식은 상위 컴포넌트(App)가 재 렌더링 시, render 함수가 계속 재정의되는 것을 방지할 수 있다. (code sand box)
4. Tip
const Title = (props) => (
<>
{props.renderFirstComponent()}
{props.renderSecondComponent()}
{props.Children()}
</>
);
class App extends React.Component{
<Title
renderFirstComponent={() => <h1> First </h1> } }
renderSecondComponent={() => <h1> Second </h1> } }
>
{() => <h1> Childern <h1>}
</Title>
}
Render Props 패턴 사용에 한 가지 팁이 있다면, render 외에 다양한 prop 이름을 사용할 수 있다는 것이다. 지금까지는 render라는 이름으로만 렌더링 로직을 전달했지만, 실제로는 위처럼 renderFirstComponent, renderSecondComponent와 같은 다양한 이름을 사용할 수 있다. 또한, children도 props.Children이라는 이름으로 접근 가능하기 때문에, render 로직을 children으로 전달할 수도 있다.
Render Props: https://www.patterns.dev/posts/render-props-pattern/
공식문서: https://ko.reactjs.org/docs/render-props.html
Render Props 콜백 헬 : https://dmitripavlutin.com/solve-react-render-props-callback-hell/