- React로 Drag and Drop List 만들기 #1
- React로 Drag and Drop List 만들기(Swap) #2
- React로 Drag and Drop List 만들기(Sortable) #3
드디어 마지막 목적지 Sortable list를 만들어 보자.
우선 css부터 한번 작성해보자.
위아래로 움직이는 애니메이션을 주기 위해 transform과 transition을 사용한다.
- transform : 해당 요소의 크기, 위치 등을 조정한다.
- transition : 변화(width, height, transform 등의 변화)에 대한 딜레이(?)를 조정한다.
transform 사용
선택한 요소가 다른 요소 위에 들어갈 경우(ondragenter) 위아래로 움직여야하기 때문에 transform에서 translate를 이용해 위치를 이동한다. 이전에 만든 list 요소 하나의 height가 41px 이며, 위로 올라갈 경우 moveup 클래스를, 밑으로 내려갈 경우 movedown 클래스를 추가하고, transition을 통해 움직이는 느낌을 주려고 한다.
li { | |
cursor: grab; | |
padding: 10px 15px; | |
transition: transform 200ms ease 0s; | |
&.grabbing { | |
cursor: grabbing; | |
} | |
&.move_up { | |
transform: translate(0, -41px) | |
} | |
&.move_down { | |
transform : translate(0, 41px); | |
} | |
} |
생각해야하는 내용
위의 작업을 하려면 다음과 같은 조건이 필요하다. 위의 코드는 필자의 깃허브에 있다. 자세히 보기
- 선택한 요소보다 위에 있는 요소는 밑으로 내려와야한다.
- 선택한 요소보다 아래에 있는 요소는 위로 내려와야한다.
- 따라서 선택한 요소의 정보가 필요하며 이동시 곂쳐지는 요소가 위에 있는지, 밑에 있는지 판단할 수 있어야한다.
- 만약 움직였다가 Drop하지 않고 다시 원래 위치로 이동할 경우 움직였던 요소들도 원래대로 돌아와야한다.
1, 2, 4번을 해결하기 위에서는 현재 선택한 요소 정보(GrabData)와 선택한 요소가 다른 요소위에 있을 경우 해당 요소의 정보(TargetData)가 필요하다. TargetData를 이탈 할 경우도 체크해야하니 Drag and Drop API에서 다음과 같은 API를 사용할 예정이다.
- GrabData를 가져올 수 있는 ondragstart
- TargetData를 가져올 수 있는 ondragenter
- TargetData를 이탈했을 경우 정보를 가져올 수 있는 ondragleave
간단히 말해 Twitter를 잡아서(e.target = Twitter) Github 위에 지나갈 경우와 Github를 이탈할 경우(e.target = Github)를 체크하면 된다.
let GrabData = null;
const onDragStart = (e) => {
console.log(e.target); // e.target is Twitter
};
const onDragEnter = (e) => {
console.log(e.target); // e.target is Github
};
const onDragLeave = (e) => {
console.log(e.target); // e.target is Github
};
onDragStart에서 GrabData 정보를, onDragEnter / onDragLeave 에서 TargetData를 확인 할 수 있기에 이를 어떻게 활용해야하나 하다가 onDragStart에서 GrabData를 state로 저장하고, onDragEnter 에서 GrabData와 TargetData의 상하관계를 판단해서 transform을 추가하고, onDragLeave에서 추가된 transform을 지우기로 했다.
const _onDragEnter = e => { | |
let grabPosition = Number(grab.dataset.position); | |
let targetPosition = Number(e.target.dataset.position); | |
if (grabPosition < targetPosition) e.target.classList.add("move_up"); | |
else if (grabPosition > targetPosition) e.target.classList.add("move_down"); | |
} | |
const _onDragLeave = e => { | |
e.target.classList.remove("move_up"); | |
e.target.classList.remove("move_down"); | |
} |
문제점
- GrabData가 TargetData위에 올라가면(onDragEnter) 움직이는데 움직이면서 위치가 한칸 위/아래로 움직여서 이탈처리가 된다. 이탈이 되면 onDragLeave가 되니 원래대로 돌아온다. 이걸 계속 반복하니 이상하게 된다.
- 어느순간 Drop을 하면 뭔가 빈 리스트가 생긴다.
- 뭔가 순차적으로 위/아래로 이동해야 하는데 안된다.
생각해보기
우선 onDragEnter와 onDragLeave에서 transform 클래스를 추가/제거 하는 코드를 지우고 다르게 transform을 적용할 수 있는 방법을 찾다가 moveup, movedown의 정보를 따로 저장하고 return()에서 해당 값을 확인해서 랜더링 할 수 있는 방법을 생각했다.
결과
설명
const _SocialNetworks = [ | |
{ title: "Twitter", color: "white", backgroundColor: "Red" }, | |
{ title: "Facebook", color: "black", backgroundColor: "Orange" }, | |
{ title: "Line", color: "black", backgroundColor: "Yellow" }, | |
{ title: "Instagram", color: "white", backgroundColor: "Green" }, | |
{ title: "Telegram", color: "white", backgroundColor: "Blue" }, | |
{ title: "KaKao", color: "white", backgroundColor: "DarkBlue" }, | |
{ title: "LinkedIn", color: "white", backgroundColor: "Purple" }, | |
] | |
const _initGrabData = { | |
target: null, | |
position: null, | |
move_up: [], | |
move_down: [], | |
updateList: [] | |
} | |
const App = () => { | |
const [lists, setLists] = React.useState(_SocialNetworks); | |
const [grab, setGrab] = React.useState(_initGrabData); | |
const [isDrag, setIsDrag] = React.useState(false); | |
React.useEffect(() => {}, [grab]); | |
const _onDragOver = e => { e.preventDefault(); } | |
const _onDragStart = e => { | |
setIsDrag(true); | |
setGrab({ | |
...grab, | |
target: e.target, | |
position: Number(e.target.dataset.position), | |
updateList: [...lists] | |
}); | |
e.target.classList.add("grabbing"); | |
e.dataTransfer.effectAllowed = "move"; | |
e.dataTransfer.setData("text/html", e.target); | |
} | |
const _onDragEnd = e => { | |
setIsDrag(false); | |
e.target.classList.remove("grabbing"); | |
e.dataTransfer.dropEffect = "move"; | |
setLists([...grab.updateList]); | |
setGrab({ | |
target: null, | |
move_up: [], | |
move_down: [], | |
updateList: [] | |
}); | |
e.target.style.visibility = "visible"; | |
} | |
const _onDragEnter = e => { | |
let grabPosition = Number(grab.target.dataset.position); | |
let listPosition = grab.position; | |
let targetPosition = Number(e.target.dataset.position); | |
let move_up = [...grab.move_up]; | |
let move_down = [...grab.move_down]; | |
let data = [...grab.updateList]; | |
data[grabPosition] = data.splice(targetPosition, 1, data[grabPosition])[0]; | |
if (listPosition > targetPosition) { | |
move_down.includes(targetPosition) ? move_down.pop() : move_down.push(targetPosition); | |
} else if (listPosition < targetPosition) { | |
move_up.includes(targetPosition) ? move_up.pop() : move_up.push(targetPosition); | |
} else { | |
move_down = []; | |
move_up = []; | |
} | |
setGrab({ | |
...grab, | |
move_up, | |
move_down, | |
updateList: data, | |
position: targetPosition | |
}) | |
} | |
const _onDragLeave = e => { | |
if (e.target === grab.target) { | |
e.target.style.visibility = "hidden"; | |
} | |
} | |
return ( | |
<Contaier> | |
<List | |
onDragOver = { | |
_onDragOver | |
} | |
> | |
{ | |
lists.map((sns, index) => { | |
let classNames = "" | |
grab.move_up.includes(index) && (classNames = "move_up") | |
grab.move_down.includes(index) && (classNames = "move_down") | |
return ( | |
<ListItem | |
key = { index } | |
data-position = { index } | |
className = { classNames } | |
isDrag = { isDrag } | |
onDragStart = { _onDragStart } | |
onDragEnd = { _onDragEnd } | |
onDragEnter = { _onDragEnter } | |
onDragLeave = { _onDragLeave } | |
draggable | |
style = {{ | |
backgroundColor: sns.backgroundColor, | |
color: sns.color, | |
fontSize: "bold" | |
}} | |
> | |
{ | |
sns.title | |
} | |
</ListItem> | |
) | |
}) | |
} | |
</List> | |
<div> MoveUp: { grab.move_up } </div> | |
<div> MoveDown: { grab.move_down } </div> | |
</Contaier> | |
) | |
} | |
const Contaier = window.styled.div ` | |
background-color: black; | |
width: 100vw; | |
height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
flex-direction: column; | |
`; | |
const List = window.styled.ul ` | |
width: 300px; | |
list-style: none; | |
`; | |
const ListItem = window.styled.li ` | |
cursor: grab; | |
padding: 10px 15px; | |
${props => props.isDrag && "transition: transform 200ms ease 0s"}; | |
&.grabbing { | |
cursor: grabbing; | |
} | |
&.move_up { | |
transform: translate(0, -41px) | |
} | |
&.move_down { | |
transform : translate(0, 41px); | |
} | |
`; | |
ReactDOM.render( < App / > , document.querySelector("#root")) |
- [11-17] _initGrabData : 좀 더 상세화 된 grab 정보를 state로 저장하기 위한 기본 틀
- [22] isDrag : 현재 드래그 중인지 아닌지의 정보를 담은 state
- [28-40] _onDragStart : 드래그를 시작하기 때문에 isDrag를 true로 변경하고, 현재 선택된 요소 정보(li tag, position 정보)를 저장하고 현재 리스트 정보를 다음 리스트(updateList)로 판단하여 저장한다.
- [42-57] _onDragEnd : 드래그가 끝났기 때문에 isDrag를 false로 변경하고, 다음 리스트(updateList)를 현재 리스트로 변경한다.
-
[59-86] _onDragEnter : 필요한 정보를 가져오고 필요에 맞게 설정한다.
- 선택된 요소의 position 정보(grabPosition)
- 상하관계를 가지는 타겟 요소의 position 정보(targetPosition)
- updateList에서 선택된 요소의 position 정보(listPosition)
- 현재 리스트 중 moveup, movedown 클래스를 적용시킬 index 배열(moveup, movedown)
- [102-105, 111] moveup, movedown 배열과 index의 포함관계에 따라 클래스 적용
마치면서
좋은 라이브러리를 사용하지 않고 오로지 나의 힘으로 해당 기능을 완성했다. 처음엔 뭐가 잘못됐는지 뭐가 다른지를 찾는것에 오래 걸렸다. 하지만 아직 부족한 것이 많은 기능이기에 조금씩 더 보완하면서 지식을 다듬고 싶다.