Drag And Drop
react-beautiful-dnd 라이브러리를 사용하려면 사용하려는 부분을 DragDropContext - Droppable - Draggable 컴포넌트 순으로 감싸줘야 한다.
드래그가 가능한 범위는 위에서 네모 표시를 한 구간이다. 저 부분이 ListsContainer 컴포넌트이므로 해당 컴포넌트를 DragDropContext 컴포넌트로 감싸준다.
import { DragDropContext } from 'react-beautiful-dnd';
...
<div className={board}>
<DragDropContext onDragEnd={handleDragEnd}>
<ListsContainer lists={lists} boardId={getActiveBoard.boardId} />
</DragDropContext>
</div>
...
onDragEnd 이벤트는 드래그가 끝났을 때 발생하는 이벤트이다. 드래그가 끝나면 재정렬이 이루어져야 하므로 boardSlice에 sort 함수를 정의한다. 같은 리스트로 드래그했을 경우와 다른 리스트로 드래그했을 경우를 나누어서 코드를 작성한다.
type TSortAction = {
boardIndex: number;
droppableIdStart: string;
droppableIdEnd: string;
droppableIndexStart: number;
droppableIndexEnd: number;
draggableId: string;
}
...
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
)
// 변경시키는 아이템 배열에서 제거 & 리턴
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!);
}
}
이제 드래그가 끝났을 때 동작할 함수를 정의한다. 재정렬을 하고, 로그를 추가한다.
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),
droppableIdStart: 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())
})
);
}
드래그해서 놓을 수 있는 장소를 Droppable 컴포넌트로 감싸서 정의한다. 놓을 곳이 List 컴포넌트이므로 List 컴포넌트를 Droppable 컴포넌트로 감싸고, 해당 List의 id를 props로 전달한다. list를 감싸고 있던 div 태그도 다음과 같이 설정한다.
import { Droppable } from 'react-beautiful-dnd';
...
const List: FC<TListProps> = ({
list,
boardId
}) => {
...
return (
<Droppable droppableId={list.listId}>
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={listWrapper}
>
...
</div>
)}
</Droppable>
)
}
마지막으로 Task 컴포넌트도 Draggable 컴포넌트로 감싸준다.
import { Draggable } from 'react-beautiful-dnd';
...
const Task: FC<TTaskProps> = ({
index,
id,
// boardId,
taskName,
taskDescription
}) => {
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>
)
}
Firebase
연결하기
- 파이어베이스 사이트에 접속하여 시작하기를 누르고 위 그림의 프로젝트 추가를 누른다.
- 프로젝트 이름 입력
- Google 애널리틱스 사용 설정 off 후 프로젝트 만들기 클릭
우리가 만든 앱은 웹이기 때문에 위의 빨간 네모 클릭
앱 닉네임 입력하고 앱 등록 클릭
네모 친 부분을 복사하여 firebase.ts 파일을 생성하고 붙여 넣는다.
// Initialize Firebase
export const app = initializeApp(firebaseConfig);
firebase.ts 파일에서 위와 같이 app을 export 한다.
콘솔로 이동 버튼을 누르고 Authentication을 클릭한다.
시작하기를 누르면 위와 같은 화면이 뜨고, Google을 클릭한다.
프로젝트 지원 이메일을 입력하고 저장 버튼을 누른다.
이제 설정이 완료되었다.
로그인 기능
로그인한 사용자를 의미하는 userSlice를 정의하고, hooks 아래에 useAuth.ts 파일을 생성한다.
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;
},
removeUser: (state) => {
state.email = '';
state.id = '';
}
}
})
export const { setUser, removeUser } = userSlice.actions;
export const userReducer = userSlice.reducer;
// useAuth.ts
import { useTypedSelector } from "./redux"
export function useAuth() {
const { id, email } = useTypedSelector((state) => state.user);
return {
isAuth: !!email,
email,
id
}
}
로그인 버튼은 위 사진과 같이 우측 상단에 위치한다. 따라서 BoardList 컴포넌트에 아이콘을 추가한다.
로그인된 사용자의 존재 유무에 따라 다른 아이콘을 출력한다.
import { GoogleAuthProvider, getAuth, signInWithPopup, signOut } from 'firebase/auth';
import { app } from '../../firebase';
import { removeUser, setUser } from '../../store/slices/userSlice';
import { useAuth } from '../../hooks/useAuth';
...
const BoardList: FC<TBoardListProps> = ({
activeBoardId,
setActiveBoardId
}) => {
const dispatch = useTypedDispatch();
...
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
const { isAuth } = useAuth();
const handleLogin = () => {
signInWithPopup(auth, provider)
.then(userCredential => {
dispatch(
setUser({
email: userCredential.user.email,
id: userCredential.user.uid
})
);
})
.catch(error => {
console.error(error);
})
}
const handleSignOut = () => {
signOut(auth)
.then(() => {
dispatch(
removeUser()
);
})
.catch((error) => {
console.error(error);
})
}
return (
<div className={container}>
<div className={title}>
게시판:
</div>
...
<div className={addSection}>
{
isFormOpen ?
<SideForm setIsFormOpen={setIsFormOpen} /> :
<FiPlusCircle className={addButton} onClick={() => setIsFormOpen(!isFormOpen)} />
}
</div>
{
isAuth
?
<GoSignOut className={addButton} onClick={handleSignOut} />
:
<FiLogIn className={addButton} onClick={handleLogin} />
}
</div>
)
}
배포하기
Github Action과 Firebase Hosting을 이용해서 배포를 한다. 따라서 Github에서 repository를 먼저 생성해줘야 한다.
다음 패키지를 설치한다.
npm i -g firebase-tools
다음 명령어를 실행하여 로그인을 수행한다.
firebase login
다음 명령어로 build를 수행한다.
npm run build
아래 명령어를 실행하여 firebase 초기화를 진행한다.
firebase init
- Are you ready to proceed?
→ y - Which Firebase features do you want to set up for this directory? Press Space to select features, then Enter to confirm your choices.
→ Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys - Please select an option
→ Use an existing project - Select a default Firebase project for this directory
→ firebase에서 생성했던 프로젝트 선택 - What do you want to use as your public directory?
→ dist - Configure as a single-page app (rewrite all urls to /index.html)?
→ y - Set up automatic builds and deploys with GitHub?
→ y - File dist/index.html already exists. Overwrite?
→ n - For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository)
→ 닉네임/repository 이름 입력 - Set up the workflow to run a build script before every deploy?
→ n - Set up automatic deployment to your site's live channel when a PR is merged?
→ y - What is the name of the GitHub branch associated with your site's live channel?
→ main
.github/workflows 아래에 있는 firebase-hosting-merge.yml 파일과 firebase-hosting-pull-request.yml 파일에서 아래와 같이 run 부분을 추가하고, git add, commit, push를 수행한다.
Github의 해당 repository에 들어가서 아래 부분을 클릭한다.
Actions를 클릭하면 workflow가 run 되고 있다. 다 되고 나면 다음과 같이 완료되었다는 표시가 생긴다.
클릭해서 들어가면 배포된 url이 적혀있다. 저기로 들어가면 만들었던 앱을 확인할 수 있다.
배운 점
- react-beautiful-dnd 라이브러리를 사용하려면 사용하려는 부분을 DragDropContext - Droppable - Draggable 컴포넌트 순으로 감싸줘야 한다.
- firebase를 사용하여 로그인 기능을 구현할 수 있다.
- Github Action과 Firebase Hosting을 이용해서 배포를 할 수 있다.
'데브코스' 카테고리의 다른 글
[13주차 - DAY3] 오픈 소스(2) (0) | 2024.05.22 |
---|---|
[13주차 - DAY2] 오픈 소스 (0) | 2024.05.21 |
[12주차 - DAY5] 게시판 만들기(3) (0) | 2024.05.17 |
[12주차 - DAY4] 게시판 만들기(2) (0) | 2024.05.16 |
[12주차 주간 발표] 인덱스 (0) | 2024.05.15 |