so woon!

[REACT] Drag and Drop - onDragEnd, 재정렬 + React.memo() 본문

ReactJS/개념정리

[REACT] Drag and Drop - onDragEnd, 재정렬 + React.memo()

xowoony 2023. 4. 28. 15:21

학습일 : 2023. 04. 28


 

이번에는 아이템을 드롭했을 때 재정렬 될 수 있도록 구현해보고자 한다.


이걸 위해 recoil로 돌아가서,
todo state를 위한 atom을 만들도록 하겠다.
key는 toDo,
default는 이미 우리가 가지고 있던 그 배열을 쓰도록 하겠다.


<atom.tsx>

import { atom } from "recoil";

export const toDoState = atom({
  key: "toDo",
  default: ["a", "b", "c", "d", "e", "f"],
});

 


atom의 값을 가져오고 싶으면 useRecoilValue를 사용하면 됐었다.
atom을 수정하는 것 까지 할려면 useRecoilState를 쓰면 된다.

 

 



드래그 해서 리스트를 재정렬하게 하려면
onDragEnd 함수를 살펴보아야 한다.
onDragEnd 함수는 드래그가 끝났을 때 실행되는 함수이다.

 

 


일단 콘솔에 찍어 확인해보면

function App() {
  const [toDos, setToDos] = useRecoilState(toDoState);
  // onDragEnd : 드래그가 끝났을 때 실행되는 함수
  const onDragEnd = () => {
    console.log("draggin finished!");
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Wrapper>
        <Boards>
          <Droppable droppableId="one">
            {(magic) => (
              <Board ref={magic.innerRef} {...magic.droppableProps}>
                {toDos.map((toDo, index) => (
                  <Draggable key={index} draggableId={toDo} index={index}>
                    {(magic) => (
                      <Card
                        ref={magic.innerRef}
                        {...magic.draggableProps}
                        {...magic.dragHandleProps}
                      >
                        {toDo}
                      </Card>
                    )}
                  </Draggable>
                ))}
                {magic.placeholder}
              </Board>
            )}
          </Droppable>
        </Boards>
      </Wrapper>
    </DragDropContext>
  );
}

export default App;

 

 

 

실행결과

드래그가 끝났을 시점에 콘솔에 문구가 출력이 됨을 볼 수 있다.

 

 

 

 

드래그가 끝났을 경우 옮겨준 대로 순서를 수정하고 싶다면 어떻게 해야할까?

 


onDragEnd는 어떤 일이 일어났는지에 대한 정보로 많은 argument를 주는데
arguments를 console.log를 통해 확인해보겠다.

 


<App.tsx>

 const onDragEnd = (args:any) => {
    console.log(args);
  }

 

 


실행결과


내가 드래그 한 것이 draggableId로 f라고 알려주고 있으며,
destination 안 droppableId 로 내가 끌어서 도착한 곳 (one) 을 알려주고 있다.
또한 그곳의 index가 0임을 알 수 있다.




droppableId는 나중에 여러 개의 보드가 생기게 되면 유용할 것이다.
이것으로 도착지가 또 다른 보드인지 알 수 있게 된다.


콘솔에 찍혀나오는 정보 중 source와 destination의 droppableId가 같게 나오는데
이것은 내가 만든 보드가 하나라서 그런 것이다.
source의 index는 내가 드래그를 시작한 위치의 index가 나오게 된다.
지금은 보드 자체가 droppable이기 때문에 source와 같아지는 것이다.

 


onDragEnd 함수는 우리에게 result와 provided라는 인수를 return 한다.
따라서 타입을 정해주도록 하겠다.

 

 


<App.tsx>

// DropResult를 import 해준다.
import {
  DragDropContext,
  Draggable,
  Droppable,
  DropResult,
} from "react-beautiful-dnd";


function App() {
  const [toDos, setToDos] = useRecoilState(toDoState);
  // onDragEnd : 드래그가 끝났을 때 실행되는 함수
  // onDragEnd 에 destination과 source를 넘겨주도록한다.
  // DropResult를 적어주어 타입스크립트에게 뭔지 설명해주도록 한다.
  const onDragEnd = ({ destination, source }: DropResult) => {

};

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Wrapper>
        <Boards>
          <Droppable droppableId="one">
            {(magic) => (
              <Board ref={magic.innerRef} {...magic.droppableProps}>
                {toDos.map((toDo, index) => (
                  <Draggable key={index} draggableId={toDo} index={index}>
                    {(magic) => (
                      <Card
                        ref={magic.innerRef}
                        {...magic.draggableProps}
                        {...magic.dragHandleProps}
                      >
                        {toDo}
                      </Card>
                    )}
                  </Draggable>
                ))}
                {magic.placeholder}
              </Board>
            )}
          </Droppable>
        </Boards>
      </Wrapper>
    </DragDropContext>
  );
}

export default App;





로직
1. array로부터 source.index를 지운다. (source.index는 최근에 움직여진 item의 index임)
2. 지운걸 destination의 index에 추가시킨다.

 

 

 

하지만 splice 함수에서 중요한 점은 한자리에서 array를 수정하고 변형시킨다는 것이다.
기본적으로 array에서 splice를 사용하면 그 array를 수정한다는 것을 의미한다.
이렇게 새로운 것을 return 해 더이상 같은 array 안에 splice로 잘라준 아이템은 남아있지 않게 되고

mutation이 되는 것이다.


mutation을 하지 않는 방식인 non-mutation을 예를 들자면



보다시피 name은 변하지 않았다.
name으로 대문자를 얻게 되었지만 name자체는 변하지 않은 것이다.


나는 state를 mutate 하지 않을 것이기 때문에 이것은 꽤 중요한 문제이다.

 

 



<App.tsx>
setToDos 함수는 atom을 수정할 2가지 방법을 전달해줬다.
1. 하나는 그냥 값을 보내주는 것이고,
2. 다른 하나는 현재의 값을 argument로 주고 새로운 state를 return 하는 함수를 주는 것이었다.
이번 경우에는 현재의 값을 가져올 것이다.

 


어떤 일이 벌어지던지 우리는 새로운 수정된 array를 return 해주어야 한다.
우린 모든 toDos를 변형시킬 수 없기 때문에 복사해주도록 하겠고 toDosCopy라고 부르겠다.

 

 

로직에 대한 설명은 주석으로 적겠음

function App() {
   const [toDos, setToDos] = useRecoilState(toDoState);
  // onDragEnd : 드래그가 끝났을 때 실행되는 함수
  // destination : 드래그 끝나는 시점의 도착지 정보
  // source : 드래그 시작 정보 - 움직임을 시작한 아이템의 index, droppableId를 알려줌
  const onDragEnd = ({ draggableId, destination, source }: DropResult) => {
    // 그자리에 그대로 놓아서 destination이 없는 경우 그대로 리턴 조치.
    if (!destination) return;
    // oldToDos 작성
    setToDos((oldToDos) => {
      // 모든 toDos를 변형시킬 수 없기 때문에 복사를 하겠음
      const toDosCopy = [...oldToDos];
      // 1. source.index에서 아이템을 삭제한다.
      toDosCopy.splice(source.index, 1); // source.index 즉 시작시점부터 1개만 지움
      // 2. item을 다시 destination.index에 넣고, 아무것도 추가하지 않고 item을 넣는다.
      // (item은 draggabledId 이다.)
      // (때때로 destination이 없을 수도 있다. 유저가 그자리에 그대로 둘 경우엔)
      toDosCopy.splice(destination?.index, 0, draggableId);
      return toDosCopy;
    });
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Wrapper>
        <Boards>
          <Droppable droppableId="one">
            {(magic) => (
              <Board ref={magic.innerRef} {...magic.droppableProps}>
                {toDos.map((toDo, index) => (
                  // 버그가 발생할 수 있음 - draggable의 key와 draggableId가 같아야 함
                  // react.js에서 우리는 key를 숫자인 index로 주는 것에 익숙하지만
                  // 이 경우에는 key는 draggableId와 무조건 같아야 한다.
                  <Draggable key={toDo} draggableId={toDo} index={index}>
                    {(magic) => (
                      <Card
                        ref={magic.innerRef}
                        {...magic.draggableProps}
                        {...magic.dragHandleProps}
                      >
                        {toDo}
                      </Card>
                    )}
                  </Draggable>
                ))}
                {magic.placeholder}
              </Board>
            )}
          </Droppable>
        </Boards>
      </Wrapper>
    </DragDropContext>
  );
}

export default App;

 

 

실행결과

드래그 앤 드롭으로 재정렬 할 수 있게 되었다.

 

 

 

 

위 영상을 보면 드래그 하고 놓았을 때 글씨가 약간씩 흔들리거나 깜빡거림이 보이는데
이것은 react.js가 모든 Draggable을 다시 렌더링 하고 있기 때문에 발생하는 현상이다.



src 안 Components 폴더를 생성하고 그 안에 DragabbleCard.tsx 파일을 만들어준다.

 


그리고 <Draggable></Drabbable>전체를 복사해서
DragabbleCard.tsx 파일로 옮겨준다.

import 해주고
Card의 css도 가져와주고
interface를 만들어 준다.
{ toDo, index }:IDragabbleCardProps를 전달해준다.

 


<DragabbleCard.tsx>

import { Draggable } from "react-beautiful-dnd";
import styled from "styled-components";

const Card = styled.div`
  border-radius: 5px;
  margin-bottom: 5px;
  background-color: ${(props) => props.theme.cardColor};
  padding: 10px 10px;
`;

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

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

export default DragabbleCard;





<App.tsx>
<DragabbleCard key={toDo} toDo={toDo} index={index} />
를 써준다.

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Wrapper>
        <Boards>
          <Droppable droppableId="one">
            {(magic) => (
              <Board ref={magic.innerRef} {...magic.droppableProps}>
                {toDos.map((toDo, index) => (
                 <DragabbleCard key={toDo} toDo={toDo} index={index} />
                ))}
                {magic.placeholder}
              </Board>
            )}
          </Droppable>
        </Boards>
      </Wrapper>
    </DragDropContext>
  );
}

export default App;






<DragabbleCard.tsx>
Card가 몇번이나 렌더링 되었는지 콘솔에 찍어보도록 하겠다.

function DragabbleCard({ toDo, index }: IDragabbleCardProps) {
  console.log(toDo, "렌더링 됨.");
  return (
    <Draggable key={toDo} draggableId={toDo} index={index}>
      {(magic) => (
        <Card
          ref={magic.innerRef}
          {...magic.draggableProps}
          {...magic.dragHandleProps}
        >
          {toDo}
        </Card>
      )}
    </Draggable>
  );
}

export default DragabbleCard;




실행결과
드래그 앤 드롭을 하게 되면 순서가 바뀌는 컴포넌트만 다시 렌더링 되는 것이 아니라
모든 컴포넌트가 렌더링 되고 있음을 볼 수 있다.

 

 



이것을 해결하기 위해 React.memo 라는 것을 사용하면 되는데
react memo는 react.js에게 제발 이 컴포넌트는 렌더링하지 말아달라고 말해주는 역할을 한다.
(prop이 바뀌지 않는다면 말이다.)

 

 



prop이 같다면 element를 다시 렌더링 하고 싶지 않다.

사용방법은 다음과 같다.
Dragabblecard.tsx 에서

기존의 export인
export default DragabbleCard;
에서

export default React.memo(DragabbleCard);
로 수정하면 된다.


실행결과

이제 순서가 바뀌는 친구들만 렌더링 되는 걸 볼 수 있다.




 

 

 

Comments