컴포넌트에 날개를 달아줘, 리액트 Higher-order Component (HoC)


리액트 Higher-order Component (HOC)

코드를 작성하다보면, 자주 반복해서 작성하게 되는 코드들이 있습니다. 우리는 주로 그러한 것들을 함수화하여 재사용 하곤 하죠. 컴포넌트 또한 비슷하죠. 같은 UI 관련 코드가 재사용 될 수 있다면 우리는 컴포넌트를 만들어서 컴포넌트를 재사용합니다. 자, 그런데 컴포넌트 기능 상에서도, 자주 반복되는 코드들이 나타날 수 있습니다. 소프트웨어 개발 원리 중에서 DRY 라는 개념이 있죠 – 같은 작업을 반복하지 마라 (Don’t repeat yourself)

리액트 컴포넌트를 작성하게 될 때 반복될 수 있는 코드들은, HOC 를 만들어서 해결해줄 수 있습니다. HOC 는, 하나의 함수인데요, 함수를 통하여 컴포넌트에 우리가 준비한 특정 기능을 부여합니다. 직접 만들어볼 때 까지는 감이 잡히지 않을 수도 있습니다. 한번, 직접 만들어봅시다!

이 튜토리얼은 Codsandbox 에서 진행하겠습니다. 프로젝트 열기

반복되는 코드 발견하기

Post.js 코드를 읽어보세요.

import React, { Component } from 'react';
import axios from 'axios';

class Post extends Component {
  state = {
    data: null
  }
  
  async initialize() {
    try {
      const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1');
      this.setState({
        data: response.data
      });
    } catch (e) {
      console.log(e);
    }
  }

  componentDidMount() {
    this.initialize();  
  }


  render() {
    const { data } = this.state;
    
    if (!data) return null;

    return (
      <div>
        { JSON.stringify(data) }    
      </div>
    );
  }
}


export default Post;

이 컴포넌트에서는, 특정 주소에 GET 요청을 날리고, 결과물을 state 의 data 안에 담습니다. 그리고, 해당 data 를 JSON 형태 그대로 렌더링을 해주죠.

Comments.js 도 확인해보세요.

import React, { Component } from 'react';
import axios from 'axios';

class Comments extends Component {
  state = {
    data: null
  }

  async initialize() {
    try {
      const response = await axios.get('https://jsonplaceholder.typicode.com/comments?postId=1');
      this.setState({
        data: response.data
      });
    } catch (e) {
      console.log(e);
    }
  }

  componentDidMount() {
    this.initialize();
  }


  render() {
    const { data } = this.state;

    if (!data) return null;

    return (
      <div>
        {JSON.stringify(data)}
      </div>
    );
  }
}


export default Comments;

그냥 거의 똑같습니다. 우선, 이렇게 반복되는 코드를 발견하는것이 첫번째 단추입니다.

HOC 작성하기

우리는 이 반복되는 코드를 없애기 위해서 하나의 함수를 작성합니다. 주로 HOC 의 이름을 만들땐 with_____ 형식으로 짓습니다. 예를들어, 우리는 웹요청을 하는 HOC 를 만들테니 withRequest 라고 지어주도록 하겠습니다.

HOC 의 원리는, 파라미터로 컴포넌트를 받아오고, 함수 내부에서 새 컴포넌트를 만든 다음에 해당 컴포넌트 안에서 파라미터로 받아온 컴포넌트를 렌더링하는 것입니다. 그리고, 자신이 받아온 props 들은 그대로 파라미터로 받아온 컴포넌트에게 다시 주입해주고, 필요에 따라 추가 props 도 넣어줍니다 (예를들어 우리의 경우엔 웹요청 결과물이 되겠죠)

우선 HOC의 틀을 작성해보겠습니다.

withRequest.js

import React, { Component } from 'react';

const withRequest = (url) => (WrappedComponent) => {
  return class extends Component {
    render() {
      return (
        <WrappedComponent {...this.props}/>
      )
    }
  }
}

export default withRequest;

위 코드를 보면, 함수에서 또 다른 함수를 리턴하도록 했습니다. (url, WrappedComponent) 형식이 아니라, (url) => (WrappedComponent) 로 한 이유는, 나중에 여러개의 HOC 를 합쳐서 사용하게 될 때 더욱 편하게 사용하기 위함입니다 – compose 같은 함수를 통하여 호출을 간소화 할 수 있죠.

그럼, HOC에 기능을 붙여볼까요?

import React, { Component } from 'react';
import axios from 'axios';

const withRequest = (url) => (WrappedComponen) => {
  return class extends Component {

    state = {
      data: null
    }

    async initialize() {
      try {
        const response = await axios.get(url);
        this.setState({
          data: response.data
        });
      } catch (e) {
        console.log(e);
      }
    }

    componentDidMount() {
      this.initialize();
    }

    render() {
      const { data } = this.state;
      return (
        <WrappedComponent {...this.props} data={data}/>
      )
    }
  }
}

export default withRequest;

axios 를 통하여 받은 data 를 파라미터로 받은 컴포넌트에 넣어주도록 설정을 했습니다.

HOC 사용하기

HOC 를 다 만들었다면, 사용하는 일만 남았습니다! Post 와 Comments 에서 한번 사용을 해보겠습니다.

Post.js

import React, { Component } from 'react';
import withRequest from './withRequest';

class Post extends Component {
  render() {
    const { data } = this.props;
    
    if (!data) return null;

    return (
      <div>
        { JSON.stringify(this.props.data) }    
      </div>
    );
  }
}


export default withRequest('https://jsonplaceholder.typicode.com/posts/1')(Post);

컴포넌트를 내보낼때 이렇게 사용하면 됩니다.

혹은,

const PostWithData = withRequest('https://jsonplaceholder.typicode.com/posts/1')(Post)
export default PostWithData;

의 형식으로 해도 되겠죠.

Comments 도 마찬가지로 withRequest 를 사용해줄까요?

Comments.js

import React, { Component } from 'react';
import withRequest from './withRequest';

class Comments extends Component {
  render() {
    const { data } = this.props;

    if (!data) return null;

    return (
      <div>
        {JSON.stringify(data)}
      </div>
    );
  }
}


export default withRequest('https://jsonplaceholder.typicode.com/comments?postId=1')(Comments);

결과물: https://codesandbox.io/s/134ypjk10l

정리

이렇게 HOC 를 사용함으로서, 반복되는 코드들이 많이 사라졌습니다. 이렇게 웹요청을 하는 외에도, LifeCycle 메소드를 붙여준다던지, Redux 에서 특정 값을 받아와서 주입해준다던지, 다국어지원을 한다던지 여러가지 일들을 할 수 있습니다.

recompose 라는 라이브러리는, 페이스북 개발자가 만든 유용한 HOC 컬렉션 라이브러리인데요, 활용하면 매우 쓸모있습니다. 종류는 굉장히 많은데, 이제 HOC 가 어떤 역할을 하는지 알았으니, recompose 의 API 문서 를 쭉 훑어보시면 어떤 것들을 할 수있는지 감을 잡으실 수 있을 것입니다.

  • 김은정

    안녕하세요 리액트 입문자입니다!
    해당 예제를 따라하고 있는데요 url을 바꾸고 싶은데 잘 되지 않네요
    예를 들어서 request를 공통으로 만들더라도 postId를 입력한 값으로 넣고 싶은데요
    props값으로 받아서 넣을수도없고.. 해서 질문을 드립니다. postid를 입력받은 값으로 수정하거나 url을 변경하려면 어떤 방식을 써야하나요?
    이 예제에서는 단순히 정해진 url만 활용가능한건가요?

  • 깅미정

    withRequest해서 받아온 결과물을 현재 state에 담으려면 어떻게 해야하나요..? 그리고 라이프사이클에 넣지않고 함수에 트리거하려면 어떻게 해야하나요?

    • Charles Kwoncheol Shin

      1. withRequest해서 받아온 결과물을 현재 state에 담으려면 어떻게 해야하나요..?
      -> withRequest에서 받아온 값을 State에 담고 싶다는 말씀은 그 값이 바뀔 수 있기 때문인 듯한데, 그 값이 바뀌기 위해서는 withRequest의 파라미터(위 예제에서는 url)가 바뀌어야 합니다.(아닌 경우도 있겠지만 거의 모든 경우에서 그럴 것입니다)
      그러므로 HOC를 사용하는 컴포넌트의 props 값이 자동으로 바뀔 것입니다. 즉, state를 사용할 필요가 없을 듯합니다.

      만약 컴포넌트의 다른 state에 따라서 그 값이 조금 변하길 원한다면 그냥 새로운 state를 생성하면 될 듯합니다!

      2. 라이프사이클에 넣지않고 함수에 트리거하려면 어떻게 해야하나요?
      -> 자동으로 함수가 실행되길 원하시는 거라면 리액트 컴포넌트 클래스에서는
      1) contructor에서 함수를 실행시키거나
      2) componentDidMount에서 실행시키는 방법이 있습니다.

      초기화 함수는 componentDidMount 에서 실행시키는 것을 권고하는 것 같습니다.

      혹시 제 답변중 잘못되거나 모호한 것이 있다면 피드백 부탁드립니다!

  • Hyunseo Kang

    아… 이게 Hoc이구나… 안그래도 계속 프로젝트 몇개 진행하면서 A container, B container, C container 를 만들면 A, B, C 에 중복되는 코드가 너무 많이 생겨서 아 왜이렇게 비생산적일까 생각이 들었는데 이렇게 처리하는군요… 역시 공부를 안하면 뒤쳐진다는 생각을 하며 또 배워갑니다.

    그리고 다른 라이브러리 쓰다보면 withTheme, withWidth, with 어쩌고 하는게 다 Hoc 이 었군요..

  • Hudi

    4번째 코드에서 const withRequest = (url) => (WrappedComponen) => {

    WrappedComponen”t”

    오타가 하나 있습니다!

  • yusung lee

    굳 입니다!

  • 정리가 잘 되어있네요 잘 배우고 갑니다!!

  • kiki

    안녕하세요! 리액트를 공부하는 학생입니다. HOC를 사용하게되면 컴포넌트가 업데이트가 되는 것이 아니라 계속 랜더링이 되는건가요? 저는 loading에 사용중인데 업데이트가 되는 것이 아니라 그냥 마운트가 되더라구요. 혹시 잘 알고계신분 답변 부탁드릴 수 있을까요?