2. Higher-Order Component (HOC)
우리는, 리액트를 사용하여 UI 를 개발하면서, 비슷한 생김새를 가진 UI 들은 컴포넌트로 분류하여 재사용을 합니다. 하지만, 생김새는 다른데 로직만 비슷하다면 어떨까요?
예를들어 위 스크린샷처럼, 폼을 다루는 컴포넌트를 만든다고 가정하겠습니다. 하나는 로그인폼이고, 하나는 블로그에 글을 작성하는 폼인데요, 생김새는 다른데, 내부적으로 작동하는 로직은 거의 비슷합니다.
한번, 얼마나 비슷한지 코드를 확인해볼까요?
import React, { Component } from 'react';
class LoginForm extends Component {
state = {
username: '',
password: ''
};
handleChange = e => {
const { name, value } = e.target;
this.setState({
[name]: value
});
};
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state);
};
render() {
const { username, password } = this.state;
return (
<form className="LoginForm" onSubmit={this.handleSubmit}>
<input
onChange={this.handleChange}
value={username}
name="username"
placeholder="계정"
/>
<input
onChange={this.handleChange}
value={password}
name="password"
type="password"
placeholder="비밀번호"
/>
<button>로그인</button>
</form>
);
}
}
export default LoginForm;
import React, { Component } from 'react';
class BlogPostForm extends Component {
state = {
title: '',
content: '',
tags: ''
};
handleChange = e => {
const { name, value } = e.target;
this.setState({
[name]: value
});
};
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state);
};
render() {
const { title, content, tags } = this.state;
return (
<form className="BlogPostForm" onSubmit={this.handleSubmit}>
<input
value={title}
onChange={this.handleChange}
name="title"
placeholder="제목"
/>
<textarea
value={content}
onChange={this.handleChange}
name="content"
placeholder="내용"
/>
<input
value={tags}
onChange={this.handleChange}
name="tags"
placeholder="태그"
/>
<button>작성</button>
</form>
);
}
}
export default BlogPostForm;
state, handleChange, handleSubmit 이 거의 재사용되고 있습니다. 단지 state 안에 들어있는 키의 이름이 다를 뿐이죠.
HOC 는 함수를 사용하여 이러한 로직들을 재사용 할 수 있게 해줍니다. HOC 함수는, 다음과 같은 형식을 이루고 있습니다:
import React, { Component } from 'react';
const withSomething = (options) => (BaseComponent) => {
return class WithSomething extends Component {
render() {
return (<BaseComponent {...this.props} somethingExtra={1}/>)
}
}
}
export default withSomething;
만약에 옵션이 필요 없는 경우에는 options 부분이 생략 될 수도 있습니다:
import React, { Component } from 'react';
const withSomething = (BaseComponent) => {
return class WithSomething extends Component {
render() {
return (<BaseComponent {...this.props} somethingExtra={1}/>)
}
}
}
export default withSomething;
사용 할 때는, 사용하려는 컴포넌트쪽에서 불러와서 이런식으로 사용합니다:
import withSomething from './withSomething';
const MyComponent = () => <div>Hello World!</div>
// withSomething 에서 넣어주는 기능이 적용된 MyComponent 를 내보냄
export default withSomething(options)(MyComponent);
// 옵션이 필요없는 HOC 라면, withSomething(MyComponent)
여기서 생기는 의문점은, 왜 옵션을 굳이 또 하나의 함수로 감싸서 작성하는지? 입니다. 사실 이렇게 해도 충분히 작동을 하긴 할텐데요..
import React { Component } from 'react';
const withSomething = (BaseComponent, options) => {
// ...
}
export default withSomething;
이렇게 하지 않는 이유는, 우리가 만약에 HOC 를 여러개 사용하게 될 때 불편하기 때문입니다.
만약에 함수로 감싸는 형태로 일관적인 방식으로 한다면 withSomething 과 withOtherThing 과 withAnything 이라는 HOC 를 같이 사용하게 될 때 다음과 같은 형식으로 작성 할 수 있습니다.
export default withSomething(someOptions)(
withOtherThing(otherOptions)(
withAnything(anyOptions)(MyComponent)
)
);
이것도 사실 가독성이 그렇게 좋은 편은 아닙니다. 이 때 compose 라는 함수를 사용하면
const compose = (...fns) =>
fns.reduceRight((prevFn, nextFn) =>
(...args) => nextFn(prevFn(...args)),
value => value
);
const enhance(
withSomething(someOptions),
withOtherThing(otherOptions),
withAnything(anyOptions)
)
export default enhance(MyComponent);
이렇게 높은 가독성을 보이는 코드를 작성하실 수 있습니다.
이 compose 함수는 직접 다른 파일에 작성해서 불러와서 사용을 하셔도 되고, 보통 나중에 자주 사용하게 될 수 있는 lodash, redux, recompose 등의 라이브러리에도 내장되어있으니 그걸 사용하시면 됩니다.
HOC 로 반복되는 폼 로직, 해결해보자
위 샌드박스에서 함께 반복되는 로직을 HOC 로 만들어서 코드를 리팩토링해주겠습니다!
HOC 틀 만들기
우선, withForm.js 라는 파일을 만들어서 HOC 의 틀을 만들어주겠습니다.
HOC 의 이름을 지을때는 보통 with... 이런식으로 짓는게 일반적입니다. 하지만, 딱히 정해진 규칙은 아닙니다!
src/withForm.js
import React, { Component } from 'react';
const withForm = (initialForm, resetOnSubmit) => BaseComponent => {
return class WithForm extends Component {
render() {
return (
<BaseComponent
{...this.props}
/>
);
}
}
}
export default withForm;
우리가 만들 withForm 에서는 initialForm 과 resetOnSubmit 이라는 옵션을 전달 해 줄것입니다. initialForm 은 초기에 컴포넌트가 들고있을 상태 객체를 전달하며, resetOnSubmit 은 submit 이벤트 발생 시 폼을 초기화 할지 말지 정합니다.
그리고, BaseComponent 에서는 {...this.props}
가 사용되었는데요, 이 spread (...
) 연산자가 JSX 안에서 사용되면, 해당 객체 안에 들어있는 값이 모두 props 로 전달됩니다.
예시:
const object = {
details: true,
name: 'hello'
};
<MyComponent {...object} />
// = <MyComponent details={true} name="hello" />
그래서, 다시 {...this.props}
에 대하여 설명을 드리자면, 이 코드가 나중에 우리가 이 함수를 통해 만든 컴포넌트를 사용하게 될 때 자신이 받은 props 를 그대로 BaseComponent 에게 전달해줍니다.
HOC 기능 구현하기
이제 withForm HOC 에 form 상태를 관리하는 로직을 구현해주겠습니다.
src/withForm.js
import React, { Component } from 'react';
const withForm = (initialForm, resetOnSubmit) => BaseComponent => {
return class WithForm extends Component {
state = initialForm || {};
handleChange = e => {
const { name, value } = e.target;
this.setState({
[name]: value
});
};
handleSubmit = e => {
e.preventDefault();
if (this.props.onSubmit) {
this.props.onSubmit(this.state);
}
if (resetOnSubmit) {
this.setState(initialForm);
}
};
render() {
return (
<BaseComponent
{...this.props}
form={this.state}
onChange={this.handleChange}
onSubmit={this.handleSubmit}
/>
);
}
};
};
export default withForm;
state = initialForm || {};
컴포넌트에서 사용되는 state 는 HOC 를 사용 할 때 전달해줄 initialForm 값을 사용하도록 했으며 만약에 해당 값이 존재하지 않을 시 비어있는 객체를 넣어주도록 처리를 해주었습니다.
handleChange = e => {
const { name, value } = e.target;
this.setState({
[name]: value
});
};
그리고, handleChange 에서는 e.target 에서 받아오게될 input 의 name, value 값을 참조하여 state 에 변화를 주도록 처리를 해주었습니다.
handleSubmit = e => {
e.preventDefault();
if (this.props.onSubmit) {
this.props.onSubmit(this.state);
}
if (resetOnSubmit) {
this.setState(initialForm);
}
};
handleSubmit 에서는 e.preventDefault()
를 사용하여 새로고침을 방지하고, onSubmit props 를 받았을 시 해당 함수에 현재 state 를 전달하여 호출해주고, 만약 resetOnSubmit 값이 true 면 state 를 초기상태로 되돌려놓습니다.
render() {
return (
<BaseComponent
{...this.props}
form={this.state}
onChange={this.handleChange}
onSubmit={this.handleSubmit}
/>
);
}
그리고 render 부분, 이 부분이 가장 중요한데요, BaseComponent가 원래 받았던 props 를 넣어주고, 추가적으로 우리가 방금 만든 함수들과 컴포넌트의 state 를 props 로 전달을 해줍니다.
구현은 끝났습니다! 이제 withForm 을 사용하여 컴포넌트 코드들을 깔끔하게 정리해주겠습니다.
src/LoginForm.js
import React, { Component } from 'react';
import withForm from './withForm';
class LoginForm extends Component {
render() {
const { username, password } = this.props.form;
return (
<form className="LoginForm" onSubmit={this.props.onSubmit}>
<input
onChange={this.props.onChange}
value={username}
name="username"
placeholder="계정"
/>
<input
onChange={this.props.onChange}
value={password}
name="password"
type="password"
placeholder="비밀번호"
/>
<button>로그인</button>
</form>
);
}
}
export default withForm({ username: '', password: '' }, true)(LoginForm);
기존에 구현했던 state, handleChange, handleSubmit 은 날려주고, render 함수에서 props 로 받아온 form, onSubmit, onChange 를 사용하도록 구현을 해줍니다. 이전과 똑같이, 잘 작동하나요?
이 컴포넌트에서는, state 도, 커스텀 함수도, 사용하지 않으니 그냥 함수형 컴포넌트로 구현해도 무방합니다!
한번 함수형 컴포넌트로 작성해보겠습니다.
src/LoginForm.js
import React, { Component } from 'react';
import withForm from './withForm';
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 withForm({ username: '', password: '' }, true)(LoginForm);
훨씬 깔금해졌지요?
src/BlogPostForm.js
BlogPostForm 컴포넌트도 똑같은 원리로 정리를 해주겠습니다. 여기서는 resetOnSubmit 값을 생략해주었습니다. 때문에, 작성을 눌러도 값이 초기화되지는 않습니다.
import React from 'react';
import withForm from './withForm';
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 withForm({
title: '',
content: '',
tags: ''
})(BlogPostForm);
어떤가요? HOC 를 잘 활용하면 반복되는 로직들을 따로 정리하여 훨씬 짧은 코드로 기능들을 구현 할 수 있게 됩니다.