사내에서 처음에는 단순 테이블로 데이터 노출을 요청하여 간단하게 구현할 수 있었습니다.
하지만 추후 확장/축소 기능에 대한 요청을 주셔서 아주 당황스러웠더랬죠.
예상치 못한 추가 요청도 해결해 나가는 것이 우리 아니겠습니까. 살짝 당황했지만 검색해 보니 자료가 있어 간단하게 구현해낼 수 있었습니다(휴~~) 구현 과정을 글로 공유드려봅니다.
Material UI, Ant Design 등... 많은 종류의 React UI 라이브러리가 있습니다. 하지만 shadcn/ui는 자신을 UI 라이브러리가 아니고, 복사하여 붙여 넣을 수 있는 컴포넌트 모음이라고 소개합니다. 아래는 shadcn/ui의 소개 문구입니다.
This is NOT a component library. It's a collection of re-usable components that you can copy and paste into your apps.
shadcn/ui는 의존성으로 추가하지 않고 그대로 코드를 복사해 와서 사용합니다. 따라서 코드에 대한 소유권과 제어권을 사용자에게 부여하기 때문에 필요한 컴포넌트만 복사하여 자유롭게 커스텀 및 사용할 수 있습니다.
또한 Radix UI를 기반으로 컴포넌트가 구성되어 최신 접근성도 지원하고 있으며, CSS 변수와 Tailwind CSS와 같은 유틸리티 기반 프레임워크를 활용하여, 추가적인 인라인 스타일이나 과도한 CSS 파일 없이도 손쉽게 컴포넌트를 스타일링할 수 있습니다.
이러한 특징을 갖고 있는 shadcn/ui을 프로젝트에 설정하고, 확장/축소 가능한 테이블 컴포넌트를 만드는 방법까지 알아보도록 하겠습니다.
설치는 프로젝트 환경에 따라 달라지므로 공식문서를 참고하여 설치를 진행합니다.
Next.js 환경일 경우에는 npx shadcn@latest init
명령어를 사용하며, 아래와 같은 질문들을 선택하면 답변을 기반으로 components.json
파일이 구성됩니다.
Which style would you like to use? › (New York/Default)
Which color would you like to use as base color? › (Neutral/Gray/Zinc/Stone/Slate)
Do you want to use CSS variables for colors? › (no/yes)
style에 선택의 차이는 링크에서 확인할 수 있습니다.
요약하자면 뉴욕스타일이 좀 더 사이즈가 작고 현대적인 스타일로 링크에서 디자인을 확인해 보고 프로젝트에 맞게 선택할 수 있습니다.
base color 또한 공식문서 Themes, Colors 페이지에서 색상 확인 후 베이스가 될 색상을 지정할 수 있습니다.
app
├── layout.tsx
└── page.tsx
└── globals.css ✅
components
├── ui ✅
│ └── ...
lib
└── utils.ts ✅
components.json ✅
tailwind.config.js ✅
...
설치 후에는 ✅ 체크한 파일들이 추가, 수정됩니다.
앞서 선택한 설정을 기반으로 components.json
파일이 생성되며 복사하는 컴포넌트들은 components/ui 경로에 추가됩니다.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
위는 lib 폴더에 추가되는 utils.ts
파일의 코드입니다.
cn
함수는 Tailwind CSS 클래스를 쉽게 병합하거나 조건부로 적용할 수 있게 합니다.
clsx
는 조건부 클래스 추가를 지원하며, tailwind-merge
는 클래스를 자동으로 병합하여 중복 및 충돌 문제를 방지합니다. 아래 코드는 cn
함수를 사용했을 때와, 사용하지 않았을 때의 사용 예시입니다.
<div className={`${isActive ? "bg-blue-100" : "bg-gray-100"} text-white`} />
<div className={cn(isActive ? "bg-blue-100" : "bg-gray-100", "text-white")} />
이제 본론으로 들어가 확장/축소가 가능한 테이블을 만들어보겠습니다.
npx shadcn@latest add table
명령어로 table 컴포넌트를 추가합니다.
아래는 shadcn/ui 공식문서에 있는 table 컴포넌트 데모 코드입니다.
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const invoices = [...]
export function TableDemo() {
return (
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.invoice}>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.paymentStatus}</TableCell>
<TableCell>{invoice.paymentMethod}</TableCell>
<TableCell className="text-right">{invoice.totalAmount}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total</TableCell>
<TableCell className="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
)
}
확장/축소가 가능한 테이블을 만들기 위해 npx shadcn@latest add collapsible
명령어를 통해 collapsible 컴포넌트도 추가합니다.
아래는 shadcn/ui 공식문서에 있는 collapsible 컴포넌트 데모 코드입니다.
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
export function CollapsibleDemo() {
return (
<Collapsible>
<CollapsibleTrigger>Can I use this in my project?</CollapsibleTrigger>
<CollapsibleContent>
Yes. Free to use for personal and commercial projects. No attribution required.
</CollapsibleContent>
</Collapsible>
)
}
데모 코드의 구조를 살펴보면 Collapsible
로 전체를 감싸고, trigger가 되는 부분은 CollapsibleTrigger
를 사용하며, 확장/축소가 되는 콘텐츠는 CollapsibleContent
에 작성합니다.
이 구조를 테이블에 적용시켜 확장/축소가 가능한 테이블을 만들 수 있습니다.
1. Collapsible 컴포넌트로 TableRow 감싸기
table의 열을 확장/축소 가능하게 만들어야 하므로 TableRow
를 Collapsible
컴포넌트로 감싸줍니다.
이때 Collapsible
에 asChild를 작성해야 합니다. 테이블 요소이기 때문에 tr
, td
와 같은 구조를 지켜주기 위해 asChild
를 작성하여 Collapsible
이 자체적으로 렌더링 하는 태그를 제거하고, 자식 컴포넌트의 태그를 사용하도록 만듭니다.
export default function TableDemo() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<Collapsible key={invoice.invoice} asChild> ☑️
<TableRow>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.paymentStatus}</TableCell>
<TableCell>{invoice.paymentMethod}</TableCell>
<TableCell className="text-right">
{invoice.totalAmount}
</TableCell>
</TableRow>
</Collapsible>
))}
</TableBody>
</Table>
)
}
2. trigger가 될 요소에 CollapsibleTrigger 감싸기
trigger가 될 요소에 CollapsibleTrigger
컴포넌트를 감싸줍니다.
저는 새로운 컬럼을 만들어 trigger가 될 버튼을 만들고 CollapsibleTrigger
컴포넌트로 감싸줬습니다.
다만, CollapsibleTrigger
컴포넌트는 button
으로 렌더링 되어, button
을 trigger로 사용하는 경우 1번과 같이 asChild을 작성하여 자식 컴포넌트 태그를 렌더링 하게 해야 합니다.(button
자식에 button
이 존재할 수 없기 때문에)
또한 shadcn/ui는 lucide-react를 icon 라이브러리로 사용하여 ChevronDown 아이콘을 가져와 사용했습니다.
...
import { ChevronDown } from "lucide-react" ☑️
export default function TableDemo() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead className="w-20"></TableHead> ☑️
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<Collapsible key={invoice.invoice} asChild>
<TableRow>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.paymentStatus}</TableCell>
<TableCell>{invoice.paymentMethod}</TableCell>
<TableCell className="text-right">
{invoice.totalAmount}
</TableCell>
<TableCell className="text-center"> ☑️
<CollapsibleTrigger asChild>
<Button size="icon">
<ChevronDown />
</Button>
</CollapsibleTrigger>
</TableCell>
</TableRow>
</Collapsible>
))}
</TableBody>
</Table>
)
}
3. CollapsibleContent 추가
trigger가 되는 TableRow
요소의 아랫줄에 확장 콘텐츠가 나타나야 되므로 반복되고 있는 TableRow
의 형제에 CollapsibleContent
컴포넌트를 추가합니다.
Collapsible
은 asChild
를 사용하여 자식 요소를 렌더링 하고 있는데, 자식 요소가 2개가 되었으므로 프래그먼트로 감싸주어야 합니다.CollapsibleContent
의 자식 요소는 TableRow
와 TableCell
이 되어야 합니다.CollapsibleContent
의 너비는 colSpan
으로 지정 가능합니다. colSpan
을 컬럼수 만큼 지정하여 한 줄을 가득 채우고 다른 태그들을 활용하여 내용을 작성할 수 있습니다."use client"를 작성해 주지 않으면 에러가 발생하여, client 컴포넌트로 사용해야 합니다. (참고링크)
"use client" ☑️
export default function TableDemo() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead className="w-20"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<Collapsible key={invoice.invoice} asChild>
<> ☑️
<TableRow>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.paymentStatus}</TableCell>
<TableCell>{invoice.paymentMethod}</TableCell>
<TableCell className="text-right">
{invoice.totalAmount}
</TableCell>
<TableCell className="text-center">
<CollapsibleTrigger asChild>
<Button size="icon">
<ChevronDown />
</Button>
</CollapsibleTrigger>
</TableCell>
</TableRow>
<CollapsibleContent asChild> ☑️
<TableRow>
<TableCell colSpan={5} className="py-5">
<div className="text-base font-bold mb-2">제목</div>
<div className="bg-white/10 p-5 rounded-lg">내용</div>
</TableCell>
</TableRow>
</CollapsibleContent>
</>
</Collapsible>
))}
</TableBody>
</Table>
)
}
이렇게 확장/축소 가능한 테이블을 구현했습니다.
프래그먼트
와 asChild
작성에만 유의한다면 shadcn/ui의 table과 collapsible 컴포넌트를 활용해 간단하게 구현할 수 있습니다.
자료 조사 중 JavaScript Rising Stars라는 글을 발견했습니다. 한 해 동안 가장 많은 GitHub Star를 받은 Javascript 프로젝트 순위에 대한 글인데, shadcn/ui가 2년 연속 1위를 했다고 합니다.
처음 shadcn/ui를 사용했을 때 컴포넌트가 복사된다는 개념을 접하고 신세계였는데 이러한 도구들을 적절하게 사용하면 컴포넌트 제작을 효율적으로 할 수 있을 것 같습니다.
부럽다 저는 쓰레기같은 Nuxt UI 썼는데