React JS 마스터클래스(TRELLO CLONE 2 )

짜스의 하루 ·2024년 6월 13일

React.memo

React의 memo는 성능 최적화 도구 --> 이 도구는 컴포넌트를 메모이제이션하여, 동일한 props로 다시 렌더링될 때 불필요한 재렌더링을 방지

메모이제이션 (Memoization): 결과를 기억해두고, 동일한 입력이 주어졌을 때는 다시 계산하지 않고 기억해둔 결과를 반환하는 방식. memo는 이러한 메모이제이션을 통해 컴포넌트의 불필요한 재렌더링을 막는다.

우리가 작성했던 코드를 DragabbleCard.tsx DragabbleCard 부분을 따로 빼서 깔끔하게 정리한 후,
console.log(toDo, rendering) 콘솔을 찍어보면서 렌더링이 얼마나 되는지 확인해 보았다.
나는 단순히 f와 e의 자리를 바꾼 것인데, 나머지 a,b,c,d가 모두 다시 리렌더링이 되고 있다.
--> 불필요한 렌더링이 발생하고 있다는 점이다

지금은 간단한 동작만 수행하지만, 복잡한 계산이나 api를 불러온다면, 불필요한 곳까지 리렌더링이 이루어지면 코드가 너무 느려지지 않을까?!

이때, 우리가 React.memo를 통해서 리렌더링을 방지할 수 있다.

React.memo(DragabbleCard) 이렇게 작성을 해두면,
react에게 prop이 변하지 않았다면, DraggbleCard를 다시 렌더링 하지마 ! 라고 이야기 해준다.
또한, prop가 변한 item만 다시 렌더링이 되도록 지시!를 내려준다고 생각하면 될 것 같다

위와 같이 f-> e의 자리만 바꾸었을 때, 바뀐 f와 e만 렌더링이 이루어지고, 나머지 a,b,c,d는 리렌더링이 이루어지지 않는 것을 확인할 수 있다!


3개의 보드판 생성하기

이제 보드판 3개를 만들 시간이다
TO_DO, DOING, DONE 의 역할을 할 보드판을 3개를 만들어야 한다
컴포넌트를 각각 만들 수 있겠지만, map()을 사용해서 루프를 돌리면 될 것 같다!

ITodoState

  • ITodoState는 키가 string이고 값이 string[] 인 객체를 나타낸다.
  • 이 인터페이스는 각 보드( to_do, doing, done)가 문자열 배열을 값으로 가지는 객체임을 정의한다,

toDoState

  • key: Recoil atom을 고유하게 식별하는 문자열 키
  • default: toDoState의 초기 상태를 정의 --> 초기 상태는 다음과 같이 세 개의 보드를 가지고 있다.
to_do: ['a', 'b']
doing: ['c', 'b', 'e']
done: ['f']

그럼 이제 Board.tsx Board 를 따로 컴포넌트를 만들도록 하겠다. Board는 toDos(배열)과 boardId를 props로 받아야 한다.
boardId

  • 식별자: boardId는 Board 컴포넌트의 고유한 식별자로 사용된다. 드래그 앤 드롭 영역을 구별하는 데 필요한 ID이다.
  • 드래그 앤 드롭 식별: react-beautiful-dnd 라이브러리에서 Droppable 컴포넌트의 droppableId로 사용되어, 어떤 보드에 어떤 할 일을 드롭할 수 있는지 구분한다.
    예를 들어, 여러 개의 보드가 있을 때 각 보드를 구별하는 역할을 합니다.

toDos

  • 할 일 목록: toDos는 해당 보드에 속한 할 일들의 목록입니다. 각 보드마다 서로 다른 할 일 목록을 가지며, 이 목록은 드래그 앤 드롭 가능한 개별 아이템들을 의미한다.
  • 렌더링 데이터: Board 컴포넌트는 toDos 배열을 순회하여 각각의 할 일을 화면에 렌더링한다 -> 각 할 일은 DragabbleCard 컴포넌트로 나타낸다.

근데 여기서 중요한 점이 있다.

이제 toDoState의 default값이 배열이 아니기 때문에 (객체가 되었다) map()을 사용하지 못한다는 점이다.
그럼 어떻게 해야 할까?

Object.keys()


Object.keys(x).map(boardId => x[boardId])

  • Object.keys(x): 객체 x의 키(a,b) 들을 배열로 반환한다.
  • .map(boardId => x[boardId]) : 반환된 키 배열을 순회하며 각 키에 대해 x 객체에서 해당 값을 가져온다.

쉽게 설명하자면,
Object.keys(x) : ['a', 'b']를 반환
--> ['a', 'b'].map(boardId => x[boardId]) :

  • 첫 번째 반복: boardId가 'a' 일 때 x['a']는 [1, 2, 3]
  • 두 번째 반복: boardId가 'b'일 때 x['b']는 [4, 5, 6]이다.

이걸

to_do: ['a', 'b'],
doing: ['c', 'b', 'e'],
done: ['f'],

에 적용해볼까 !

Object.keys(toDo)를 하면서 toDO 객체에 있는 key들을 모두 불러온 뒤(to_do, doing, done) 이들을 각각 toDo[boardId]를 통해
-> toDo[to_do], toDo[doing], toDo[done] 이런 식으로 할 수 있게 된다!
그럼, toDo에 to_do를 불러오면서 ['a', 'b'] 로 불러올 수 있는 것이다!

이제 코드에 적용을 해보자
<Board/> 에서 받을 수 있는 받아야 하는 props는 toDos, boardId 이기 때문에,
{Object.keys(toDos).map((boardId) => 를 통해서 toDos의 key를 배열로 받아온 뒤, 그것들을 각각을 boardId에 받아온다.
이후, <Board/> 컴포넌트에 boardId, toDos에 넘겨준다.

boardId --> to_do, doing, done과 같이 board의 위치
toDos --> toDos[boardId]
const [toDos, setTodos] = useRecoilState(toDoState); toDoState에서 불러온 toDos의 default 값들을 불러오는 것!

이라고 생각하면 된다.

이렇게 적용시키면,


하나의 Board에서 이동시켜보기


info를 콘솔에 찍어서 어떤 것들이 필요할 까 살펴보아야 한다.


여기서 필요한 것들은 draggableId, source, destination 이다.
draggableId : 이동한 item이 무엇인지 알기 위해
source : 이동하려고 하는 item의 현재 index, droppableId (어떤 Board에 있는지)
destination : 이동을 한 index, droppableId(어떤 Board로 이동했는지) 이다.

이제 이 정보를 가지고, 코드를 작성해보자.

우선, 하나의 board 안에서만 이동이 가능하게 하기 위해서

if(source.droppableId === destination.droppableId) 를 체크한 뒤, 코드를 실행시켜야 한다.

또한, 변화가 일어난 Board만 복사하고 싶다
고로, allBoards[source.droppableId] 를 통해
source.droppableId는 드래그가 시작된 보드의 ID를 나타낸다.
allBoards[source.droppableId] 는 해당 보드의 할 일 목록을 나타내는 배열을 가져오게 된다.

따라서 const boardCopy = [...allBoards[source.droppableId]] 는 source.droppableId에 해당하는 보드의 할 일 목록 배열을 복사하는 코드가 된다.

이렇게 받아온 boardCopy 의 해당 source.index에서 제거하고, destination.index(드롭한 곳)에 해당 draggableId를 추가한다.

여기서 중요한 점!
toDoState의 default 형태를 유지하기 위해서 변화된 보드와 변화되지 않는 보드를 모두 포함하여 반환해야 한다. 반환되는 객체는 원래의 allBoards 객체와 동일한 구조여야 한다.
--> 따라서 변화된 보드의 할 일 목록을 업데이트하고, 나머지 보드의 할 일 목록은 변경되지 않은 상태로 유지하면 된다.

return 값에 {
  ...allBoards, // 나머지 변화되지 않는 보드
  [source.droppableId]: boardCopy // 변화된 보드의 할 일 목록을 업데이트
}

이제 다른 Board로도 이동시켜보자 !


간단하게 그림을 보면서 어떻게 해야하는지 생각해보자

자, Doing -> c를 Done -> f뒤에 배치하고 싶다.

Doing : ['d','e'],
Done : ['f','c']가 되어야 한다는 의미이다.

여기서 source -> Doing, destination -> Done을 나타내는건 이제 다 알것이다!
(이동을 시작한 Doing이 source, 드롭을 한 Done이 destination)

그럼 soure가 진행된 곳과, destionaion이 진행된 곳 두곳을 복사해야 한다는 이야기이다!
이제 코드로 작성해보자

if (destination.droppableId !== source.droppableId) { : 드래그가 끝난 곳과 드래그가 시작한 곳의 droppableId 즉 board가 다를 때 수행하는 코드임을 알려준다.

const sourceBoard = [...allTodos[source.droppableId]];
const destinationBoard = [...allTodos[destination.droppableId]];
  • sourceBoard: 드래그가 시작된 보드의 할 일 목록을 복사한 배열
  • destinationBoard: 드롭된 위치의 보드의 할 일 목록을 복사한 배열
sourceBoard.splice(source.index, 1);
destinationBoard.splice(destination.index, 0, draggableId);
  • 드래그를 시작한 아이템을 sourceBoard에서 제거하고, destinationBoard에 드롭한 위치에 추가한다.
return {
  ...allTodos,
  [source.droppableId]: sourceBoard,
  [destination.droppableId]: destinationBoard,
};
  • 새로운 상태 반환: 변경된 보드들을 포함하는 새로운 상태 객체를 반환한다.
    이 때, 기존의 모든 할 일 목록(allTodos)을 그대로 유지하면서, 변경된 보드들만 새로운 배열로 갱신하게 된다.

이렇게 코드를 작성하게 되면,

이제 다른 Board에도 잘 옮겨지는 것을 확인할 수 있다!


움직일 때마다 색상 변경~?

이제 옮겨지는 것까지 완료를 했다. 옮겨질 때, 색상도 같이 변경되면 옮겨지는 효과도 더 줄 수 있을 것 같다.

card가 떠날 때, 새로운 card가 들어올 때, 기본 배경 이런 식으로 3가지 효과를 주면 좋을 것 같다

이때 줄 수 있는 속성은 <Droppable> 컴포넌트의 children로 제공되는 함수들을 살펴보면 된다.
isDraggingOver={info.isDraggingOver}

  • 역할: info.isDraggingOver 는 사용자가 드래그하는 항목이 현재 이 Droppable 컴포넌트 위에 있는지 여부를 나타낸다.
  • 값: boolean 값으로, 사용자가 드래그하고 있는 요소가 이 Droppable 영역 위에 있을 때 true가 된다.
  • 사용: 드래그 앤 드롭 시 시각적 피드백을 제공하기 위해 사용된다.
    예를 들어, 드래그 항목이 이 Droppable 영역 위에 있을 때 영역의 배경색을 변경할 수 있다.

isDraggingFromThis={Boolean(info.draggingFromThisWith)}

  • 역할: info.draggingFromThisWith 는 사용자가 드래그하는 항목의 ID를 제공한다.
    따라서 Boolean(info.draggingFromThisWith)는 이 Droppable 영역에서 드래그가 시작되었는지 여부를 나타낸다.
  • 값: boolean 값으로, 이 Droppable 영역에서 드래그가 시작되었을 때 true가 된다.
  • 사용: 드래그 앤 드롭이 시작된 원래 영역에서 특정 시각적 피드백을 제공하기 위해 사용된다. 예를 들어, 이 영역에서 아이템이 드래그된 경우 다른 스타일을 적용할 수 있다.


코드를 이렇게 적용해 보았다.
<Droppable> 컴포넌트는 magic, info의 인자를 넘겨준다.

  • magic: magic 객체는 Droppable의 ref와 props를 제공하여 드롭 가능한 영역을 설정하는 데 사용된다.
  • info: info 객체는 드래그 앤 드롭의 상태를 설명한다.

props.isDraggingOver가 true인 경우 :
드래그 항목이 이 영역 위에 있을 때, 배경색이 pink로 설정

props.isDraggingFromThis가 true인 경우 : 드래그 항목이 이 영역에서 드래그가 시작된 경우, 배경색이 red로 설정

드래그가 이 영역과 관련이 없을 때, 배경색이 blue로 설정 된다는 것을 의미한다.


Ref

'ref' --> React에서 DOM 요소 또는 클래스 컴포넌트 인스턴스에 직접 접근하거나 제어할 때 사용하는 특수한 속성을 의미한다.

이제, 직접 ToDo를 입력할 수 있도록 !
<input><button> 태그를 만들어보자

 <input placeholder="grab me" />
<button onClick={onClick}>click me</button>

button에는 onClick 이벤트 함수를 주면서, button이 클릭될 때, input창이 focus() 되도록 적용하고 싶다.

inputRef
const inputRef = useRef<HTMLInputElement>(null) :

  • 참조 생성: useRef 훅을 사용하여 inputRef라는 ref 객체를 생성한다. 초기값은 null이다.
  • 타입 지정: TypeScript에서 useRef<HTMLInputElement>를 사용하여 참조가 HTML 입력 요소임을 지정한다.

<input ref={inputRef} placeholder="grab me" /> : ref를 <input> 요소에 연결하여, inputRef가 해당 DOM 노드(입력 필드)를 참조하게 한다.

const onClick = (event: React.FormEvent<HTMLButtonElement>) => {
  inputRef.current?.focus();
};
  • onClick 핸들러: 버튼 클릭 시 inputRef.current를 통해 입력 필드 DOM 노드에 접근한다.
  • 포커스 설정: inputRef.current?.focus()를 호출하여 해당 입력 필드에 포커스를 설정한다.

그런데 Board 컴포넌트에는 한가지 ref가 더 쓰인다

magic.innerRef

  • 드롭 가능한 영역 참조: magic.innerRefreact-beautiful-dnd 라이브러리에서 제공하는 ref로, 드롭 가능한 영역을 참조한다.
  • 연결: Area 컴포넌트의 ref 속성에 magic.innerRef를 전달하여 react-beautiful-dnd가 이 영역을 인식하고 관리할 수 있게 한다.
  • 드롭 가능 영역 속성 전달: ...magic.droppableProps 를 Area에 전달하여 드롭 가능한 영역의 속성을 적용한다.

inputRef: useRef를 사용하여 생성한 참조를 <input> 요소에 연결하여 포커스 설정에 사용.
magic.innerRef: react-beautiful-dnd에서 제공하는 ref로, 드래그 앤 드롭 가능한 영역을 참조하여 라이브러리의 드래그 앤 드롭 동작을 구현.


Input 창에서 입력 받고, toDo로 생성하기 시작!


이제 atom의 toDoState의 default 값을 객체 안에

즉 , {To DO : [], Doing : [], Done : []} 형태로 수정해 주었다.
이전에는 내가 기본값을 주었다면, 이제는 입력을 받아야 하기 때문에, 빈 배열로 주었으며,
ITodo interface를 제공해주어, ITodo가 어떤 타입인지 지정해 주었으며, toDoState의 타입을 지정하는 ITodoState의 배열의 형태를 같이 이정해 주었따.

이제 input 박스를 생성하기 위해서 --> form을 생성해야 한다.
--> react-hook-form을 사용해보았다.

const onDragEnd = (info: DropResult) => {
    const { draggableId, destination, source } = info;
    console.log(info);
    if (!destination) {
      return;
    }
    if (source.droppableId === destination?.droppableId) {
      setTodos((allBoards) => {
        const boardCopy = [...allBoards[source.droppableId]];
        const taskObj = boardCopy[source.index];
        boardCopy.splice(source.index, 1);
        boardCopy.splice(destination.index, 0, taskObj);

        return {
          ...allBoards,
          [source.droppableId]: boardCopy,
        };
      });
    }

    if (destination.droppableId !== source.droppableId) {
      setTodos((allTodos) => {
        const sourceBoard = [...allTodos[source.droppableId]];
        const taskObj = sourceBoard[source.index];
        const destinationBoard = [...allTodos[destination.droppableId]];

        sourceBoard.splice(source.index, 1);
        destinationBoard.splice(destination.index, 0, taskObj);

        return {
          ...allTodos,
          [source.droppableId]: sourceBoard,
          [destination.droppableId]: destinationBoard,
        };
      });
    }
  };
  

같은 보드 내에서의 작업 이동 처리

  • 검사: source와 destination의 droppableId가 같은지 확인. 즉, 같은 보드 내에서의 이동인지 확인.
  • setTodos: 상태 관리 함수, 여기서 allBoards는 전체 보드 상태를 나타냅니다.
  • boardCopy: 현재 보드의 할 일 목록을 복사.
  • taskObj: source.index에 있는 작업 객체를 가져옴.
  • splice:
    boardCopy에서 source.index 위치의 작업을 제거.
    destination.index 위치에 작업을 삽입.
  • return: 보드 상태를 업데이트. 원본 보드의 ID를 키로 하여 수정된 boardCopy로 업데이트.

다른 보드 간의 작업 이동 처리

  • 검사: source와 destination의 droppableId가 다른지 확인. 즉, 다른 보드 간의 이동인지 확인.
  • setTodos: 상태 관리 함수, 여기서 allTodos는 모든 보드의 상태를 나타낸다.
  • sourceBoard: 드래그가 시작된 보드의 할 일 목록 복사.
  • destinationBoard**: 드래그가 끝난 보드의 할 일 목록 복사.
  • taskObj: source.index 위치의 작업 객체를 가져옴.
  • splice:
    sourceBoard에서 source.index 위치의 작업을 제거.
    destinationBoard에서 destination.index 위치에 작업을 삽입.
  • return: 보드 상태를 업데이트. 원래 보드의 ID와 새 보드의 ID를 키로 하여 수정된 보드 배열로 상태를 업데이트.

이후, 원래는 string으로 받아왔던 것들을 --> object로 받아오는 코드로 수정하면 된다!

그럼 이렇게 각각의 항목에서 투두 입력도 잘 되고, 이동도 잘 되는 모습을 볼 수 있다!

profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글