Render Prop
우리가 일전에 HoC 를 통하여 반복되는 로직을 효율적으로 재사용 하는 방법을 알아봤었습니다. 이번 강좌에서 알아볼 render props 는, HoC 에서 처럼 반복되는 로직을 쉽게 재사용 할 수 있게 해주고, 컴포넌트 코드를 작성하는 과정에서 컴포넌트를 함수로 감싸는 것이 아니라, JSX 에서 렌더링 하는 방식으로 사용 할 수 있게 해주는 패턴입니다.
Render Prop 은, 단순히 props 에 JSX 를 렌더링하는 함수를 전달 하는 것 입니다. 그리고 render 함수에서, 전달받은 함수에, 넣어주고 싶은 값을 함수의 인자로 전달하여 사용합니다.
import React, { Component } from 'react';
class Example extends Component {
state = {
value: 'hello!'
};
render() {
return this.props.render(this.state.value);
}
}
export default Example;
그리고 이 컴포넌트를 사용 할 때엔 이렇게 사용하죠:
import React, { Component } from 'react';
import Example from './Example';
class App extends Component {
render() {
return (
<div>
<Example render={value => <h1>{value}</h1>} />
</div>
);
}
}
export default App;
그럼, 다음과 같이 나타나게 됩니다.
음? 뭐가 어떻게 된거죠?
한 30초 동안만 이 코드의 구조를 보면서 이해를 해보세요. 그냥 단순히, props 로 value 라는 값을 전달받는 render 라는 함수를 설정해 주었고, Example 에선 이 함수에 value 값을 넣어주기 위해서 컴포넌트의 render 함수에서 this.state.value 를 함수의 인자로 전달해주었죠.
자! 그럼 이걸 가지고 무엇을 할 수 있을까요?
Render Prop 예제 #1 폼 관리
일단 여기서 팩트 하나! 모든 HoC 는 Render Prop 으로도 구현이 가능합니다.
그래서 기존에 HoC 로 해결하던 것들은 Render Prop 으로도 해결 가능합니다.
한번, 다음 코드를 확인해보세요, 두개의 폼이 있는데, 각 컴포넌트에서 비슷한 코드가 사용됩니다.
FormOne.js
import React, { Component } from 'react';
class FormOne extends Component {
state = {
name: '',
phone: ''
};
handleChange = e => {
this.setState({
[e.target.name]: e.target.value
});
};
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state);
this.setState({
name: '',
phone: ''
});
};
render() {
const { name, phone } = this.state;
return (
<form onSubmit={this.handleSubmit}>
<input
name="name"
placeholder="이름"
value={name}
onChange={this.handleChange}
/>
<input
name="phone"
placeholder="전화번호"
value={phone}
onChange={this.handleChange}
/>
<button type="submit">OK</button>
</form>
);
}
}
export default FormOne;
FormTwo.js
import React, { Component } from 'react';
class FormOne extends Component {
state = {
name: '',
phone: ''
};
handleChange = e => {
this.setState({
[e.target.name]: e.target.value
});
};
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state);
this.setState({
name: '',
phone: ''
});
};
render() {
const { name, phone } = this.state;
return (
<form onSubmit={this.handleSubmit}>
<input
name="name"
placeholder="이름"
value={name}
onChange={this.handleChange}
/>
<input
name="phone"
placeholder="전화번호"
value={phone}
onChange={this.handleChange}
/>
<button type="submit">OK</button>
</form>
);
}
}
export default FormOne;
폼을 만들때마다 이런 비슷한 코드를 자꾸 작성하게 되는데요... 이걸 Render Prop 으로 해결해봅시다.
FormManager.js
import React, { Component } from 'react';
class FormManager extends Component {
static defaultProps = {
form: {}
};
constructor(props) {
super(props);
this.state = props.defaultForm;
}
handleChange = e => {
this.setState({
[e.target.name]: e.target.value
});
};
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state);
this.setState(this.props.form);
};
render() {
return this.props.render({
form: this.state,
onSubmit: this.handleSubmit,
onChange: this.handleChange
});
}
}
export default FormManager;
사용 할 땐 이렇게 사용하면 됩니다.
App.js
import React, { Component } from 'react';
import FormOne from './FormOne';
import FormTwo from './FormTwo';
import FormManager from './FormManager';
class App extends Component {
handleSubmitFormOne = form => {
console.log('Submit Form One', form);
};
handleSubmitFormTwo = form => {
console.log('Submit Form Two', form);
};
render() {
return (
<div>
<FormManager
defaultForm={{ name: '', phone: '' }}
onSubmit={this.handleSubmitFormOne}
render={({ form, onSubmit, onChange }) => (
<FormOne
name={form.name}
phone={form.phone}
onSubmit={onSubmit}
onChange={onChange}
/>
)}
/>
<FormManager
defaultForm={{ username: '', password: '' }}
onSubmit={this.handleSubmitFormTwo}
render={({ form, onSubmit, onChange }) => (
<FormTwo
username={form.username}
password={form.password}
onSubmit={onSubmit}
onChange={onChange}
/>
)}
/>
</div>
);
}
}
export default App;
조금씩 이해가 가기 시작했나요?
Render Prop 예제 #2 API 호출
예제를 한가지 더 다뤄보면서 이 패턴에 조금 더 익숙해져봅시다!
아래 프로젝트에서 작업하겠습니다:
이전에 우리가 withRequest 라는 HoC 를 만들어서 반복되는 코드를 재사용 할 수 있도록 해줬었죠? 이번에는 render prop 으로 이걸 해결해봅시다!
DataFetcher.js
import React, { Component } from "react";
import axios from "axios";
class DataFetcher extends Component {
state = {
loading: false,
data: null,
error: null
};
async initialize() {
this.setState({
loading: true
});
try {
const response = await axios.get(this.props.url);
this.setState({
loading: false,
data: response.data
});
} catch (e) {
this.setState({
loading: false,
error: e
});
}
}
componentDidMount() {
this.initialize();
}
render() {
if (this.state.error) {
return <div>에러발생!</div>
}
if (this.state.loading || !this.state.data) {
return <div>로딩중..</div>;
}
return this.props.render(this.state.data);
}
}
export default DataFetcher;
요청 관련 작업을 하고, 로딩중일땐 "로딩중" 이란 텍스트가 뜨게 했고, 에러시엔 "에러 발생!" 이 뜨게 했습니다. 요청이 정상적으로 이뤄졌다면, props 로 받은 render 에 현재 데이터를 넣어서 전달해줍니다.
그리고, Posts 와 Comments 컴포넌트는 다음과 같이 간소화 시키세요.
Post.js
import React from "react";
const Post = ({ title, body }) => (
<div>
<h2>{title}</h2>
<p>{body}</p>
</div>
);
export default Post;
Comments.js
import React from "react";
const Comments = ({ comments }) => (
<ul>{comments.map(comment => <li key={comment.id}>{comment.body}</li>)}</ul>
);
export default Comments;
그리고, DataFetcher 를 통해서 다음과 같이 요청을 처리하시면 됩니다!
App.js
import React, { Component } from "react";
import DataFetcher from "./DataFetcher";
import Post from "./Post";
import Comments from "./Comments";
class App extends Component {
render() {
return (
<div>
<h1>포스트</h1>
<DataFetcher
url="https://jsonplaceholder.typicode.com/posts/1"
render={data => <Post title={data.title} body={data.body} />}
/>
<hr />
<h1>덧글</h1>
<DataFetcher
url="https://jsonplaceholder.typicode.com/comments?postId=1"
render={data => <Comments comments={data} />}
/>
</div>
);
}
}
export default App;
참 쉽죠?
children 으로 render prop
우리가 작성한 코드에서는, this.props.render
를 통하여 render prop 을 구현해주었는데요, children 을 사용하셔도 무방합니다. 하지만 americanexpress 에 있는 포스트에서는 이를 그닥 추천하진 않습니다. 저 또한 해당 포스트를 읽어보니 children 으로 함수를 전달하는건 그렇게 좋은것 같지는 않습니다. 물론, 리액트 v16.3 에 도입된 Context Consumer 는 chlidren 을 사용하긴 하지만요.
위 포스트에서의 주요 요점은, "우리가 변수명을 지을 때, 의미 있는 이름을 사용해야 하는데 children 를 함수로 사용하는것은 전혀 의미있지 않다" 입니다.
HoC 와 Render Prop 의 주요 차이
Render Prop 을 사용함으로서 최고 이점은 컴포넌트가 가지고 있는 기능 - LifeCycle API, JSX 등을 온전히 누릴 수 있습니다. HoC 는, 적용하려면 컴포넌트를 또 따로 만들어야 하는 반면, Render Prop 은 이미 만들어진 컴포넌트를 가지고 JSX 단에서 기능을 붙여줄 수 있습니다. 추가적으로, 원래는 HoC 의 파라미터로 넣어줘야 할 설정들을 동적으로 처리해줄 수도 있죠.
추가적으로 HoC 는 props 이름이 충돌 될 수있는데 render prop 은 그럴 일이 없죠. 이런 식으로 사용하면 되니까요;
<RenderProp
render={foo => <RenderPopTwo
render={bar => <div><{foo} {bar}</div>
/>}
}/>
상황에 따라 HoC 가 간결 할 수도 있습니다. 예를들어서, 리덕스의 connect 함수는 HoC 로 사용하는 편이 조금 더 보기 쉽겠죠?
Render Prop 패턴을 잘 알아두시면, 나중에 유용하게 써먹을 수 있으실 겁니다! :)