들어가며
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 라이브러리에서 사용되는 것을 참고하여 예시를 전달해주셨습니다. 레이아웃이 필요한 다양한 컴포넌트들에 유용하게 사용하고 있어 좋은 코드 공유해주신 현기 선임님께 다시 한번 감사드립니다.