본문 바로가기
Project/Knoticle

[Knoticle] 좋은 UX를 위한 기능&도전 2 (드래그앤드롭)

by Cafe Mocha 2022. 12. 16.

Knoticle 서비스에서 책에서 글의 위치를 결정하는 기능은 매우 중요한 기능이다.

책과 글을 기반으로 만들어진 프로젝트에서 글의 위치를 결정하는 기능은 사용자의 좋은 경험에 중요한 지표가 될 것이다.

초기 기획단계에서 여러가지 방법을 논의했었다.

  1. 버튼으로 위치를 변경시키는 방법
  2. 마지막 위치로 고정시키고, 추후 책 수정에서 변경시키는 방법
  3. 드래그앤드롭

최종적으로는 드래그앤드롭으로 결정되고, 구현을 시작했다.

구현 방법의 고민

드래그앤드롭을 구현하기 위해 직접 구현하는 방법, 라이브러리를 사용하는 방법 중 고민을 시작했다.

가장 해보고 싶은 방법은 직접 구현하는 것이지만, 팀프로젝트를 완결성있게 완성시키는 것이 가장 중요했기 때문에 라이브러리를 사용해서 구현하기로 결정했다.

선정에 고민했던 라이브러리는 하기와 같다.

  1. react-dnd
  2. react-beautiful-dnd
  3. react-draggable

react-dnd

상대적으로 가볍고, 커스텀해서 사용하기 편하다.

터치 백엔드 npm으로 터치를 지원한다.

공식문서에 사용예시가 많아서 참고해서 라이브러리를 익히기 쉽다.

react-beautiful-dnd

react-beautiful-dnd는 UI/UX나 퍼포먼스가 좋은 동작이 predefined 되어있는 것이 특징이다.

신경쓰지않아도 좋은 퍼포먼스 동작이 들어가있어 이쁜 동작을 만들 수 있다.

하지만, 용량이 react-dnd의 두배이상이다.

react-draggable

react-draggable은 드래그로 어떠한 아이템 간의 순서 변경 측면보다, 윈도우즈에서 윈도우를 드래그해서 위치를 바꾸는 부분에서 강점이 있는 라이브러리로 프로젝트와 연관성이 낮아 제외했다.

결론적으로, 가볍고 추후 커스텀해서 사용하기 편리하다는 장점과 터치 지원으로 모바일 구현에서 사용할 수 있는 장점으로 react-dnd를 사용해서 구현했다.

드래그앤드롭 구현

드래그앤드롭을 구현하면서 책 수정모달, 글 발행모달, 스크랩모달 등 많은 곳에서 사용해야 하기 때문에 추상화해서 구현하려고 노력했다.

DragArticle.tsx

사용하려는 컴포넌트를 <DndProvider>로 감싸고 Props로 backend를 내려준다.

backend는 필수로 HTML5Backend를 내려준다.

HTML5Backend는 React-DnD에서 지원하는 기본 백엔드이다.

터치 지원을 위해서는 TouchBackend를 내려줘서 사용할 수 있다.

import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

import { Container } from '@components/common/DragDrop/Container';

export interface EditScrap {
  id: number;
  order: number;
  article: {
    id: number;
    title: string;
  };
}
export interface ContainerState {
  data: EditScrap[];
  isContentsShown: boolean;
}

export default function DragArticle({ data, isContentsShown }: ContainerState) {
  return (
    <DndProvider backend={HTML5Backend}>
      <Container data={data} isContentsShown={isContentsShown} />
    </DndProvider>
  );
}

Container.tsx

실제로 옮길 요소를 감싸고 있는 Container로 움직이는 요소를 찾고 전역상태로 관리하는 데이터에 update해주는 함수가 들어있다.

findScrap,moveScrap은 하위 컴포넌트로 내려준다.

import { useEffect, memo, useCallback } from 'react';
import { useDrop } from 'react-dnd';

import update from 'immutability-helper';
import { useRecoilState } from 'recoil';

import scrapState from '@atoms/scrap';

import { ListItem } from '../ListItem';
import ContainerWapper from './styled';

const ItemTypes = {
  Scrap: 'scrap',
};

export interface EditScrap {
  id: number;
  order: number;
  article: {
    id: number;
    title: string;
  };
}
export interface ContainerState {
  data: EditScrap[];
  isContentsShown: boolean;
}

export const Container = memo(function Container({ data, isContentsShown }: ContainerState) {
  const [scraps, setScraps] = useRecoilState<EditScrap[]>(scrapState);

  useEffect(() => {
    if (!data) return;
    setScraps(data);
  }, []);

  const findScrap = useCallback(
    (id: string) => {
      const scrap = scraps.filter((c) => `${c.article.id}` === id)[0] as {
        id: number;
        order: number;
        article: {
          id: number;
          title: string;
        };
      };
      return {
        scrap,
        index: scraps.indexOf(scrap),
      };
    },
    [scraps]
  );

  const moveScrap = useCallback(
    (id: string, atIndex: number) => {
      const { scrap, index } = findScrap(id);
      setScraps(
        update(scraps, {
          $splice: [
            [index, 1],
            [atIndex, 0, scrap],
          ],
        })
      );
    },
    [findScrap, scraps, setScraps]
  );

  const [, drop] = useDrop(() => ({ accept: ItemTypes.Scrap }));
  return (
    <ContainerWapper ref={drop}>
      {scraps.map((scrap, index) => (
        <ListItem
          key={scrap.article.id}
          id={`${scrap.article.id}`}
          text={scrap.article.title}
          moveScrap={moveScrap}
          findScrap={findScrap}
          isShown={index < 4}
          isContentsShown={isContentsShown}
        />
      ))}
    </ContainerWapper>
  );
});

ListItem.tsx

실제로 움직이는 요소들이 있는 컴포넌트이다.

useDrag와 useDrop Hooks를 사용해 드래그앤드롭을 구현한다.

드래그앤드롭을 하면 내가 선택한 요소의 id와 overIndex로 위치를 조정한다.

import { memo } from 'react';
import { useDrag, useDrop } from 'react-dnd';

import Article from './styled';

const ItemTypes = {
  Scrap: 'scrap',
};

export interface ScrapProps {
  id: string;
  text: string;
  moveScrap: (id: string, to: number) => void;
  findScrap: (id: string) => { index: number };
  isShown: boolean;
  isContentsShown: boolean;
}

interface Item {
  id: string;
  originalIndex: number;
}

export const ListItem = memo(function Scrap({
  id,
  text,
  moveScrap,
  findScrap,
  isShown,
  isContentsShown,
}: ScrapProps) {
  const originalIndex = findScrap(id).index;

  // Drag
  const [{ isDragging }, drag] = useDrag(
    () => ({
      // 타입설정 useDrop의 accept와 일치시켜야함
      type: ItemTypes.Scrap,
      item: { id, originalIndex },
      // Return array의 첫번째 값에 들어갈 객체를 정의한다.
      collect: (monitor) => ({
        isDragging: monitor.isDragging(),
      }),
      // 드래그가 끝났을때 실행한다.
      end: (item, monitor) => {
        const { id: droppedId } = item;
        const didDrop = monitor.didDrop();
        if (!didDrop) {
          moveScrap(droppedId, originalIndex);
        }
      },
    }),
    [id, originalIndex, moveScrap]
  );
  // Drop
  const [, drop] = useDrop(
    () => ({
      accept: ItemTypes.Scrap,

      hover({ id: draggedId }: Item) {
        if (draggedId !== id) {
          const { index: overIndex } = findScrap(id);
          moveScrap(draggedId, overIndex);
        }
      },
    }),
    [findScrap, moveScrap]
  );

  return (
    <Article ref={(node) => drag(drop(node))} isShown={isContentsShown ? true : isShown}>
      {text}
    </Article>
  );
});

실제 사용 예시

책 수정

내가 사용하고 싶은 데이터를 props로 내려줘서 사용할 수 있다.

...
<DragArticleWrapper isContentsShown={isContentsShown}>
	<DragArticle data={scraps} isContentsShown={isContentsShown} />
</DragArticleWrapper>
...

 

정리

처음으로 드래그앤드롭을 구현해봤는데 완성하니 정말 좋은 경험이었다.

버튼으로 변경하는 것보다 좋은 UX로 글 순서를 바꿀 수 있는 것 같다.

남은 고민은 드래그 핸들을 만들거나, 모바일에 대응하고 더 좋은 UX를 줄 수 있도록 커스텀 하는 것이다.

 

Reference

https://react-dnd.github.io/react-dnd/examples/sortable/cancel-on-drop-outside

 

React DnD

 

react-dnd.github.io

https://itchallenger.tistory.com/608

 

React DnD 튜토리얼

원문 https://react-dnd.github.io/react-dnd/docs/tutorial React DnD react-dnd.github.io 마이크로소프트 엔지니어의 컴포넌트 아키텍처 설계 프로세스가 드러나는 읽을만한 글이라 정리해둔다 들어가기에 앞서 : Re

itchallenger.tistory.com

https://channel.io/ko/blog/react-dnd-tips-tricks

 

블로그 - React DnD Tips & Tricks

React DnD는 아이템 간의 순서 변경 등 드래그 액션이 편리한 UX를 만들 수 있는 환경에서 사용됩니다. 아래는 채널톡에서 React DnD를 사용하는 예시입니다.

channel.io