headless로 Accordion 만들어보기

cansweep·2023년 3월 11일
1
post-thumbnail

headless component란?

디자인까지 완성된 컴포넌트는 필요할 때 바로 가져다 쓸 수 있다는 장점이 있지만 여러 페이지에서 해당 기능을 가진 컴포넌트를 써야 하지만 각각 요구하는 디자인이 다를 때 대응하기 어렵다는 단점이 있다.

headless component는 디자인없이 로직만 존재하는 컴포넌트로 위 상황처럼 페이지 별로 디자인이나 기능이 달라지는 경우 쓰기 좋다.

그러니까, headless로 아코디언을 만든다고 하면 단순히 열고 닫는 것만 컴포넌트에서 관리하게 하고 아코디언을 열고 닫는 버튼의 위치, 배경 색과 같은 디자인 요소는 컴포넌트 외부에서 따로 관리하게 하는 것이다.

이렇게 되면 단순히 열고 닫는 로직을 가진 아코디언 컴포넌트에 다양한 디자인을 입혀 각 페이지에서 사용할 수 있고 추후 한 페이지에서 디자인이 변경된다고 하더라도 다른 아코디언 컴포넌트까지 수정할 필요가 없어진다.

Accordion 만들기

atom 세팅

먼저 아코디언이 현재 열려있는지, 아니면 닫혀있는지의 상태를 저장할 곳을 생성한다. 어느 곳에다 만들고 상태를 관리해도 상관은 없지만 나는 전역으로 사용하기 위해서 jotai라는 상태 관리 라이브러리를 사용했다.

import { atom, useAtom } from "jotai";

const initValue = false;
const accordionAtom = atom(initValue);

부모 컴포넌트

부모 컴포넌트는 정말 간단하게 만들 수 있다. 아래에서 아코디언의 Head와 Body를 각각 구현할 것이기 때문에 이들을 모아줄 수 있는 곳만 만들면 된다.

function Accordion({ children }: PropsWithChildren) {
  return <Wrapper>{children}</Wrapper>;
}

아코디언의 Head에서는 아코디언을 열고 닫는 이벤트를 관리한다.
따라서 onClick 이벤트 핸들러에서 아까 전역으로 관리하기로 했던 atom의 값을 현재 값의 반대값으로 바꿔준다.

function Head({ children, ...rest }: PropsWithChildren) {
  const [isOpen, setIsOpen] = useAtom(accordionAtom);
  
  return (
    <HeadWrapper onClick={() => setIsOpen((cur) => !cur)} {...rest}>
      {children}
      {isOpen ? <span>접기</span> : <span>펼치기</span>}
    </HeadWrapper>
  );
}

Body

아코디언의 body는 숨겼다가 open시 보여주고 싶은 것들이 들어있다.
여기서도 보여주고 싶은 값들을 children에 담아 그대로 보여주면 된다.

function Body({ children, ...rest }: PropsWithChildren) {
  return (
    <BodyWrapper>
      <div {...rest}>{children}</div>
    </BodyWrapper>
  );
}

아코디언 안에 들어갈 Head와 Body가 모두 만들어졌다면 이들을 Accordion으로 모아주면 된다.

Accordion.Head = Head;
Accordion.Body = Body;

이렇게 하면 아코디언의 상태를 내부적으로 관리하면서도 Head와 Body를 각각 관리할 수 있게 된다.

Animation

아코디언을 열고 닫을 때 애니메이션을 추가해보았다.
ref로 아코디언의 body를 잡아 isOpen 상태에 따라 유동적으로 height을 바꿔주었다.
그러면서 transition을 사용해 즉각적으로 바뀌는 게 아니라 0.6s에 걸쳐 바뀌도록 해주었다.

function Body({ children, ...rest }: PropsWithChildren) {
  const bodyRef = useRef<HTMLDivElement>(null);
  const [isOpen] = useAtom(accordionAtom);

  useEffect(() => {
    if (!bodyRef.current) return;

    const element = bodyRef.current;

    if (isOpen) {
      element.style.height = `${element.scrollHeight}px`;
    } else {
      element.style.height = "0px";
    }
  }, [isOpen]);

  return (
    <BodyWrapper ref={bodyRef}>
      <div {...rest}>{children}</div>
    </BodyWrapper>
  );
}

const BodyWrapper = styled.div`
  transition: all 0.6s ease-out;
  overflow: hidden;
`;

결과 & 사용 예시

import Accordion from "../../components/css/accordion/Accordion";

function AccordionPage() {
  return (
    <Accordion>
      <Accordion.Head>
        <h1>아코디언 제목</h1>
      </Accordion.Head>
      <Accordion.Body>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor,
        recusandae illum. Officia iste rem autem debitis cum perspiciatis
        adipisci, blanditiis libero aut eveniet, itaque numquam repudiandae
        tempore impedit voluptatem nostrum accusantium sapiente ab nobis
        delectus eius incidunt nam mollitia iusto. Iste quibusdam laborum
        accusamus labore itaque unde hic aliquid eligendi ea culpa nemo,
        voluptatem voluptatibus esse exercitationem qui quo, delectus
        consequuntur veniam voluptate repellendus a. Ullam velit hic esse amet
        aspernatur magnam molestias consectetur ipsa sunt eum quaerat,
        doloremque natus aliquid totam quis debitis recusandae facere tenetur
        labore omnis corrupti sequi! In, quae corrupti. Cumque in suscipit
        officiis labore alias?
      </Accordion.Body>
    </Accordion>
  );
}

export default AccordionPage;

예시를 위해 간단하게 스타일링을 적용해보았다.
비록 지금은 못생긴 아코디언이지만 이 틀에 여러 스타일링을 적용하면 하나의 Accordion 컴포넌트로 다양한 스타일의 아코디언을 만들 수 있다.

profile
하고 싶은 건 다 해보자! 를 달고 사는 프론트엔드 개발자입니다.

0개의 댓글