리액트 (React) 로 Sortable 컴포넌트 만들기입니다.
강의 보고 이해가 안가서, 복습할 겸 정리하는 글이다보니
정확도가 높지않을 수 있습니다. 그래도 구현은 했으니, 참고해주세요
https://www.npmjs.com/package/@imsmallgirl/sortable-list
완성본
폴더 구성
data
export const data = [
{content : 'react'},
{content : 'vue'},
{content : 'angular'},
{content : 'js'},
{content : 'redux'}
]
CSS 기본 세팅
SortableListItem.jsx
받아올 props 와 useRef() 설정해주기
import React, { useRef } from 'react'
function SortableListItem({index, draggable, children, onDragStart, onDropItem, onClickItem}) {
const itemRef = useRef(null)
return (
<li ref={itemRef}
className="item"
>
</li>
)
}
export default SortableListItem
받아올 props 에 대한 설명
1. index : 데이터를 map() 을 사용해서 SortableListItem 을 동적으로 생성할 때, index 의 값을 받아올 수 있기때문에 index 를 받아온다.
2. draggable : 드래그 여부를 알기 위해서 받아오는 값
3. children : 자식 컴포넌트 또는 html 엘리먼트가 어떻게 구성되어있는지 모르기때문에, 사용함 (밑에서 자세히 설명)
4. onDragStart : 리스트 아이템이 (li) 가 드래그 시작할 때 실행시킬 함수
5. onDropItem : 드래그가 완료되어서, 순서가 바뀔 때 데이터를 변화시키는 함수
6. onClickItem : 아이템을 클릭했을 때 해당 아이템의 인덱스를 alert () 메서드로 알려주기 위한 함수 ( 즉, 인덱스를 사용해 아이템 마다 실행시킬 함수를 만들어도됨 )
useRef() 을 사용하는 이유
1. 우리가 선택하려는 아이템을 사용하기 위해서 ( current ) 사용
2. 리렌더링을 하지 않고 컴포넌트의 속성만 조회하고 수정할 수 있기 때문에
props.children
- react 안의 keyword
- 주로 자식 컴포넌트 또는 html 엘리멘트가 어떻게 구성되어있는지 모르는 경우, 화면에 표시해야하는 경우에 사용함.
- 태그와 태그 사이의 모든 내용을 표시하기 위해 사용되는 특수한 props.
- 코드의 재사용성을 향상하며 JSX 요소를 좀 더 유연하고 밀접하게 다룰 수 있음.
- 태그와 태그 사이의 모든 요소들을 자식 취급하지는 않음.
사용방법 참고영상 : https://www.youtube.com/watch?v=C9GwW7lvg2o
참고 문서 : https://developer-talk.tistory.com/226
리스트 아이템 (li) 들의 드래그 이벤트에 맞춰서 클래스 추가해주기
import React, { useRef } from 'react'
function SortableListItem({index, draggable, children, onDragStart, onDropItem, onClickItem}) {
const itemRef = useRef(null)
const onDragStartItem = () => {
itemRef.current.classList.add('dragstart')
onDragStart(index)
}
const onDragEnd = () =>
itemRef.current.classList.remove('dragstart')
const onDragEnter = () =>
itemRef.current.classList.add('dragover')
const onDragLeave = () =>
itemRef.current.classList.remove('dragover')
const onDragOver = (e) => e.preventDefault()
const onDrop = () => {
itemRef.current.classList.remove('dragover')
onDropItem(index)
}
const onClick = () => onClickItem(index)
return (
<li ref={itemRef}
className="item"
draggable={draggable ? draggable : false}
onDragStart={onDragStartItem}
onDragEnd={onDragEnd}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
onDrop={onDrop}
onClick={onClick}
>
{children}
</li>
)
}
export default SortableListItem
이벤트에 대한 설명
1. draggable : 받아온 draggable 이 true 이면 , 요소와 상호동작을 할때마다 드래그 드롭 이벤트들이 발생하게 한다.
draggable={draggable ? draggable : false}
2. onDragStart : 드래그하려고 시작할 때 드래그 하려는 요소에게 "dragstart" 클래스를 추가해주고,
SortableList.jsx 에서 받아온 onDragStart() 라는 함수에게 index 를 파라미터로 전달해주어 실행시킨다. ( SortableList.jsx, App.js 참고 )
const onDragStartItem = () => { itemRef.current.classList.add('dragstart') onDragStart(index) }
3. onDragEnd : 드래그하다가 마우스 버튼을 놓는 순간에 "dragstart" 클래스를 제거해준다.
const onDragEnd = () => itemRef.current.classList.remove('dragstart')
4. onDragEnter : 마우스가 대상 요소의 위로 처음 진입할 때 "dragover" 클래스를 추가해준다. (border-top-color 변경 클래스)
const onDragEnter = () => itemRef.current.classList.add('dragover')
5. onDragLeave : 드래그가 끝나서 마우스가 대상 요소의 위에서 벗어날 때 "dragover" 클래스를 제거한다 (border-top-color 제거)
const onDragLeave = () => itemRef.current.classList.remove('dragover')
6. onDragOver : 드래그하면서 마우스가 대상 요소의 영역 위치에 자리 잡고 있을 때 기본 동작을 막아주는 event.preventDefault() 를 실행시켜준다.
* 기본적으로 HTML 요소는 다른 요소의 위에 위치할 수 없음
=> 다른 요소 위에 위치할 수 있도록 만들기 위해서는 놓일 장소에 있는 요소의 기본 동작을 막아야함.const onDragOver = (e) => e.preventDefault()
7. onDrop : dragover 이벤트가 실행되고 있는 요소의 드래그를 끝내면 "dragover" 라는 클래스를 제거해준다 (border-color 제거)
SortableList.jsx 에서 받은 onDropItem() 에 index를 넣어 실행시켜준다 ( SortableList.jsx, App.js 참고 )const onDrop = () => { itemRef.current.classList.remove('dragover') onDropItem(index) }
8. onClick : 클릭했을 때, 클릭한 인덱스를 alert () 메서드로 알려주기 위해 onClickItem () 함수 실행 ( SortableList.jsx, App.js 참고 )
const onClick = () => onClickItem(index)
주의할 점
- drop, dragover 이벤트는 필수로 사용해야하는 이벤트
- dragover 이벤트를 적용하지 않으면 drop 이벤트가 작동하지 않음
dragleave
- dragenter 이벤트와 동작이 겹칠 수 있기 때문에 preventDefault() 로 제한하며 둘이 결합하여 사용해야함
drop
- drop 이벤트 역시 드롭될 요소에는 preventDefault() 를 사용하지 않으면 정상적인 동작이 되지 않을 수 있음
SortableList.jsx
App.js 에서 받아 올 props 설정
import React, { useCallback, useState } from 'react'
import SortableListItem from './SortableListItem'
import "./SortableList.css"
function SortableList({data, onDropItem, onClickItem, renderItem}) {
return (
)
}
export default SortableList
props 에 대한 설명
1. data : testData.js 에서 받아오는 data 들
2. onDropItem : 드래그가 끝났을 때 발생할 함수
3. onClickItem : list item (li) 를 클릭할 때 발생할 함수
4. renderItem : App.js 에서 전달한 함수 ( App.js 에서 자세히 설명 )
기본 값 설정하기 (useState)
function SortableList({data, onDropItem, onClickItem, renderItem}) {
const [startIndex, setStartIndex] = useState(0)
const [listData, setListData] = useState(data)
return (
)
}
export default SortableList
1. SortableListItem.jsx 에서 클릭한 index 의 값을 받아서 저장할 곳 ( 드래그를 시작한 index )
2. 바뀌는 데이터를 listData 에 저장해, SortableListItem 을 동적으로 생성하기위해 만들어주고, 기본 값은 data 로 한다.
onDragStart ()
function SortableList({data, onDropItem, onClickItem, renderItem}) {
const [startIndex, setStartIndex] = useState(0)
const [listData, setListData] = useState(data)
const onDragStart = (index) => setStartIndex(index)
return (
)
}
export default SortableList
1. SortableListItem.jsx에서 받아온 index 값을 startIndex 에 저장해준다
onDrop () - 드래그가 완료되었을 때, 실행 될 함수
import React, { useCallback, useState } from 'react'
import SortableListItem from './SortableListItem'
import "./SortableList.css"
function SortableList({data, onDropItem, onClickItem, renderItem}) {
const [startIndex, setStartIndex] = useState(0)
const [listData, setListData] = useState(data)
const onDragStart = (index) => setStartIndex(index)
const onDrop = useCallback((dropIndex) => {
const dragItem = listData[startIndex]
const list = [...listData]
list.splice(startIndex, 1)
const newListData = startIndex < dropIndex ?
[...list.slice(0, dropIndex-1), dragItem, ...list.slice(dropIndex-1, list.length)]
: [...list.slice(0, dropIndex), dragItem, ...list.slice(dropIndex, list.length)]
setListData(newListData)
onDropItem(newListData)
},[startIndex, onDropItem, listData])
return (
)
}
export default SortableList
1. SortableListItem.jsx 에서 받아온 dropIndex(드래그가 완료되어서 바뀐 index) 값을 매개변수로 받아온다.
2. dragItem 은 listData의 데이터에서 dropIndex (드래그가 완료되어서 바뀐 index) 를 저장해준다.
3. list 는 listData 의 모든 데이터를 잠시 저장해놓는다
4. list 에서 splice() 메서드를 사용해 드래그 되는 데이터를 삭제해준다.
5. newListData 는 드래그가 된 새로운 데이터 (즉, 바뀐 데이터) 를 저장해두는 곳이다.
6. startIndex (처음 드래그한 아이템 index) 보다 dropIndex (드래그해서 둔 곳의 index) 보다 작을 경우를 삼항 연산자를 통해서 코드를 작성해준다 ( 자세한 설명은 바로 밑에서 )
7. ListData 에 새로 수정된 데이터를 넣어준다.
8. onDropItem() 함수에도 새로 수정된 데이터를 넣어서 console.log를 통해서 새로 바뀐 데이터의 정보를 볼 수 있게 한다.
9. useCallback 을 사용해 자식 컴포넌트에게 props 로 전달하는 함수의 재실행이 일어나지 않도록 해준다. ( 밑에서 자세한 설명 )
=> startIndex ( 드래그를 시작할 때 바뀌는 값) , onDropItem (newListData 가 바뀌기 때문에), listData (받아오는 data 가 달라질 수 있기 때문에)
8번 설명
- true 일 경우 저장되는 데이터들에 대한 순서 설명
1. 잠시 저장해놓은 모든 list 데이터에서 첫번째부터 dropIndex (드래그해서 둔 곳의 index) 에서 1을 뺀 개수의 데이터를 반환한 배열
console.log(dropIndex, dropIndex - 1)
=> 4 , 3
첫번째 데이터 (0) 을 4번째로 이동했을 때 dropIndex
...list.slice(0, dropIndex-1)
드래그 완료된 곳에 앞부분만 잘라서 배열로 반환한다
2. 처음으로 드래그를 시작한 아이템 ( 위치를 옮기려고 하는 데이터 )
3. 모든 list 에서 dropIndex (드래그해서 둔 곳의 index) 에서 1을 뺀 순서 (index) 에서 list.length 만큼 자른 뒷 데이터를 반환한 배열
console.log(...list.slice(dropIndex-1, list.length))
=> {content: 'js'} {content:'redux'}
첫번째 데이터 (0) 을 index 2번째로 이동했을 때 반환된 배열
...list.slice(dropIndex-1, list.length)
드래그 완료된 곳이 index 2번째라고 하면 1번째까지 list 의 개수만큼 잘라낸 데이터를 반환한다.
list.length 는 4로 처음부터 list에서 드래그 시작한 데이터를 잘라낸 데이터를 반환했기 때문에
(즉, 드래그를 시작한 데이터는 제외하고 계산)
- false 일 경우 저장되는 데이터들에 대한 순서 설명 ( false 일 경우라면 드래그를 해서 위치 이동을 안했다는 의미 )
1. 드래그를 시작한 데이터를 제외한 모든 list 에서 첫번째 index까지 dropIndex 개수 만큼 잘라내 배열을 반환한다.
console.log(...list.slice(0, dropIndex))
=> {content:'react'}
예를 들어 index 2번째 요소를 1번째로 옮겼을 때, 첫번째 순서에서 1개 잘라준 배열을 반환함
2. 처음으로 드래그를 시작한 아이템 ( 위치를 옮기려고 하는 데이터 )
3. dropIndex 까지 list.length(4) 개수 만큼 잘라내 배열을 반환한다
console.log(...list.slice(dropIndex, list.length))
=> {content: 'vue'} {content: 'js'} {content: 'redux'}
index 2번째 데이터를 1번째로 이동했을 때, 1번째 다음부터 list.length(4) 만큼 잘라준 데이터를 반환함
이걸 이해하려면 slice() 배열 함수에 대해 더 알아야합니다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
useCallback
- 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용
- 함수를 useCallback 으로 감싸주면 react가 함수를 저장하고 두 번째 매개변수인 dependencies가 변경되지 않는다면 함수를 재생성하지 않기 때문에 해당 함수를 props로 가지는 자식 컴포넌트들은 함수가 바뀌었다고 생각하지 않게 된다.
useCallback(fn, deps)
fn : 첫번째 인자로는 인라인 콜백과 의존성 값의 배열을 받음
deps : 의존성 배열인 deps에 변경을 감지해야할 값을 넣어주게 되면 deps 값이 변경될 때마다 콜백 함수를 새로 생성
참고 : https://velog.io/@rjsdnql123/TIL-React.useCallback-%EC%9D%B4%EB%9E%80
listData.map() 을 사용해, SortableListItem 동적으로 생성하기
import React, { useCallback, useState } from 'react'
import SortableListItem from './SortableListItem'
import "./SortableList.css"
function SortableList({data, onDropItem, onClickItem, renderItem}) {
const [startIndex, setStartIndex] = useState(0)
const [listData, setListData] = useState(data)
const onDragStart = (index) => setStartIndex(index)
const onDrop = useCallback((dropIndex) => {
const dragItem = listData[startIndex]
const list = [...listData]
list.splice(startIndex, 1)
console.log(...list.slice(dropIndex, list.length))
const newListData = startIndex < dropIndex ?
[...list.slice(0, dropIndex-1), dragItem, ...list.slice(dropIndex-1, list.length)]
: [...list.slice(0, dropIndex), dragItem, ...list.slice(dropIndex, list.length)]
setListData(newListData)
onDropItem(newListData)
},[startIndex, onDropItem, listData])
return (
<ul className='sortable-list'>
{listData.map((item,index) => (
<SortableListItem
key={index}
index={index}
draggable={true}
onDropItem={onDrop}
onDragStart={onDragStart}
onClickItem={onClickItem}
>
{renderItem(item,index)}
</SortableListItem>
))}
<SortableListItem
key={listData.length}
index={listData.length}
draggable={false}
onDropItem={onDrop}
/>
</ul>
)
}
export default SortableList
1. ul 에 className 으로 'sortable-list' 를 추가해준다. (css)
2. listData 의 배열 데이터들을 map () 을 사용해 item (data) , index 를 매개변수로 받아와준다.
3. SortableListItem 을 import 해와서 컴포넌트를 동적으로 생성해준다.
4. renderItem 함수는 App.js 를 보면서 이해하면 좋을 듯. renderItem 안에 매개변수로 map 으로 돌려서 반환된 값을 넣어준다.
SortableListItem 에게 보내는 props 에 대한 설명
1. key : 매개변수로 받은 index 를 넣어서 유니크한 Key 값을 준다
2. index : 매개변수로 받은 index 를 넣어서 index 값을 props 로 전달 ( current index 값을 알아낼 수 있음 )
3. draggable : true 를 주는 아이는 드래그를 할 실제 요소고, false 는 마지막 요소에 두어서, 마지막 요소 위로 드래그 할 수 있도록 해주기 위해서 가상으로 만들었다고 생각하면 됨. (어차피 드래그 이벤트 안됨)
4. onDropItem : onDrop에서 드래그 완료한 index 값을 받아와야하기 때문에 onDrop 함수를 props 로 보낸다.
5. onDragStart : 드래그를 시작한 index 를 받아와야하기 때문에 onDragStart 함수를 props로 보낸다.
6. onClickItem : 아이템을 클릭했을 때, index 값을 알람으로 보여줘야하기 때문에 App.js 에서 받아온 함수 (onClickItem) 을 보낸다.
App.js
import SortableList from "./lib/SortableList";
import {data} from "./TestItem/testData"
import TestItem from "./TestItem/TestItem";
function App() {
const onClickItem = (index) => alert(index)
const onDropItem = (newList) => console.log(newList)
return (
<SortableList
data={data}
renderItem={(item,index) => <TestItem data={item} index={index}/>}
onDropItem={onDropItem}
onClickItem={onClickItem}
/>
);
}
export default App;
1. 클릭한 index 를 SortableListItem.jsx 받아와, alert() 으로 몇번 째 데이터를 클릭했는지 알려준다.
2. 바뀐 데이터를 보여줄 수 있도록 SortableList.jsx 에서 데이터를 받아와, console.log 로 데이터를 확인할 수 있도록 한다.
3. SortableList 컴포넌트를 가져와준다.
SortableList 에 전달하는 props 에 대한 설명
1. data : testData.js 에서 data 를 가져와서 props로 전달
2. renderItem : SortableList.jsx 에서 item,index를 받아와 TestItem (밑에서 만듬) 에 전달해주는 함수
3. onDropItem : index 값을 알아내기 위해서 자식 컴포넌트에게 함수를 전달한다.
4. onClickItem : 위와 동일함
TestItem.jsx
import React from 'react'
import "./TestItem.css"
function TestItem({data, index}) {
return (
<div className="test-item">
<div>
<p>content : {data.content}</p>
<p>index : {index}</p>
</div>
</div>
)
}
export default TestItem
1. 만들어놓은 TestItem.css 를 import 해준다.
2. App.js 에서 props 로 받아온 데이터를 설정해준다.
3. test-item 이라는 클래스를 div 에게 추가해준다. ( 데이터 개수만큼 동적으로 생성 = SortableListItem )
4. content 에는 data (item)의 content 데이터를 동적으로 넣어준다.
5. index 에는 현재 index 의 값을 넣어준다. ( 드래그를 할때마다 동적으로 변경됨 )
'React > 응용프로젝트' 카테고리의 다른 글
[노마드 코더] React로 영화 앱 만들기3 (0) | 2022.11.11 |
---|---|
[노마드코더] React로 영화 앱 만들기2 (0) | 2022.11.11 |
[노마드코더] React로 영화 앱 만들기1 (0) | 2022.11.10 |