약관동의 페이지를 만들때, 아코디언 형태로 약관내용을 클릭하면 확인할 수 있도록 하고 체크박스와 제목은 보이는 구조를 만들고 싶었다.
⇒ 이용약관에서 누를 수 있는 제목버튼과 체크박스는 그대로 둔 상태로, 컨텐츠만 열리고 닫히는 구조
하나의 기능을 하는 컴포넌트 내 존재하는 부모 컴포넌트가 자식 컴포넌트를 조작할 수 있는 방법이 없을까 찾아봤더니 React의 Children
cloneElement
를 사용할 수 있었다.
Children
React.Children.map(children, callback)
형태는 React 엘리먼트의 자식 요소들을 순회하며 각각의 요소에 대해 callback
함수를 호출해준다. 이때 첫 번째 매개변수로 현재 자식 요소를 받는다
cloneElement
React.cloneElement(element, props)
형태는 주어진 element
(React 엘리먼트)를 복제하고, 추가로 전달된 props
를 기존 props
와 병합하여 새로운 props로 설정한 후 이 새로운 엘리먼트를 반환한다.
// Accordion.js
import React, { useState } from 'react';
import { css } from '@emotion/react';
const accordionStyle = css`
margin-top: 30px;
`;
const accordionItemStyle = css`
border: 1px solid;
padding: 20px;
button {
font-size: 20px;
font-weight: 700;
}
.contents-wrap {
.contents {
padding: 10px 0;
background-color: #ddd;
}
.checkbox-wrap {
margin-top: 10px;
label {
margin-left: 5px;
}
}
}
&.opened {
.contents {
display: block;
}
}
&.closed {
.contents {
display: none;
}
}
`;
export const Accordion = ({ children, initOpen = false }) => {
const [activeIndex, setActiveIndex] = useState(initOpen ? 0 : null);
const handleToggle = (index) => {
setActiveIndex(prevIndex => (prevIndex === index ? null : index));
};
return (
<div css={accordionStyle} className="accordion">
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
isOpen: activeIndex === index,
onToggle: () => handleToggle(index)
})
)}
</div>
);
};
export const AccordionItem = ({ children, title, isOpen, onToggle, checkAll = false }) => {
return (
<div css={accordionItemStyle} className={`accordion-item ${isOpen ? 'opened' : 'closed'} ${checkAll && 'check-all'}`}>
<button type="button" className='title-btn' onClick={onToggle}>
{title}
</button>
<div className="contents-wrap">
{children}
</div>
</div>
);
};
컴포넌트 사용 형태
import {Accordion, AccordionItem} from "@/components/Accordion";
<Accordion initOpen={true}>
<AccordionItem
title={`약관(1)`}
>
<div className='contents'>
약관(1)의 내용<br />
약관(1)의 내용<br />
약관(1)의 내용<br />
</div>
<div className='checkbox-wrap'>
<input id='chk1' type="checkbox"/>
<label htmlFor="chk1">체크박스</label>
</div>
</AccordionItem>
<AccordionItem
title={`약관(2)`}
>
<div className='contents'>
약관(2)의 내용<br />
약관(2)의 내용<br />
약관(2)의 내용<br />
</div>
<div className='checkbox-wrap'>
<input id='chk2' type="checkbox"/>
<label htmlFor="chk2">체크박스</label>
</div>
</AccordionItem>
...
</Accordion>
위의 코드에서 React.Children.map
은 Accordion
컴포넌트의 자식들인 AccordionItem
컴포넌트들을 순회하며, 각 AccordionItem
에 대해 다음과 같이 동작한다.
isOpen
은 현재 열려있는 아코디언 아이템의 index 와 현재 순회중인 아이템의 index 를 비교하여 열린 상태를 결정onToggle
은 현재 아이템의 index 를, 클릭 이벤트가 발생할 때마다 토글하는 handleToggle
함수를 삽입결과적으로, 각 AccordionItem
에는 isOpen
과 onToggle
프로퍼티가 주어져서 해당 아이템이 열린 상태인지를 결정하고, 클릭 시 토글하는 기능을 수행할 수 있게 됐다.
initOpen
이라는 props 를 이용해 첫 칸이 열려있는 기능도 추가설명대로 해당 prop 을 카멜케이스로 안쓰고 isOpened ⇒ isopened 로 바꿨더니 또 다음과 같은 에러 발생
결국 타입이 boolean 인 값을 string 타입으로 바꾸라는 내용대로 수정을 했더니 에러 경고창이 더이상 뜨지 않았다.
카멜케이스 수정 isOpened
⇒ active
String 타입으로 수정
// Accordion
active: String(activeIndex === index)
// AccordionItem
className={`accordion-item ${active === 'true' ? 'opened' : 'closed'}`}
최종 결과:
export const Accordion = ({ children, initOpen = false }) => {
const [activeIndex, setActiveIndex] = useState(initOpen ? 0 : null);
const handleToggle = (index) => {
setActiveIndex(prevIndex => (prevIndex === index ? null : index));
};
return (
<div css={accordionStyle} className="accordion">
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
isOpen: activeIndex === index,
onToggle: () => handleToggle(index)
})
)}
</div>
);
};
export const AccordionItem = ({ children, title, isOpen, onToggle, checkAll = false }) => {
return (
<div css={accordionItemStyle} className={`accordion-item ${active === 'true' ? 'opened' : 'closed'} ${checkAll && 'check-all'}`}>
<button type="button" className='title-btn' onClick={onToggle}>
{title}
</button>
<div className="contents-wrap">
{children}
</div>
</div>
);
};
또한 리액트 부트스트랩의 라이브러리로 쉽고 간편하게 아코디언을 구현할 수 있다. 링크