이번 과제는 4명의 팀원이 각자 브랜치 생성 ➡ PR 생성 ➡ 코드리뷰 ➡ 수정 ➡ 머지의 과정을 거쳐 협업하여 진행했습니다.😊
과제를 마치고, 총 16개의 조 중에 제일 우수한 프로젝트로 평가 받아서 기분이 매우 좋습니다.🎉
코드 리뷰 또한 좋은 예시로 선정되었습니다.🎉
Git
> 코드 리뷰와 브랜치 관리프리온보딩 과정을 하면서 Live Share를 통한 협업을 여러번 진행했는데, 이번 과제는 Live Share 기능을 사용하지 않고, 각자 분담된 기능을 브랜치로 만들고 PR을 올리면 모든 팀원이 코드 리뷰 하는 방식으로 진행되었다.
한 달 넘게 같이한 팀원들과 익숙해졌는데, 새로 조가 바뀌면서 상혁님, 이슬님, 준희님과 git 컨벤션이라던지, 이슈 관리, 브랜치 및 PR 관리 등 어떻게 해야할지 처음부터 다시 정하는 시간을 가졌다.
코드 리뷰!
➡ Git에 팀원분들이 PR을 올리면, 꼼꼼하게 코드를 보려고 노력했다. 이해가 안되면 서로 설명을 해주고 피드백을 주고 받았다. 코드 리뷰가 너무 좋았던게 내가 미쳐 생각하지 못했던 부분을 말씀해주셔서 생각해보고 더 좋은 방향으로 리팩토링을 진행할 수 있었다. (물론 구현해야 할 남은 기능도 있는데, 현재 기능을 리팩토링 한다고 밤을 새웠지만...그래도 팀원분들이 더 쉽게 이해하고 사용하시는 걸 보니 뿌듯했다!)
Drag and Drop 기능은 처음 사용해봤다. 사실 이벤트 리스너를 통해서 기능을 구현할 수 있다는 것 조차 몰랐었다..
기능을 구현하기 전, 구글링을 통해 예시 코드를 참고하고 상하로 리스트를 움직이는 법에 대해서 공부했다.
그러나! 우리의 투두리스트는 칸반보드 형태로 상태에 따라 좌우로도 움직여야 했다. 기존 코드에서 보다 더 복잡해졌지만 다들 해본 적 없는 기능에 도전정신을 가지며 즐겁게 과제를 진행하였다.
위에서 말한, 코드리뷰를 통해 리팩토링한 기능이다. Portal
을 사용해서 모달창 기능을 구현했고, 투두리스트 수정 버튼을 클릭하면 모달창이 생성되는 구조라서, 처음에는 수정 버튼이 있는 곳에 모달 관련 컴포넌트를 넣었다.
그런데 투두리스트를 생성할 때, 입력 validation을 만족하지 못하면 모달창을 재사용 했으면 좋겠다는 의견을 받았다.
Context API
를 활용하여 dispatch(nodal("띄울 모달명"))
으로 띄울 모달명을 셋팅하면, App.tsx
에서 모달명에 맞는 화면을 보여줄 수 있도록 변경했다.
리팩토링 한 결과 팀원분들이 머지 후 쉽게 모달창을 띄울 수 있게 되었다!
과제를 처음 받고 내용을 읽어보니, 글로만 읽고 구현하면 서로 다르게 이해한 부분이 생길 수 있겠다 하는 생각이 들었다. 또 새로운 조편성으로 팀원들이 바뀌다보니 말로만 의견을 전달하면 서로 잘못 이해하는 기능이 생기지 않을까 염려되었다.
그래서 모두가 피그마에 접속해, 우리가 구현하고자 하는 칸반보드 형태의 투두리스트를 만들어 보았다. 사실 피그마를 전문적으로 사용하지는 못하는데, 팀원분 중에 디자이너로 일하셨던 분이 계셔서 많은 도움을 받았다.
PR을 올리고 머지하면서 느낀점은, 모든 팀원분들이 정말 피그마와 동일하게 만들어주셨다!😊
과제를 제출하기 전, 4명이서 모두 Live Share로 같은 코드를 보면서 수정해야할 부분에 대해 의논하고 리팩토링을 진행하였는데, 특히 디자인 관련해서 "사용성을 위해 svg 버튼에 패딩주기 또는 색상 선정을 위한 팁" 등 디테일 한 부분까지 말씀해주셔서 많은 도움이 되었다.
[공통]
json
)[상세기능]
➡ 기존에 진행했던 투두리스트와 다른 점인 애니메이션(Drag & Drop) 부분 구현한 내용을 작성
📌 1. Drag & Drop 기능을 커스텀 훅으로 생성해서 관리하고자 했다.
handleDragStart
: 사용자가 요소를 끌어 시작할 때 발생handleDragEnter
: 드롭 타겟 들어가면 이벤트가 발생handleDragOver
: 드롭 타겟 위에 드래그되는 경우에 이벤트가 발생handleDragLeave
: 드래그 요소가 드롭 대상을 떠날 때 발생// utils/hooks/useTodoItemDnD.ts 파일 생성
import { useState } from 'react';
export const useTodoItemDnD = (id: number) => {
const [isDragging, setisDragging] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
setisDragging(true);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', `${id}`);
};
const handleDragEnter = () => {
setIsDragOver(true);
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragOver(true);
};
const handleDragLeave = () => {
setIsDragOver(false);
};
return {
isDragging,
isDragOver,
handleDragStart,
handleDragEnter,
handleDragOver,
handleDragLeave,
setIsDragOver,
};
};
📌 2. TodoItem
컴포넌트를 감싸는 최상단에 useTodoItemDnD
관련 이벤트 리스너를 등록한다.
// TodoItem.ts
import { useTodoItemDnD } from 'utils/hooks';
...
const {
isDragOver,
handleDragStart,
handleDragOver,
handleDragEnter,
handleDragLeave,
setIsDragOver,
} = useTodoItemDnD(todo.id);
...
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
setIsDragOver(false);
const movingTarget = e.dataTransfer.getData('text/plain');
dispatch(swap({first: +movingTarget, second: todo.id})); // ✅
};
...
<ItemContainer
draggable
isDragOver={isDragOver}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
...
</ItemContainer>
📌 3. reducer에서 dispatch(swap())
액션이 실행될 경우, Todo의 상태에 따라 순서를 변경하거나 state를 변경한다.
// context/TodoContext/reducer.ts
export default function reducer(state: IState, action: Action): IState {
const { type, payload } = action;
switch (type) {
...
case SWAP:
return { ...state, todos: swapTodos(state.todos, payload) }; // ✔
...
default:
return state;
}
}
...
const swapTodos = (prevTodos: ITodos, payload: ISwap): ITodos => { // ✔
const { first: firstId, second: secondId } = payload;
if (firstId === secondId) return prevTodos;
const firstTodo = prevTodos.find((todo) => todo.id === firstId);
const secondTodo = prevTodos.find((todo) => todo.id === secondId);
if (!firstTodo || !secondTodo) return prevTodos;
if (firstTodo.status === secondTodo.status) {
// ✅ Todo의 상태가 동일하여 상하로 움직인 경우
const firstIndex = prevTodos.findIndex((todo) => todo.id === firstId);
const secondIndex = prevTodos.findIndex((todo) => todo.id === secondId);
if (firstIndex === -1 || secondIndex === -1) return prevTodos;
const newTodos = [...prevTodos];
[newTodos[firstIndex], newTodos[secondIndex]] =
[newTodos[secondIndex], newTodos[firstIndex]];
return newTodos;
}
else {
// ✅ Todo의 상태가 달라서, Todos의 상태도 업데이트 해야하는 경우.
const newTodos = [...prevTodos];
const firstIndex = prevTodos.findIndex((todo) => todo.id === firstId);
const firstTodo = newTodos.splice(firstIndex, 1)[0];
const secondIndex = newTodos.findIndex((todo) => todo.id === secondId);
firstTodo.status = newTodos[secondIndex].status;
newTodos.splice(secondIndex, 0, firstTodo);
return newTodos;
}
};
📌 4. TodoBox
에 TodoItem
을 Drag&Drop 할 수도 있다. TodoBox
도 useTodoItemDnD
관련 이벤트 리스너를 등록한다.
// TodoBox.ts
import { useTodoItemDnD } from 'utils/hooks';
...
const {
isDragOver,
setIsDragOver,
handleDragStart,
handleDragOver,
handleDragLeave
} = useTodoBoxDnD(ref);
...
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
if (!ref || !ref.current) return;
if (!ref.current.isSameNode(e.target as Node)) return;
const id = +e.dataTransfer.getData('text/plain');
console.log('TodoBox', id, status);
dispatch(update({ id, status })); // ✅ update 액션을 통해 state를 변경한다.
setIsDragOver(false);
};
...
<TodoSectionWrapper
ref={ref}
draggable
isDragOver={isDragOver}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
...
</TodoSectionWrapper>