Headless UI v1.6, Tailwind UI 팀 관리, Tailwind Play 개선 사항 등

Date

오랜만에 우리가 진행 중인 작업에 대해 글을 쓰게 되었습니다. 사실 공유할 내용이 너무 많아서, 이 업데이트를 작성하는 주요 동기는 다음 주에 더 많은 내용이 나올 예정이기 때문입니다. 이미 출시한 내용을 공유하지 않으면 새로운 내용을 공유할 수 없다는 느낌이 들었습니다.

그러니 수영복을 입고 라운지 의자에 편안히 앉아 CSS 비타민을 흡수할 준비를 하세요.


Headless UI v1.6 출시

몇 주 전, 우리는 Tailwind UI에 React와 Vue 지원을 추가하기 위해 만든 스타일이 없는 UI 라이브러리인 Headless UI의 새로운 마이너 버전을 출시했습니다.

자세한 내용은 릴리스 노트를 확인해 보세요. 여기서는 주요 변경 사항을 간략히 소개합니다.

다중 선택 지원

ComboboxListbox 컴포넌트에 새로운 multiple prop을 추가하여 여러 옵션을 선택할 수 있게 되었습니다.

multiple prop을 추가하고 배열을 value로 바인딩하면 바로 사용할 수 있습니다:

function MyCombobox({ items }) {
  const [selectedItems, setSelectedItems] = useState([])

  return (
    <Combobox value={selectedItems} onChange={setSelectedItems} multiple>
      {selectedItems.length > 0 && (
        <ul>
          {selectedItems.map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      )}
      <Combobox.Input />
      <Combobox.Options>
        {items.map((item) => (
          <Combobox.Option key={item} value={item}>
            {item}
          </Combobox.Option>
        ))}
      </Combobox.Options>
    </Combobox>
  )
}

더 자세한 내용은 combobox 문서listbox 문서를 참고하세요.

Nullable 콤보박스

v1.6 이전에는 콤보박스의 내용을 삭제하고 탭을 이동하면 이전에 선택한 옵션이 복원되었습니다. 이는 대부분의 경우 유용하지만, 때로는 콤보박스의 값을 완전히 지우고 싶을 때도 있습니다.

이제 nullable prop을 추가하여 이를 가능하게 했습니다. 이 prop을 추가하면 이전 값이 복원되지 않고 값을 삭제할 수 있습니다:

function MyCombobox({ items }) {
  const [selectedItem, setSelectedItem] = useState([])

  return (
    <Combobox value={selectedItems} onChange={setSelectedItem} nullable>
      <Combobox.Input />
      <Combobox.Options>
        {items.map((item) => (
          <Combobox.Option key={item} value={item}>
            {item}
          </Combobox.Option>
        ))}
      </Combobox.Options>
    </Combobox>
  )
}

쉬운 HTML 폼 지원

이제 Listbox, Combobox, Switch, RadioGroup 같은 폼 컴포넌트에 name 속성을 추가하면, 컴포넌트의 값과 동기화되는 숨겨진 입력 필드를 자동으로 생성합니다.

이를 통해 일반 폼 제출이나 Remix<Form> 컴포넌트 같은 것을 사용해 서버로 데이터를 쉽게 보낼 수 있습니다.

<form action="/projects/1/assignee" method="post">
  <Listbox
    value={selectedPerson}
    onChange={setSelectedPerson}
    name="assignee"
  >
    {/* ... */}
  </Listbox>
  <button>Submit</button>
</form>

이 기능은 숫자나 문자열 같은 간단한 값뿐만 아니라 객체와도 작동합니다. 객체는 1996년부터 사용된 대괄호 표기법을 사용해 여러 필드로 자동 직렬화됩니다.

<input type="hidden" name="assignee[id]" value="1" />
<input type="hidden" name="assignee[name]" value="Durward Reynolds" />

이 내용을 다른 도메인에서 다시 읽고 싶다면 문서를 확인해 보세요.

스크롤 가능한 다이얼로그 개선

다이얼로그는 세상에서 가장 만들기 어려운 요소 중 하나입니다. 우리는 스크롤 문제와 오랫동안 씨름해왔고, 드디어 v1.6에서 모든 문제를 해결했다고 생각합니다.

핵심은 “외부 클릭 시 닫기” 기능의 동작 방식을 변경한 것입니다. 이전에는 실제 다이얼로그 뒤에 배치하는 Dialog.Overlay 컴포넌트를 사용했고, 그 위에 클릭 핸들러를 추가해 클릭 시 다이얼로그를 닫도록 했습니다. 원칙적으로 이 방식의 단순함은 정말 마음에 들었습니다. 특정 엘리먼트를 클릭했는지 감지하는 것은 특정 엘리먼트 외의 다른 것을 클릭했는지 감지하는 것보다 훨씬 덜 까다롭습니다. 특히 다이얼로그 내부에 포털 등을 통해 렌더링된 다른 요소가 있는 경우에는 더욱 그렇습니다.

하지만 이 방식의 문제는 스크롤이 필요한 긴 다이얼로그가 있을 때, 오버레이가 스크롤바 위에 위치하게 되어 스크롤바를 클릭하려고 하면 다이얼로그가 닫히는 것이었습니다. 이는 원하는 동작이 아니죠!

이 문제를 해결하기 위해, 우리는 비호환적인 변경 없이 새로운 Dialog.Panel 컴포넌트를 추가했습니다. 이제는 오버레이를 클릭할 때가 아니라, 이 컴포넌트 외부를 클릭할 때마다 다이얼로그를 닫도록 변경했습니다:

<Dialog
  open={isOpen}
  onClose={closeModal}
  className="fixed inset-0 flex items-center justify-center ..."
>
  <Dialog.Overlay className="fixed inset-0 bg-black/25" />
  <div className="fixed inset-0 bg-black/25" />

  <div className="bg-white shadow-xl rounded-2xl ...">
  <Dialog.Panel className="bg-white shadow-xl rounded-2xl ...">
    <Dialog.Title>Payment successful</Dialog.Title>
    {/* ... */}
  </div>
  </Dialog.Panel>
</Dialog>

새로운 패널 컴포넌트를 사용한 더 완전한 예제는 업데이트된 다이얼로그 문서에서 확인할 수 있습니다.

더 나은 포커스 트래핑

다이얼로그가 세상에서 가장 만들기 어려운 것 중 하나인 이유 중 하나는 포커스 트래핑 때문입니다. 우리는 처음에 탭 키를 가로채고 다음/이전 엘리먼트에 수동으로 포커스를 이동시켜 포커스 트랩의 끝에 도달하면 다시 첫 번째 항목으로 돌아가는 방식으로 시도했습니다.

이 방법은 포커스 트랩 내부에 포털을 사용하기 시작할 때까지는 괜찮게 작동했습니다. 하지만 이제는 다이얼로그 내부에 개념적으로는 있지만 스타일링을 위해 포털로 렌더링된 데이트피커나 다른 요소에 탭으로 이동할 수 있기 때문에 관리가 거의 불가능해졌습니다.

Robin은 이 문제에 대한 정말 멋진 해결책을 제안했습니다. 이 방법은 매우 간단합니다 — 탭 작동 방식을 수동으로 제어하려고 하지 말고, 포커스 트랩의 시작과 끝에 보이지 않는 포커스 가능한 엘리먼트를 추가하는 것입니다. 이제 이 센티넬 엘리먼트 중 하나가 포커스를 받을 때마다, 사용자가 첫 번째 엘리먼트인지 마지막 엘리먼트인지, 그리고 사용자가 앞으로 탭을 이동했는지 뒤로 이동했는지에 따라 실제로 포커스가 이동해야 할 위치로 포커스를 이동시키면 됩니다.

이 방법을 사용하면 탭 키를 전혀 가로챌 필요가 없습니다 — 브라우저가 모든 작업을 수행하도록 하고, 센티넬 엘리먼트가 포커스를 받을 때만 수동으로 포커스를 이동시키면 됩니다.

이 방법을 알아낸 후, 몇 가지 다른 라이브러리들도 이미 같은 방법을 사용하고 있다는 것을 알게 되었습니다. 그래서 이 방법이 완전히 새로운 것은 아니지만, 이 기술을 생각해내지 못한 사람들에게는 꽤 영리하고 공유할 가치가 있다고 생각했습니다.


Tailwind UI의 팀 관리 기능

우리가 처음 Tailwind UI를 출시했을 때, “팀”은 단지 저와 Steve뿐이었습니다. 그래서 우리 둘만으로 작업하면서 제품을 출시하려면 많은 부분을 단순하게 유지해야 했습니다.

그 중 하나가 팀 라이선스였습니다. 우리는 멋진 팀원 초대 기능 같은 것을 제공하지 않았고, 대신 사용자들에게 팀원들과 Tailwind UI 자격 증명을 공유하도록 요청했습니다. 이 방식은 제품을 출시하는 데 충분했습니다. Tailwind UI는 사용자별로 다른 경험을 제공하지 않기 때문에, 팀원 모두가 동일한 경험을 얻을 수 있었기 때문입니다.

또한, 팀원들의 이메일 주소를 수집하고, 이를 어떤 폼에 입력한 다음, 각자에게 초대 이메일을 보내고, 그들이 초대를 수락하도록 하는 과정은 관리상의 지옥처럼 느껴졌습니다. 특히 모든 사람이 로그인 후 동일한 경험을 얻는다면 더욱 그랬습니다.

하지만 어떤 자격 증명을 공유하는 것은 상당히 저급한 방식이며, 우리가 자랑스럽게 여길만한 디자인 결정은 아니었습니다. 저는 Tailwind UI와 은행 계좌에 동일한 비밀번호(slayerfan1234)를 사용합니다. 이걸 누구와도 공유하고 싶지 않습니다!

그래서 몇 주 전에 우리는 이 문제를 해결하고 무언가를 만들어보기로 결정했습니다.

복사 가능한 초대 링크와 팀원 목록이 있는 인터페이스

우리가 선택한 방식은 순수하게 링크 기반의 초대 시스템입니다. 초대 링크를 복사하여 Slack/Discord 등에서 팀원들과 공유하고, 필요할 때 링크를 재설정할 수 있습니다. 또한 “멤버” 또는 “소유자” 권한을 부여할 수 있으며, 이는 팀원 관리나 결제 내역 조회 권한을 제어합니다.

이를 통해 지루한 데이터 입력 없이도 팀원을 쉽게 초대할 수 있으며, 누군가가 팀을 떠나면 공유 비밀번호를 변경하는 대신 UI에서 바로 접근을 해제할 수 있습니다.

이 기능은 Tailwind UI 팀 계정을 가진 모든 사용자에게 제공됩니다. 드롭다운 메뉴를 열고 “My Team”을 클릭하여 팀 이름을 지정하고 동료를 초대할 수 있습니다.

Tailwind UI 웹사이트에서 팀 라이선스를 구매하거나, 개인 라이선스를 가지고 있다면 팀 라이선스로 업그레이드하여 팀과 함께 Tailwind UI를 사용할 수 있습니다.


Tailwind UI의 Vue 예제를 <script setup>으로 업데이트하기

Tailwind UI에 Vue 지원을 출시한 이후, Vue 3의 새로운 <script setup> 문법이 단일 파일 컴포넌트를 작성하는 권장 방식으로 자리 잡았습니다.

우리는 Tailwind UI의 모든 Vue 예제를 이 새로운 형식으로 업데이트했으며, 이를 통해 많은 보일러플레이트 코드를 제거할 수 있었습니다:

<template>
  <Listbox as="div" v-model="selected">
    <!-- ... -->
  </Listbox>
</template>

<script setup>
import { ref } from 'vue'
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '@headlessui/vue'
import { CheckIcon, SelectorIcon } from '@heroicons/vue/solid'

const people = [
  { id: 1, name: 'Wade Cooper' },
  // ...
]

const selected = ref(people[3])
</script>

가장 큰 장점은 더 이상 components 아래에 명시적으로 컴포넌트를 등록할 필요가 없다는 것입니다. 스코프 내에 있는 모든 컴포넌트는 템플릿에서 자동으로 사용할 수 있습니다.

<script setup>을 사용하면 Headless UI의 React 버전에서처럼 Listbox.Button과 같은 네임스페이스 컴포넌트를 사용할 수도 있습니다. 아직 Headless UI를 이 방식으로 업데이트하지는 않았지만, 곧 업데이트할 예정입니다. 이를 통해 많은 import 문을 줄일 수 있을 것입니다.


VS Code를 위한 새로운 Tailwind CSS 언어 모드

Tailwind는 @tailwind@apply와 같은 비표준 at-규칙을 많이 사용하기 때문에, 일반 CSS 언어 모드를 사용하면 VS Code에서 린트 경고가 발생합니다.

이 문제를 해결하기 위해, 우리는 항상 PostCSS Language Support 플러그인을 사용하도록 권장해 왔습니다. 이 플러그인은 이러한 경고를 제거하지만, 다른 CSS IntelliSense 기능도 모두 제거합니다.

그래서 몇 주 전, 우리는 Tailwind CSS IntelliSense 확장의 일부로 첫 번째 Tailwind CSS 언어 모드를 출시했습니다. 이 언어 모드는 내장된 CSS 언어 모드를 기반으로 하여 Tailwind 전용 구문 강조를 추가하고, 일반적으로 보게 되는 린트 경고를 수정하며, 유지하고 싶은 CSS IntelliSense 기능을 잃지 않습니다.

내장 CSS 언어 모드를 사용할 때 린트 경고가 표시되는 샘플 CSS 코드와 Tailwind CSS 언어 모드를 사용할 때 린트 경고가 없는 샘플 CSS 코드.

Tailwind CSS IntelliSense의 최신 버전을 다운로드하고 CSS 파일의 언어 모드로 “Tailwind CSS”를 선택하여 사용해 보세요.

Tailwind Play의 “생성된 CSS” 패널

지난 몇 달 동안 Tailwind Play에 여러 가지 작은 개선 사항을 추가했는데, 그중에서도 가장 마음에 드는 것은 새로운 “생성된 CSS” 패널입니다.

Tailwind Play 인터페이스와 해당 플레이그라운드에서 생성된 CSS를 보여주는 패널

이 패널은 여러분의 HTML에서 생성된 모든 CSS를 보여주고, 레이어별로 필터링할 수 있어 문제 해결에 매우 유용합니다. 내부적으로 우리는 이 기능을 항상 사용하여 클래스가 감지되지 않는 이상한 문제를 디버깅하고, 이를 해결하기 위해 필요한 끔찍한 정규식 수술을 수행합니다.

또한 각 패널에 “정리” 버튼(Cmd + S)을 추가하여 코드를 자동으로 포맷하고(클래스를 정렬합니다!) “복사” 버튼(Cmd + A Cmd + C, 하지만 이미 알고 계시겠죠)도 추가했습니다.


Refactoring UI 웹사이트 리디자인

2018년 12월 Refactoring UI를 출시했을 때, 스티브와 나는 출시 전날 밤 1시쯤에 최종 랜딩 페이지를 디자인하고 구축했습니다.

당시 우리는 멋진 랜딩 페이지를 디자인했지만, 메일링 리스트에 보낼 공지 이메일을 작성하면서 “이 이메일 내용이 정말 훌륭하고, 현재 랜딩 페이지 디자인보다 훨씬 매력적이다”라는 생각이 들었습니다.

하지만 이 내용은 기존 디자인에 잘 맞지 않았기 때문에, 마지막 순간에 모든 디자인을 버리고 새로운 내용을 바탕으로 훨씬 간단한 페이지를 급하게 만들었습니다. 결과는 괜찮았지만, 우리가 원했던 아름다운 경험과는 거리가 있었습니다.

그래서 몇 주 전, 우리는 드디어 새로운 디자인을 결정했습니다.

리디자인된 Refactoring UI 웹사이트의 헤더 섹션.

나는 이 책에 대해 여전히 매우 자랑스럽게 생각합니다. 아마도 우리가 만든 그 어떤 것보다도 더 그럴 것입니다. Goodreads에서 4.68점의 평점을 받았고, 1100개 이상의 평가와 200개 가까운 리뷰가 달렸습니다. 자체 출판 전자책으로서는 정말 놀라운 성과라고 생각합니다.

앞으로 우리가 배운 모든 것을 담아 두 번째 판을 출간할 날을 기대하고 있습니다!


Tailwind CSS 템플릿이 곧 출시됩니다

트위터에서 조금 공개했지만, 지난 몇 달 동안 우리는 본격적인 Tailwind CSS 웹사이트 템플릿을 열심히 작업해 왔습니다.

여기 그 중 하나를 미리 보여드리겠습니다. Next.js와 Stripe의 새로운 Markdoc 라이브러리를 사용해 만든 문서 사이트 템플릿입니다:

모바일과 데스크톱 레이아웃, 그리고 라이트와 다크 색상 스킴을 포함한 문서 사이트 디자인 아트보드.

이 템플릿들을 공개하게 되어 정말 기쁩니다. Tailwind UI를 제품으로서 자랑스럽게 생각하지만, 복사-붙여넣기 가능한 코드 스니펫 형식의 한계는 여러분에게 컴포넌트화, 중복 최소화, 그리고 완전한 프로덕션 준비 웹사이트로서의 설계 방법을 보여줄 기회가 없다는 점이었습니다.

지금 작업 중인 템플릿들은 그 격차를 메우는 데 탁월할 것입니다. 여러분의 프로젝트를 시작하기 위한 아름다운 템플릿을 제공하는 것 외에도, 코드를 파헤쳐서 우리가 Tailwind CSS로 웹사이트를 어떻게 구축하는지 정확히 연구할 수 있을 것입니다.

아직 정확한 출시일은 정해지지 않았지만, 다음 달 안에 무언가를 공개할 수 있기를 바라고 있습니다. 더 많은 진전이 있을 때마다 공유하겠습니다!