기능과 UI를 분리하는 Headless Component 이야기
Headless Component란?
UI 컴포넌트는 크게 로직과 상태, 그리고 UI 요소로 구성되지요. Headless는 단어에서 드러나듯이 "머리 없는"이라는 뜻으로, 머리(head)는 사용자에게 보여지는 UI를 상징하니, 시각적인 부분을 직접 제어하지 않는 것을 의미하는 거예요. 따라서, Headless Component는 UI가 없이 로직이나 상태 관리만을 담당하는 컴포넌트를 의미합니다. 이러한 컴포넌트는 UI의 구현을 외부에 위임하며, 개발자는 이를 사용하여 자신만의 커스텀 UI를 쉽게 만들 수 있습니다.
나오게 된 배경
기존의 전통적인(?) UI 라이브러리로 상징되는 MUI(Material UI)나 ant design은 로직과 UI, 스타일이 통합된 형태예요. 크게 손댈 것 없이 바로 갖다쓸 수 있고 취향에 따라서는 미려하고 훌륭한 스타일이라서 만족스럽고 편한 도구지요. 하지만 로직과 스타일 모두 이런 UI 라이브러리에 강하게 종속되고 의존하게 되며, 맞춤 구현(customization)할 여지도 적은 단점이 있습니다. Theme 기능으로 색상이나 크기를 바꾸는 정도죠. 게다가 사용할 하위 컴포넌트에 상태 정보와 스타일 정보를 전달하게 되는 불편하고 찝찝한 구현을 하도록 몰리기도(?) 해요.
무엇보다도 자체 디자인 시스템을 구축한 프로젝트나 팀에서는 이런 UI 라이브러리를 사용하거나 재사용하기 어려워요. 몇 년 전에 React와 ant design을 사용하는 프로젝트에서 디자이너의 디자인 산출물과 ant design이 표현하는 UI가 미묘하게 달라 hacky하게 처리하며 몹시 찝찝해했던 기억이 새록새록 떠오르네요.
“아, 저 UI 라이브러리에서 로직과 상태 관리만 딱 떼어내 쓰고 싶은데...”
이런 생각을 저 말고도 많은 사람이 했나봅니다. Headless Component는 그런 고민과 한계를 극복하려는 패턴이거든요.
위 이미지는 웹 개발에서 Headless Component 개념을 도식화한 거예요. 중앙에 있는 층이 바로 Headless 컴포넌트로, 시각적인 UI를 가지고 있지 않으며 데이터 및 상호작용 로직을 담당합니다. 맨 오른쪽에 있는 View 계층은 어떠한 상태가 있고 어떻게 관리되는지는 모릅니다. 서로 의존성이 없이 분리된 것이죠. 이로써, Headless Component는 유연한 UI 개발을 가능하게 하며, 개발자가 자신의 요구사항에 맞추어 UI를 자유롭게 설계할 수 있도록 합니다.
푸딩캠프 사례 : TanStack Table + shadcn/ui + tailwindcss
(1) 배경
푸딩캠프는 학습 관리 시스템(LMS)을 자체 개발하고 있어요. 이번 달에 예정된 베타 버전에 본격 적용되는데, 주요 기능 중 하나는 학습자 별로 학습 상황을 테이블로 펼쳐 보는 거지요. 행 방향(x축)엔 학습자들이, 열 방향(y축)엔 코스 별 학습 프로그램이 나열되어 어느 학습자에게 어떤 도움이 필요한지, 설계한 의도대로 학습이 이뤄지는지 관찰하는 것이 목적입니다.
이 기능은 행과 열 모두 동적으로 변해야 합니다. 그런데 행(row)은 동적으로 상태를 다루는 데 전혀 지장없지만 열(column)도 동적으로 상태를 편하고 직관적인 코드로 관리할 수 있는 테이블 컴포넌트를 찾기 어렵더라고요. 게다가 푸딩캠프에서 React로 구현한 UI 영역은 shadcn/ui를 사용하고, 세부 스타일링은 tailwindcss를 사용하고 있던 터라 복잡한 테이블 로직을 푸딩캠프 환경에 녹여내야 했죠.
(2) TanStack Table
관리자 페이지엔 테이블 컴포넌트가 무수히 사용되는 터라 고민하던 중, TanStack Table을 접했습니다. 구 )React Query, 현 TanStack Query로 유명한 TanStack에서 개발하는 Headless 테이블 컴포넌트예요. 푸딩캠프에 필요한 기능을 갖추면서도 테이블 로직과 상태 관리만 제공하니 딱 상황에 적격이죠.
(3) tailwindcss
tailwindcss는 유틸리티 우선(utility-first) CSS 프레임워크로 많은 CSS를 미리 정의한 유틸리티 클래스로 제공해요. 쉽게 말해서 스타일 정보를 작은 단위에 담고 CSS의 스타일 정보를 유추하기 좋은 이름을 붙인 유틸리티지요. 유지보수하기 까다로운 CSS 스타일 정보를 코드 차원에서 일관성 있고 확장하기 좋게 구성해서 유지보수하기 좋아 애용하고 있습니다.
(4) shadcn/ui
shadcn/ui는 복붙(copy/paste)을 미덕으로 하는 UI 컴포넌트예요. 로직이나 상태는 UX 차원에서 최소한으로 담고 있고, 스타일은 tailwindcss에 의존해요. tailwindcss에 의존한다고 해도 tailwindcss의 클래스 이름을 사용할 뿐이어서 tailwindcss가 없어도 각 컴포넌트는 제 역할을 충실히 합니다. 그래서 의존성 설치를 하지 않고, 컴포넌트 코드를 통채로 복사(hard copy)할 수 있어요. 실제로 shadn/ui의 컴포넌트를 설치하면 해당 컴포넌트의 소스 파일이 복사되어 위치해서 package.json에 의존성이 추가되지 않죠.
이 셋을 조합해 학습자의 학습 상황을 모니터링하는 테이블을 구현하다보니 이런 생각이 들었어요.
-
shadn/ui : HTML
-
tailwindcss : CSS
-
TanStack Table : JavaScript
좋았던 점
(1) 효율적인 기술스택 운용과 일관성 유지
푸딩캠프는 아주 작은 팀이 개발하고 있어요. 그래서 관리 요소를 늘리지 않는 게 중요하죠. 늘리더라도 기존에 사용하던 기술스택과 연동이 쉬워야하며, 적용을 한 이후에도 코드와 기술스택의 일관성을 최대한 유지해야 하죠.
그런 점에서 TanStack Table, shadcn/ui, tailwindcss는 만족스럽습니다. shadcn/ui는 사실상 코드를 복사하는 컴포넌트 틀이다보니 로직이나 상태를 주입하기 위해 고민하는 일이 없습니다. 그냥 HTML 에 CSS를 적용하는 느낌 정도예요.
tailwindcss는 디자인 시스템이 없는 상황에서 코드 차원에서 일관성을 유지하면서도 기능적으로 의존하지 않는데다, 다른 기술스택과 연동하거나 접목하는 데 있어서 학습 비용이 거의 들지 않고요. 스타일 이름만 갖다 쓰는 느낌이거든요.
마지막으로 TanStack Table은 초기 학습 곡선이 어느 정도 가파른 편입니다. 테이블 컴포넌트가 제대로 만들기엔 까다롭기도 하고요. 하지만 이해하고 나면 화면에 표시(렌더링)하는 데 들이는 구현과 학습 비용은 거의 들지 않아요.
예시 코드인데, 마치 원래 한 몸이었던 것처럼 위화감 없이 조합되었지만, 실제로는 서로를 의존하지 않아요.
(2) 인공지능 활용성 증대
저는 디자인 작업을 Vercel의 v0에 종종 맡기는데요. 특히 유용한 점은 v0가 tailwindcss를 기가 막히게 잘 쓴다는 점이에요. 게다가 로직이 없는 UI 코드만 생성해주죠. 그래서 마치 shadcn/ui 컴포넌트를 복사해 사용하듯이 v0가 생성한 코드를 거의 그대로 가져다 컴포넌트를 만들고, Headless 컴포넌트로 로직을 쉽게 적용합니다.
서버측 렌더링 부분도 shadcn/ui와 비슷한 Pines UI와 tailwindcss를 사용하기 때문에 Back-end 기술스택과 Front-end 기술스택이 매우 상이한데도 구현은 동일 기술스택을 쓰는 느낌을 받습니다. 인공지능을 활용할 때도 거의 동일하게 작업이 이뤄지고요.