React-5/1,2

하이어오더 컴포넌트

커링의 작동원리와 하이어오더 컴포넌트에 대한 개념을 배웁니다.
컴포넌트를 확장하여 코드를 재활용하려면 커링을 이해해야 하고,
하이어오더 컴포넌트를 구현하려면 데코레이터 패턴 을 사용해야 합니다.

05-1 커링과 조합 개념 공부하기

커링 개념은 반환 값이 함수인 디자인 패턴 을 뜻하며, 중복된 코드를 반복적으로 입력하지 않고
원하는 기능을 조합해 적재적소에 사용할 수 있다는 장점이 있습니다.

// 간단한 커링 예제
function mul(a,b) {
	return a * b;
}
function mulTwo(a) {
	return mul(a, 2);
}
// mulTwo 함수는 mul 함수를 재활용하여 a에 2를 곱한값을 반환합니다. 
// 이를 이용하여 n배 함수를 생성해주는 커링 함수를 만들수 있습니다
function mulX(x){
  return function(a){
    return mul(x,a);
  }
}
//화살표 함수로 변경
const mulX = x => a => mul(x,a);

//커링 함수의 활용 n배
const mulThree = mulX(3);
const mulFour = mulX(4);
//n배 함수의 활용
const result1 = mulThree(3); // 3 * 3 = 9 
const result2 = mulFour(3); // 3 * 4 = 12
//바로 사용또한 가능
const result1 = mulX(3)(3);

예제를 통해 사용법을 알았는데 의문이 하나 생깁니다 mul(3,3)을 한것과 다름이 없는데 굳이 사용해야 될까요? 해당 방식을 사용하게 된다면 인수를 한꺼번에 전달해야 하므로 mul(3)을 실행한 다음 다른 작업을 할 수 없습니다. 즉, 이 방법으로는 함수를 재활용하기 어렵습니다.
여기서 집중해야 될게 커링 함수의 특징인 인수를 나눠서 전달해도 실행 할수 있다는 점입니다.

함수조합 기법 알아보기

// Ex( x값에 따라 (x*2)*3)+4 를 해주는 커링함수
const formula = x => addFour(mulThree(mulTwo(x)));
// 함수의 적용순서가 오른쪽에서 왼쪽으로 가다보니 가독성이 떨어지고 이해하기 어렵다는 문제가 발생합니다.
// 실수로 수식을 읽는 순서대로 함수를 조합하여 잘못된 결과를 가져올 수도 있습니다.
const formulB = x => mulTwo(mulThree(addFour(x)));

compose() 함수를 사용하면 해당 문제를 해결할 수 있습니다


const formula = compose([mulTwo,mulThree,addFour]);
// compose 함수는 커링 기법으로 구현된 하이어오더 컴포넌트들을 다중으로 조합하여 컴포넌트에 적용할 때 간결하고 이해하기 쉽게 표현하는데 매우 유용하게 사용됩니다.
//compose 함수 만들어보기
[func1,func2,func3].reduce(
  function (prevFunc, nextFunc){
    return function(value){
      return nextFunc(prevFunc(value));
    }
  },
  function(k) {return k; }
  };

실무에서 사용하는 함수 조합 기법

  1. arguments를 사용하여 배열 인자 대신 나열형 인자로 함수 구조를 유연하게 만들기
    arguments 자바스크립트의 특수 변수로 함수안에서 전달된 모든 인자 목록을 배열과 유사한 나열형 자료로 저장해 둡니다.
function compose() {
  const funcArr = Array.prototype.slice.call(arguments);
  return funcArr.reduce(
    function (prevFunc, nextFunc) {
      return function (value) {
        return nextFunc(prevFunc(value));
      };
    },
    function (k) {
      return k;
    },
  );
}
const formulaWithCompose = compose(mulTwo, mulThree, addFour);
  1. arguments를 활용하여 하나의 실행 인자 value를 다중 인자로 사용 가능하게 확장하기
function compose() {
  const funcArr = Array.prototype.slice.call(arguments);
  return funcArr.reduce(
    function (prevFunc, nextFunc) {
      return function (value) {
        const args = Array.prototype.slice.call(arguments);
        return nextFunc(prevFunc.apply(null, args));
        // apply 함수는 인자에 전달된 배열을 전달받아 나열형 인자로 실행되도록 돕습니다.
        // prevFunc() 함수가 3개의 인자를 전달받는 구조(prevFunc(a,b,c)라면 args 변수에 [1,2,3]이 전달되면)
        // a,b,c,에 1,2,3 값을 할당할 것입니다. 
        // apply() 함수의 첫 번째 인자로 전달한 null은 함수에 포함된 this의 값을 정의합니다.
      };
    },
    function (k) {
      return k;
    },
  );
}
const formulaWithCompose = compose(mulTwo, mulThree, addFour);
  1. 전개 연산자로 더 간결하게 만들기

function compose(...funcArr) {
  return funcArr.reduce(
    function (prevFunc, nextFunc) {
      return function (...args) {
        return nextFunc(prevFunc(...args));
      };
    },
    function (k) {
      return k;
    },
  );
}
const formulaWithCompose = compose(mulTwo, mulThree, addFour);

05-2 하이어오더 컴포넌트 기초 개념 공부하기

하이어오더 컴포넌트를 알아보기전 디자인 패턴에 대해 알아봅시다
디자인 패턴은 코드 중 활용도가 높은 구현 방식을 모아둔 비밀 레시피와 같습니다. 여러분이 작성한 코드의 구조를 더 견고히 하고, 손쉽게 재활용 할 수 있게 해줍니다. 앞에서 공부한 커링도 디자인 패턴의 일종입니다. 리액트 컴포넌트에도 디자인 패턴을 적용할 수 있습니다.이번에 알아볼 디자인 패턴은 데코레이터 패턴이며 이후 데코레이터 패턴을 적용하여 하이어오더 컴포넌트까지 개념을 확장 시켜보도록 하겠습니다.

상속 패턴보다 데코레이터 패턴이 필요한 이유

데코레이터 패턴을 이해하려면 상속 패턴과 상속 패턴의 단점을 알아야 합니다.
상속 패턴 : 공통 기능은 부모에게 물려받고 추가 기능만 구현하여 중복 코드 양을 줄일 수 있다.

Button 기능을 상속 받는 경우 Loading,Tooltip,Submit 모두다 다른 클래스지만 공통된 Button 이란 기능만 상속 받고 나머지는 추가 기능만 구현

단점
만약 전송 버튼에 로딩 상태를 표시하는 기능을 넣고 싶을 경우 기존 상속 구조를 그대로 사용하면 되나, 추가 설명까지 넣고 싶어 TooltipButton까지 넣게 된다면 복잡해진다.

  • 추가 설명 표시 버튼이 강제로 로딩 버튼의 기능을 물려 받았습니다.(원치않은 상속)
  • 추가 설명 표시 버튼을 상속한 버튼들이 있다면 이 버튼들의 상속 구조 역시 변경됩니다.
  • 상속 구조가 깊기 때문에 상속 항목을 하눈에 파악하기 어렵습니다.

데코레이터 패턴

데코레이터 패턴 은 클래스 간의 종속성 없이 기능만을 공유합니다.

사진을 보면 알 수 있듯 기능자체만을 구현하여 각각의 독립적인 객체를 생성한것을 볼 수 있습니다. 자바 언어에서는 데코레이터 패턴을 구현하기 위해 인터페이스를 사용하고 자바스크립트의 경우 커링을 사용합니다.

하이어오더 컴포넌트의 개념

하이어오더 컴포넌트 라는 이름은 자바스크립트의 고차 함수(Higher-order function)에서 유래되었습니다. 자바스크립트에서 커링 함수를 고차 함수라고 부릅니다.

1. 하이어오더 컴포넌트는 함수나 클래스 형태의 컴포넌트를 모두 반환할 수 있습니다.
하이어오더 컴포넌트는 기존 컴포넌트에 기능을 덧입혀 새 컴포넌트로 반환하는 함수를 말합니다. 비유를 하자면 게임 캐릭터에 장비(기능)을 착용하는 것과 비슷합니다.

	// 함수형 컴포넌트를 반환하는 하이어오더
	function higherOrderComponent(Component) {
      return function Enhanced(props){
        	return <Component {...props} />;
      }
    }
	//class 형 컴포넌트 반환하는 하이어오더
	function higherOrderComponent(Component) {
      return class Enhanced extends React.Component {
		render() {
          	return <Component { ...this.props} />;
        }
      }
      //확장 컴포넌트의 형태는 개발자가 직접 지정하면 됩니다.
      // 생명주기 함수를 확장한 하이어오더 컴포넌트를 구성해야되는 경우 클래스형

2. 하이어오더 컴포넌트는 기존 컴포넌트에 연결된 프로퍼티를 모두 전달해 주어야 합니다.
기존 컴포넌트에서 추가 확장한 것이 하이어오더 컴포넌트 이기에 연결된 프로퍼티는 모두 전달 해주어야 합니다.
3. 하이어오더 컴포넌트와 확장 컴포넌트의 이름은 with로 시작합니다.
리액트 개발자들이 암묵적으로 하이어오더 컴포넌트와 확장 컴포넌트의 이름에 with를 붙입니다.

하이어오더 컴포넌트 간단하게 살펴보기

  1. 리액트 크롬 확장 플러그인 설치하기

    크롬 웹 스토어에 React Developer Tools 검색하여 확장프로그램 추가

  2. Components 탭 확인하기

    개발자도구에 [Component]탭을 누르면 리액트의 구조가 나타납니다. 이 탭은 성능 분석을 위해 사용합니다

  3. 하이어오더 컴포넌트 구현하기

	./src/05/withHoc.jsx
	//하이어오더 컴포넌트 기본 뼈대입니다.
	import React from 'react';
	
	export default function withHoC(WrappedComponent) {
      return class WithHoC extends React.Component {
        render() {
			return <WrappedComponent {...this.props} />;
        }
      }
    }
  1. 확장 컴포넌트 생성하기
import React from 'react';
import { storiesOf } from '@storybook/react';

import Button from '../04/Button';
import Text from '../04/Text';
import withHoC from '../05/withHoC';

const ButtonWithHoc = withHoC(Button);
const TextWithHoc = withHoC(Text);

storiesOf('WithHoC', module)
  .addWithJSX('기본 설정', () => (
    <div>
      <ButtonWithHoc>안녕하세요</ButtonWithHoc>
      <TextWithHoc>안녕하세요</TextWithHoc>
    </div>
  ))
  .addWithJSX('Large 예제', () => (
    <div>
      <ButtonWithHoc large>안녕하세요</ButtonWithHoc>
      <TextWithHoc large>안녕하세요</TextWithHoc>
    </div>
  ));

해당 스토리북을 실행하고 개발자도구로 보면 두개의 컴포넌트는 withHoC 하이어오더 컴포넌트로 나오게 된다. 둘다 동일한 하이어오더 컴포넌트를 사용했기 때문.

확장 컴포넌트가 기존 컴포넌트 이름 함께 출력하기

import React from 'react';

export default function withHoC(WrappedComponent) {
  return class WithHoC extends React.Component {
    static displayName = `withHoC(${WrappedComponent.name})`;
  //Button 개발자도구 -> withHoC -> Withstyles[withHoC] -> Button[withStyles] 
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

withHoC -> withStyles -> Button 순으로 감싸고 있는 형태라 개발자도구 Component에서도 똑같이 출력됩니다. 그럼 Button[withHoC]로 나타내기 위해서는 어떻게 해야될까요? 기존 컴포넌트의 displayName을 참조해야 합니다.

import React from 'react';

export default function withHoC(WrappedComponent) {
  const { displayName, name } = WrappedComponent;
  const wrappedComponentName = displayName || name;
  return class WithHoC extends React.Component {
    static displayName = `withHoC(${WrappedComponent.wrappedComponentName})`;
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}
// Button 개발자도구 -> withHoC -> Button[withHoC] -> Button[withStyles]

하이어오더 컴포넌트 기능 구현

실무에서 주로 사용하는 로딩 상태 표시 컴포넌트를 구현 해보겠습니다.
로딩 상태 표시 화면은 화면 표시 기능만 하면 되므로 클래스형 컴포넌트가 아닌 함수형 컴포넌트로 반환하도록 하겠습니다.

./src/05/withLoading

import React from 'react';

export default function withLoading(WrappedComponent) {
  const { displayName, name: componentName } = WrappedComponent;
  const wrappedComponentName = displayName || name;

  function WithLoading(props) {
    if (props.isLoading) {
      return '로딩 중';
    }

    return <WrappedComponent {...props} />;
  }

  WithLoading.displayName = `withLoading(${wrappedComponentName})`;
  return WithLoading;
}

isLoading 은 로딩 표시 기능을 추가할 때만 필요합니다. 즉 ,확장 컴포넌트에는 필요하지 않으므로 구조 할당 표현식으로 isLoading을 제외하여 확장 컴포넌트에 전달합니다.

import React from 'react';

export default function withLoading(WrappedComponent) {
  const { displayName, name: componentName } = WrappedComponent;
  const wrappedComponentName = displayName || componentName;

  function WithLoading({ isLoading, ...otherProps }) {
    if (isLoading) {
      return '로딩 중';
    }

    return <WrappedComponent {...otherProps} />;
  }

  WithLoading.displayName = `withLoading(${wrappedComponentName})`;
  return WithLoading;
}

로딩중 외에도 다양한 메시지를 보내고 싶을땐 어떻게 하면 될까요?
하이어오더 컴포넌트의 loadingMessage라는 매개변수를 추가하여 로딩 메시지를 전달받는 방법을 사용할 수 있습니다.

import React from 'react';

export default function withLoading(WrappedComponent, loadingMessage = '로딩 중') {
  const { displayName, name: componentName } = WrappedComponent;
  const wrappedComponentName = displayName || componentName;

  function WithLoading({ isLoading, ...otherProps }) {
    if (isLoading) {
      return loadingMessage;
    }

    return <WrappedComponent {...otherProps} />;
  }

  WithLoading.displayName = `withLoading(${wrappedComponentName})`;
  return WithLoading;
}

매개변수를 추가하였는데 하이어오더에는 암묵적으로 컴포넌트 인자 하나만 전달한다
라는 규칙이 있습니다. 규칙을 어기지 않고 메시지를 받을수 있는 방법이 없을까요?


import React from 'react';

export default function (loadingMessage = '로딩 중') {
  return function withLoading(WrappedComponent) {
    const { displayName, name: componentName } = WrappedComponent;
    const wrappedComponentName = displayName || componentName;

    function WithLoading({ isLoading, ...otherProps }) {
      if (isLoading) {
        return loadingMessage;
      }

      return <WrappedComponent {...otherProps} />;
    }

    WithLoading.displayName = `withLoading(${wrappedComponentName})`;
    return WithLoading;
  };
}

다중 커링을 이용하면 규칙에 위배 되지 않고도 메시지를 전달받는게 가능합니다.
스토리북에 추가하여 서로 다른 로딩 메시지 출력 확인

./src/stories/WithLoadingStory.jsx
// 
import React from 'react';
import { storiesOf } from '@storybook/react';

import Button from '../04/Button';
import Text from '../04/Text';
import withLoading from '../05/withLoading';

const TextWithLoading = withLoading('로딩 중')(Text);
const ButtonWithLoading = withLoading(<Button disabled>로딩 중...</Button>)(Button);

storiesOf('WithLoading', module)
  .addWithJSX('기본 설정', () => (
    <div>
      <ButtonWithLoading>안녕하세요</ButtonWithLoading>
      <TextWithLoading>안녕하세요</TextWithLoading>
    </div>
  ))
  .addWithJSX('isLoading 예제', () => (
    <div>
      <ButtonWithLoading isLoading>안녕하세요</ButtonWithLoading>
      <TextWithLoading isLoading>안녕하세요</TextWithLoading>
    </div>
  ));

좋은 웹페이지 즐겨찾기