[React] 공통기능분리(1) - 고차컴포넌트(Higher-Order-Components)

권준혁·2020년 11월 1일
6

React

목록 보기
15/20
post-thumbnail
post-custom-banner

안녕하세요!
이번 포스팅은 공통기능을 관리하기 위해 사용하는 고차컴포넌트에 대한 포스팅입니다.

컴포넌트를 작성하다보면 반복적인 로직을 작성하는 경우가 생깁니다.
고차컴포넌트는 컴포넌트를 입력받아 어떤 기능을 수행 후 컴포넌트를 반환합니다.
반복적인 로직을 고차컴포넌트를 이용해 추상화하면 공통기능을 분리할 수 있습니다.

고차컴포넌트의 패턴에 대해서 한 번 살펴보겠습니다.

고차함수

다음 코드는 고차함수입니다.

const getLog = () => {console.log('원래기능 , 로그출력')}   // (1)
const higherFunc = (inputFunc) => {           // (2)
    console.log('higherFunc의 어떤기능 수행됨')
    return inputFunc
}
const higherOrder = higherFunc(getLog)
higherOrder()
  1. 먼저 getLog라는 함수가 있습니다. 단순하게 콘솔에 로그를 출력하는 함수입니다.
  2. 고차함수 higherFunc는 함수를 입력받아 "higherFunc의 어떤기능"을 수행하고 입력받은 함수를 리턴합니다.
  3. higherOrder에 higherFunc(getLog)를 초기화 한 뒤, higherOrder을 실행합니다.

결과

higherFunc의 어떤기능 수행됨
원래기능 , 로그출력

고차함수의 어떤 기능이 수행된 뒤, 원래기능인 로그출력이 실행됩니다.
higherFunc의 인수에 함수를 넣으면, 고차함수의 어떤 기능을 수행 한 뒤, 원래 함수의 기능을 수행합니다.


마운트 시 로그출력해주는 고차컴포넌트

이번엔
각 컴포넌트들이 마운트 될 때마다 콘솔에 로그를 출력해주는 고차컴포넌트를 작성해보겠습니다.
고차컴포넌트의 기능은 "콘솔에 로그출력"입니다.

// withMountEvent.js
import React from "react";
export default function withMountEvent(InputComponent, componentName) {
    return class OutputComponent extends React.Component {
        componentDidMount () {
            console.log(`${componentName} 컴포넌트가 마운트됨`)
        }
        render () {
            return (<InputComponent {...this.props}/>)
        }
    }
}

고차 컴포넌트를 만들었습니다.
이 함수는 class형 컴포넌트를 리턴합니다.
그리고 컴포넌트가 마운트됐을 때, 로그를 출력하는 기능을 수행하기위해 componentDidMount()생명 주기에서 로그를 출력하게 만들었습니다.

*중요한 점은 render함수내의 return부분에서 명시적으로 {...this.props} 를 전달해줘야 한다는 것입니다. * 여기서 this는 outputComponent를 가리킵니다. 이렇게 해야 추가적인 props가 들어왔을 때도 속성값들까지 동적으로 정상반환 됩니다.

그럼 고차컴포넌트에 inputComponent로 쓰일 컴포넌트를 만들어보겠습니다.

// MyComponent.js
import React from 'react'
import withMountEvent from "./withMountEvent";

class MyComponent extends React.Component {
    state = {
    }
    render () {
        return (
        <div><p>MyComponent입니다. {JSON.stringify(this.props)}</p></div>
        )
    }
}
export default withMountEvent(MyComponent,'MyComponent')

일반적인 컴포넌트와 같지만, withMountEvent라는 함수에 감싸 export합니다.
이렇게 하면 withMountEvent가 리턴하는 값이 export되게 됩니다.

// App.js
import React from 'react';
import MyComponent from "./HiherTest/MyComponent";
function App() {
  return (
    <div className="App">
      <MyComponent name='kwon' age='30' todo='sleeping'/>
    </div>
  );
}
export default App;

App.js에서의 MyComponent는 실제로는 withMountEvent함수가 리턴하는 OutputComponent입니다. OutputComponent는 componentDidMount가 실행되는 시점에 콘솔에 ${componentName} 컴포넌트가 마운트됨 이라는 메세지를 출력한 뒤, MyComponent를 반환하게 됩니다.

정리해보겠습니다.

  • MyComponent.js에서 withMountEvent함수로 감싼 뒤 export
  • App.js에서 사용하는 MyComponent는 실제로, withMountEvent가 반환하는 OutputComponent임, OutputComponent가 마운트 시 로그출력 기능 수행 후 MyComponent를 반환함
  • 마운트 시 로그출력 같은 공통로직들을 같은 방법으로 고차컴포넌트를 이용할 수 있음

이어서! 다른 예제를 한 번 더 해보겠습니다.

마운트 여부를 알려주는 고차컴포넌트

자주 쓰이는 패턴이라고 합니다.
withHasMounted.js를 만들고 고차함수를 만듭니다.

// withHasMounted.js
import React from 'react'
export default function withHasMounted (InputComponent) {
    return class OutputComponent extends React.Component {
        state = {
            hasMounted : false
        };
        componentDidMount () {
            this.setState({hasMounted:true});
        }
        render () {
            const {hasMounted} = this.state;
            return <InputComponent {...this.props} hasMounted={hasMounted}/>
        }
    }
}

hasMounted를 상태값으로 관리하고 아까와 마찬가지로 componentDidMount에서 hasMounted를 true로 변경합니다. 리턴하는 컴포넌트에 {...this.props}를 명시적으로 잘 적어 준 뒤, hasMounted라는 속성값 하나를 추가합니다.
마운트가 된 이후에는 hasMounted라는 속성값이 true입니다.
따라서 InputComponent에서는 마운트 됐는지 여부를, 속성값을 이용해 쉽게 알 수 있습니다.


로그인 여부에 따라 다르게 보여주는 고차컴포넌트

조건부 렌더링으로 구현할 수 있겠지만, 공통기능관리 라는 목적을 잃어버립니다.
로그인여부에 따라 input컴포넌트를 보여줄지 or 안보여줄지 를 결정할 수 있습니다.

// withOnlyLogin.js
export default function withOnlyLogin (InputComponent) {
    return function ({isLogin, ...rest}) {
        if(isLogin) {
            return <InputComponent {...rest}/>
        } else {
            return <p>권한이 없습니다.</p>
        }
    }
}

isLogin속성값에 따라 다르게 리턴합니다.
여기서 마찬가지로 주의할 점은 props를 제대로 전달해줘야 하는 것인데, Spread Syntax가 역시 유용합니다. isLogin을 제외한 나머지 props를 리턴하는 InputComponent에 전달합니다.
클래스형 컴포넌트를 리턴하는 경우에는 this.props로 접근이 가능했지만 함수형으로 가볍게 전달하기위해서는 함수의 인수에 ...rest로 매개변수들을 모두 사용할 수 있게 작성해야합니다.


클래스 상속을 이용한 고차컴포넌트

function withSomething(InputComponent) {
  return class OutputComponent extends InputComponent {
    //...
  }
}

이 전의 코드들에서는 render()내에서만 Input컴포넌트를 그대로 반환하는 코드들이었지만, InputComponent를 상속하는 컴포넌트를 반환하게 되면 InputComponent의 상태값, 속성값을 포함해, 생명주기들에도 접근할 수 있습니다.

다음 코드는 디버깅시에 활용하는 예 입니다.

디버깅에 사용

export default function withDebug (InputComponent) {
    return class OutputComponent extends InputComponent {
        render () {
            return (
                <React.Fragment>
                    <p>{`props : ${JSON.stringify(this.props)}`}</p>
                    <p>{`state : ${JSON.stringify(this.state)}`}</p>
                    {super.render()}   {/*InputComponent의 render호출*/}
                </React.Fragment>
            )
        }
    }
}

InputComponent를 상속하고있기 때문에 this로 상태값과 속성값에 접근이 가능합니다.
그리고 super.render()로 InputComponent의 render함수에 접근이 가능합니다.
생명주기인 render에 접근이 가능하기 때문에, 마찬가지로 다른 생명주기들에도 접근이 가능하지만! 다른 생명주기들을 호출하는 것은 금지입니다.


여러개의 고차 컴포넌트 사용

고차함수와 마찬가지로 여러 고차컴포넌트를 사용 가능합니다.

withMountEvent(withDebug(MyComponent));

고차컴포넌트를 사용하기 위한 컨벤션

고차컴포넌트를 너무 사용하다보면 렌더링성능에 좋지않고 리액트 개발자툴에서 디버깅시에도 불편해집니다. 그리고 정적메서드는 출력되는 컴포넌트에 전달되지 않습니다.
이런 문제점을 일부 해결하기 위해 정해진 컨벤션(관례)들이 있다고 합니다.

  • displayName을 설정해주면 리액트 개발자툴에서 디버깅이 편해진다.
  • 정적메서드를 전달하기 위해 hoist-non-react-statics 패키지를 사용한다.

displayName설정하기

recompose패키지를 이용합니다.

npm i recompose
// withSomething.js
import getDisplayName from 'recompose/getDisplayName';

export default function withSomething (InputComponent) {
  class OutputComponent extends React.Component {
     // ...
  }

  OutputComponent.displayName = 
  `withSomething(${ getDisplayName(InputComponent) })`;
  return OutputComponent;
}

정리하자면 , 출력되는 OutputComponent의 displayName이란 속성값에 getDisplayName함수를 이용해 입력을 해주면 개발자툴에 보기좋게 출력된다는 것 입니다.
그러면 확인해보겠습니다.

(1/2) X recompose패키지 미사용

(2/2) O recompose패키지 사용

리액트 개발자툴에서 확연히 보기 쉬워졌습니다!


이번엔 정적메서드를 전달하는 방법, hoist-non-react-statics패키지를 사용해보겠습니다.

설치

npm i hoist-non-react-statics

작성

// withSomething.js
import getDisplayName from 'recompose/getDisplayName';
import hoistNonReactStatic from 'hoist-non-react-statics';

export default function withSomething (InputComponent) {
  class OutputComponent extends React.Component {
     // ...
  }

  OutputComponent.displayName = 
  `withSomething(${ getDisplayName(InputComponent) })`; // displayName설정

  hoistNonReactStatic(OutputComponent,InputComponent); // 정적메서드 전달
  return OutputComponent;
}

hoistNonReactStatic메서드가 InputComponent의 모든 정적메서드를 OutputComponent로 전달해줍니다.

책 실전 리액트 프로그래밍을 참고해 작성했습니다.


이상으로 포스팅 마치겠습니다!
읽어주셔서 감사합니다.

profile
웹 프론트엔드, RN앱 개발자입니다.
post-custom-banner

1개의 댓글

comment-user-thumbnail
2021년 12월 2일

기가막히네요

답글 달기