본문 바로가기
개발/react

React 코드 재사용(composition과 고차 컴포넌트, custom hook)

by amkorousagi 2021. 3. 28.

React의 코드 재사용(또는 캡슐화) 방법에 대해 알아봅시다.

 

react code reuse
합성, 고차 컴포넌트, 커스텀 훅

 

코드 재사용이 필요한 이유

너무나도 고전적인 주제지만 그래도 모든 기술은 어떤 문제의 solution으로 나온 것이니,

짚고 넘어가도록 하겠습니다.

 

코드 재사용은 크게 다음 두 가지의 장점이 있습니다.

  • 구현할 때, 시간과 리소스를 절약
  • 중복성을 줄임

같은 코드를 두 번 치지 않음으로 시간과 리소스를 절약하는 것은 당연합니다.

 

그렇다면 중복성을 줄이는 것에서 오는 이점은 무엇일까요?

만약, 재사용하는 코드를 update를 하면, 해당 코드를 재사용하는 모든 곳에서의 해당 update가 적용됩니다.

이런 일관성은 프로그램의 유지/보수를 쉽게 합니다.

 

React에서 코드 재사용을 위한 기술은 다음과 같이 여러 가지가 있습니다.

Component

React의 Component는 그 자체로 다른 component에서 import 되어 재사용될 수 있습니다.

또는 고차원 함수(hooks에서는 customize hook)의 형태로 state 관리 로직으로 그대로 재사용할 수 있습니다.

아니면 합성을 통해서도 쉽게 재사용될 수 있죠.

차례차례 알아보도록 합시다.

 

Component 합성(composition)

React는 강력한 합성 모델을 가지고 있으며, 상속 대신 합성을 사용하여 컴포넌트 간에 코드를 재사용하는 것이 좋습니다.
- ko.react.org -

React에서는 Component를 import 하는 것만이 아니라 import 한 Component를 Composition(합성)할 수 있습니다.

 

합성이라고는 해서 복잡한 문법이 필요한 것이 아닙니다.

그저 props로 전달되는 다른 component를 함께 렌더링 하여 return 하는 것뿐이죠.

다음과 같이 일반적인 props 전달 방법과 같습니다.

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

 

다만 컴포넌트의 경우 JSX 특성상 다음과 같이도 props로 컴포넌트를 전달해줄 수 있습니다.

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}
function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

<FancyBorder> JSX 태그로 감싸져 있는 모든 component 들은 props.child에 담겨서 전달됩니다.

 

이렇게 간단하게 컴포넌트 간의 Composition을 구현할 수 있는 이유는 React가 선언적이며 함수형 프로그래밍이기 때문입니다.

각 Component들은 그 자체로 VDOM node를 생성합니다.

각 Component들을 쓰기 위해서 class처럼 new를 쓰거나 할 필요가 없다는 의미죠.

해당 Component를 호출하는 것 만으로 온전한 역할을 다합니다.

 

이렇듯 React는 일반적인 OOP에 비해 props로 간편하게 합성을 구현할 수 있습니다.

 

고차 Component

고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술입니다. 고차 컴포넌트(HOC)는 React API의 일부가 아니며, 리액트의 구성적 특성에서 나오는 패턴입니다.

구체적으로, 
고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다.

컴포넌트는 props를 UI로 변환하는 반면에, 고차 컴포넌트는 컴포넌트를 새로운 컴포넌트로 변환합니다.

- ko.react.org -

우리는 고차 Component를 통해 컴포넌트 로직을 재사용할 수 있습니다.

이것은 React Hooks의 custom Hook이 컴포넌트 로직을 재사용하는 것과 용도가 같습니다.

 

고차 컴포넌트는 입력된 컴포넌트를 수정하지 않으며 상속을 사용하여 동작을 복사하지도 않습니다. 오히려 고차 컴포넌트는 원본 컴포넌트를 컨테이너 컴포넌트로 포장(Wrapping)하여 조합(compose)합니다. 고차 컴포넌트는 사이드 이펙트가 전혀 없는 순수 함수입니다.
- ko.react.org -

고차 Component를 사용하는 방법 역시 매우 간단합니다!

그저 Component를 반환하는 JS 함수를 마음대로 선언하면 됩니다!

 

다음 코드 고차 컴포넌트를 정의하는 코드입니다.

// 이 함수는 컴포넌트를 매개변수로 받고..
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} />;
    }
  };
}

위 코드와 같이 component 로직을 재사용할 수 있도록,

Component를 반환하는 JS 함수가 고차 컴포넌트 그 자체입니다!

 

위 코드는 selectData에 대한 컴포넌트 로직을 설정하고, props로 받은 컴포넌트를 반환하는 컴포넌트를 반환합니다.

일반 JS 함수이므로 원하는 만큼 인수를 추가하거나 변경할 수 있습니다.

 

앞서 말했듯이 고차 컴포넌트는 컴포넌트 로직을 재사용할 수 있도록 추상화할 수 있다는 이점이 있습니다.

 

또, 다음과 같이 정의한 고차 컴포넌트를 사용합니다.

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

 

Custom Hook

자신만의 Hook을 만들면 컴포넌트 로직을 함수로 뽑아내어 재사용할 수 있습니다.
- ko.react.org -

우리는 Custom Hook을 통해 컴포넌트 로직을 재사용할 수 있습니다.

이것은 고차 컴포넌트가 컴포넌트 로직을 재사용하는 것과 같은 용도입니다.

custom hook은 단지, Hook에 대한 컴포넌트 로직 추상화 방법일 뿐입니다.

 

두 개의 자바스크립트 함수에서 같은 로직을 공유하고자 할 때는 또 다른 함수로 분리합니다. 컴포넌트와 Hook 또한 함수이기 때문에 같은 방법을 사용할 수 있습니다!

사용자 정의 Hook은 이름이 use로 시작하는 자바스크립트 함수입니다. 사용자 Hook은 다른 Hook을 호출할 수 있습니다.

- ko.react.org -

 

Custom hook을 사용하는 방법 역시 믿을 수 없이 간단합니다!

그저 특정 state를 반환하는 JS 함수를 기술하면 됩니다.

 

다음의 코드는 custom hook을 정의하는 코드입니다.

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

useState를 통해 hook을 정의하고,

useEffect를 통해 lifecycle에서 state의 변화를 기술하고 있습니다.

그리고 state를 반환합니다.

 

그런데, 이런 custom hook을 사용하기 위해서는 다음의 규칙을 따라야 합니다.

  • custom hook은 조건부 함수이여서는 안됩니다.
  • 이름은 반드시 use로 시작해야 합니다, 그렇지 않으면 hooks 규칙 위반 여부를 자동으로 체크하는 기능을 사용하지 못합니다.
  • 일반적인 Component와 동일하게 useState(또는 다른 hooks)들은 custom hook의 가장 처음에 호출되어야 합니다.

정의한 custom hook은 다음과 같이 사용합니다.

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

 

 

다음은 원래 (custom hook 이 쓰이지 않은) 예시 코드입니다.

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

 

위 두 코드는 완전히 동일하게 동작합니다.

사용자 정의 Hook은 React의 특별한 기능이라기보다 기본적으로 Hook의 디자인을 따르는 관습입니다.
- ko.react.org -

그저 공통의 코드를 뽑아 새로운 함수로 만든 것뿐이지, custom hook은 특별한 기능이 아님을 React에서는 강조합니다.

일반 JS 함수 사이에서의 공통 코드 추상화와 동일합니다.

 

같은 hook을 사용하는 두 개의 컴포넌트는 state를 공유하지 않습니다. 완전히 독립된 state를 얻습니다.

React 입장에서는 custom hook을 호출하는 컴포넌트는,

custom hook 안의 useState와 useEffect를 직접 호출하는 것과 다름이 없습니다.

 

그렇다고 custom hook 후에 useEffect를 호출하는 것을 두려워하지 마세요.

하나의 컴포넌트 안에서 useState와 useEffect를 여러 번 부를 수 있고 이들은 모두 완전히 독립적입니다.
- ko.react.org -

위 설명과 같이 우리는 useState와 useEffect를 여러 번 부를 수 있습니다.

 

또 그저 JS 함수이기 때문에 다음 코드와 같이 hook에서 hook으로 정보를 전달할 수 있습니다.

  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);

 

 

 

 

참고한 사이트:

 

자신만의 Hook 만들기 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

합성 (Composition) vs 상속 (Inheritance) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

'개발 > react' 카테고리의 다른 글

react build  (0) 2021.03.29
react fragment  (0) 2021.03.29
React Virtual DOM  (0) 2021.03.28
props vs. state 2 (그리고 this)  (0) 2021.03.27
Props vs. State  (0) 2021.03.25

댓글