[React.js] 아코디언 컴포넌트

hyejinJo·2023년 10월 4일
0

React

목록 보기
7/9
post-thumbnail

약관동의 페이지를 만들때, 아코디언 형태로 약관내용을 클릭하면 확인할 수 있도록 하고 체크박스와 제목은 보이는 구조를 만들고 싶었다.
⇒ 이용약관에서 누를 수 있는 제목버튼과 체크박스는 그대로 둔 상태로, 컨텐츠만 열리고 닫히는 구조

하나의 기능을 하는 컴포넌트 내 존재하는 부모 컴포넌트가 자식 컴포넌트를 조작할 수 있는 방법이 없을까 찾아봤더니 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.mapAccordion 컴포넌트의 자식들인 AccordionItem 컴포넌트들을 순회하며, 각 AccordionItem에 대해 다음과 같이 동작한다.

  1. isOpen 은 현재 열려있는 아코디언 아이템의 index 와 현재 순회중인 아이템의 index 를 비교하여 열린 상태를 결정
  2. onToggle 은 현재 아이템의 index 를, 클릭 이벤트가 발생할 때마다 토글하는 handleToggle 함수를 삽입

결과적으로, 각 AccordionItem에는 isOpenonToggle 프로퍼티가 주어져서 해당 아이템이 열린 상태인지를 결정하고, 클릭 시 토글하는 기능을 수행할 수 있게 됐다.

  • Accordion 컴포넌트에 initOpen 이라는 props 를 이용해 첫 칸이 열려있는 기능도 추가

결과:


경고 에러 발생

설명대로 해당 prop 을 카멜케이스로 안쓰고 isOpened ⇒ isopened 로 바꿨더니 또 다음과 같은 에러 발생

결국 타입이 boolean 인 값을 string 타입으로 바꾸라는 내용대로 수정을 했더니 에러 경고창이 더이상 뜨지 않았다.

  1. 카멜케이스 수정 isOpenedactive

  2. 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>
  );
};


+ 리액트 부트스트랩

또한 리액트 부트스트랩의 라이브러리로 쉽고 간편하게 아코디언을 구현할 수 있다. 링크



참고: https://velog.io/@boyeon_jeong/React.Children.map-함수

profile
FE Developer 💡

0개의 댓글