โ ๏ธ ์ ๋ฆฌํ ๋ด์ฉ์ ์คํ๋ ์๋ชป๋ ์ ๋ณด๊ฐ ์์ ์ ์์ต๋๋ค. ๋๊ธ๋ก ์๋ ค์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค.
๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ์ปดํฌ๋ํธ ๋ก์ง์ ์ฌ์ฌ์ฉํ๊ธฐ ์ํ React์ ๊ณ ๊ธ ๊ธฐ์ ์ด๋ค. ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ React API์ ์ผ๋ถ๊ฐ ์๋๋ฉฐ, React์ ๊ตฌ์ฑ์ ํน์ฑ์์ ๋์ค๋ ํจํด์ด๋ค.
๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ ธ์ ์ ์ปดํฌ๋ํธ๋ฅผ ๋ฐํํ๋ ํจ์์ด๋ค.
const EnhancedComponent = higherOrderComponent(WrappedComponent);
์ปดํฌ๋ํธ๋ props๋ฅผ UI๋ก ๋ณํํ๋ ๋ฐ๋ฉด์, ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ์ปดํฌ๋ํธ๋ฅผ ์๋ก์ด ์ปดํฌ๋ํธ๋ก ๋ณํํ๋ค.
Cross-Cutting Concerns๋?
ํก๋จ ๊ด์ฌ์ฌ๋? - ๋ธ๋ก๊ทธ(ํก๋จ ๊ด์ฌ์ฌ) ์ฐธ์กฐ
๊ท๋ชจ๊ฐ ํฐ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋์ผํ ํจํด์ด ๋ฐ๋ณต์ ์ผ๋ก ๋ฐ์ํ๋ค๊ณ ๊ฐ์ ํด๋ณด์. ๊ทธ๋ ๊ฒ ๋๋ฉด ์ด ๋ก์ง์ ํ ๊ณณ์์ ์ ์ํ๊ณ ๋ง์ ์ปดํฌ๋ํธ์์ ๋ก์ง์ ๊ณต์ ํ ์ ์๊ฒ ํ๋ ์ถ์ํ๊ฐ ํ์ํ๋ค. ์ด๋ฌํ ๊ฒฝ์ฐ์ ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ฉด ์ข๋ค.
DataSource
ย ๋ฅผ ๊ตฌ๋
ํ๋ย CommentList
ย ๋ย BlogPost
ย ๊ฐ์ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ๋ ํจ์๋ฅผ ์์ฑํ ์ ์๋ค. ๊ตฌ๋
ํ ๋ฐ์ดํฐ๋ฅผ prop์ผ๋ก ์ ๋ฌ๋ฐ๋ ์์ ์ปดํฌ๋ํธ๋ฅผ ํ๋ผ๋ฏธํฐ ์ค ํ๋๋ก ๋ฐ๋ ํจ์๋ฅผ ๋ง๋ ๋ค.
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
์ฒซ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋ ๋ํ๋ ์ปดํฌ๋ํธ์ด๊ณ , ๋ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋ DataSource์ ํ์ฌ props๋ฅผ ๊ฐ์ง๊ณ ์ปดํฌ๋ํธ์์ ๊ด์ฌ์๋ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ๋ค.
CommentListWithSubscription
ย ๊ณผย BlogPostWithSubscription
ย ๊ฐ ๋ ๋๋ง๋ ๋ย CommentList
์ย BlogPost
ย ๋ย DataSource
ย ์์ ๊ฐ์ฅ ์ต๊ทผ์ ๊ฒ์๋ ๋ฐ์ดํฐ๋ฅผย data
ย prop์ผ๋ก ์ ๋ฌํ๋ค.
// ์ด ํจ์๋ ์ปดํฌ๋ํธ๋ฅผ ๋งค๊ฐ๋ณ์๋ก ๋ฐ๊ณ ..
function withSubscription(WrappedComponent, selectData) {
// ...๋ค๋ฅธ ์ปดํฌ๋ํธ๋ฅผ ๋ฐํํ๋๋ฐ...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... ๊ตฌ๋
์ ๋ด๋นํ๊ณ ...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... ๋ํ๋ ์ปดํฌ๋ํธ๋ฅผ ์๋ก์ด ๋ฐ์ดํฐ๋ก ๋๋๋ง
// ์ปดํฌ๋ํธ์ ์ถ๊ฐ๋ก props๋ฅผ ๋ด๋ ค์ฃผ๋ ๊ฒ์ ์ฃผ๋ชฉ!
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ์ ๋ ฅ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ ํ์ง ์์ผ๋ฉฐ ์์์ ์ฌ์ฉํ์ฌ ๋์์ ๋ณต์ฌํ์ง๋ ์๋๋ค. ์คํ๋ ค ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ์๋ณธ ์ปดํฌ๋ํธ๋ฅผ ์ปจํ ์ด๋ ์ปดํฌ๋ํธ๋ก ํฌ์ฅํ์ฌ ์กฐํฉํ๋ค. ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ์ฌ์ด๋ ์ดํํธ๊ฐ ์ ํ ์๋ ์์ ํจ์์ด๋ค.
๊ณ ์ฐจ ์ปดํฌ๋ํธ ๋ด๋ถ์์ ์ปดํฌ๋ํธ์ ํ๋กํ ํ์ ์ ์์ ํ์ง ์๋๋ก ํ๋ค.
function logProps(InputComponent) {
InputComponent.prototype.componentDidUpdate = function(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
};
// ์๋ณธ์ ์
๋ ฅ์ ๋ฐํํ๋ค๋ ๊ฒ์ ์ด๋ฏธ ๋ณํ๋์๋ค๋ ์ ์ ์์ฌํ๋ค.
return InputComponent;
}
// EnhancedComponent ๋ props๋ฅผ ๋ฐ์ ๋ ๋ง๋ค log๋ฅผ ๋จ๊ธด๋ค.
const EnhancedComponent = logProps(InputComponent);
์์ ์ฝ๋์๋ ๋ช ๊ฐ์ง ๋ฌธ์ ๊ฐ ์๋ค.
๊ทธ ์ค ํ๋๋ ์
๋ ฅ๋ ์ปดํฌ๋ํธ๋ฅผ ํ์ฅ๋ ์ปดํฌ๋ํธ์ ๋ณ๋๋ก ์ฌ์ฌ์ฉํ ์ ์๋ค๋ ๊ฒ์ด๋ค. ๋ ์ค์ํ ๊ฒ์,ย componentDidUpdate
๋ฅผ ๋ณํํ๋ย EnhancedComponent
์ ๋ ๋ค๋ฅธ HOC๋ฅผ ์ ์ฉํ๋ฉด ์ฒซ ๋ฒ์งธ HOC์ ๊ธฐ๋ฅ์ ๋ฌด์๋๋ค. ์ด HOC๋ ์๋ช
์ฃผ๊ธฐ ๋ฉ์๋๊ฐ ์๋ ํจ์ ์ปดํฌ๋ํธ์์๋ ์๋ํ์ง ์๋๋ค.
๋ณ๊ฒฝ๋ HOC๋ ๋์ถ๋ ์ถ์ํ์ ๋๋ค. Consumer๋ ๋ค๋ฅธ HOC์์ ์ถฉ๋์ ํผํ๊ธฐ ์ํ์ฌ ์ด๋ป๊ฒ ๊ตฌํ๋์ด์๋์ง ๋ฐ๋์ ์์์ผ ํ๋ค. HOC๋ ๋ณ๊ฒฝ๋์ ์ ์ ๋ ฅ ์ปดํฌ๋ํธ๋ฅผ ์ปจํ ์ด๋ ๊ตฌ์ฑ์์๋ก ๊ฐ์ธ์ ์กฐํฉ์ ์ฌ์ฉํด์ผ ํ๋ค.
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
// ๋ค์ด์จ component๋ฅผ ๋ณ๊ฒฝํ์ง ์๋ container์ด๋ค.
return <WrappedComponent {...this.props} />;
}
}
}
์ ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ ํผํ๋ฉด์ ํ๋กํ ํ์ ์ ์ง์ ๋ณ๊ฒฝํ๋ ๋ฒ์ ๊ณผ ๋์ผํ๊ฒ ์๋ํ๋ค.
๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ์ ์๋ฅผ ๊ณผ๊ฐํ๊ฒ ๋ณ๊ฒฝํด์๋ ์๋๋ค. ๊ณ ์ฐจ ์ปดํฌ๋ํธ์์ ๋ฐํ๋ ์ปดํฌ๋ํธ๋ ๋ํ๋ ์ปดํฌ๋ํธ์ ๋น์ทํ ์ธํฐํ์ด์ค๊ฐ ์์ด์ผ ํ๋ค. ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ํน์ ๊ด์ฌ์ฌ์ ๊ด๋ จ์ด ์๋ props๋ฅผ ํ์ฉํด์ผ ํ๋ค. ๋๋ถ๋ถ์ ๊ณ ์ฐจ ์ปดํฌ๋ํธ์๋ ๋ค์๊ณผ ๊ฐ์ ๋ ๋๋ง ๋ฉ์๋๊ฐ ํฌํจ๋์ด ์๋ค.
render() {
// ์ด HOC์๋ง ํด๋น๋๋ฏ๋ก ์ถ๊ฐ๋ props๋ ๊ฑธ๋ฌ๋ด์ด ์ด HOC์ ์ ๋ฌ๋์ง ์๋๋ก ํ๋ค.
const { extraProp, ...passThroughProps } = this.props;
// ์ด Props๋ ์ผ๋ฐ์ ์ผ๋ก Status๊ฐ ๋๋ Instance method ์ด๋ค.
const injectedProp = someStateOrInstanceMethod;
// wrapped component์ props๋ฅผ ์ ๋ฌํฉ๋๋ค.
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ๋๋๋ก ๋จ์ผ ์ธ์๋ก ๋ํ๋ ์ปดํฌ๋ํธ๋ง ๋ฐ์ ๋๋ ์๋ค.
const NavbarWithRouter = withRouter(Navbar);
๊ณ ์ฐจ ์ปดํฌ๋ํธ์ ๋ํ ๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ์ฌ์ฉ์ ๋ค์๊ณผ ๊ฐ๋ค.
// React Redux์ `connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
์์ ์ฝ๋๋ ์๋์ ๊ฐ์ด ๋ถํดํ ์ ์๋ค.
// connect๋ ๋ค๋ฅธ ํจ์๋ฅผ ๋ฐํํ๋ ํจ์์ด๋ค.
const enhance = connect(commentListSelector, commentListActions);
// ๋ฐํ๋ ํจ์๋ Redux store์ ์ฐ๊ฒฐ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ฐํํ๋ ๊ณ ์ฐจ ํจ์ ์ปดํฌ๋ํธ ์ด๋ค.
const ConnectedComment = enhance(CommentList);
connect
ย ํจ์์ ์ํด ๋ฐํ๋ ๊ฒ๊ณผ ๊ฐ์ ๋จ์ผ ์ธ์ ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ย Component => Component
ย ํน์ง์ ๊ฐ์ง๊ณ ์์ต๋๋ค. ์ถ๋ ฅ ํ์
์ด ์
๋ ฅ ํ์
๊ณผ ๋์ผํ ํจ์๋ ์ ๋ง ์ฝ๊ฒ ์กฐํฉํ ์ ์๋ค.
๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ๋ฐฉ๋ฒ์ HOC์ ์ด๋ฆ์ผ๋ก ๋ด๋ถ ์ปดํฌ๋ํธ๋ช
์ ๊ฐ์ธ๋ ๊ฒ์ด๋ค. ๋ฐ๋ผ์ HOC์ ์ด๋ฆ์ดย withSubscription
์ด๊ณ , HOC ๋ด๋ถ์ ์ปดํฌ๋ํธ์ ์ด๋ฆ์ดย CommentList
ย ์ธ ๊ฒฝ์ฐ, ๋์คํ๋ ์ด ๋ค์์ย WithSubscription(CommentList)
์ ์ฌ์ฉํ๋ค.
unction withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
์ฆ, ๊ณ ์ฐจ ์ปดํฌ๋ํธ ์ด๋ฆ(์ปดํฌ๋ํธ์ด๋ฆ) ํ์์ผ๋ก ๋ง๋๋ ๊ฒ์ด๋ค. ๋ฆฌ์กํธ ๊ฐ๋ฐ์ ๋๊ตฌ์์๋ ๊ณ ์ฐจ ์ปดํฌ๋ํธ ์ด๋ฆ ๋์ ์ ์ค์ ๋ํํ ์ปดํฌ๋ํธ ์ด๋ฆใ ๋ฅด ๋ณด์ฌ์ค๋ค.
render() {
// render๊ฐ ํธ์ถ๋ ๋๋ง๋ค ์๋ก์ด ๋ฒ์ ์ EnhancedComponent๊ฐ ์์ฑ๋๋ค.
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// ๋๋ฌธ์ ๋งค๋ฒ ์ ์ฒด ์๋ธํธ๋ฆฌ๊ฐ ๋ง์ดํธ ํด์ ํ ๋ค์ ๋ง์ดํธ ๋๋ค!
return <EnhancedComponent />;
}
์ฌ๊ธฐ์ ์ฑ๋ฅ์์ ๋ฌธ์ ๋ฟ๋ง ์๋๋ผ ์ปดํฌ๋ํธ๊ฐ ๋ค์ ๋ง์ดํธ ๋๋ฉด์ ์ปดํฌ๋ํธ์ state์ ์ปดํฌ๋ํธ์ ํ์ ํญ๋ชฉ๋ค์ด ์์ค๋๋ค. ๋์ ์ ์ปดํฌ๋ํธ์ ์ ์ ๋ฐ๊นฅ์ HOC๋ฅผ ์ ์ฉํ์ฌ ์ปดํฌ๋ํธ๊ฐ ํ ๋ฒ๋ง ์์ฑ๋๋๋ก ํ๋ค. ๊ทธ๋ฌ๋ฉด ํด๋น component๋ ์ฌ๋ฌ๋ฒ ๋ ๋๋ง์ด ๋๋๋ผ๋ ์ผ๊ด์ฑ์ ์ ์งํ๋ค.
์ปดํฌ๋ํธ์ HOC๋ฅผ ์ ์ฉํ๋ฉด, ๊ธฐ์กด ์ปดํฌ๋ํธ๋ ์ปจํ ์ด๋์ ์ปดํฌ๋ํธ๋ก ๊ฐ์ธ์ง๋ค. ์ฆ, ์ ์ปดํฌ๋ํธ๋ ๊ธฐ์กด ์ปดํฌ๋ํธ์ ์ ์ ๋ฉ์๋๋ฅผ ๊ฐ์ง๊ณ ์์ง ์๋๋ค.
// ์ ์ ํจ์๋ฅผ ์ ์
WrappedComponent.staticMethod = function() {/*...*/}
// HOC๋ฅผ ์ ์ฉ
const EnhancedComponent = enhance(WrappedComponent);
// ํฅ์๋ ์ปดํฌ๋ํธ์๋ ์ ์ ๋ฉ์๋๊ฐ ์๋ค.
typeof EnhancedComponent.staticMethod === 'undefined' // true
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ฉ์๋๋ฅผ ๋ฐํํ๊ธฐ ์ ์ ์ปจํ ์ด๋์ ๋ณต์ฌํ๋ค.
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// ๋ณต์ฌ ํ ๋ฉ์๋๋ฅผ ์ ํํ ์์์ผ ํจ
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ ๋ชจ๋ props๋ฅผ ๋ํ๋ ์ปดํฌ๋ํธ์ ์ ๋ฌํ๋ ๊ฒ์ด ์์น์ด์ง๋ง, refs์์๋ ์๋ํ์ง ์๋๋ค.
์ปดํฌ๋ํธ๊ฐ HOC์ ๊ฒฐ๊ณผ์ธ ์๋ฆฌ๋จผํธ์ ref๋ฅผ ์ถ๊ฐํ๋ ๊ฒฝ์ฐ, ref๋ ๋ํ๋ ์ปดํฌ๋ํธ๊ฐ ์๋ ๊ฐ์ฅ ๋ฐ๊นฅ์ชฝ ์ปจํ ์ด๋ ์ปดํฌ๋ํธ์ ์ธ์คํด์ค๋ฅผ ๋ํ๋ธ๋ค.
์ฐธ์กฐ โ ๐ป Ref ์ ๋ฌํ๊ธฐ(feat. ๋ฆฌ์กํธ ๊ณต์๋ฌธ์)
๋ก์ง์ ํ ๊ณณ์์ ์ ์ํ๊ณ ๋ง์ ์ปดํฌ๋ํธ์์ ๋ก์ง์ ๊ณต์ ํ ์ ์๊ฒ ํ๋ ์ถ์ํ๊ฐ ํ์ํ ๋ ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ค๋ ๊ฒ์ ๊ธฐ์ตํด์ผ๊ฒ ๋ค. ์ฆ, ํก๋จ ๊ด์ฌ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฒ์ด ๊ณ ์ฐจ ์ปดํฌ๋ํธ์ ์ญํ ์ธ ๊ฒ์ด๋ค.
ํก๋จ ๊ด์ฌ์ฌ๋ผ๋ ๊ฒ๋ ์ฒ์ ์์๊ณ ์ด๋จ ๋ hoc ๋ฅผ ์ฌ์ฉํด์ผํ๋์ง ์๊ฒ ๋์๋ค.