프론트엔드 역량/CS 기초, 알고리즘, 자료구조, 시스템 디자인

[라이브러리] shadcn-Accodion(토글ui)

FS29 2025. 4. 2. 15:04

https://ui.shadcn.com/

 

Build your component library - shadcn/ui

A set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks. Open Source. Open Code.

ui.shadcn.com

샤드씨엔은 매우 간편한 라이브버리로, 위 공식문서에 나와있는대로 세팅하면 됩니다.

지난 인턴생활하면서 가장 많이 활용한 라이브러리인데, 정리를 해볼까 합니다.

React vite typescript 기반 개발을 할 때에 ui요소 중 Input, Button, Form, FormItem등 사용해봤지만 재미있던 건 Accordion입니다. 아래에 나와있는 링크에 들어가보면 어떻게 활용해야하는지 코드로 나와있습니다.

https://ui.shadcn.com/docs/components/accordion

 

Accordion

A vertically stacked set of interactive headings that each reveal a section of content.

ui.shadcn.com

 

제가 쓴 패키지매니저로는 pnpm이며 명령어는 아래와 같습니다.

pnpm dlx shadcn@latest add accordion

 

이러면 ui/common에 자동으로 생성이 됩니다. 

자동생성된 코드를 보면 아래와 같습니다.

'use client'

import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { cn } from '@ui/common/lib/utils'
import { ChevronDownIcon } from 'lucide-react'
import * as React from 'react'

function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
  return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}

function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
  return (
    <AccordionPrimitive.Item
      data-slot="accordion-item"
      className={cn('border-b last:border-b-0', className)}
      {...props}
    />
  )
}

function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
  return (
    <AccordionPrimitive.Header className="flex">
      <AccordionPrimitive.Trigger
        data-slot="accordion-trigger"
        className={cn(
          'flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
          className
        )}
        {...props}
      >
        {children}
        <ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  )
}

function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
  return (
    <AccordionPrimitive.Content
      data-slot="accordion-content"
      className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
      {...props}
    >
      <div className={cn('pt-0 pb-4', className)}>{children}</div>
    </AccordionPrimitive.Content>
  )
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

 

 

 

공식문서에서는 아래와 같이 사용하면 된다고 나와있습니다.

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

<Accordion type="single" collapsible>
  <AccordionItem value="item-1">
    <AccordionTrigger>Is it accessible?</AccordionTrigger>
    <AccordionContent>
      Yes. It adheres to the WAI-ARIA design pattern.
    </AccordionContent>
  </AccordionItem>
</Accordion>

위 코드를 참고하여 입맛에 맛게 조리해주면 됩니다.

 

아래 코드는 제가 적용한 실제 코드입니다.

import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ui/common/components/accordion'

export default function FaqList({
  filteredItems,
}: {
  filteredItems: { id: number; category: string; title: string; answer: string }[]
}) {
  return (
    <div className="w-full max-w-(--content-width) rounded-lg border border-gray-06">
      {filteredItems.map((faq) => {
        return (
          <div key={faq.id} className="flex flex-col border-b border-gray-05 px-6 py-4">
            <Accordion type="single" className="border-0" collapsible>
              <AccordionItem value="item-1">
                <div className="flex items-center justify-between">
                  <div className="flex text-body-01 font-semibold">
                    <p className="mr-2 text-gray-03">[{faq.category}]</p>
                    <p>{faq.title}</p>
                  </div>
                  <AccordionTrigger></AccordionTrigger>
                </div>
                <AccordionContent>
                  <div className="mt-5 flex text-body-02 text-gray-03">
                    <p className="mr-2 text-body-01 font-bold text-key">A.</p>
                    {faq.answer}
                  </div>
                </AccordionContent>
              </AccordionItem>
            </Accordion>
          </div>
        )
      })}
    </div>
  )
}

 

 


이밖에 써먹은 shadcn - Form, FromItem

import { Button, Input, LineTabs } from '@/shared/components'
import { useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { Form, FormItem } from '@ui/common/components/form'

const findSchema = z.object({
  name: z.string().nonempty('이름을 입력해주세요'),
  number: z.string().nonempty('휴대폰 번호를 입력해주세요'),
})

type FormValues = z.infer<typeof findSchema>

export function AccountFindForm() {
  const [activeTab, setActiveTab] = useState('일반회원')
  const navigate = useNavigate()

  const mockAccounts = [{ id: 'ACEID', name: '홍길동', phone: '01012345678', value: '일반회원' }]

  const form = useForm<FormValues>({
    resolver: zodResolver(findSchema),
    defaultValues: { name: '', number: '' },
    mode: 'onChange',
  })

  const onSubmit = (data: FormValues) => {
    const found = mockAccounts.find(
      (acc) => acc.name === data.name && acc.phone === data.number && acc.value === activeTab
    )

    if (found) {
      navigate({
        to: '/auth/find-account/complete',
        state: ((prev) => ({ id: found.id })) as any,
      })
    }
  }

  return (
    <section>
      <div className="flex flex-col items-center justify-center">
        <h1 className="mb-2 text-title-01 font-semibold">아이디 찾기</h1>
        <h6 className="mb-4 text-body-02">
          이름과 {activeTab === '일반회원' ? '휴대폰 번호' : '사업자등록번호'}를 입력해주세요
        </h6>
      </div>

      <div className="mb-7">
        <LineTabs
          tabs={[
            { label: '일반회원', value: '일반회원' },
            { label: '기업회원', value: '기업회원' },
          ]}
          activeTab={activeTab}
          setActiveTab={setActiveTab}
        />
      </div>

      <Form form={form} onSubmit={onSubmit} className="space-y-7">
        <FormItem
          name="name"
          label="이름"
          labelClassName="mb-2 text-body-02 font-semibold"
          messageClassName="mt-1 text-body-02 text-[#F24B4E]"
          circleX={false}
        >
          <Input placeholder="이름을 입력해주세요" />
        </FormItem>

        <FormItem
          name="number"
          label={activeTab === '일반회원' ? '휴대폰 번호' : '사업자등록번호'}
          labelClassName="mb-2 text-body-02 font-semibold"
          messageClassName="mt-1 text-body-02 text-[#F24B4E]"
          circleX={false}
        >
          <Input type="number" placeholder="숫자만 입력 가능" />
        </FormItem>

        <Button className="bg-key" type="submit" disabled={!form.formState.isValid}>
          확인
        </Button>
      </Form>
    </section>
  )
}