yourTube
는 다른영상에 빠져들지 않고 (유튜브에는 시간뺏길 영상이 너무나도 많다 ㅠㅜ) 내가 꾸민 유튜브를 이용해 필요한 영상만 볼 수 있게 만들고 있는 프로젝트이다.
여기서 내가 꾸미는 페이지를 드래그엔 드랍으로 구현하는 것이 목표였다.
리액트 드래그앤드랍을 검색하던중 npm트렌드 사이트에서 위 그래프를 보게되었다.
1. 나는 네모난 재생목록들을 grid로 배열해야했다.
2. 그리고 네모난 재생목록들을 Resizing 할 수 있는 기능도 구현해야 했다.
두가지를 하기위해 좀 더 자유도가 높은 react-dnd
를 이용했다.
react-beautiful-dnd
는 자유도(드래그앤드랍으로 순서변경, 다른 박스로 드래그앤드랍 정도만 가능.)는 적지만 간단한 기능만 구현할것이라면 자연스러운 고급 애니메이션 까지 적용 되어있는 react-beautiful-dnd
를 추천한다.
react-dnd 공식 사이트에 있는 여러가지 예시 코드들이 react-dnd
를 이해하는데 매우 많은 도움이 되었다. 공식 사이트의 업데이트가 매우 활발 하니 공식 사이트에서 예시를 참조해서 만드는 것이 가장 최신버전으로 만드는 방법이다.(내가 기능을 구현한 이후 며칠 후 같은 예시지만 다른코드들로 대체 되어 있었고, 그리고 내가 구현할 당시에도 참고하려고 했던 블로그와 코드가 달라 애를 먹어 결국 공식 사이트의 예시를 참고해 만들었다.)
우선 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
컴포넌트의 코드이다.
useDrag
와 useDrop
을 '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들은 매우 잘 설계되어있다는 것을 느끼게 되었고, 거기서 많은 힌트를 얻었다.
아이폰 사용중 홈화면 아이콘들을 정렬하다 느낀 것인데, 아이콘의 위에 호버되었을 때 위치가 변경되는 것이 아니라 아이콘의 좌우 공간에 호버 되었을때 아이콘의 위치가 바뀌는 모습이 매우 자연스럽게 느껴져 프로젝트에 적용시키게 됨.
첫 포스트라 횡설수설함이 심한거 같은데 꾸준히 글을 쓰며 설명하는 능력을 길러야 겠다.
다음 포스트는 그리드로 나열된 네모들을 드래그로 네모의 크기를 변경할수 있게 구현한 코드를 포스팅 해보겠다.
멋진 프로젝트네요! 드래그앤드랍 기능 구현할때 한번 적용해봐야 겠어요.