TIL/WID: What I Did

2월 20일 WID : Devextreme의 TabPanel을 정복하다!

반응형

우리 회사에서는 UI 라이브러리로 devextreme이라는 라이브러리를 사용한다. 이번에는 팝업 안에서 Tab 메뉴를 이동하는 동시에, 각 개별 Tab메뉴 내에서 TreeView를 활용하여 selectedItems를 받아야 하는 업무가 주어졌다.


어려웠던 점

최초 팝업 오픈 시 NavButtons 미노출되는 현상

TabPanel에서는 showNavButtons라는 옵션으로 탭메뉴가 많아지고 길어졌을 때 좌우 이동 버튼을 제공하는데, 팝업에서 띄워서 그런지 최초 팝업 노출시 NavButtons의 노출 조건에 맞지 않아 최초 팝업 오픈 시 탭메뉴가 오른쪽으로 쭉 삐져나오는 현상을 겪었다.

showNavButtons
Specifies whether navigation buttons should be available when tabs exceed the UI component's width.

이것은 devextreme의 jQuery 라이프사이클을 React에서 인식하지 못하는 문제로 생각되어 forceRender를 사용하여 해결했다.

Panel의 Top Border

탭을 선택하면 Panel의 top border가 1px 두꺼워지는 것처럼 보임 -> 해결하지 못했음 

*메모: 정수로 딱 안 떨어지면 두께가 좀 달라져 보일 수 있어요 / Blitz 


devextreme의 TabPanel 데모

처음에는 단순히 itemRender props를 통해 TreeView를 렌더링하게 만들었는데 이렇게 했더니 selectedOptions를 받아오는데 6회의 렌더링이 돌아서 몹시 당황했다. TreeView에서 아이템 하나 선택할 때마다 리렌더링 되고 포커스도 최상단으로 올라붙고... 난리도 아니어서 고쳐야겠던 참에 고쳐달라고 피드백이 와서 작업에 착수했다. 

UI 구조

UI와 각 부분에 해당하는 데이터의 구조는 아래와 같았다.

  • Popup 상단 > Apply 버튼: 탭별로 선택한 TreeViewItem들을 모조리 가져다가 적용하는 버튼
  • Tabs > Tab: redux에서 불러온 tabData를 뿌려주는 영역 (여기부터 TabPanel)
  • TreeView > items: 선택된 tabData에 따라 recursive한 items를 뿌려주어 checkbox형태로 멀티 셀렉트가 가능한 영역

TabPanel 최초 적용 시 모습

  <TabPanel
  	  ref={tabPanelRef}
	  keyExpr={'index'}
          dataSource={dataSource}
          itemRender={()=> 
          	<CustomizedTreeView 
            	itemsByTab={itemsByTab} 
                selectedItems={selectedItems} 
                setSelectedItems={setSelectedItems}/>
            }
        />

처음에는 위와 같이 itemRender를 통해 트리뷰 컴포넌트를 뿌려주고 있었고, props로 setState 액션을 보내주고 있었기 때문에 렌더링의 늪에서 빠져나올 수 없었다. (지금 생각해보니 이 때에도 dataSource에서 뭔가 받을 수 있지 않았을까 싶은데 내일 출근하면 테스트를 해봐야겠다)

TabPanel 렌더링의 늪에서 빠져나온 모습

 <TabPanel
   	  ref={tabPanelRef}
          keyExpr={'index'}
          dataSource={dataSource}
          itemComponent={CustomizedTreeView}
        />

그리고 위와 같이 고쳐주자 렌더링의 늪에서 간단히!!! 빠져나올 수 있었다. 위와 같이 keyExpr을 지정해주고 dataSource에 해당 키값을 지정해 주면 탭을 선택할 때마다 알아서 해당하는 itemComponent를 보여주고 나머지는 hidden처리 해준다. 그런고로 다른 탭의 TreeView들도 없어진게 아닌 hidden 처리 된 것이기 때문에 탭을 바꾸어도 이전에 선택했지만 저장은 하지 않은 결과물이 날아가지 않고 내가 적용 버튼을 누를 때까지 보존된다!

DataSource 구성하기

데이터소스는 원하는대로 구성해주면 된다. 나는 대략 다음과 같이 구성하였다.

export const dataSource = [
  {
    title: 'title1',
    itemList: [{...},{...},{...},{...},{...},{...}] // 가공한 아이템 리스트
    selectedItemIds: [1,2,3,4,5] // 기존에 선택되어 있는 상태인 아이템의 id값
  },
  {
    title: 'title2',
    itemList: [{...},{...},{...},{...},{...},{...}] 
    selectedItemIds: [1,2,3,4,5]
  },
  {
    title: 'title3',
    itemList: [{...},{...},{...},{...},{...},{...}] 
    selectedItemIds: [1,2,3,4,5] 
  },
  {
    title: 'title4', 
    itemList: [{...},{...},{...},{...},{...},{...}] 
    selectedItemIds: [1,2,3,4,5] 
  },
  {
    title: 'title5', 
    itemList: [{...},{...},{...},{...},{...},{...}] 
    selectedItemIds: [1,2,3,4,5] 
  },
  {
    title: 'title6', 
    itemList: [{...},{...},{...},{...},{...},{...}] 
    selectedItemIds: [1,2,3,4,5] 
  },
];

주입한 DataSource 활용하기

이것을 CustomizedTreeView에서는 data로 받아온다. 

import React from 'react';
import Item from './Item.tsx';

function CustomizedTreeView({ data }) {
  const { title, itemList, selectedItemIds } = data
  const [filteredItemList, setFilteredItemList] = React.useState([])
  
  // ... itemList를 가공하는 과정
  // ... onSelectionChanged 과정
      data.selectedItemListByTabId = leavesOnly.map((leave) => leave.itemData) 
      // 이렇게 넣어주면 해당하는 TreeView의 data에 선택한 item데이터가 업데이트 된다. setState에서 해방!
  // ... 기타 등등
  
  return (
 	<TreeView
        id="treeview"
        items={filteredItemList}
        //... 기타 옵션
        itemRender={(item) => (
        <div>
          {item.title}
        </div>
      />
	);
}

export default CustomizedTreeView;

TabPanel에서 선택값 모두 가져오기

마지막으로 apply 버튼을 선택할 때 선택한 item의 id값이 필요한데, TabPanel에 걸어두었던 ref를 사용하여 한번에 가져올 수 있다!

const handleClickApplyBtn = () => {
    const selectedItemList = tabPanelRef.current.props.dataSource.map((source) => source.selectedItemList) // 데이터소스에서 필요한 데이터만 뽑아준다
    const selectedItemIds = selectedItemList.flat().map((menu) => menu.id) // flat()으로 2차원 배열을 1차원 배열로 만들어준다
    rowItemData.setValue(selectedItemIds) // 처음에 itemData를 가져왔던 데이터소스에 setValue 해준다
    handleIsVisible() // 팝업을 닫아줌
  }

완성!!!

728x90
반응형