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 로 반복되는 폼 로직, 해결해보자

Edit 반복되는 Form Logic

위 샌드박스에서 함께 반복되는 로직을 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);

Edit 반복되는 Form Logic

어떤가요? HOC 를 잘 활용하면 반복되는 로직들을 따로 정리하여 훨씬 짧은 코드로 기능들을 구현 할 수 있게 됩니다.

results matching ""

    No results matching ""