3. Render Props 패턴
소개
지난 섹션에서 배운 HOC 는 컴포넌트를 함수로 감싸서 특정 기능을 부여해줬다면, 우리가 이번에 배울 Render Props 는 JSX 단에서 유사한 작업을 할 수 있게 해줍니다.
HOC 는 이런 형태로 로직을 재사용했다면:
export default withForm(
{
username: '',
password: ''
},
true
)(LoginForm);
Render Props 는 이런 형태로 로직을 재사용합니다:
<FormManager
initialForm={{
username: '',
password: ''
}}
onSubmit={this.handleSubmitLogin}
>
{({ form, onChange, onSubmit }) => (
<LoginForm onChange={onChange} onSubmit={onSubmit} form={form} />
)}
</FormManager>
RenderProps 는 완전히 리액트스러운 로직을 위한 코드 재사용방식입니다. JSX 로 모든걸 컨트롤 할 수 있습니다. HOC와의 주요 차이점은, HOC 는 사용하기위해서 무조건 새로운 컴포넌트를 만들어야하는 방면에, Render Props 는 그렇지 않습니다. 때문에, HOC 를 사용하기 위해서 만드는 컴포넌트의 이름을 고민 할 필요 조차 없습니다.
한번 Render Props 의 기초부터 알아볼까요?
Render Prop 은 주로 두가지 형태로 사용됩니다.
첫번째 방식은 "render" 라는 이름을 가진 props 를 전달하는 것 입니다.
<MyComponent
render={({ name }) => (
<div>
Hello <b>{name}</b>!
</div>
)}
/>
import React, { Component } from 'react';
class MyComponent extends Component {
render() {
return this.props.render({ name: 'World' });
}
}
export default MyComponent;
살짝, callback 함수의 컴포넌트 버전이라고 생각하시면 이해하기 쉽습니다. 보통 props 는 부모컴포넌트가 자식컴포넌트한테 전달해주는데, 여기선 반대로 자식 컴포넌트가 부모컴포넌트한테 특정 값을 쏴주고 있습니다.
그리고 두번째 방식은 render 라는 이름 대신에 그냥 children 자체를 함수로 전달해주는 것 입니다.
<MyComponent>
{({ name }) => (
<div>
Hello <b>{name}</b>!
</div>
)}
</MyComponent>
import React, { Component } from 'react';
class MyComponent extends Component {
render() {
return this.props.children({ name: 'World' });
}
}
export default MyComponent;
이것은 그냥 형식상의 차이일뿐, 작동방식은 완전 동일합니다. render 라는 이름으로 props 를 전달할지, children 으로 넣어줄지의 문제인데, 이 두가지 방식은 개발자들의 취향에 따라 사용이 되고 있습니다.
반복되는 폼 로직 Render Props 로 구현하기
그럼 Render Props 를 사용해서 반복되는 폼 로직을 구현해보겠습니다. 여기서 사용되는 코드는 HOC 를 배울때 사용했던 코드와 완전 동일합니다.
위 샌드박스에서부터 시작하겠습니다.
Render Props 를 구현 할 FormManager 컴포넌트 틀 갖추기
우리가 앞으로 만들 Render Props 컴포넌트는, FormManager 라고 이름을 지어주겠습니다. HOC는 보통 with.... 로 이름을 짓는것과 달리, Render Props 를 사용하는 컴포넌트는 기능을 명시해주는 이름으로 짓습니다. (물론 무조건 정해진 규칙은 아닙니다.)
FormManager.js 라는 컴포넌트를 만들어서 Render Props 컴포넌트의 틀을 갖춰주세요.
src/FormManager.js
import React, { Component } from 'react';
class FormManager extends Component {
constructor(props) {
super(props);
this.state = {};
}
handleChange = e => {};
handleSubmit = e => {};
render() {
return this.props.children({
form: this.state,
onChange: this.handleChange,
onSubmit: this.handleSubmit
});
}
}
export default FormManager;
state 와 handleChange, handleSubmit 의 틀만 갖춰주었습니다. 그리고 이 값과 함수들은 모두, 앞으로 자신이 전달받게 될 children 함수에 파라미터로 넣어져서 호출됩니다.
폼 기능 구현하기
이어서 기능을 구현하겠습니다.
src/FormManager.js
import React, { Component } from 'react';
class FormManager extends Component {
static defaultProps = {
initialForm: {} // 없으면 그냥 빈 객체 사용
};
constructor(props) {
super(props);
this.state = props.initialForm;
}
handleChange = e => {
this.setState({
[e.target.name]: e.target.value
});
};
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state);
if (this.props.resetOnSubmit) {
this.setState(this.props.initialForm);
}
};
render() {
return this.props.children({
form: this.state,
onChange: this.handleChange,
onSubmit: this.handleSubmit
});
}
}
export default FormManager;
헷갈리는 함수가 없이 그냥 리액트 컴포넌트죠? 반복되는 로직에서 사용 할 옵션같은 것도, 모두 props 로 관리합니다.
FormManager 사용하기
한번 사용을 해볼까요? 우선 LoginForm 과 BlogPostForm 에서 state 와 커스텀 함수들을 모두 날려주고, props 에서 모든걸 받아와서 사용하는 형태로 변경을 해주겠습니다.
src/BlogPostForm.js
import React, { Component } from 'react';
export const BlogPostForm = ({ form, onChange, onSubmit }) => {
const { title, content, tags } = form;
return (
<form className="BlogPostForm" onSubmit={onSubmit}>
<input
value={title}
onChange={onChange}
name="title"
placeholder="제목"
/>
<textarea
value={content}
onChange={onChange}
name="content"
placeholder="내용"
/>
<input value={tags} onChange={onChange} name="tags" placeholder="태그" />
<button>작성</button>
</form>
);
};
export default BlogPostForm;
src/LoginForm.js
import React, { Component } from 'react';
const LoginForm = ({ form, onChange, onSubmit }) => {
const { username, password } = form;
return (
<form className="LoginForm" onSubmit={onSubmit}>
<input
onChange={onChange}
value={username}
name="username"
placeholder="계정"
/>
<input
onChange={onChange}
value={password}
name="password"
type="password"
placeholder="비밀번호"
/>
<button>로그인</button>
</form>
);
};
export default LoginForm;
이제, App 에서 LoginForm 과 BlogPostForm 을 각각 FormManager 로 감싸줘봅시다!
src/App.js
import React, { Component } from 'react';
import Layout from './Layout';
import BlogPostForm from './BlogPostForm';
import LoginForm from './LoginForm';
import Output from './Output';
import FormManager from './FormManager';
class App extends Component {
state = {
blogPost: null,
login: null
};
handleSubmitLogin = login => {
this.setState({
login
});
};
handleSubmitBlogPost = blogPost => {
this.setState({
blogPost
});
};
render() {
const { blogPost, login } = this.state;
return (
<Layout
login={
<FormManager
initialForm={{
username: '',
password: ''
}}
onSubmit={this.handleSubmitLogin}
>
{({ form, onChange, onSubmit }) => (
<LoginForm onChange={onChange} onSubmit={onSubmit} form={form} />
)}
</FormManager>
}
blogPost={
<FormManager
initialForm={{
title: '',
content: '',
tags: ''
}}
onSubmit={this.handleSubmitBlogPost}
resetOnSubmit={true}
>
{({ form, onChange, onSubmit }) => (
<BlogPostForm
onChange={onChange}
onSubmit={onSubmit}
form={form}
/>
)}
</FormManager>
}
output={<Output blogPost={blogPost} login={login} />}
/>
);
}
}
export default App;
Render Props 를 사용해보니 어떤가요? 정말 리액트스럽지 않나요? 나중에 배우게 될 Context API 에서도, 이 Render Props 패턴을 사용하게 됩니다.