Component 재사용 방법


:raising_hand: 프론트엔드 개발관련 공부내용을 기록하는 포스트 입니다.


1. 개요


ReactComponent를 설계할때 많이 사용하는 재사용에 대해서 알아보자.

공부를 하던 중 관련하여 기억할만한 부분이 있어 한번 포스팅해본다.

먼저 React에서 컴포넌트를 설계할때 방법들 중 아래의 2가지를 살펴보겠다.

  1. Class 상속
  2. props 를 활용한 컴포넌트 담기

결과적으로 React공식 문서에서 아래와 같이 Class 상속으로 인한 재사용은 권장하지 않는다는 글이 있다.

Facebook에서는 수천 개의 React 컴포넌트를 사용하지만,
컴포넌트를 상속 계층 구조로 작성을 권장할만한 사례를 아직 찾지 못했습니다.

props와 합성은 명시적이고 안전한 방법으로 컴포넌트의 모양과 동작을 커스터마이징하는데
필요한 모든 유연성을 제공합니다.
컴포넌트가 원시 타입의 값, React 엘리먼트 혹은 함수 등 어떠한 props도 받을 수 있다는 것을 기억하세요.

UI가 아닌 기능을 여러 컴포넌트에서 재사용하기를 원한다면,
별도의 JavaScript 모듈로 분리하는 것이 좋습니다.
컴포넌트에서 해당 함수, 객체, 클래스 등을 import 하여 사용할 수 있습니다.
상속받을 필요 없이 말이죠.

- Reactjs.org -

기억해야 하는 부분은 구현부분 보다 어떤 방식으로 설계를 하는지만 간단하게 기록해놓도록 하자.

먼저 예시로 작성하게 될 기능은 아래 그림처럼 추천 혹은 최근 검색어를 클릭했을때 데이터를 가져와 리스트업 하는 부분이다.

image

이부분을 구현하기 위해 세가지 Component가 생성된다.

  1. List.js (목록을 리스트하는 주체)
  2. KeywordList.js (List.js 를 상속받아 추천 검색어 리스트를 완성)
  3. HistoryList.js (List.js 를 상속받아 추천 검색어 리스트를 완성)

이렇게 3가지로 예시로 사용한다.

2. 상속


// List.js
class List extends React.Component {
  constructor() {
    super();

    this.state = { data: [] };
  }

  renderItem(item, index) {
    throw "renderItem()을 구현하세요";
  }

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

    return (
      <ul className="list">
        {data.map((item, index) => (
          // 5
          <li key={item.id} onClick={() => onClick(item.keyword)}>
            {this.renderItem(item, index)}
          </li>
        ))}
      </ul>
    );
  }
}

// KeywordList.js

class KeywordList extends List {
  componentDidMount() {
    const data = store.getKeywordList();
    this.setState({ data });
  }

  renderItem(item, index) {
    return (
      <>
        <span className="number">{index + 1}</span>
        <span>{item.keyword}</span>
      </>
    );
  }
}
  1. List.jsReact.component Class 를 상속받아 생성자로 상태 값을 선언
  2. render()함수에서 실제 Element를 리턴하며, 그 안에서 renderItem() 함수를 호출한다.
  3. renderItem은 추상 메서드로 작성하고 상속하는 주체에서 구현하여 사용한다.

이제 이 클레스를 사용할 KeywordList.js 를 살펴본다.

  1. 마운트 직전 실행되는 HookcomponentDidMount()에서 추천검색어 리스트를 가져와 상태값에 업데이트 해준다.
  2. List.js 함수를 상속받아 renderItem 함수를 실제구현한다. 이 함수에서 리스트에 정의될 li태그 Element를 리턴한다.

이러한 방식으로 상속을 통해 Component 간 재사용을 구현하는 학습을 진행하였다.

이제 비교대상인 props를 통해 어떻게 변경될지 확인해보자.

3.Props 를 이용한 컴포넌트 재사용


3-1. 컴포넌트 담기

이 방식은 주체가 List.js였던 상속과는 달리 KeywordList에서 List.js의 컴포넌트에 Props를 통해 데이터를 넘겨주고 그에 따라 렌더링을 하는 방식이다.

// KeywordList.js

class KeywordList extends React.Component {
  constructor() {
    super();
    this.state = { keywordList: [] };
  }

  componentDidMount() {
    const keywordList = store.getKeywordList();
    this.setState({ keywordList });
  }

  render() {
    const { onClick } = this.props;
    const { keywordList } = this.state;

    return (
      <List
        data={keywordList}
        onClick={onClick}
        renderItem={(item, index) => (
          <>
            <span className="number">{index + 1}</span>
            <span>{item.keyword}</span>
          </>
        )}
      />
    );
  }
}

// List.js

const List = ({ data = [], onClick, renderItem }) => {
  return (
    <ul className="list">
      {data.map((item, index) => (
        <li key={item.id} onClick={() => onClick(item.keyword)}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
};

위 코드를 보면 KeywordList에서 이전 List.js의 코드와 비슷한걸 볼 수 있다.

  1. 먼저 React.component를 상속받고 생성자를 통해 상태값을 정의한다.
  2. 마운트 훅에서 데이터를 가져오고 상태를 업데이트한다.
  3. List 컴포넌트에 data, onClick, renderItemProps 데이터를 넘겨주어 List.js에서 완성된 Element를 리턴하게 된다.

위 코드를 보면 List.js는 클레스 컴포넌트가 아닌 함수 컴포넌트로 변경하였다. 실제 state 를 변경할 필요가 없어 함수 컴포넌트를 사용하였다.

위와 같은 방식으로 HistoryList 컴포넌트에서도 동일하게 List 컴포넌트에 Props 데이터를 넘겨 Element를 완성할 수 있다.

3-2. 특수화

이 방식은 props 를 통해 Flag 값을 넘겨 동일한 Component를 재사용 하는 방식이다.

예를 들면 위의 코드를 아래와 같이 작성한다고 생각해보자.

// KeywordList.js

class KeywordList extends React.Component {
  render() {
    const { onClick } = this.props;
    const { keywordList } = this.state;

    return <List hasIndex data={keywordList} onClick={onClick} />;
  }
}

// HistoryList.js

class HistoryList extends React.Component {
  render() {
    const { onClick } = this.props;
    const { historyList } = this.state;

    return (
      <List
        hasDate
        data={historyList}
        onClick={onClick}
        onRemove={(keyword) => this.handleRemove(keyword)}
      />
    );
  }
}

// List.js

const List = ({
  data = [],
  hasIndex = false,
  hasDate = false,
  onClick,
  onRemove,
}) => {
  const handleClickRemove = (event, keyword) => {
    event.stopPropagation();
    onRemove(keyword);
  };

  // 2
  return (
    <ul className="list">
      {data.map(({ id, keyword, date }, index) => (
        <li key={id} onClick={() => onClick(keyword)}>
          {/* 3 */}
          {hasIndex && <span className="number">{index + 1}</span>}
          <span>{keyword}</span>
          {hasDate && <span className="date">{formatRelativeDate(date)}</span>}
          {/* 4 */}
          {!!onRemove && (
            <button
              className="btn-remove"
              onClick={(event) => handleClickRemove(event, keyword)}
            />
          )}
        </li>
      ))}
    </ul>
  );
};

3-1의 컴포넌트 담기 방법과는 다르게 hasIndex, hasDate를 통해 Flag 값을 재사용하는 Component(List.js)에 넘겨준다.

List.js에서는 해당 값을 보고 Element를 다르게 Return 하여 주는 방식이라고 기억하면 될 것같다.

이외에도 여러가지 설계하는 방법이 있겠지만 아직까지는 정확히 감이 오지 않아 일단 이정도로 컴포넌트 설계부분은 포스팅을 마무리 지어야겠다.

참고 사이트


  1. https://jeonghwan-kim.github.io/series/2021/04/15/lecture-react-component.html#state-%EB%81%8C%EC%96%B4-%EC%98%AC%EB%A6%AC%EA%B8%B0

  2. https://ko.reactjs.org/docs/composition-vs-inheritance.html