HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
💻
프로그래밍
/
👨🏽‍💻
Headless한 데이터 테이블 컴포넌트 개발기
👨🏽‍💻

Headless한 데이터 테이블 컴포넌트 개발기

태그
컴포넌트 개발
관심사 분리
대주제
학습포스팅
상태
완료
수정필요 부분
중분류
React
생성일
Dec 20, 2022 05:55 AM

목차

1) Intro 2) Why Headless 3) dataTable 개발과정/고민사항 4) 결과 및 한계점

Intro

notion image
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)에 맞게 배치해야한다.
      • graph LR id1[DataSource, TableModel] -- 정의된 모델에 맞게 source를 배치 --> DataTable
  • 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만을 렌더링
      • graph LR id1[DataSource, TableModel] -- filterQuery --> id2[filtering]-- filtered source를 배치 --> DataTable
         

2) TableModel이 가지고 있는 인터페이스는 무엇이어야 할까

interface TableModel { label: stirng // header에서 보여줄 label값 accesor: string // key of DataSource header?: { render: ({headerProps}) => <CustomElment />} cell? : { render: ({cellProps}) => <CustomElment />} }[]
  • DataTable 은 dataSource를 tableModel 에 맞게 배치된 2x2 형태의 Table 엘리먼트로 정의하였다.
    • accessor
      Cell 에 보여주기 위한 data를 찾기 위한 key 값으로, 반드시 DataSource의 포함되어 있는 key값이어야만 한다.
      render
       
      headerProps cellProps
      custom 값을 표현하기 위한 함수로 필요 위치에 따라 header, cell 프로퍼티의 함수로 정의할 수 있다. 5000 이라는 price를 5,000원 으로 파싱하거나, 경우에 따라 Element형태로 가공할 수 있어야 한다.
      render함수에서의 props 값들은 구현단에서 지정해줄 수 있으며, 일반적으로 각 header와 cell에서의 value를 포함하고 있는 useDataTable hook 에서 내려주는 값으로 지정한다.
tableModel 구현체 ( 화살표 눌러 펼치기 )
notion image
  • TableModel
    • const productTableModel = [ { label: "대표 이미지", accessor: "imageUrl", header: { render: ({ headerProps }) => <div>썸네일</div> }, cell: { render: ({ cellProps }) => ( <Image src={cellProps.value}/> ), }, }, { label: "가격", accessor: "price", cell: { render: ({ cellProps }) => `${cellProps.value.toLocaleString("ko-KR")}원` }, }, ...]
  • 구현부 ProductDataTable
    • headerGroups와 rowModel에서는 현제 Cell의 value정보를 포함하고 있다.
    • ... const { headerGroups, rowModel } = useDataTable<Product>({ dataSource, tableModel, }); ... return( <THead> <TRow> {headerGroups.map((header) => { return <THeadCell>{header.render({ headerProps: header })}</HeadCell>; })} </TRow> </THead> <TBody> {rowModel.map((row) => ( <TRow key={row.id}> {row.cells.map((cell) => { return <TCell>{cell.render({ cellProps: cell })}</Cell>; })} </TRow> ))} </TBody> )
 

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결합도를 낮출 수 있고, 각 컴포넌트의 역할과 책임을 명확하게 구분할 수 있다.
graph LR id1[Category, Search] -- 약속된 queryParameter 형식 --> id2[queryString]-- filterQuery를 받아 filtering 적용 --> id3[DataTable]
notion image
 

결과 및 한계점

결과

  • CodeSandbox Demo (Github 보기)

한계점

[1. 다양한 data type들에 대한 임시 처리]
지금 현재 사용가능한 data들에 초점을 맞추다 보니, 값이 Array로 들어오는 경우