๐๐ป Drag And Drop ๊ธฐ๋ฅ ๊ตฌํ
๐จ ๊ฐ์ฌ๋๊ป์๋ `react-beautiful-dnd`๋ฅผ ์ฌ์ฉํ์ จ์ง๋ง ํด๋น ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ ์ง๋ณด์ ์ค๋จ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ํ์ฌ ๋ฆฌ์กํธ ๋ฒ์ ๊ณผ ๋ง์ง ์์ ์๋ฌ๊ฐ ๋ฐ์ํด ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ `@hello-pangea/dnd`๋ฅผ ์ฌ์ฉํ๋ค.
GitHub - hello-pangea/dnd: ๐ Beautiful and accessible drag and drop for lists with React. โญ๏ธ Star to support our work!
๐ Beautiful and accessible drag and drop for lists with React. โญ๏ธ Star to support our work! - hello-pangea/dnd
github.com
- ์ค์น `npm install @hello-pangea/dnd`
๊ตฌ์กฐ
- `<DragDropContext />`
- ๋๋๊ทธ๋๋ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๊ณ ์ถ์ ๋ถ๋ถ์ ๊ฐ์ธ์ฃผ๋ ํ๊ทธ
- ์ฌ์ฉ ์ `onDragEnd` ํ์
- `<Droppable />`
- ๋๋๊ทธํด์ ๋๋ํ ๋ถ๋ถ์ ๊ฐ์ธ์ฃผ๋ ํ๊ทธ
- ์ฌ์ฉ ์ `droppableId`ํ์
- `<Draggable />`
- ๋๋๊ทธํ ์์ดํ ์ ๊ฐ์ธ์ฃผ๋ ํ๊ทธ
- ์ฌ์ฉ ์ `draggableId`์ `index` ํ์
- `provided.placeholder`
- ๋๋๊ทธ ์์ ์ด ์์ฐ์ค๋ฝ๊ฒ ๋๊ปด์ง๋๋ก ์ถ๊ฐ
task ์ฑ์ ์ ์ฉํด๋ณด๊ธฐ
// App.tsx
<div className={board}>
<DragDropContext onDragEnd={handleDragEnd}>
<ListContainer lists = {lists} boardId={getActiveBoard.boardId}/>
</DragDropContext>
</div>
// List.tsx
return (
<Droppable droppableId={list.listId}>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={listWrapper}
>
//...
{provided.placeholder}
// ...
</div>
)}
</Droppable>
)
// Task.tsx
return (
<Draggable draggableId={id} index={index}>
{provided => (
<div
className={container}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className={title}>{taskName}</div>
<div className={description}>{taskDescription}</div>
</div>
)}
</Draggable>
)
๋จผ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ปดํฌ๋ํธ๋ก ์์ญ์ ์ก์์ฃผ์๋ค.
sort: (state, {payload}: PayloadAction<TSortAction>) => {
// same list
if(payload.droppableIdStart === payload.droppableIdEnd) {
const list = state.boardArray[payload.boardIndex].lists.find(
list => list.listId === payload.droppableIdStart
)
// ๋ณ๊ฒฝ์ํค๋ ์์ดํ
์ ๋ฐฐ์ด์์ ์ง์์ค
// return ๊ฐ์ผ๋ก ์ง์์ง ์์ดํ
์ก์์ค
const card = list?.tasks.splice(payload.droppableIndexStart, 1);
list?.tasks.splice(payload.droppableIndexEnd, 0, ...card!);
}
// other list
if(payload.droppableIdStart !== payload.droppableIdEnd) {
const listStart = state.boardArray[payload.boardIndex].lists.find(
list => list.listId === payload.droppableIdStart
)
const card = listStart?.tasks.splice(payload.droppableIndexStart, 1);
const listEnd = state.boardArray[payload.boardIndex].lists.find(
list => list.listId === payload.droppableIdEnd
)
listEnd?.tasks.splice(payload.droppableIndexEnd, 0, ...card!);
}
}
๋๋๋์์ ๋ ์ฒ๋ฆฌ๋ ์ก์
์ ์ ์ํด ์ฃผ์๋ค.
๊ฐ์ ๋ฆฌ์คํธ์์์ ๋๋๋์๋ค๋ฉด ์์๋ฅผ ๋ฐ๊ฟ์ฃผ๊ณ
๋ค๋ฅธ ๋ฆฌ์คํธ๋ก ์ฎ๊ฒจ์ก๋ค๋ฉด ํด๋น ์์น์ ์ธ๋ฑ์ค์ ์ถ๊ฐํ๋๋ก ํด์ฃผ์๋ค.
์ฌ๊ธฐ์ `...card!` ๋๋ํ๋ฅผ ๋ถ์ฌ์ค ์ด์ ๋ undefined์ ๋ํ ์๋ฌ๊ฐ ๋ฐ์ํด undefined๊ฐ ์๋ค๊ณ ๋ช
์ํด์ค ๊ฒ์ด๋ค.
const handleDragEnd = (result: any) => {
const {destination, source, draggableId} = result;
const sourceList = lists.filter(
list => list.listId === source.droppableId
)[0];
dispatch(
sort({
boardIndex: boards.findIndex(board => board.boardId === activeBoardId),
draggableIdStart: source.droppableId,
droppableIdEnd: destination.droppableId,
droppableIndexStart: source.index,
droppableIndexEnd: destination.index,
draggableId
})
)
dispatch(
addLog({
logId: v4(),
logMessage: `
๋ฆฌ์คํธ "${sourceList.listName}"์์
๋ฆฌ์คํธ "${lists.filter(list=>list.listId === destination.droppableId)[0].listName}์ผ๋ก
${sourceList.tasks.filter(task => task.taskId === draggableId)[0].taskName}์ ์ฎ๊น.
`,
logAuthor: "User",
logTimestamp: String(Date.now())
})
)
}
๋ง์ง๋ง์ผ๋ก onDragEnd์ ๋ฃ์ด์ค ๋ฉ์๋์ ํด๋น ์ก์ ์ ์ถ๊ฐํด์ฃผ๋ฉด ์์ฑ
๐๏ธ ๋ก๊ทธ์ธ & ๋ก๊ทธ์์
Firebase | Google's Mobile and Web App Development Platform
๊ฐ๋ฐ์๊ฐ ์ฌ์ฉ์๊ฐ ์ข์ํ ๋งํ ์ฑ๊ณผ ๊ฒ์์ ๋น๋ํ๋๋ก ์ง์ํ๋ Google์ ๋ชจ๋ฐ์ผ ๋ฐ ์น ์ฑ ๊ฐ๋ฐ ํ๋ซํผ์ธ Firebase์ ๋ํด ์์๋ณด์ธ์.
firebase.google.com
ํ์ด์ด๋ฒ ์ด์ค๋ฅผ ํตํด ๊ตฌ๊ธ ๋ก๊ทธ์ธ์ ๊ตฌํํ๋ ค ํ๋ค.
์ฐ์ธก ์๋จ `Go to console`๋ก ๋ค์ด๊ฐ ํ์ด์ด๋ฒ ์ด์ค ํ๋ก์ ํธ๋ฅผ ์์ฑํ๋ค.
์์ ์ ํ๋ก์ ํธ์ ๋ง๋ ํ๋ซํผ์ผ๋ก ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค.
๋๋ ๋ฆฌ์กํธ ์น์ด๊ธฐ ๋๋ฌธ์ ์น์ ์ถ๊ฐํ๊ธฐ๋ก ์งํํ๋ค.
๊ทธ ํ ์๋ด์ ๋์ค๋ ๋๋ก SDK๋ฅผ ์ค์นํด์ค ํ
`src/firebase.ts`์ ์ ์ฝ๋๋ฅผ ์ถ๊ฐํด์คฌ๋ค.
`app` ๊ฐ์ฒด๋ฅผ ๋ค๋ฅธ ํ์ผ์์ ์ฌ์ฉํ๊ธฐ ์ํด export๋ง ์ถ๊ฐํด์ฃผ์๋ค.
๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด ํ์ด์ด๋ฒ ์ด์ค์์ ์ธ์ฆ ์ค์ ์ ํด์ค์ผ ํ๋ค.
๋ค์ํ ๋ฐฉ์์ ์ ํํ ์ ์๋๋ฐ ์ฐ์ ๊ตฌ๊ธ๋ก ์งํํ๋ค.
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
import { app } from '../../firebase'
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
const handleLogin = () => {
signInWithPopup(auth, provider) // ํ์
์ผ๋ก ๋ก๊ทธ์ธ์ฐฝ ๋์
.then(userCredential => { // userCredential : ๋ก๊ทธ์ธํ ์ ์ ์ ๋ณด๊ฐ ๋ด๊น
console.log(userCredential);
})
}
์์ ์ค์นํ ํ์ด์ด๋ฒ ์ด์ค SDK๋ฅผ ํตํด ํ์
์ฐฝ์ผ๋ก ๊ตฌ๊ธ ๋ก๊ทธ์ธ์ด ์งํ๋๋๋ก ํ์๋ค.
์ฝ์๋ก ํ์ธํด๋ณด๋ฉด ๋ก๊ทธ์ธํ ๊ณ์ ์ ์ ๋ณด๊ฐ ์ ๋ฐ์์ ธ ์ค๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ด์ ์ด ์์ user์ ์๋ ์ ๋ณด๋ฅผ store์ ์ ์ฅํด์ผ ํ๋ค.
// userSlice.tsx
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
email: '',
id: ''
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action) => {
state.email = action.payload.email;
state.id = action.payload.id;
}
}
})
export const {setUser} = userSlice.actions;
export const userReducer = userSlice.reducer;
// BoardLisr.tsx
const handleLogin = () => {
signInWithPopup(auth, provider)
.then(userCredential => {
console.log(userCredential);
dispatch(
setUser({
email: userCredential.user.email,
id: userCredential.user.uid
})
)
})
.catch(error => {
console.log(error);
})
}
๊ธฐ๋ณธ๊ฐ์ ๋น ๋ฌธ์์ด๋ก ๋๊ณ ๋ก๊ทธ์ธํ ๋ ์ด๋ฉ์ผ๊ณผ id๊ฐ์ ์ถ๊ฐํด์ฃผ๋๋ก ํ๋ค.
// userSlice.ts
removeUser: (state) => {
state.email = '';
state.id = '';
}
๋ก๊ทธ์์์ ๋ฐ๋๋ก ๋ค์ ๋น ๋ฌธ์์ด๋ก ์ ํ์ํจ๋ค.
๋ง์ง๋ง์ผ๋ก ๋ก๊ทธ์ธ ์ฌ๋ถ์ ๋ฐ๋ผ ๋ฒํผ์ ๋ค๋ฅด๊ฒ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด ํ
ํ๋๋ฅผ ์ถ๊ฐํ๋ค.
import { useTypedSelector } from "./redux"
export function useAuth() {
const { id, email } = useTypedSelector((state) => state.user);
return {
isAuth: !!email,
email,
id
}
}
email์ ๊ฐ์ด ์กด์ฌํ๋ฉด isAuth๋ฅผ true๋ก ์ค์ ํด ๋ก๊ทธ์ธํ ์ํ์์ ๋ํ๋ธ๋ค.
const { isAuth } = useAuth();
const handleSignOut = () => {
signOut(auth)
.then(() => {
dispatch(
removeUser()
)
})
.catch(error => {
console.log(error);
})
}
return (
{isAuth ?
<GoSignOut className={addButton} onClick={handleSignOut}/>
:
<GoSignIn className={addButton} onClick={handleLogin}/>
}
)
์ด์ isAuth์ ๋ฐ๋ผ ๋ก๊ทธ์ธ ๋ฒํผ๊ณผ ๋ก๊ทธ์์ ๋ฒํผ์ด ๊ตฌ๋ถ๋๋ค.
๐ ๋ฐฐํฌํ๊ธฐ
๋ฐฐํฌ์ญ์ ํ์ด์ด๋ฒ ์ด์ค๋ฅผ ์ด์ฉํ๋ค.
๋จผ์ ๊นํ๋ธ์ ์ฝ๋๋ฅผ ๋ชจ๋ ์ฌ๋ ค์ค ํ ํฐ๋ฏธ๋์์ `npm install -g firebase-tools`๋ฅผ ์ค์นํด์ค๋ค.
`firebase login`์ผ๋ก ๋ก๊ทธ์ธ์ ํด์ฃผ๊ณ ๋ฐฐํฌ๋ฅผ ์ํ ๋น๋ํ์ผ์ ๋ง๋ค๊ธฐ ์ํด `npm run build`์ ์งํํ๋ค.
distํ์ผ์ด ์์ฑ๋์์ผ๋ฉด ๋น๋ ์ค๋น ์๋ฃ
์ด์ ํ์ด์ด๋ฒ ์ด์ค๋ก ๋ฐฐํฌํ๊ธฐ ์ํด `firebase init`์ ํ๋ค.
๋์ค๋ ์ง๋ฌธ์ ๋ํด ์์ฒ๋ผ ๋ตํด์ฃผ๋ฉด ๋๋ค.
๋๋ฒ์งธ ์ง๋ฌธ์์ ์ ํํ๋ ๊ฒ์ด ์๋๋ฐ ์คํ์ด์ค๋ฐ๋ก ์ ํ์ ํ ํ ์ํฐ๋ฅผ ๋๋ฌ์ผํ๋ค.
(์ฌ๊ธฐ์ ๊ณ์ ์ํฐ๋๋ฅด๋ค๊ฐ 5๋ฒ์ ๋ค์ ํ๋ค ..ใ )
์ค์ ์ ๋ค ํด์ค ํ ์ปค๋ฐ, ํธ์ฌ๋ฅผ ใ ก์งํํ๋ฉด
๊นํ๋ธ ์ก์ ์ ์๋์ผ๋ก ์ฌ๋ผ๊ฐ์๋ค.
๊ทธ๋ฐ๋ฐ ์ฒ์์ ์๋ฌ๊ฐ ๋ฐ์ํด์ ๋ณด๋
` .github\workflows\firebase-hosting-merge.yml`๊ณผ ` firebase-hosting-pull-request.yml` ํ์ผ์
` - run: npm install && npm run build`์ด ๋น ์ ธ์์๋ค.
์ถ๊ฐ ํ ๋ค์ ์ปค๋ฐ, ํธ์ฌ์งํํด๋ณด๋
Vite + React + TS
react-task-app-4f172.web.app
์ฑ๊ณต !
'๐๏ธ DevCourse > Frontend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[TIL] Week 13 ๋ฆฌ์กํธ ๋ ์ด์์ ๊ตฌ์ฑ๊ณผ ์คํ์ผ ์์คํ (0) | 2025.04.18 |
---|---|
[TIL] Week 13 React ํ๋ก์ ํธ : CRA์ Vite (0) | 2025.04.17 |
[TIL] Week 13 ๋ฆฌ์กํธ๋ก Task ์์ฑ ์ฑ ๋ง๋ค๊ธฐ 3 (0) | 2025.04.15 |
[TIL] Week 12 ๋ฆฌ์กํธ๋ก Task ์์ฑ ์ฑ ๋ง๋ค๊ธฐ2 (0) | 2025.04.13 |
[TIL] Week 12 ๋ฆฌ์กํธ๋ก Task์์ฑ ์ฑ ๋ง๋ค๊ธฐ (0) | 2025.04.11 |