Blog

테크

shadcn/ui로 Headless UI 사용해 보기

#조사#개발환경#프론트엔드#Frontend#CSS#TailwindCSS#theme

2024-04-17

shadcn/ui로 Headless UI 사용해 보기

shadcn/ui로 Headless UI 사용해 보기

카테고리
테크
제품
작성자
생성일
Apr 17, 2024 07:23 AM
생성자
마지막 수정시간
Last updated April 18, 2024
상태
작성중
블로그게시
게시
태그
조사
개발환경
프론트엔드
Frontend
CSS
TailwindCSS
theme
마지막 수정자
게시자
hyerexx
블로그카테고리
tech
업로드표시날짜
Apr 17, 2024
블로그_관련제품
ibalance
요약
headless UI와 shadcn/ui를 도입하여 UI 작업을 효율적으로 진행하기
블로그_이벤트
ibalance
최근 다양한 프로젝트들을 진행하면서 UI 작업에 상당히 많은 리소스를 소모하게 되었습니다. 반복되는 UI 작업으로 피로도가 높아지자 어떻게 해야 더 효율적인 작업이 가능할지 고민하게 되었는데, 개중 Headless UI라는 방법론에 대해 소개해 보려 합니다.

headless UI가 무엇인가요?

headless UI는 시각적 디자인과 컴포넌트의 기능을 분리하여 각각 독립적으로 개발된 디자인 시스템을 말합니다. 컴포넌트의 외관과 동작이 분리되어 있기 때문에 디자인과 기능을 독립적으로 조정할 수 있어 유연한 대응이 가능합니다. 또, 높은 재사용성으로 구현에 걸리는 시간이 줄어드는 등 장점이 많기 때문에 상당한 최신 웹 애플리케이션에서 채택하고 있기도 합니다.

shadcn-ui

headless UI의 일종으로, Radix UI를 기반으로 tailwind css를 적용하여 작성한 재사용 가능한 컴포넌트 모음입니다 (공식 문서에서 NOT a componant library, but collection of the re-usable components라고 명시하고 있습니다.). 라이브러리가 아니기 때문에 의존성으로 명시할 필요가 없으며, 모든 컴포넌트 블록은 복사 → 붙여넣기를 통해 내 프로젝트의 일부로 만들고, 자유롭게 수정할 수 있습니다.
Headless UI가 디자인과는 완전히 분리된, 기능 위주의 UI를 뜻하긴 하지만 shadcn-ui는 프로토타입으로 사용할 수 있을 정도의 심플한 디자인은 제공하고 있습니다. 하지만 className을 통해 쉽게 커스텀 할 수 있도록 twMergeclsx를 차용하고 있고, cva를 통해 variant를 적용할 수도 있습니다. 여기까지만 보면 장점뿐이지만 단점도 물론 존재합니다.
Form, Data-table 등의 컴포넌트는 react-hook-form, zod, tanstack/react-table 등의 라이브러리를 기반으로 하고 있는데 원하는 만큼 컨트롤하면서 적용하기 까다로워 시간이 꽤 소요되는 편입니다. 단발성으로 사용한다면 도입하는 의도와는 배치되는 부분이 있기도 합니다. 그러나 Checkbox, Radio button, Toggle 등 작으면서도 까다로운 요소들을 구현하는 데에는 많은 도움이 될 수 있기 때문에 이런 부분부터 차근차근 시도해 보시기를 권장합니다.
 

사용해 보기

최신 버전의 Next.js를 기반으로 shadcn-ui를 적용한 로그인 페이지를 만드는 과정을 소개하겠습니다. 예시는 아래의 레포지토리에서도 확인해 보실 수 있습니다.

1. 초기화

shadcn-ui를 사용하기 위한 초기화 작업을 거칩니다. 이 과정을 통해 component.json을 생성하고, 기타 필요한 설정을 추가하게 됩니다.
npx shadcn-ui@latest init
Which style would you like to use? › Default Which color would you like to use as base color? › Slate Do you want to use CSS variables for colors? › no / yes
 
초기화 후의 프로젝트 구조입니다. global.css에도 shadcn-ui가 추가한 변수가 추가되어 있고, tailwind 구성 파일에서도 extends로 적용된 모습을 확인해 볼 수 있습니다.
notion image
global.css에 추가된 변수들. hsl표기를 사용하고 있다.
global.css에 추가된 변수들. hsl표기를 사용하고 있다.

2. 사용하고 싶은 UI 추가하기

예시로 만들어 볼 화면입니다.
언제나 필요한 로그인
언제나 필요한 로그인
공식 문서에서 복사 후 내 프로젝트에 붙여넣기 해 사용할 수도 있지만, 더 편리하게 이용하기 위해 CLI를 활용해 보겠습니다. 작업할 UI를 확인하고, 필요한 컴포넌트를 내 프로젝트에 추가합니다.
npx shadcn-ui@latest add card input label checkbox switch button
물론 모든 컴포넌트를 한 번에 추가할 수도 있습니다.
npx shadcn-ui@latest add -a
하지만, 굳이 필요 없는 컴포넌트까지 가져올 필요는 없겠죠? Input처럼 별다른 의존성이 없는 컴포넌트도 있지만, Form이나 Data-table처럼 다른 라이브러리를 필요로 하는 컴포넌트도 있기 때문에 필요한 컴포넌트만 선택적으로 추가하는 것을 권장합니다.
 
컴포넌트를 추가한 뒤에는 컴포넌트 경로에 대한 path alias를 지정하고, 컴포넌트 경로에 index.ts 파일을 추가하는 것을 권장합니다. 훨씬 깔끔하게 import 할 수 있으니까요!
notion image
notion image
 

3. UI 작업하기

추가한 컴포넌트들로 필요한 UI를 만들어 보겠습니다. 가장 바깥쪽에 있는 Card 먼저 추가해 볼게요.
"use client"; import { Card } from "@ui"; export function LoginExample() { return ( <main className="w-screen h-screen flex justify-center items-center"> <Card.Card> <Card.CardHeader> <Card.CardTitle>Title</Card.CardTitle> </Card.CardHeader> <Card.CardContent>Content</Card.CardContent> <Card.CardFooter>Footer</Card.CardFooter> </Card.Card> </main> ); }
notion image
이게 shadcn-ui에서 제공하는 기본 디자인입니다. 뭐가.. 많이 없죠? 이제 가로 길이를 조정해 보겠습니다. shadcn-ui는 내부적으로 twMerge와 clsx를 사용하고 있기 때문에 커스텀 스타일 적용이 용이합니다.
className을 처리하는 유틸 함수
className을 처리하는 유틸 함수
shadcn-ui가 제공하고 있는 Card 컴포넌트. 기본 스타일 값과 사용자가 입력한 값을 cn 함수의 매개변수로 넘기고 있다.
shadcn-ui가 제공하고 있는 Card 컴포넌트. 기본 스타일 값과 사용자가 입력한 값을 cn 함수의 매개변수로 넘기고 있다.
커스텀 스타일을 적용하는 모습
커스텀 스타일을 적용하는 모습
 
이제 Card를 적용한 것과 같이 Input, Button, Checkbox, Label, Switch를 추가해 처음에 봤던 예시처럼 만들어 보도록 하겠습니다.
notion image
"use client"; import { Button, Card, Checkbox, Input, Label, Switch } from "@ui"; export function LoginExample() { return ( <main className="w-screen h-screen flex justify-center items-center"> <Card.Card className="w-[30rem]"> <Card.CardHeader> <Card.CardTitle>로그인</Card.CardTitle> </Card.CardHeader> <Card.CardContent className="flex flex-col gap-y-2 pb-4"> <Input placeholder="아이디를 입력하세요." /> <Input placeholder="비밀번호를 입력하세요." type="password" /> </Card.CardContent> <Card.CardFooter className="block"> <div className="w-full flex justify-between mb-2"> <div className="flex items-center space-x-2"> <Checkbox id="keep" /> <Label htmlFor="keep">로그인 유지하기</Label> </div> <div className="flex items-center space-x-2"> <Label htmlFor="id-security">IP 보안</Label> <Switch id="ip-security" /> </div> </div> <Button className="w-full">로그인</Button> </Card.CardFooter> </Card.Card> </main> ); }
완성된 기본 UI

4. 디자인 적용하기

하지만 우리에게 필요한 건 shadcn-ui의 기본 디자인이 아닌 프로덕트 디자인입니다. 그리고 내부에서 다양하게 활용할 수 있도록 variant도 지정할 수 있으면 좋겠죠? 추가한 컴포넌트들에 커스텀 설정을 추가해 보겠습니다.
우선, 기본적으로 적용되어 있는 primary 색상 값을 변경해 보겠습니다.
/* global.css */ @layer base { :root { ...; --primary: 214 100% 46%; /* before: 222.2 47.4% 11.2% */ ...; } }
notion image
global.css에서 —primary 값을 변경하니 Button 컴포넌트의 색상이 변경되었습니다. 그런데 Input 컴포넌트의 focus 색상은 변경되지 않았네요. Input 컴포넌트에 프로덕트에서 활용할 새로운 variant를 추가해 보겠습니다.
 
shadcn-ui에서 제공하는 Input 컴포넌트는 아래와 같이 생겼습니다.
notion image
 
이제 cva를 이용해 shadcn-ui에서 제공하는 기본 스타일은 default 값으로 분리하고, 우리가 원하는 스타일은 myInput이라는 variant로 분리해 보겠습니다. 먼저, cva의 첫 번째 매개 변수로 Input 컴포넌트에 공통 적용할 스타일을, 두 번째 매개변수로 variants 값을 전달합니다. myInput에서는 border-radius 값을 제거하고, focus 상태에서 outline 색상을 primary로 변경했습니다.
const InputVariants = cva( "flex h-10 w-full px-3 py-2 text-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", { variants: { variant: { default: "rounded-md border border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", myInput: "border border-input bg-background focus:outline focus:outline-primary", }, }, defaultVariants: { variant: "default", }, } );
이제 InputPropsextends하여 Input 컴포넌트에 variants를 넘겨주고, 적용합니다.
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>, VariantProps<typeof InputVariants> {} const Input = React.forwardRef<HTMLInputElement, InputProps>( ({ className, variant, type, ...props }, ref) => { return ( <input type={type} className={cn(InputVariants({ variant, className }))} ref={ref} {...props} /> ); } );
 
적용된 모습입니다. 첫 번째 Input은 myInput으로, 두 번째 인풋은 default로 들어가 있습니다. (default는 생략할 수 있습니다.)
notion image
<Input variant="myInput" placeholder="아이디를 입력하세요." /> <Input variant="myInput" placeholder="비밀번호를 입력하세요." type="password" />
컴포넌트 props로 variant 값을 전달합니다.
 
약간 아쉬우니 Dialog도 추가해 볼까요? 비밀번호 재설정 메일 발송을 요청하는 dialog를 추가해 보겠습니다.
npx shadcn-ui@latest add dialog
CLI로 Dialog 컴포넌트를 추가하고,
import { Button, Dialog, Input } from "@ui"; import { useState } from "react"; export function IdentityRecoveryDialog() { const [open, setOpen] = useState<boolean>(false); return ( <Dialog.Dialog open={open} onOpenChange={setOpen}> <Dialog.DialogTrigger asChild> <Button variant="link">비밀번호를 잊어버리셨나요?</Button> </Dialog.DialogTrigger> <Dialog.DialogContent> <Dialog.DialogHeader> <Dialog.DialogTitle>비밀번호 재설정</Dialog.DialogTitle> <Dialog.DialogDescription>회원가입시 기입한 이메일로 비밀번호 재설정 링크를 보내 드립니다.</Dialog.DialogDescription> </Dialog.DialogHeader> <div className="flex flex-col space-y-2"> <Input variant="myInput" placeholder="아이디를 입력하세요." /> <Input variant="myInput" placeholder="가입시 사용한 이메일 주소를 입력하세요." /> </div> <Dialog.DialogFooter> <Button className="w-full" onClick={() => setOpen(false)}>비밀번호 재설정</Button> </Dialog.DialogFooter> </Dialog.DialogContent> </Dialog.Dialog> ); }
컴포넌트를 작성합니다. 필요하다면 스타일이나 variant도 추가합니다.
추가한 Dialog를 부모 컴포넌트에서 import해 줍니다.
추가한 Dialog를 부모 컴포넌트에서 import해 줍니다.
 

5. 테마 변경하기

global.css 파일에 테마별 변수 설정을 해 줍니다. 그리고 html에서 반영하면 테마를 설정할 수 있습니다. 자세한 적용 방법은 sokkanji의 Tailwind CSS 멀티 테마 구현하기 아티클에서 확인해 보실 수 있습니다.
notion image
 
@layer base { :root { ... } html[data-theme="pink"] { --primary: 320 97% 58%; } }

마무리

Headless UI와 그 일종인 shadcn-ui에 대하여 소개해 보았습니다. 이러한 접근 방식은 반복되는 작업을 줄여주고 개발에 소요되는 시간을 줄여 전체 작업 과정을 보다 효율적으로 만들어 줍니다. 커스터마이징도 꽤 편하게 가능하니 개별 프로젝트에 맞는 디자인 시스템을 구축하기 용이하다는 점도 큰 장점입니다.
팀엘리시움에서는 다음 프로젝트부터 Headless UI를 본격적으로 도입할 예정입니다. 실제 프로덕션 환경에서는 어떤 일들이 있었는지 곧 소개해 드리도록 하겠습니다. 혹시 tailwind css를 사용하고 있다면 shadcn-ui로 가볍게 도전해 보는 건 어떨까요? 😎
 

참고


hyerexx

관련된 이야기

한의사가 직접 개발한 체형분석기 아이밸런스

제품

한의사가 직접 개발한 체형분석기 아이밸런스