antd 기반 프로젝트에서 사용자가 리스트 내용을 입력하고 드래그 앤 드롭으로 순서를 변경하는 기능을 개발하게 되었다.
작업 비용을 줄이기 위해 드래그에서 라이브러리를 사용하기로 했고 react-sortable-hoc를 선정하였다. 다른 프로젝트의 유사한 기능에 사용한 히스토리가 있어 참고에 용이했으며 러닝커브가 낮다고 느꼈기 때문이다.
1. 사용 방법
1-1. 설치
npm install react-sortable-hoc --save
yarn add react-sortable-hoc
1-2. 드래그
1-2-1. 아이템 빈 공간을 잡고 드래그
리스트 컴포넌트를 SortableContainer로 래핑하고 리스트 아이템 컴포넌트를 SortableElement로 래핑하면 간단히 사용할 수 있다.
아래는 antd form에 설정된 프로젝트 리스트 값을 Form.List
로 출력한 코드이다.
(state로 리스트를 관리하는 예시는 공식 문서에서 제공한다)
App.tsx
import { FC } from 'react'
import styled from 'styled-components'
import { Button, Form } from 'antd'
import { useForm } from 'antd/lib/form/Form'
import ItemList from './ItemList'
interface Project {
title: string
description: string
}
const INIT_PROJECT_LIST: Project[] = [
{
title: '[개인] 포트폴리오 블로그 개발',
description: 'Gatsby로 블로그를 만들었어요',
},
{
title: '[팀] 테스트 메이커 서비스 개발',
description: '쉽게 테스트를 제작할 수 있는 기능 구현',
},
]
const App: FC = () => {
const [form] = useForm<Project[]>()
return (
<Wrapper>
<Form form={form} onFinish={value => console.log(value)}>
<Form.List name="projects" initialValue={INIT_PROJECT_LIST}>
{(fields, methods) => (
<ItemList
fields={fields}
helperClass="drag-project-item"
useWindowAsScrollContainer
onSortEnd={({ oldIndex, newIndex }) => methods.move(oldIndex, newIndex)}
/>
)}
</Form.List>
<Button htmlType="submit">완료</Button>
</Form>
</Wrapper>
)
}
const Wrapper = styled.div`
width: 500px;
`
export default App
<Form.List>
의 children으로fields
(아이템 배열)를 받아<ItemList>
에 전달한다.- onSortEnd : 드래그가 끝났을 때 호출되는 콜백.
method.move
로 순서를 변경한다. - helperClass : 드래그중인 아이템에 추가할 클래스 이름
- useWindowController : 스크롤 컨테이너를 window로 설정한다. 드래그 중인 아이템이 view를 벗어났을 때 window가 스크롤 되기 때문에 좋은 UX를 제공할 수 있고,
false(default)
로 되어있을 때 간헐적으로 드래그 위치가 맞지 않는 오류가 있었어서true
로 설정하는 것을 권장한다.
App.css
.drag-project-item {
background-color: ghostwhite;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
z-index: 100;
}
helperClass
에 설정한 이름으로 드래그 중인 아이템을 스타일을 설정한다.
ItemList.tsx
import { ComponentClass } from 'react'
import { FormListFieldData } from 'antd/lib/form/FormList'
import { SortableContainer, SortableContainerProps } from 'react-sortable-hoc'
import Item from './Item'
interface Props {
fields: FormListFieldData[]
}
const ItemList: ComponentClass<SortableContainerProps & Props> = SortableContainer(
({ fields, ...rest }: Props) => {
return (
<ul>
{fields.map((fieldItem, index) => (
<Item key={fieldItem.key} fieldItem={fieldItem} index={index} />
))}
</ul>
)
},
)
export default ItemList
- 리스트 컴포넌트는
SortableContainer
로 래핑한다. 주의할 점은fields.map
상위에 element로 래핑해야 한다. <Item>
에index
,key
를 필수로 전달한다. 이 때SortableElement
로 래핑한 컴포넌트 내에서 index를 읽을 수 없기 때문에 필요하다면 다른 이름으로 전달해야 한다.
Item.tsx
import React from 'react'
import styled from 'styled-components'
import { Form, Input } from 'antd'
import { FormListFieldData } from 'antd/lib/form/FormList'
import { SortableElement, SortableElementProps } from 'react-sortable-hoc'
interface Props {
fieldItem: FormListFieldData
}
const Item: React.ComponentClass<SortableElementProps & Props> = SortableElement(
({ fieldItem }: Props) => {
const { key, name: fieldName, ...restField } = fieldItem
return (
<Wrapper>
<Form.Item label="프로젝트 제목" name={[fieldName, 'title']} {...restField}>
<Input name="title" />
</Form.Item>
<Form.Item label="프로젝트 내용" name={[fieldName, 'description']} {...restField}>
<Input name="description" />
</Form.Item>
</Wrapper>
)
},
)
const Wrapper = styled.li`
padding: 20px;
border-radius: 5px;
`
export default Item
아이템 컴포넌트는 SortableElement
로 래핑하고 fieldItem
을 <Form.Item>
에 전달한다.
1-2-2. 드래그 핸들러를 잡고 드래그
아이템 내 특정 요소를 잡고 드래그하려면 아이템 컴포넌트에 SortableHandle로 래핑한 드래그 핸들러 컴포넌트를 추가한다.
App.tsx
...
<ItemList
fields={fields}
helperClass="drag-project-item"
useWindowAsScrollContainer
useDragHandle
onSortEnd={({ oldIndex, newIndex }) =>
methods.move(oldIndex, newIndex)
}
/>
...
- useDragHandle : 드래그 핸들러를 사용하기 위해
true
로 설정한다.
DragHandler.tsx
import { SortableHandle } from 'react-sortable-hoc'
const DragHandler = SortableHandle(() => {
return <span className="drag-handler">=</span>
})
export default DragHandler
SortableHandle
로 래핑하여 드래그 핸들러 컴포넌트를 생성한다.
Item.tsx
...
const Item: React.ComponentClass<SortableElementProps & Props> = SortableElement(
({ fieldItem }: Props) => {
const { key, name: fieldName, ...restField } = fieldItem
return (
<Wrapper>
<DragHandler />
...
</Wrapper>
)
},
)
...
아이템 컴포넌트 원하는 위치에 <DragHandler>
를 추가한다.
2. 마치며
소개한 기능 외에도 react-sortable-hoc는 다양한 props를 제공한다. 드래그 방향 제한, 트랜지션 설정, 컨테이너 변경, 정렬 콜백 등이 기본으로 있어서 커스텀이 수월했다.
또한 래핑만 하면 동작하기 때문에 다른 드래그 앤 드롭 라이브러리에 비해 사용법이 간단하다. (공식 문서도 이를 강점으로 내세운다)
그러나 react-sortable-hoc에 사용하는 findDOMNode가 더 이상 React에서 채택되지 않아 추후 기능에 영향을 미칠 수 있다. 이를 대비하여 react-sortable-hoc의 모든 기능을 지원하며 더 현대적인 dnd-kit를 추천하고 있다.
개발 기간이 우선이라면 react-sortable-hoc로 구현 후 추후 라이브러리를 교체하거나, 안정성이 우선이라면 dnd-kit 외 다른 라이브러리를 검토하여 적용할 수 있겠다. antd와 사용 가능한 라이브러리는 Antd Design Third-Party Libraries에 나와있다.