React Element 복제 & 변경하여 사용하기

cloneElement 함수 사용법


들어가며

React 환경에서 컴포넌트 설계할 때 Tab, Accordion 유형은 부모, 자식의 컴포넌트 구성으로 부모 컴포넌트가 자식 컴포넌트로 props의 값이 동적으로 넣어줘야 하는 경우가 있습니다. React Element를 조작하는 API인 cloneElement 이용하면 props를 수정하여 전달할 수 있습니다.


CloneElement 개념

선택한 요소(element)를 복사하여 새로운 객체를 반환해줄 때, 요소 고유의 key나 ref 이외에 새롭게 정의한 속성(config)을 전달하여 생성할 수 있습니다.

React.cloneElement(element, [config], [...children]);

CloneElement을 이용한 Tab 컴포넌트

탭을 구성하기 위한 조건은 다음과 같습니다.

  • 탭 버튼 : id, aria-controls 속성, 탭 패널 : id, aria-labelledby 속성 매칭
  • 탭 초기 활성화 상태
  • 탭 버튼 클릭 시, 탭 패널 활성화 여부 변경

탭 버튼과 패널의 속성값을 매칭할 때 각 컴포넌트에 부여하지 않고 최상의 컴포넌트인 <TabWrap>에 고유 아이디를 추가하고 <TabControlWrap>, <TabPanelWrap /> 값을 필요한 접두사, 접미사를 추가하여 전달해 줄 수 있습니다.

탭 초기 활성화와 탭 버튼 클릭에 따른 활성화 상태도 최상의 컴포넌트 <TabWrap>에서 전달하면 편리하게 사용할 수 있습니다.

이처럼 부모 컴포넌트에서 일괄적으로 관리 하면 코드를 조금 더 간결하게 작성하여 사용성도 높이고 오타나 수정에 따른 불필요한 시간을 절약할 수도 있습니다.

// Before
<TabWrap>
  <TabControlWrap id="tab-default-control" ariaControls="tab-default-panel" active={0} />
  <TabPanelWrap id="tab-default-panel" ariaLabelledby="tab-default-control" active={0} />
</TabWrap>
// After

<TabWrap id="tab-default" active={0}>
  <TabControlWrap />
  <TabPanelWrap />
</TabWrap>

<TabControlWrap>, <TabPanelWrap /> 컴포넌트에는 부모로부터 받은 id에 탭 버튼에는 “control”, 탭 패널에는 “panel” 텍스트를 값을 추가하여 매칭해 줄 수 있습니다.

// 컴포넌트 상세 내용
const TabWrap = ({ id, active, children }) => {
  const { cloneElement } = React;
  const tabChildren = children.map((el, index) => {
    return cloneElement(el, {
      key: `tab-default0${index + 1}`,
      id,
      active,
    });
  });

  return <div className="tab-wrap">{tabChildren}</div>;
};

const TabControlWrap = ({ id, active }) => {
  return (
    <div className="tab-control-wrap">
      <button
        type="button"
        id={`${id}-control-01`}
        role="tab"
        aria-selected={active === 0}
        aria-controls={`${id}-panel-01`}
        className="tab-control"
      >
        Tab Control 1
      </button>
      <button
        type="button"
        id={`${id}-control-02`}
        role="tab"
        aria-selected={active === 1}
        aria-controls={`${id}-panel-02`}
        className="tab-control"
      >
        Tab Control 2
      </button>
    </div>
  );
};

const TabPanelWrap = ({ id, active, children }) => {
  const { cloneElement } = React;
  const panel = children.map((el, index) => {
    return cloneElement(el, {
      key: index,
      id: `${id}-panel-0${index + 1}`,
      label: `${id}-control-0${index + 1}`,
      activeStatus: active === index,
    });
  });
  return (
    <div className="tab-panel-wrap">
      <div
        className="tab-panel"
        role="tabpanel"
        id={`${id}-panel-01`}
        aria-labelledby={`${id}-control-01`}
        hidden={active !== 0}
      >
        Tab Panel 1
      </div>
      <div
        className="tab-panel"
        role="tabpanel"
        id={`${id}-panel-02`}
        aria-labelledby={`${id}-control-02`}
        hidden={active !== 1}
      >
        Tab Panel 2
      </div>
    </div>
  );
};

실제 동작하는 코드는 아래에서 확인해주세요.


 마치며

Accordion, Tab 컴포넌트 어떻게 구성해야 할 지 고민했을 때 현기 선임님께서 기존 React 라이브러리에서 사용되는 것을 참고하여 예시를 전달해주셨습니다. 레이아웃이 필요한 다양한 컴포넌트들에 유용하게 사용하고 있어 좋은 코드 공유해주신 현기 선임님께 다시 한번 감사드립니다.

참고문서


추천 글