기타

Headless UI란?

citron031 2023. 9. 3. 13:11

Headless UI는 UI 구성 요소들을 시각적인 디자인 없이 제공하는 방식을 말한다.

즉, 구성 요소가 제공되지만 UI 디자인이 제공되지는 않기에 직접 스타일을 작성해야 한다.

☁️ 이는 기존의 Material-UI나 Chakra UI같은 UI 컴포넌트 라이브러리와의 차이점으로 UI 컴포넌트 라이브러리는 완전한 스타일과 디자인이 구축된 UI가 제공된다.

일반적인 UI 라이브러리나 프레임워크는 시각적인 스타일과 디자인이 내장되어 있으며, 이를 커스터마이징하거나 디자인을 변경하는 데에는 일부 제한이 있을 수 있지만, Headless UI는 이러한 디자인적인 제약을 없애고, 개발자가 커스텀 디자인을 자유롭게 적용할 수 있다.

 

이러한 디자인 없는 구성 요소들은 개발자가 UI의 동작을 정의하고 접근성과 상호작용을 쉽게 구현하는 데에 도움을 준다.


❄️ Headless UI는 "Headless"라는 용어를 사용하여 제목(Head)만 남기고 본문(Contents)을 제거한 상태를 가리키는데, 이는 시각적인 부분(헤드) 없이 로직이나 동작(콘텐츠)만을 포함하는 구성 요소를 의미한다.

이러한 방식으로 구현된 Headless UI는 주로 모던 프론트엔드 프레임워크나 라이브러리에서 사용되며, 개발자들이 필요에 맞게 디자인을 직접 적용하면서도 접근성과 상호작용을 향상시키는 데에 도움을 준다.

이러한 구성 요소들은 개별적으로 또는 다른 UI 라이브러리와 조합하여 사용할 수 있으며, 커스터마이징이 용이하다는 장점을 가진다.

 

대표적인 React의 Headless UI 라이브러리는 radix-ui와 Adobe에서 관리하는 react-aria가 있다.

이 라이브러리의 사용법은 mui와 같은 컴포넌트 라이브러리와 유사하지만, 스타일링을 직접 해야하는 점이 다르다.

 

🌈 실제 프로덕트를 만들 때, 기본적으로 주어지는 UI 컴포넌트 라이브러리를 사용하면, 디자인이 어디서 본듯한, 다소 널리 사용되는 UI 디자인이 되어버린다.

🦄 프로덕트에서는 스타일의 개성도 중요한 부분중 하나이기에, 개발자가 사용하기 쉬운 UI 컴포넌트를 제공 받으면서도, CSS는 처음부터 원하는대로 커스텀할 수 있는 Headless UI의 장점이 있는 것 같다.

ex. radix-ui의 dialog 예제 (공식 문서 참조)

- npm i @radix-ui/colors @radix-ui/react-dialog @radix-ui/react-icons

 

☀️ DialogDemo.tsx

import React from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import styles from "./DialogDemo.module.css";

const DialogDemo = () => (
  <Dialog.Root>
    <Dialog.Trigger asChild>
      <button className={`${styles.Button} ${styles.violet}`}>
        Edit profile
      </button>
    </Dialog.Trigger>
    <Dialog.Portal>
      <Dialog.Overlay className={styles.DialogOverlay} />
      <Dialog.Content className={styles.DialogContent}>
        <Dialog.Title className={styles.DialogTitle}>Edit profile</Dialog.Title>
        <Dialog.Description className={styles.DialogDescription}>
          Make changes to your profile here. Click save when you're done.
        </Dialog.Description>
        <fieldset className={styles.Fieldset}>
          <label className={styles.Label} htmlFor="name">
            Name
          </label>
          <input
            className={styles.Input}
            id="name"
            defaultValue="Pedro Duarte"
          />
        </fieldset>
        <fieldset className={styles.Fieldset}>
          <label className={styles.Label} htmlFor="username">
            Username
          </label>
          <input
            className={styles.Input}
            id="username"
            defaultValue="@peduarte"
          />
        </fieldset>
        <div
          style={{ display: "flex", marginTop: 25, justifyContent: "flex-end" }}
        >
          <Dialog.Close asChild>
            <button className={`${styles.Button} ${styles.green}`}>
              Save changes
            </button>
          </Dialog.Close>
        </div>
        <Dialog.Close asChild>
          <button className={styles.IconButton} aria-label="Close">
            <Cross2Icon />
          </button>
        </Dialog.Close>
      </Dialog.Content>
    </Dialog.Portal>
  </Dialog.Root>
);

export default DialogDemo;

👍 globals.css

@import "@radix-ui/colors/black-alpha.css";
@import "@radix-ui/colors/green.css";
@import "@radix-ui/colors/mauve.css";
@import "@radix-ui/colors/violet.css";

/* reset */
button,
fieldset,
input {
  all: unset;
}

✦ DialogDemo.module.css

.DialogOverlay {
  background-color: var(--black-a9);
  position: fixed;
  inset: 0;
  animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
}

.DialogContent {
  background-color: white;
  border-radius: 6px;
  box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
    hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 90vw;
  max-width: 450px;
  max-height: 85vh;
  padding: 25px;
  animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
.DialogContent:focus {
  outline: none;
}

.DialogTitle {
  margin: 0;
  font-weight: 500;
  color: var(--mauve-12);
  font-size: 17px;
}

.DialogDescription {
  margin: 10px 0 20px;
  color: var(--mauve-11);
  font-size: 15px;
  line-height: 1.5;
}

.Button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  padding: 0 15px;
  font-size: 15px;
  line-height: 1;
  font-weight: 500;
  height: 35px;
}
.Button.violet {
  background-color: white;
  color: var(--violet-11);
  box-shadow: 0 2px 10px var(--black-a7);
}
.Button.violet:hover {
  background-color: var(--mauve-3);
}
.Button.violet:focus {
  box-shadow: 0 0 0 2px black;
}
.Button.green {
  background-color: var(--green-4);
  color: var(--green-11);
}
.Button.green:hover {
  background-color: var(--green-5);
}
.Button.green:focus {
  box-shadow: 0 0 0 2px var(--green-7);
}

.IconButton {
  font-family: inherit;
  border-radius: 100%;
  height: 25px;
  width: 25px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--violet-11);
  position: absolute;
  top: 10px;
  right: 10px;
}
.IconButton:hover {
  background-color: var(--violet-4);
}
.IconButton:focus {
  box-shadow: 0 0 0 2px var(--violet-7);
}

.Fieldset {
  display: flex;
  gap: 20px;
  align-items: center;
  margin-bottom: 15px;
}

.Label {
  font-size: 15px;
  color: var(--violet-11);
  width: 90px;
  text-align: right;
}

.Input {
  width: 100%;
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  padding: 0 10px;
  font-size: 15px;
  line-height: 1;
  color: var(--violet-11);
  box-shadow: 0 0 0 1px var(--violet-7);
  height: 35px;
  color: #fff;
}
.Input:focus {
  box-shadow: 0 0 0 2px var(--violet-8);
}

@keyframes overlayShow {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes contentShow {
  from {
    opacity: 0;
    transform: translate(-50%, -48%) scale(0.96);
  }
  to {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
}

 

위의 예제에서는 Headless UI를 사용하고, CSS 스타일링은 따로 CSS Module을 사용하였다.

이처럼, UI 구성요소만 가져오고 스타일링은 따로 하는 방식이 Headless UI 컴포넌트를 사용한 UI 구축 방법이다.