so woon!

[REACT] Drag and Drop - 사용자로부터 입력받아 card 생성하기 본문

ReactJS/개념정리

[REACT] Drag and Drop - 사용자로부터 입력받아 card 생성하기

xowoony 2023. 5. 6. 21:20

학습일 : 2023. 05. 06


 

사용자로부터 뭔가를 입력받아 card 생성해보고자 한다.

이전에는 임의로 string array로 만들어줬었다.

 

 

이제 form을 만들어 보고자 한다.

 


터미널에 입력 후 설치

npm install react-hook-form



form을 만들기 위해 react-hook-form에서 useForms를 import

import { useForm } from "react-hook-form";



인터페이스 작성

interface IForm {
  toDo: string;
}



form에게 인터페이스 적용

const { } = useForm<IForm>();




useForm은 register와 더불어
setValue와 handleSubmit도 줌

 const { register, setValue, handleSubmit} = useForm<IForm>();



form을 만들어 주고

<Form>
    <input
      {...register("toDo", { required: true })}
      type="text"
      placeholder={`${boardId}인 메모를 입력하세요`}
    />
</Form>





form이 valid할 때 호출되는 함수
 

const onValid = (data:IForm) => {
    
  };




그리고 Form에 onSubmit을 작성해준다.
여기서 중요한 건 react-hook-form을 통해 handleSubmit을 먼저 호출해야 한다.
그다음 거기에서 onValid를 호출한다.

<Form onSubmit={handleSubmit(onValid)}>
    <input
      {...register("toDo", { required: true })}
      type="text"
      placeholder={`${boardId}인 메모를 입력하세요`}
    />
</Form>




그리고 기존의 data에서
 

const onValid = ({toDo}: IForm) => {
    setValue("toDo", "");
  };



{toDo} 로 변경해주고




<atom.tsx>
인터페이스를 만들어줌

interface ITodo {
  id:number;
  text: string;
}



그리고 기존의 이 인터페이스를

interface IToDoState {
  [key: string]: string[];
}




이렇게 변경해준다.
여러개의 board와, 그 안의 toDo array들이 있다고 알려주도록 함.

interface IToDoState {
  [key: string]: ITodo[];
}

export const toDoState = atom<IToDoState>({
  key: "toDo",
  default: {
    "진행 예정": [],
    "진행 중": [],
    "완료": [],
  },
});




수정해주면 App에서 에러가 발생함
현재 App에서 board는 toDos가 그냥 string배열이라고 생각하는데
그게 아니라 이젠 IToDo들의 배열이 되었기 때문에

<App.tsx>

 <Board key={boardId} toDos={toDos[]} boardId={boardId} />


이 부분이 에러가 나게됨


<atoms.tsx>

export interface ITodo {
  id:number;
  text: string;
}



export 해주고

<Board.tsx>
toDos 부분을 기존의 

interface IBoardProps {
  toDos: string[];
  boardId: string;
}


에서 

interface IBoardProps {
  toDos: ITodo[];
  boardId: string;
}



로 변경해준다.

이 부분도
         

<Area
    isDraggingOver={info.isDraggingOver}
    isDraggingFromThis={Boolean(info.draggingFromThisWith)}
    ref={magic.innerRef}
    {...magic.droppableProps}
  >
    {toDos.map((toDo, index) => (
      <DragabbleCard key={toDo} toDo={toDo} index={index} />
    ))}
    {magic.placeholder}
</Area>



이렇게 변경해주고

  <Area
    isDraggingOver={info.isDraggingOver}
    isDraggingFromThis={Boolean(info.draggingFromThisWith)}
    ref={magic.innerRef}
    {...magic.droppableProps}
  >
    {toDos.map((toDo, index) => (
      <DragabbleCard
        key={toDo.id}
        toDoId={toDo.id}
        index={index}
        toDoText={toDo.text}
      />
    ))}
    {magic.placeholder}
  </Area>

 


그리고 여기 draggable card에서 사용하는 toDo는

<DragabbleCard key={toDo} toDo={toDo} index={index} />



여기 있는 toDo들은 prop을 확장해줘야 한다.

우리가 원하는 건 toDo id와 toDo text 이기 때문에


<DragabbleCard.tsx>
인터페이스를 기존의

interface IDragabbleCardProps {
  toDo: string;
  index: number;
}



에서 아래처럼 변경해준다.
toDoId는 number가 될 것이고,
toDoText는 string이 될 것이다.

interface IDragabbleCardProps {
  toDoId: number;
  toDoText: string;
  index: number;
}




<Board.tsx>
     

   <Area
        isDraggingOver={info.isDraggingOver}
        isDraggingFromThis={Boolean(info.draggingFromThisWith)}
        ref={magic.innerRef}
        {...magic.droppableProps}
      >
        {toDos.map((toDo, index) => (
          <DragabbleCard key={toDo.id} toDo={toDo} index={index} />
        ))}
        {magic.placeholder}
  </Area>



를 아래처럼 수정해준다.
       

 <Area
    isDraggingOver={info.isDraggingOver}
    isDraggingFromThis={Boolean(info.draggingFromThisWith)}
    ref={magic.innerRef}
    {...magic.droppableProps}
  >
    {toDos.map((toDo, index) => (
      <DragabbleCard key={toDo.id} toDoId={toDo.id} index={index} toDoText={toDo.text}/>
    ))}
    {magic.placeholder}
</Area>




이 모든 과정은 더이상 toDo가 string이 아니라

object라고 알려주기 위해 prop을 수정하는 것이다.
그리고 그 object는 id와 text를 가진다.


<DragabbleCard.tsx>

function DragabbleCard({ toDo, index }: IDragabbleCardProps) {
  return (
    <Draggable key={toDo} draggableId={toDo} index={index}>
      {(magic, snapshot) => (
        <Card
          isDragging={snapshot.isDragging}
          ref={magic.innerRef}
          {...magic.draggableProps}
          {...magic.dragHandleProps}
        >
          {toDo}
        </Card>
      )}
    </Draggable>
  );
}



를 아래 처럼 수정한다.
이제 toDo가 아니라 toDoId가 와야하고
dragabbleId에는 string이 와야 하므로 + "" 를 붙여서 string으로 만들어 준다.

function DragabbleCard({ toDoId, toDoText, index }: IDragabbleCardProps) {
  return (
    <Draggable draggableId={toDoId + ""} index={index}>
      {(magic, snapshot) => (
        <Card
          isDragging={snapshot.isDragging}
          ref={magic.innerRef}
          {...magic.draggableProps}
          {...magic.dragHandleProps}
        >
          {toDoText}
        </Card>
      )}
    </Draggable>
  );
}




이제 board는 string으로 이루어진 array를 받지 않고
IToDo로 이루어진 array를 받게 되었다!



그럼 이제 draggableCard를 수정하고 렌더할 때
object의 toDoId와 toDoText를 가져오게 된다.

<Area
    isDraggingOver={info.isDraggingOver}
    isDraggingFromThis={Boolean(info.draggingFromThisWith)}
    ref={magic.innerRef}
    {...magic.droppableProps}
  >
    {toDos.map((toDo, index) => (
      <DragabbleCard
        key={toDo.id}
        toDoId={toDo.id}
        index={index}
        toDoText={toDo.text}
      />
    ))}
    {magic.placeholder}
</Area>





다음으로, App에 문제가 있는데
예전에는 dragabbleId를 board 안에 넣는 것 뿐이었다.
이유는 dragabbleId는 단지 text 였기 때문이다.

이전에 atoms.tsx에서 board들은 string으로만 이루어진 array였어서
dragabbleId를 사용할 수 있었지만

지금은  destinationBoard안에 draggableId를 줄 수 없다.




이전에는 이렇게 생긴 array가 있었다.
a와 b는 toDo임과 동시에 draggableId이기도 했다.
["a", "b"]

 


그래서 이렇게 자리를 바꿀 수 있었다.

["b", "a"] 
각 값이 draggableId인 동시에 list 안의 item이기도 했기 때문이다.



그런데 이제는 이런 구조로 바뀐 것이다.
[ { text : "hello", id : 1 }, { text:"world", id : 2 } ]




이전에는 draggableId 자체가 string이었기 때문에 그대로 사용할 수 있었지만
이제는 보드에서 지우기 전에
task를  (복사본을 만들어) 잡아두어 다시 사용해야 한다.



<App.tsx>
App.tsx에서는 draggableId에서 에러가 발생하고 있는데,
toDo로 이루어진 array 안에 string을 넣으려고 했기 때문이다.
hover 해보면
destinationBoard는  const destinationBoard: ITodo[]
boardCopy는  const boardCopy: ITodo[]


function App() {
  const [toDos, setToDos] = useRecoilState(toDoState);
  const onDragEnd = (info: DropResult) => {
    console.log(info);
    const { destination, draggableId, source } = info;
    if (!destination) return;
    // ★ 같은 보드 내에서 재정렬하기 ★
    if (destination?.droppableId === source.droppableId) {
      setToDos((allBoards) => {
        const boardCopy = [...allBoards[source.droppableId]];
        boardCopy.splice(source.index, 1);
        boardCopy.splice(destination?.index, 0, draggableId); // 이부분과
        return {
          ...allBoards,
          [source.droppableId]: boardCopy, 
        };
      });
    }
  // ★ 서로 다른 보드 넘나들어서 재정렬하기 ★
    if (destination.droppableId !== source.droppableId) {
      setToDos((allBoards) => {
        const sourceBoard = [...allBoards[source.droppableId]];
        const destinationBoard = [...allBoards[destination.droppableId]];
        sourceBoard.splice(source.index, 1);
        destinationBoard.splice(destination?.index, 0, draggableId); // 이 부분이 에러가 생김
        return {
          ...allBoards,
          [source.droppableId]: sourceBoard,
          [destination.droppableId]: destinationBoard,
        };
      });
    }
  };



일단 재정렬하는 if 두 부분을 주석처리 해놓고
atoms.tsx에 가서
임의로 값을 넣어 준 다음

 


<atoms.tsx>

export const toDoState = atom<IToDoState>({
  key: "toDo",
  default: {
    "진행 예정": [
      { id: 1, text: "hello" },
      { id: 2, text: "hello" },
    ],
    "진행 중": [],
    "완료": [],
  },
});




<App.tsx>
console.log 해보면

function App() {
  const [toDos, setToDos] = useRecoilState(toDoState);
  const onDragEnd = (info: DropResult) => {
    const { destination, draggableId, source } = info;
    console.log(info);
    if (!destination) return;
  };






전과 똑같은 정보를 받게 되지만 한가지 다른 점이 있다.
source도 받고 destination도 받는건 같은데


draggableId가 이제는 숫자로만 되어있다.


그럼 이제 이 id를 통해서 item을 찾아와야 한다.


board를 건너 카드를 이동할거라면 item을 지워야 하고
같은 board 내에서 움직일 때는 재배치를 해야할 것이다.

우리가 정보를 object로 받지 않고 item이 가진 id로 받아 오기 때문에
이 id를 가지고 to do board 안으로 들어가서 원하는 to do (text)를 받야와야 할 것이다.



이제 to do를 받아와보자.

먼저,
taskObj를 만들고 board의 복사본인 boardCopy와 같다고 해주겠다.
그리고 안에다 source.index를 적어주면
내가 옮기려고 하는 to do object 전체를 가져다 줄 것이다.

const taskObj = boardCopy[source.index];





<App.tsx>
그리고 아까 에러가 났던 부분을 taskObj로 변경해주도록 한다.

if (destination?.droppableId === source.droppableId) {
      setToDos((allBoards) => {
        const boardCopy = [...allBoards[source.droppableId]];
        const taskObj = boardCopy[source.index]; // toDo object를 받아서
        boardCopy.splice(source.index, 1);
        boardCopy.splice(destination?.index, 0, taskObj); // toDo object (taskObj)를 다시 넣어준다.
        return {
          ...allBoards,
          [source.droppableId]: boardCopy,
        };
      });
    }



방금 한건 toDo object를 받아서 toDo object를 다시 넣어준 것이다.

서로 다른 보드를 넘나 들어서 재정렬하기 부분도 똑같은 원리로 바꿔주면 된다.



  if (destination.droppableId !== source.droppableId) {
      setToDos((allBoards) => {
        const sourceBoard = [...allBoards[source.droppableId]];
        const taskObj = sourceBoard[source.index];
        const destinationBoard = [...allBoards[destination.droppableId]];
        sourceBoard.splice(source.index, 1);
        destinationBoard.splice(destination?.index, 0, taskObj);
        return {
          ...allBoards,
          [source.droppableId]: sourceBoard,
          [destination.droppableId]: destinationBoard,
        };
      });
    }




이제 우리가 submit을 할 때
새로운 toDo object newToDo가 만들어지게 하고
setValue로 toDo를 빈 값으로 바꾸겠다.


newToDo는  id 값으로 Date.now를 줄 것이다. 단위는 밀리초이다.
그리고 text는 우리가 data에서 가져올 toDo와 같은 값으로 주겠다

const onValid = ({ toDo }: IForm) => {
    const newToDo = {
      id:Date.now(),
      text: toDo
    };
    setValue("toDo", "");
  };



newToDo는 준비가 다 되었고
이제 이 친구를 우리의 board 안에 넣어주겠다.


<atoms.tsx>
그럴려면 먼저 atoms를 import 해주어야 한다.
필요한 건 atom을 위한 이 setter함수이다.

export const toDoState = atom<IToDoState>({
  key: "toDo",
  default: {
    "진행 예정": [],
    "진행 중": [],
    "완료": [],
  },
});




setter함수를 얻기 위해서는 useSetRecoilState를 사용해서
toDoState를 import 해오면 된다.

 


<Board.tsx>

  const setToDos = useSetRecoilState(toDoState);



를 만들어서
state를 조작할 수 있게 만들어 준다.


setToDos(); 를 작성할 때 주의할 점은
현재 내가 있는 보드에만 메모를 추가해주어야 하는 것이다.

 

예를 들어,
toDo를 Done 보드에 넣고 싶다고 했을 때
Done을 제외한 나머지는 제자리에 그대로 두고
Done안에도 그대로 두고 사용자가 입력한 값만 마지막에 붙여주어야 한다.

 

그럴려면 현재 보드를 사용해서 카피하고 그걸 다시 state에 넣어야 한다.

*setter 함수는 value를 설정하는데에도 쓰이지만
이전 value에 기반해서 현재 value를 업데이트 해줄 수도 있었음.*


 

const onValid = ({ toDo }: IForm) => {
    const newToDo = {
      id:Date.now(),
      text: toDo
    };
    setToDos((allBoards) => {	// 이 부분
      return {
        ...allBoards,
        [boardId]: [...allBoards[boardId], newToDo],
      };
    });
    setValue("toDo", "");
  };




이렇게 작성해주면
원래 있던 보드들과 현재 사용자가 작성하고 있는 Done, To Do 와 같은 보드들을 리턴하고
기존의 보드 내용들에 새로 작성해준 newToDo를 더해주게 된다.


실행결과
각각의 보드에서 각 보드에만 메모가 추가되며,
다른 보드로 카드를 옮길 수 있게 된다.

 

 

 

 

Comments