올해 자바스크립트 생태계를 돌아다니다 보면 shadcn/ui라는 흥미로운 UI 라이브러리를 접했을 것입니다. 이 라이브러리는 npm 패키지로 배포되지 않고, CLI를 통해 컴포넌트의 소스 코드를 프로젝트에 포함하는 방식으로 제공됩니다. 이 라이브러리의 제작자는 shadcn/ui의 공식 웹 사이트 내에 이러한 결정을 내리게 된 이유에 대해서 작성하였습니다.
패키지로 배포하지 않고 소스코드를 복사/붙여 넣기 하는 이유는 무엇인가요?
이는 코드에 대한 소유권과 제어권을 사용자에게 부여하여, 사용자가 컴포넌트의 빌드 및 스타일을 직접 결정할 수 있도록 하기 위함입니다.
적절한 기본값으로 시작한 뒤 필요에 따라 컴포넌트들을 커스텀할 수 있습니다.
npm 패키지로 컴포넌트들을 패키징할 때 단점 중 하나는 스타일과 구현이 결합이 되어있는 것입니다. 컴포넌트의 디자인과 구현은 분리되어야 합니다.
본질적으로 shadcn/ui는 단순한 컴포넌트 라이브러리가 아니라 디자인 시스템을 코드로 선언하는 메커니즘을 제공합니다.
이 글에서는 shadcn/ui의 아키텍처와 구현 방식을 살펴보며, 앞서 언급한 목표를 달성하기 위해 어떻게 설계되었는지 분석해 보려고 합니다.
아직 shadcn/ui를 사용해 보지 않았다면 이 글의 이해를 돕기 위해 shadcn/ui docs에 접속해서 한번 사용해 보는 것도 추천합니다.
프롤로그
모든 사용자 인터페이스는 재사용할 수 있는 기본 컴포넌트와 이들의 조합으로 구성됩니다. 각 UI 컴포넌트는 고유의 동작 세트와 스타일로 표현되는 시각적 표현으로 구성됩니다.
동작
순수하게 표현을 위한 UI 컴포넌트를 제외하고, UI 컴포넌트는 사용자가 수행할 수 있는 상호작용을 인식하고 그에 따라 반응해야 합니다. 이러한 동작에 필요한 기초 요소들은 기본 브라우저 요소들에 내장되어 있고, 우리는 이를 활용할 수 있습니다. 그러나 최신 사용자 인터페이스에서는 브라우저의 기본 요소만으로는 컴포넌트의 모든 동작을 구현할 수는 없습니다. (예: 탭, 아코디언, 날짜 선택기 등) 따라서 우리가 구상한 대로 동작하고 보이는 커스텀 컴포넌트를 제작할 필요가 있습니다.
최신 UI 프레임워크를 사용하면 커스텀 컴포넌트를 표면적으로 구현하는 것은 어렵지 않습니다. 그러나 대부분의 경우 이러한 커스텀 컴포넌트 구현에서는 UI 컴포넌트 동작의 중요한 측면들이 간과되는 경향이 있습니다. 여기에는 포커스/블러 상태 인식, 키보드 탐색, WAI-ARIA 디자인 원칙 준수가 포함됩니다. 접근성을 보장하기 위해 이러한 동작은 매우 중요하지만, W3C 사양에 맞게 이러한 동작을 정확하게 구현하는 것은 매우 어려운 작업이며, 제품 개발 속도를 상당히 늦출 수 있습니다.
최신 소프트웨어 개발 문화는 빠르게 움직이기 때문에, 프런트엔드 팀에서 커스텀 컴포넌트 개발 시 접근성 가이드를 고려하기 어렵습니다. 이를 완화하는 한 가지 방법은 이미 이러한 동작을 구현한 스타일이 없는 기본 컴포넌트 세트를 개발하여 모든 프로젝트에서 사용하는 것입니다. 그러나 각 팀은 프로젝트의 시각적 디자인에 맞게 이 컴포넌트를 쉽게 확장하고 스타일할 수 있어야 합니다.
이렇게 동작 세트를 캡슐화하고 있지만 스타일이 없는 재사용할 수 있는 컴포넌트들을 Headless UI 컴포넌트라고 부릅니다. 종종 이들은 내부 상태를 읽고 제어할 수 있는 API 인터페이스를 노출하도록 설계할 수 있습니다. 이 개념은 shadcn/ui의 주요 아키텍처 요소 중 하나입니다.
스타일
UI 컴포넌트에서 시각적 표현은 가장 중요한 부분입니다. 모든 컴포넌트는 프로젝트의 전체 시각적 테마를 기반으로 한 기본 스타일을 가지고 있습니다. 컴포넌트의 시각적 요소는 두 가지로 나눌 수 있습니다. 첫 번째는 컴포넌트의 구조적 측면입니다. 예를 들어, 둥근 모서리 속성(border-radius), 치수, 간격, 글꼴 크기 및 글꼴 굵기가 여기에 해당합니다. 두 번째는 시각적 스타일로 배경색, 테두리 등이 해당합니다.
사용자 상호작용과 애플리케이션 상태에 따라 UI 컴포넌트는 다양한 상태에 있을 수 있습니다. 컴포넌트의 시각적 스타일은 현재 상태를 반영해야 하며, 사용자와 상호작용을 할 때 적절한 피드백을 제공해야 합니다. 따라서 동일한 UI 컴포넌트의 다양한 변형(variants)을 만들어야 합니다. 이러한 변형은 종종 상태를 전달하기 위해 컴포넌트의 구조와 시각적 스타일을 조정하여 만들어집니다.
소프트웨어 애플리케이션 개발 주기 동안 디자인팀은 애플리케이션에 대한 고품질 목업을 개발하면서 시각적 테마, 컴포넌트 및 변형을 기록합니다. 또한 컴포넌트의 다양한 의도된 동작도 문서화할 수 있습니다. 이러한 유형의 통합 디자인 문서는 일반적으로 디자인 시스템이라고 알려져 있습니다.
디자인 시스템이 제공되면 프런트엔드 팀은 이를 코드로 구현해야 합니다. 이들은 시각적 테마의 전역 변수와 재사용할 수 있는 컴포넌트 그리고 변형(variants)를 추출하여 구현해야 합니다. 이 접근 방식의 가장 큰 장점은 향후 디자인 시스템에 대한 모든 변경 사항을 코드에 효율적으로 반영할 수 있다는 것입니다. 이를 통해 디자인팀과 개발팀 간의 원활한 협업이 이뤄질 수 있습니다.
아키텍처 개요
앞서 설명했듯이, shadcn/ui는 디자인 시스템을 코드로 표현할 수 있게 해주는 메커니즘입니다. 이를 통해 프런트엔드 팀은 디자인 시스템을 개발 과정에서 활용할 수 있는 형식으로 변환할 수 있습니다. 이러한 워크플로우를 가능하게 하는 아키텍처는 검토할 가치가 있다고 생각합니다.
우리는 모든 shadcn/ui 컴포넌트의 디자인을 다음과 같은 아키텍처로 일반화할 수 있습니다.
shadcn/ui는 컴포넌트의 디자인과 구현은 분리되어야 한다는 핵심 원칙을 기반으로 만들어졌습니다. 따라서 shadcn/ui의 모든 컴포넌트는 두 개의 계층 구조로 구성됩니다.
- 구조와 동작 계층
- 스타일 계층
구조와 동작 계층
구조와 동작 계층에서는 컴포넌트가 headless 형태로 구현됩니다. 프롤로그에서 이야기한 것처럼, 이는 컴포넌트의 구조적 구성과 핵심 동작이 캡슐화된다는 것을 의미합니다. 키보드 내비게이션이나 WAI-ARIA 준수와 같은 까다로운 고려 사항도 이 계층에서 각 컴포넌트에 의해 구현됩니다.
shadcn/ui는 네이티브 브라우저 요소만으로는 구현할 수 없는 컴포넌트를 위해 잘 알려진 headless UI 라이브러리를 활용하고 있습니다. Radix UI는 shadcn/ui 코드 베이스에서 이러한 중요한 headless UI 라이브러리 중 하나로, Accordion, Popover, Tabs 등 자주 사용되는 여러 컴포넌트가 Radix UI 구현을 기반으로 구축되었습니다.
네이티브 브라우저 요소와 Radix UI 컴포넌트만으로 대부분의 컴포넌트 요구 사항을 충족할 수 있지만, 특화된 headless UI 라이브러리가 필요한 상황도 있습니다.
그중 하나는 폼 처리입니다. 이를 위해 shadcn/ui는 폼 상태 관리를 담당하는 headless 폼 라이브러리인 React Hook Form
을 기반으로 Form
컴포넌트를 제공합니다. shadcn/ui는 React Hook Form이 제공하는 기본 기능을 받아, 이를 조합 가능한 방식으로 감싸서 사용합니다.
테이블 뷰 처리를 위해 shadcn/ui는 headless 테이블 라이브러리인 Tanstack React Table을 사용합니다. shadcn/ui의 Table
과 DataTable
컴포넌트는 이 라이브러리를 기반으로 구축되었으며, 필터링, 정렬, 가상화 등의 테이블 뷰 처리를 위한 여러 API를 제공합니다.
캘린더 뷰, 날짜 선택기, 기간 선택기 등은 구현이 어려운 컴포넌트로 잘 알려져 있습니다. 하지만 shadcn/ui는 React Day Picker
패키지를 이러한 컴포넌트의 headless 계층을 구현하는 기본 컴포넌트로 사용합니다.
스타일 계층
shadcn/ui의 스타일 계층 핵심에는 Tailwind CSS가 자리하고 있습니다. 색상이나 둥근 모서리 속성(border-radius) 같은 속성값들은 Tailwind 설정 파일에 지정된 CSS 변수로서 global.css 파일에 저장되어 디자인 시스템 전반에서 공유됩니다. 디자인 도구로 Figma를 사용하는 경우, 이 접근 방식을 통해 디자인 시스템에서 사용할 Figma 변수를 추적할 수 있습니다.
컴포넌트의 다양한 스타일 변형을 관리하기 위해 shadcn/ui는 Class Variance Authority(CVA)를 사용합니다. CVA는 각 컴포넌트에 대한 변형 스타일을 구성하는 데 있어 매우 표현력이 뛰어난 API를 제공합니다.
shadcn/ui의 높은 수준의 아키텍처를 살펴본 만큼, 이제 여러 컴포넌트의 구현 세부 사항을 자세히 다뤄보겠습니다. 간단한 컴포넌트 중 하나부터 시작해 보겠습니다.
shadcn/ui Badge
<Badge />
컴포넌트의 구현은 비교적 간단합니다. 따라서 지금까지 논의한 개념들이 재사용할 수 있는 컴포넌트를 구축하는 데 어떻게 활용될 수 있는지 살펴보기에 좋습니다.
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
컴포넌트의 구현은 라이브러리인 class-variance-authority
의 cva
함수를 호출하는 것으로 시작됩니다. 이 함수는 컴포넌트의 변형을 선언합니다.
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
cva
함수의 첫 번째 인자는 모든 <Badge />
컴포넌트 변형에 적용되는 기본 스타일을 정의합니다. 두 번째 인자로는 컴포넌트의 가능한 변형과 사용해야 할 기본 변형을 정의하는 구성 객체를 cva
가 받습니다. 또한, tailwind.config.js
에서 정의된 디자인 시스템 토큰을 사용하는 유틸리티 스타일의 사용도 확인할 수 있으며, 이를 통해 CSS 변수를 조정하여 손쉽게 모양과 느낌을 업데이트할 수 있는 가능성이 열립니다.
cva
함수의 호출은 또 다른 함수를 반환하며, 이는 각 변형에 따라 조건부로 스타일을 적용하는 데 사용될 수 있습니다. 우리는 이를 badgeVariants
라는 변수에 저장하여, 변형 이름이 컴포넌트에 프로퍼티로 전달될 때 올바른 스타일을 적용하는 데 활용할 수 있습니다.
그다음으로는 컴포넌트의 타입을 정의하는 BadgeProps
인터페이스를 찾을 수 있습니다.
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
배지 컴포넌트의 기본 요소는 HTML div
입니다. 따라서 이 컴포넌트는 div
요소의 확장으로 노출되어야 합니다. 이는 React.HTMLAttributes<HTMLDivElement>
타입을 확장함으로써 이루어집니다. 또한, 원하는 변형을 렌더링 할 수 있도록 <Badge />
컴포넌트에서 variant
프로퍼티를 노출해야 합니다. 헬퍼 타입인 VariantProps
는 사용할 수 있는 변형을 variants
프로퍼티의 Enum으로 노출할 수 있게 해 줍니다.
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
마지막으로, Badge
를 정의하는 함수형 컴포넌트가 있습니다. 여기서 우리는 className
과 variant
를 제외한 모든 props를 props 객체로 모아서 기본 div에 스프레드 연산자로 주입하고 있습니다. 이를 통해 컴포넌트를 사용할 때 div
요소에서 사용할 수 있는 모든 props에 접근할 수 있습니다.
컴포넌트에서 스타일 적용 방식도 주목할 만합니다. variant 프로퍼티의 값이 badgeVariants
함수에 전달되며, 이 함수는 컴포넌트 변형을 렌더링 하는 데 필요한 모든 유틸리티 클래스 이름을 포함하는 클래스 문자열을 반환합니다. 그러나 우리가 앞서 언급한 함수의 반환 값과 className
프로퍼티에 전달된 값을 cn
이라는 함수를 통해 전달하고 있는 점에 유의해야 합니다. 이 값은 div 요소의 className
속성으로 평가되기 전에 처리됩니다.
이것은 실제로 shadcn/ui에서 제공하는 특수 유틸리티 함수로, 유틸리티 클래스 관리 솔루션 역할을 합니다. 이제 그 구현을 살펴보겠습니다.
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
이 유틸리티 함수는 유틸리티 클래스를 관리하는 데 도움을 주는 두 개의 라이브러리를 결합한 것입니다. 첫 번째 라이브러리는 clsx
입니다. 이 라이브러리는 className을 연결하여 조건부로 스타일을 적용할 수 있는 기능을 제공합니다.
import React from "react";
const Link = ({ isActive, children }: { isActive: boolean, children: React.ReactNode }) => {
return <a className={clsx("text-lg", { "text-blue-500": isActive })}>{children}</a>;
};
여기에서는 clsx
가 독립적으로 사용된 사례를 볼 수 있습니다. 기본적으로 Link 컴포넌트에는 text-lg
유틸리티 클래스만 적용됩니다. 그러나 isActive
프로퍼티에 true 값을 전달하면 text-blue-500
유틸리티 클래스도 컴포넌트에 적용됩니다.
하지만 때때로 clsx
만으로는 우리의 목표를 달성할 수 없는 상황도 있습니다.
import React from "react";
import clsx from "clsx";
const Link = ({ isActive, children }: { isActive: boolean, children: React.ReactNode }) => {
return <a className={clsx("text-lg text-grey-800", { "text-blue-500": isActive })}> {children}</a>;
};
이 경우, 기본적으로 text-grey-800
색상 유틸리티가 요소에 적용됩니다. 우리의 목표는 isActive
프로퍼티가 true가 될 때 텍스트 색상을 blue-500
으로 변경하는 것입니다. 하지만 Tailwind CSS의 계단식(Cascade) 특성 때문에 text-grey-800
으로 적용된 색상 스타일이 수정되지 않습니다.
여기서 tailwind-merge
라이브러리가 필요해집니다. tailwind-merge
를 사용하여 위의 코드를 수정하면 됩니다.
import React from "react";
import { twMerge } from "tailwind-merge";
import clsx from "clsx";
const Link = ({ isActive, children }: { isActive: boolean, children: React.ReactNode }) => {
return <a className={twMerge(clsx("text-lg text-grey-800", { "text-blue-500": isActive }))}>{children}</a>;
};
이제 clsx
의 출력이 tailwind-merge
를 통과하게 됩니다. tailwind-merge
는 클래스 문자열을 파싱하고 스타일 정의를 얕게 병합합니다. 즉, text-grey-800
이 text-blue-500
으로 대체되어 요소가 조건부로 적용된 새로운 스타일을 반영하게 됩니다.
이 접근 방식은 변형 구현에서 스타일 충돌이 발생하지 않도록 도와줍니다. className
프로퍼티도 cn
유틸리티를 통해 전달되므로, 필요할 경우 스타일을 쉽게 오버라이드할 수 있습니다. 하지만 이에는 대가가 따릅니다. cn
의 사용은 컴포넌트 소비자가 스타일을 즉흥적으로 오버라이드할 수 있는 가능성을 열어줍니다. 이는 코드 리뷰 단계에서 cn
이 남용되지 않았는지 검증해야 할 책임을 어느 정도 위임하게 됩니다. 반면, 이러한 동작이 전혀 필요 없다면, 컴포넌트를 수정하여 clsx
만 사용하도록 할 수 있습니다.
Badge
컴포넌트의 구현을 분석하면 SOLID 원칙과 관련된 몇 가지 패턴을 발견할 수 있습니다.
- 단일 책임 원칙 (SRP):
Badge
컴포넌트는 제공된 변형에 따라 다양한 스타일로 배지를 렌더링 하는 단일 책임을 지고 있습니다. 스타일 관리는badgeVariants
객체에 위임합니다.
2. 개방/폐쇄 원칙 (OCP):
- 코드는 새로운 변형을 기존 코드 수정 없이 추가할 수 있도록 하여 개방/폐쇄 원칙을 따릅니다. 새로운 변형은
badgeVariants
정의의variants
객체에 쉽게 추가할 수 있습니다. - 하지만 주의할 점은,
cn
이 어떻게 사용되는지에 따라 컴포넌트 소비자가className
속성을 통해 새로운 오버라이딩 스타일을 전달할 수 있다는 것입니다. 이는 컴포넌트를 수정할 수 있는 가능성을 열어줍니다. 따라서 shadcn/ui를 사용하여 자신만의 컴포넌트 라이브러리를 구축할 때, 이러한 동작을 허용할지 여부를 결정해야 합니다.
3. 의존성 역전 원칙 (DIP):
Badge
컴포넌트와 스타일은 별도로 정의됩니다.Badge
컴포넌트는 스타일 정보에 대해badgeVariants
객체에 의존합니다. 이러한 분리는 유연성과 유지 보수 용이성을 제공하여 의존성 역전 원칙을 준수합니다.
4. 일관성 및 재사용성:
- 코드는
cva
유틸리티 함수를 사용하여 변형에 따라 스타일을 관리하고 적용함으로써 일관성을 유도합니다. 이러한 일관성 덕분에 개발자는 컴포넌트를 더 쉽게 이해하고 사용할 수 있습니다. 또한,Badge
컴포넌트는 재사용이 가능하며 애플리케이션의 다양한 부분에 쉽게 통합될 수 있습니다.
5. 관심사 분리
- 스타일과 렌더링의 관심사가 분리되어 있습니다.
badgeVariants
객체는 스타일 로직을 처리하고,Badge
컴포넌트는 스타일을 렌더링 하고 적용하는 역할을 맡고 있습니다.
Badge
컴포넌트의 구현을 분석한 후, 이제 shadcn/ui의 일반적인 아키텍처에 대한 자세한 이해를 갖게 되었습니다. 그러나 이는 순전히 표시 수준의 컴포넌트였습니다. 그러므로 조금 더 인터랙티브 한 컴포넌트 몇 가지를 살펴보는 시간을 가져보겠습니다.
shadcn/ui Switch
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
여기에는 최신 사용자 인터페이스에서 특정 필드를 두 값 사이에서 전환하는 데 일반적으로 사용되는 Switch
컴포넌트가 있습니다. Badge
컴포넌트가 순전히 표현적인 것과는 달리, Switch
는 사용자 입력에 반응하고 상태를 전환하는 인터랙티브한 컴포넌트입니다. 또한, 시각적 스타일을 통해 현재 상태를 사용자에게 전달합니다.
사용자가 스위치 컴포넌트와 상호작용을 할 수 있는 주요 방법은 포인팅 장치로 스위치를 클릭하거나 탭 하는 것입니다. 포인터 이벤트에 반응하는 스위치 컴포넌트를 만드는 것은 상당히 간단하지만, 스위치가 키보드 상호작용과 스크린 리더에도 반응해야 할 때 구현의 복잡성이 크게 증가합니다. 스위치 컴포넌트에 대한 몇 가지 예상 동작은 다음과 같이 식별할 수 있습니다. 1. Tab
키를 눌렀을 때 스위치에 포커스를 맞춥니다. 2. 포커스가 맞춰진 후 Enter 키를 누르면 스위치의 상태가 전환됩니다. 3. 스크린 리더가 있을 경우, 현재 상태를 사용자에게 알립니다.
코드를 면밀히 분석하면, 스위치의 실제 구조가 <SwitchPrimitives.Root/
>와 <SwitchPrimitives.Thumb/>
복합 컴포넌트를 사용하여 구축된 것을 확인할 수 있습니다. 이러한 컴포넌트는 RadixUI headless 라이브러리에서 제공되며 스위치에 대한 예상 동작의 모든 구현을 포함합니다. 또한, 이 컴포넌트를 만들기 위해 React.forwardRef
를 활용한 점도 주목할 수 있습니다. 이는 이 컴포넌트를 들어오는 ref
에 바인딩할 수 있게 해 줍니다. 이는 포커스 상태를 추적하고 특정 외부 라이브러리와 통합해야 할 때 유용한 기능입니다. (예: React Hook Form 라이브러리와 함께 입력 컴포넌트로 사용하기 위해서는 ref
를 통해 포커스 가능해야 합니다).
앞서 얘기했듯이, RadixUI 컴포넌트는 스타일을 제공하지 않습니다. 따라서 스타일은 cn
유틸리티 함수를 통해 전달된 후 className 프로퍼티를 통해 이 컴포넌트에 직접 적용됩니다. 또한, 필요에 따라 cva
를 사용하여 컴포넌트의 변형을 생성할 수 있습니다.
결론
지금까지 논의한 shadcn/ui의 아키텍처와 구조는 나머지 shadcn/ui 컴포넌트에서도 동일한 방식으로 구현되어 있습니다. 그러나 특정 컴포넌트의 동작과 구현은 약간 더 복잡합니다. 이러한 컴포넌트의 아키텍처에 대한 논의는 별도의 글로 다루어야 할 가치가 있습니다. 따라서 길게 설명하기보다는 그 구조에 대한 개요를 제공하겠습니다.
Calendar
react-day-picker
를 headless 컴포넌트로 사용합니다.- DateTime 포매팅 라이브러리로
date-fns
를 사용합니다.
Table
과 DataTable
@tanstack/react-table
을 headless 테이블 라이브러리로 사용합니다.
Form
react-hook-form
을 폼 및 폼 상태 관리 라이브러리로 사용합니다.- 폼 로직을 캡슐화하는 유틸리티 컴포넌트가 shadcn/ui에 의해 제공됩니다. 이를 통해 입력 및 오류 메시지를 포함한 폼의 여러 부분을 조립할 수 있습니다.
- 폼의 스키마 검증 라이브러리로
zod
가 사용됩니다.zod
가 반환하는 유효성 검사 오류는<FormMessage/>
컴포넌트에 전달되어 폼 입력 옆에 오류를 표시합니다.
shadcn/ui는 프런트엔드 개발에 대한 새로운 패러다임을 도입했습니다. 전체 컴포넌트를 추상화하는 서드파티 패키지에 의존하는 대신, 컴포넌트의 구현을 소유하고 필요한 요소만 노출할 수 있습니다. 미리 구축된 컴포넌트 라이브러리의 의견이 반영된 API에 제한되지 않고, 나중에 사용자 지정할 수 있는 충분한 기본값을 가진 자신만의 디자인 시스템을 구축하세요.