react dnd 이용하여 drag & drop 구현하기

성도원·2021년 7월 12일
60

react-project

목록 보기
1/3
post-thumbnail

yourTube

yourTube는 다른영상에 빠져들지 않고 (유튜브에는 시간뺏길 영상이 너무나도 많다 ㅠㅜ) 내가 꾸민 유튜브를 이용해 필요한 영상만 볼 수 있게 만들고 있는 프로젝트이다.
여기서 내가 꾸미는 페이지를 드래그엔 드랍으로 구현하는 것이 목표였다.

내가 react dnd를 선택한 이유

리액트 드래그앤드랍을 검색하던중 npm트렌드 사이트에서 위 그래프를 보게되었다.
1. 나는 네모난 재생목록들을 grid로 배열해야했다.
2. 그리고 네모난 재생목록들을 Resizing 할 수 있는 기능도 구현해야 했다.
두가지를 하기위해 좀 더 자유도가 높은 react-dnd를 이용했다.
react-beautiful-dnd는 자유도(드래그앤드랍으로 순서변경, 다른 박스로 드래그앤드랍 정도만 가능.)는 적지만 간단한 기능만 구현할것이라면 자연스러운 고급 애니메이션 까지 적용 되어있는 react-beautiful-dnd를 추천한다.

드래그 앤 드랍 구현하기.

react-dnd 공식 사이트에 있는 여러가지 예시 코드들이 react-dnd를 이해하는데 매우 많은 도움이 되었다. 공식 사이트의 업데이트가 매우 활발 하니 공식 사이트에서 예시를 참조해서 만드는 것이 가장 최신버전으로 만드는 방법이다.(내가 기능을 구현한 이후 며칠 후 같은 예시지만 다른코드들로 대체 되어 있었고, 그리고 내가 구현할 당시에도 참고하려고 했던 블로그와 코드가 달라 애를 먹어 결국 공식 사이트의 예시를 참고해 만들었다.)

내가 참고한 react-dnd 공식 사이트의 예시 코드(리스트를 드래그앤드랍으로 순서를 바꾸는 간단한 예시 였다.) 내가 구현한 드래그앤드랍.gif

우선 npm으로 react-dnd를 추가해준다.
npm install react-dnd react-dnd-html5-backend
그리고 사용할 컴포넌트를 DndProvider로 감싸주고 백엔드를 HTML5Backend로 설정해준다.
나는 재생목록을 리사이징 하는데에도 dnd를 사용하기위해서 app.js에서 재생목록이 표시되는 Home을 감싸주었다.

import Header from './components/header/header';
import Home from './components/home/home';

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


function App({ authService, dbService, youtube }) {


  return (
    <div className={`${styles.app} ${themeClass}`}>
        <Header/>
      <DndProvider backend={HTML5Backend}>
        <Home/>
      </DndProvider>
      {player && (
        <Player/>
      )}
    </div>
  );
}

export default App;

아래는 옮겨지는 대상인 nemo 컴포넌트의 코드이다.
useDraguseDrop'react-dnd'로부터 임포트 받아 작동한다.
각각 드래그와 드랍에 관련된 함수. 자세한 설명은 코드에서 주석으로 하겠다.

import React from 'react';
import { useEffect, useRef, useState } from 'react/cjs/react.development';

import { useDrag, useDrop } from 'react-dnd';
import { ItemTypes } from '../../utils/items';

const Nemo = memo(
  ({
    id,
    index,
    nemo,
    moveNemo,
    someDragging,
    setSomeDragging,
  }) => {
    // 드래그

    const [{ isDragging }, dragRef, previewRef] = useDrag(
          //isDragging은 아이템이 드래깅 중일때 true, 아닐때 false를 리턴 받는다. 드래깅 중인 아이템을 스타일링 할때 사용했다.
          //dragRef는 리액트의 useRef처럼 작동한다. 드래그될 부분에 선언해주면된다. 네모의 타이틀이 있는 부분에 선언했다. 
          //previewRef는 드래깅될때 보여질 프리뷰 이미지를 뜻한다. 네모전체를 감싸는 div에 선언해주었다.
          //세가지 변수는 순서만 지켜서 이름은 아무렇게나 선언해주면된다. 나는 공식홈페이지의 변수명을 사용했다.
      () => ({
        type: ItemTypes.Nemo,
        //간단하게 items.js를 만들어 그안에 nemo:'nemo'이런식으로 오브젝트를 만들어 주었다.
        //굳이 이렇게 하지 않고 ItemTypes.Nemo 를 'nemo'로 해주어도 문제는 없다.
        //후에 아이템들이 여러가지가 생겼을때 관리하기 편하게 items.js에 선언해준듯하다.
        //type:에 선언되는 것은 앞으로 드래깅 될 아이템은 어떤타입이다라는 걸 선언해준다. 
        item: { id, index },
        //item:에 선언되는 것은 'nemo'로 선언한 타입의 드래깅 물체안에 넣어줄 정보를 세팅한다.
        //나는 네모의 id와,index를 넣어주었다. id와 index는 props로 받아온정보로 부터 가져왔다.
        collect: (monitor) => ({
          isDragging: monitor.isDragging(),//isDragging 변수가 현재 드래깅중인지 아닌지를 리턴해주는 부분.
        }),
        end: (item, monitor) => { //드래그가 끝났을때 작동하는 부분.
          const { id: originId, index: originIndex } = item;
          const didDrop = monitor.didDrop();
          if (!didDrop) {//didDrop이 !(아니다)라는 것은 dropRef로 선언한 태그위에 드랍되지 않음 을 의미한다.
            //그때는 원래의 위치대로 이동.
            moveNemo(originId, originIndex); 
            //moveNemo는 변경할 네모의 id와 변경될 index를 주면 순서를 바꾸어주는 함수다.
            //네모들의 state가 상위 컴포넌트인 page에 선언되어있기 때문에 page에 선언되어 있다. 
          }
        },
      }),
      [id, index, moveNemo]
    );
    
    // 드랍 *공식사이트 예시에서는 하나의 dropRef로 구현되어있지만 
    // 시행착오와 고민 끝에 dropRef를 두가지로 만들어 네모상자의 양쪽에 배치하였다.
    // 왼쪽에 드랍하면 드래그한 상자를 드랍한 상자 왼쪽 인덱스로 이동.
    // 오른쪽에 드랍하면 index + 1 하여 오른쪽 인덱스로 이동하도록 구현했다.
    const [, dropLeft] = useDrop(
      () => ({
        accept: ItemTypes.Nemo,
        canDrop: () => false,
        hover({ id: draggedId, index: orgIndex }) {
          if (draggedId !== id) {
            moveNemo(draggedId, index);
          }
        },
      }),
      [moveNemo]
    );
    const [, dropRight] = useDrop(
      () => ({
        accept: ItemTypes.Nemo,
        canDrop: () => false,
        hover({ id: draggedId, index: orgIndex }) {
          if (draggedId !== id) {
            orgIndex !== index + 1 && moveNemo(draggedId, index + 1);
          }
        },
      }),
      [moveNemo]
    );
    
    useEffect(() => {
      isDragging ? setSomeDragging(true) : setSomeDragging(false);
    }, [isDragging, setSomeDragging]);
    //이 부분은 두가지 dropRef 네모를 가리게되면서 드래깅될때만 dropRef의 z-index가 최상위로 올라와 기능을 하고
    //평소에는 맨뒤로 내려가 네모안의 비디오를 클릭할수있게 상위컴포넌트에서 someDragging이라는 변수를 만들었다.

    return (
      <div ref={previewRef} //previewRef  처음에는 프리뷰와 dropRef 이곳에 지정했다.
           style={{opacity: isDragging ? '0.3' : '1',}}> //드래그중인 아이템의 투명도를 30%로 주었다.
        <div ref={dragRef} title="다른 카드 옆으로 드래그해서 위치를 변경합니다."> //dragRef
          {nemo.nemoTitle}
        </div>
        <div className={styles.imgs}/>
        //dropRef   좀 더 자연스러운 ux를 구현하기 위해서 dropRef를 두개로 나누었다. 
        //아래 두 dropRef는 absolutef로 네모의 왼쪽 오른쪽에 배치.
        <div
          ref={dropLeft}
          className={`${styles.drop} ${styles.left}`}
          style={{ zIndex: someDragging ? 30 : 0 }} 
          // dropRef가 무언가 드래그중일때 zindex를 30주고, 평소에는 0으로 주었다.
        ></div>
        <div
          ref={dropRight}
          className={`${styles.drop} ${styles.right}`}
          style={{ zIndex: someDragging ? 30 : 0 }}
        ></div>
      </div>
    );
  }
);
export default Nemo;

아래는 상위컴포넌트에 선언된 moveNemo 함수이다.
네모의 아이디를 받으면 배열에서 해당 아이디의 네모를 제거하고,가고자 하는 인덱스로 이동시켜주는 간단한 함수.

    const moveNemo = (nemoId, toIndex) => {
        const index = order.indexOf(nemoId);
        let newOrder = [...order];
        newOrder.splice(index, 1);
        newOrder.splice(toIndex, 0, nemoId);
        setOrder(newOrder)
      };

	// 여기서 order는 네모id를 순서대로 갖고있는 배열이다.
	//ex)
	[카페뮤직BGM,마음을통닭통닭,ourneeds,musicTag,asmrSoupe]
<변경 전>네모 전체가 드랍영역이었을 때 버그. 저렇게 동작하는 이유는 작은 네모가 큰 네모의 오른쪽 부분에 닿게 되면서 큰네모의 왼쪽으로 위치가 변경되는데 그 순간 또 큰네모의 왼쪽부분에 닿게되어 오른쪽으로 위치가 변경됨(무한반복) <변경 후> 왼쪽, 오른쪽 드랍영역을 따로 div로 만들어 주었다. 대신 드랍영역비디오를 가리게 되어 비디오가 클릭안되는 문제가 생겼는데, 네모들이 포함되는 상위 page컴포넌트someDragging이라는 변수를 만들어서 무언가 드래그중일 때만 드랍영역의 z-Index 를 30으로 변경하여 해결했다.

마치며..

내가 평소에 사용하고 있는 ux들은 매우 잘 설계되어있다는 것을 느끼게 되었고, 거기서 많은 힌트를 얻었다.
아이폰 사용중 홈화면 아이콘들을 정렬하다 느낀 것인데, 아이콘의 위에 호버되었을 때 위치가 변경되는 것이 아니라 아이콘의 좌우 공간에 호버 되었을때 아이콘의 위치가 바뀌는 모습이 매우 자연스럽게 느껴져 프로젝트에 적용시키게 됨.

첫 포스트라 횡설수설함이 심한거 같은데 꾸준히 글을 쓰며 설명하는 능력을 길러야 겠다.
다음 포스트는 그리드로 나열된 네모들을 드래그로 네모의 크기를 변경할수 있게 구현한 코드를 포스팅 해보겠다.

7개의 댓글

comment-user-thumbnail
2021년 7월 12일

멋진 프로젝트네요! 드래그앤드랍 기능 구현할때 한번 적용해봐야 겠어요.

1개의 답글
comment-user-thumbnail
2021년 7월 20일

잘 읽었습니다!! 저도 한번 구현해보고 싶네요

1개의 답글
comment-user-thumbnail
2021년 7월 22일

멋지네요. 화이팅입니다

1개의 답글
comment-user-thumbnail
2021년 12월 21일

코드까지 상세하게 주석 달아서 작성해주셔서 이해하는 데 크게 어렵지 않았습니다! 좋은 글 감사합니다

답글 달기