e-commerce 프로젝트 중 상품 목록을 리스트 형식으로 보여줄 DataTable 의 구현이 필요로 하였다. 간단한 마크업과 함께 로직을 만들어 구현할 수 도 있었고, MUI/antD와 같은 UI 라이브러리 에서 제공하는 Table 컴포넌트를 쉽게 사용할 수도 있었다.
하지만, 이번 프로젝트는 ’1인분을 할 수 있는 개발자인가 스스로 테스트’ 하는 목적을 가지고 있었기 때문에, ‘내가 실무에서 이 컴포넌트를 구현한다면 어떻게 할 수 있을까!’ 즉 단순 구현을 위한 컴포넌트가 아니라, 잘 만든 컴포넌트 하나를 만들고 싶은 욕심이 생겼다.
이러한 과정에서 기존 Table을 만들 수 있는 라이브러리 들을 찾아보았고, 그 중에서도 마크업과 스타일의 제약을 받지 않으면서, 유지보수에 장점이 있는 Headless 형태의 라이브러리들이 매우 인상적이었다.
따라서 DataTable이라는 나만의 작은 Headless 라이브러리를 만들어보기로 결심하게 되었다.
Why Headless
Headless Component란
A component that doesn’t have a UI, but has the functionality.
Headless 라이브러리에서 제공하는 hooks는 어떤 마크업이나 스타일에 관여하지 않는다.
오직 해당 컴포넌트의 상태와 상태를 제어하기 위한 인터페이스만을 노출함으로, 스타일에 대한 부분은 전적으로 사용하는 개발자에게 위임하는 것이다.
따라서 Component 기반 UI 라이브러리(MUI/antD)와 같이 각 라이브러리의 독자적인 스타일 안에서 개발해야하는 제약에서 벗어날 수 있다.
Headless로 얻을 수 있는 장점
⭐
스타일 관련 코드와 상태 관련 코드를 구분하여 “관심사를 분리”할 수 있고,
이는 코드의 “유지보수에 유리”하다.
[관심사의 분리]
Headless 방식을 통해 ‘UI에 스타일을 하는 코드’와 ‘비즈니스 로직에 따라 어떻게 보여져야하는가를 결정하는 코드’를 “구분”할 수 있다.
이를 구분해야하는 이유는 간단하다. 바로 “변경 가능성” 이다.
DataTable 관련 요구사항이 변경될 때, 테이블 내부의 폰트크기/색상/정렬 등의 스타일 관련 코드는 변경 사항이 높은 반면, 주어진 data를 주어진 column에 맞게 보여주는DataTable 고유의 데이터 처리 로직은 변경 가능성이 낮다.
변경가능성에 따른 분리는 변경 요구 사항이 발생했을 때,빠르게 변경이 필요한 범위를 제한할 수 있으며,이에 따라 임시방편으로 스타일과 상태를 다루는 코드가 뒤섞여 기술 부채가 늘어나는 현상을 방지할 수 있다.
useDataTable Hooks 개발/고민과정
UI가 가지고 있는 상태를 ‘추상화’하여 그것을 모듈화하는 것으로 Headless를 구현할 수 있다. 스타일은 걷어내고 이 UI가 내포하고 있는 상태는 무엇이며 이 상태를 관리하기 위한 적절한 자료구조는 무엇인지 고민하는 것이 우선이다. 그리고 그 상태를 제어할 수 있는 최소한의 API만을 제공한다.
1) 가장 작은 형태의 DataTable 생각해보기
DataTable은 dataSource 그를 표현할 속성(column)을 기준으로 data를 배치한 테이블 이다.
즉 dataSource를 주어진 속성(tableModel)에 맞게 배치해야한다.
filter, search, sort는 부가 기능
DataTable 자체가 filter/search를 위한 UI 까지 가지는 것이 아니라,
filtering을 위해 정해진 인터페이스를 오픈하고, 이를 처리하는 로직만을 DataTable이 가지고 있어 Filter/Search 컴포넌트와의 의존성을 최대한 줄이기
filtering은 로직 filterQuery를 prop으로 받아 적용한다.
ex.{ category: ‘곡류’ , id: [1,2,3] } : 곡류 중에서 id가 1,2,3 인 Product만을 렌더링
2) TableModel이 가지고 있는 인터페이스는 무엇이어야 할까
DataTable 은 dataSource를 tableModel 에 맞게 배치된 2x2 형태의 Table 엘리먼트로 정의하였다.
accessor
Cell 에 보여주기 위한 data를 찾기 위한 key 값으로,
반드시 DataSource의 포함되어 있는 key값이어야만 한다.
render
headerPropscellProps
custom 값을 표현하기 위한 함수로 필요 위치에 따라 header, cell 프로퍼티의 함수로 정의할 수 있다.
5000 이라는 price를 5,000원 으로 파싱하거나, 경우에 따라 Element형태로 가공할 수 있어야 한다.
render함수에서의 props 값들은 구현단에서 지정해줄 수 있으며, 일반적으로 각 header와 cell에서의 value를 포함하고 있는 useDataTable hook 에서 내려주는 값으로 지정한다.
tableModel 구현체 ( 화살표 눌러펼치기)
TableModel
구현부 ProductDataTable
headerGroups와 rowModel에서는 현제 Cell의 value정보를 포함하고 있다.
3) 부가 기능과 DataTable을 분리하여 의존성 줄이기
DataTable은 카테고리 필터/검색 등의 1)필터링과 테이블 요소의 수정과 삭제 등의 2)Action 등의 부가 기능을 요구사항으로 가지게 된다.
하지만 DataTable 본래의 역할은 데이터를 받아 보여주기 위한 역할이기 때문에, filter/추가/삭제 등의 로직과 분리되고, 이 로직들이 약하게 연결되어야 한다고 생각했다.
[1. filterQuery를 통한 Filtering]
따라서 DataTable 컴포넌트와 Filter/Search 컴포넌트가 분리되어 있고, 두 컴포넌트가 queryParams를 통해 약한 연결 관계를 가지는 구조로 설계하였다.
1) Filter/Search 컴포넌트는 filter 조건을 약속된 형식의 queryString으로 표현하는 역할을 담당
2) DataTable은 현재 url로 부터 queryParameter를 읽어와 해당하는 data를 표현해주는 역할을 담당
이를 통해 UI결합도를 낮출 수 있고, 각 컴포넌트의 역할과 책임을 명확하게 구분할 수 있다.