드래그앤드롭(Drag&Drop) 구현

백우진·2023년 1월 13일
6
post-thumbnail

들어가며

프로젝트를 진행하며 드래그앤드롭으로 ITEM들을 이동을 드래그앤드랍으로 구현하기로 하고

  1. 라이브러리 없이 구현
  2. 라이브러리를 사용한 구현

두가지를 고려하였고, 현재 진행하고 있는 프로젝트의 멘토님에게 드래그앤드롭 성능면에서 어느 선택을 하면 좋을지 질문을 드렸다.
멘토님은 많은 라이브러리가 성능 면에서 고려되어 만들어졌기에 라이브러리를 사용하는것이 좋을 것이다 라고 하셨고 현업에서도 라이브러리를 많이 사용한다고 하셨다.


라이브러리 선정

드래그앤드롭을 구현할 수 있는 라이브러리가 많다.
그 중에서 react-beautiful-dnd를 사용해서 드래그앤드롭을 구현하자


react-beautiful-dnd

Jira와 Trello를 만든 atlassian 회사에서 만든 오픈 소스 라이브러리

설치

npm install react-beautiful-dnd --save

기본 요소 (3가지 요소)

  • DragDropContext
    dnd를 사용하고자 하는 어플리케이션 영역을 감싸는 Wrapper

  • Droppable
    dnd에서 Drop을 할 수 있는 영역, Draggalbe을 감싸는 Wrapper

  • Draggable
    dnd의 주체가 되는, Drag가 가능한 컴포넌트를 감싸는 Wrapper


DragDropContex 콜백 props

  • onDragStart : Drag가 시작될때 호출

  • onDragUpdate : Drag 진행중일때, 새로운변화가 있을때

  • onDragEnd : Drag가 끝났을때


초기 사용

function Board({ boardId }: any) {
  const [boards, setBoards] = useRecoilState(taskAtom); // useRecoilState를 사용해서 boards를 가져온다.
  const [selectedBoard] = JSON.parse(JSON.stringify(boards)).filter((board: any) => board.boardId === boardId); // JSON.parse(JSON.stringify()) -> 깊은 복사를 위해 사용
  let tempBoards = JSON.parse(JSON.stringify(boards)); // 원하는 값을 변경 한 뒤에 tempBoards에 넣어줄 것이다.
  let tempList = JSON.parse(JSON.stringify(selectedBoard.list)); // 원하는 값을 변경하기 위함

  const onDragEnd = (result: any) => {
    if (!result) return;
    const [reorderedItem] = tempList.splice(result.source.index, 1); // 내가 드래그 하려고 하는 요소를 tempList에서 제외한다.
    tempList.splice(result.destination.index, 0, reorderedItem); // 내가 드래그해서 가려고 하는 목적지에 reorderedItem을 넣어준다.
    selectedBoard.list = tempList; // 현재 보드의 list를 새로운 list로 변경한다.
    tempBoards = tempBoards.map((board: any) => (board.boardId === boardId ? selectedBoard : board)); // 보드배열을 반복하면서 내가 변경한 보드 ID와 배열안에 있는 보드ID 가 일치하면 변경
    setBoards(tempBoards); // taskAtom recoil 업데이트!
  };

  // 반복을 통해서 리스트들을 보여준다.

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="list" direction="horizontal">
        {(provided) => (
          <S.ListContainer {...provided.droppableProps} ref={provided.innerRef}>
            {tempList.map((list: any, index: any) => (
              <Draggable draggableId={String(index)} index={index} key={String(index)}>
                {(provided) => (
                  <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
                    <div>{list.listTitle}</div>
                    <List list={list} boardId={boardId} listIndex={index} listId={list.listId} />
                  </div>
                )}
              </Draggable>
            ))}
            {provided.placeholder}
          </S.ListContainer>
        )}
      </Droppable>
    </DragDropContext>
  );
}

export default Board;

function List({ list, boardId, listIndex, listId }: any) {
  const [boards, setBoards] = useRecoilState(taskAtom); // recoil에서 관리하는 board들을 담고 있는 데이터

  let tempBoards = JSON.parse(JSON.stringify(boards)); //보드들 정보를 tempBoards에 저장
  let [selectedBoard] = JSON.parse(JSON.stringify(boards.filter((board) => board.boardId === boardId))); // 선택한 보드 1개를 저장
  let tempList = JSON.parse(JSON.stringify(list)); // 선택한 리스트 1개를 저장
  let tempLists = selectedBoard.list; // 선택된 보드 1개에서 list 배열을 tempLists에 담는다
  let tempCards = JSON.parse(JSON.stringify(list.card)); // 선택한 리스트에서 카드 배열을 tempCards에 담는다

  const onDragEnd = (result: any) => {
    console.log(`선택한 BoardId : ${boardId}, listId:${listId}`);
    if (!result) return;
    let [reorderedItem] = tempCards.splice(result.source.index, 1); // 선택한 카드를 배열에서 빼서 reorderedItem에 담는다.
    tempCards.splice(result.destination.index, 0, reorderedItem); // 목적지에 reorderedItem을 넣는다.
    tempList.card = tempCards; // tempList 객체서 card 배열을 변경한다.
    let newLists = tempLists.map((list: any) => (list.listId === listId ? tempList : list)); //리스트 배열 중에서 listId가 일치하는 곳에 새로운 List를 넣어준다.
    selectedBoard.list = newLists; // 새로운리스트들을 선택한 보드의 리스트 배열에 넣어준다.
    let newBoards = tempBoards.map((board: any) => (board.boardId === boardId ? selectedBoard : board)); // 전체 보드에서 바뀐 보드를 업데이트 해준다.
    setBoards(newBoards); //recoil을 재설정한다.
  };
  // 반복을 통해서 리스트 들을 보여준다.

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="Card">
        {(provided) => (
          <div ref={provided.innerRef} {...provided.droppableProps}>
            <S.ListWrapper>
              {tempCards.map((card: any, index: any) => (
                <Draggable draggableId={String(index)} index={index} key={index}>
                  {(provided) => (
                    <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
                      <S.CardWrapper>{card.cardTitle}</S.CardWrapper>
                    </div>
                  )}
                </Draggable>
              ))}
            </S.ListWrapper>
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
}

실행화면 1


문제점

드래그앤드랍 가능 영역

  • 리스트 <-> 리스트
  • 같은 리스트의 카드 <-> 같은 리스트의 카드

드래그앤드랍 불가능 영역

  • 타 리스트간의 카드 교류

이러한 문제점이 있었고 코드를 새로 작성하며 해결했다.


최종

구조는 보드 -> 리스트 -> 카드 순으로 구성된다.

DragDropContext, Droppable, Draggable 범위를 아래와 같이 설정했다.

// Board.tsx
...

return (
    <>
      <DragDropContext onBeforeDragStart={onBeforeDragStart} onDragStart={onDragStart} onDragEnd={onDragEnd}>
        <Droppable droppableId="board" type="moveList" direction="horizontal">
          {(provided) => (
            <S.BoardContainer ref={provided.innerRef} {...provided.droppableProps}>
              {lists.map((list, index) => (
                <List key={list.listId} boardId={boardId} listId={list.listId} listData={list} index={index}></List>
              ))}
              <AddList></AddList>
              {provided.placeholder}
            </S.BoardContainer>
          )}
        </Droppable>
      </DragDropContext>
    </>
  );
// List.tsx
...

return (
    <Draggable draggableId={listId} index={index}>
      {(provided) => (
        <S.ListWrapper ref={provided.innerRef} {...provided.draggableProps}>
          <S.ListContent>
            <ListTitle
              dragHandleProps={provided.dragHandleProps}
              boardId={boardId}
              listId={listId}
              title={listData.listTitle}
            ></ListTitle>
            <Droppable droppableId={listId} type="moveCard">
              {(droppableProvided, droppableSnapshot) => (
                <S.ListDroppable ref={droppableProvided.innerRef}>
                  {listData.cards.map((card: CardData, index: number) => (
                    <Card
                      key={card.cardId}
                      cardId={card.cardId}
                      listId={listId}
                      boardId={boardId}
                      cardData={card}
                      index={index}
                    ></Card>
                  ))}
                  <AddCard listId={listId} />
                  {droppableProvided.placeholder}
                </S.ListDroppable>
              )}
            </Droppable>
          </S.ListContent>
        </S.ListWrapper>
      )}
    </Draggable>
  );
// Card.tsx
...

return (
      <Draggable draggableId={cardId} index={index}>
        {(draggableProvided, draggableSnapshot) => (
          <S.CardDraggable
            ref={draggableProvided.innerRef}
            {...draggableProvided.draggableProps}
            {...draggableProvided.dragHandleProps}
          >
            <S.TextAreaWrapper onClick={handleSaveModalData}>
              <S.CardHeaderWrapper>

                <S.CardTitleWrapper>{cardData.cardTitle}</S.CardTitleWrapper>

                <S.DeleteWrapper onClick={handleDeleteCard}>
                  <CgClose />
                </S.DeleteWrapper>
              </S.CardHeaderWrapper>
              <S.InformationWrapper>
                <FcClock size="20" />
              </S.InformationWrapper>
            </S.TextAreaWrapper>
          </S.CardDraggable>
        )}
      </Draggable>
  );

실행 화면


마치며

최종 코드에는 DragDropContext, Droppable, Draggable 범위만 나와있고 드래그앤드랍이 발생했을때 어떻게 처리할지에 대해서는 나와있지 않다. 아직 개발중에 있고 완벽하게 마무리되면 수정 예정이다.

profile
안녕하세요.

7개의 댓글

comment-user-thumbnail
2023년 1월 16일

안녕하세요~ 포스트 잘 보았습니다. 얘기 한번 나눠볼 수 있을까요?

2개의 답글
comment-user-thumbnail
2023년 1월 18일

퍼가요~

답글 달기
comment-user-thumbnail
2023년 1월 18일

너무 멋있어요~~

답글 달기