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

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

추가 설명!

<Board/> 컴포넌트에서 onValid ()함수에 대해서 자세하게 설명하고 넘어가겠다!

onValid 함수는 입력 폼에서 제출하기 버튼을 눌렀을 때 활성화 (물론, 오류가 발생하지 않았을 때)

  • 입력 값 처리: 폼에서 받은 toDo 값을 새로운 할 일 객체 newTodo에 할당한다. 이 객체는 id와 text 속성을 가지고 있다.
  • id는 Date.now()를 통해 현재 시간을 기준으로 생성되며,
  • text는 입력된 toDo 값이 할당된다.
  • Recoil 상태 업데이트: setToDos 함수를 사용하여 Recoil의 toDoState를 업데이트한다.
    이때, 함수형 업데이트를 통해 현재의 allBoards 객체를 spread 연산자(...)를 사용하여 복제하고, 해당 boardId의 배열에 새로운 할 일 객체 newTodo를 추가한다.

예를 들어서 설명해보겠다
예를 들어, boardId가 'todo'이고 현재 toDoState가 다음과 같다고 가정

const toDoState = {
  todo: [
    { id: 1, text: 'Complete assignment' },
    { id: 2, text: 'Prepare for presentation' }
  ],
  done: [
    { id: 3, text: 'Review feedback' },
    { id: 4, text: 'Submit report' }
  ]
};

만약 사용자가 입력 폼에서 'Study for exam'을 입력하고 제출 버튼을 누르면, onValid 함수가 아래와 같이 작동한다.

const onValid = ({ toDo: 'Study for exam' }) => {
  // 새로운 할 일 객체 생성
  const newTodo = {
    id: Date.now(), // 예를 들어 1646391893534
    text: 'Study for exam'
  };

  // setToDos를 통한 Recoil 상태 업데이트
  setToDos((allBoards) => ({
    ...allBoards,
    todo: [
      ...allBoards.todo,
      newTodo // 새로운 할 일 객체 추가됨
    ]
  }));

  // 입력 폼 초기화
  setValue('toDo', '');
};

여기서 todo => boardId 를 의미하고,
...allBoards.todo 는 이미 todo에 저장되어있는

todo: [
    { id: 1, text: 'Complete assignment' },
    { id: 2, text: 'Prepare for presentation' }
  ],

를 의미하고, 이것들과 함께 newTodo가 추가되는 것이라고 생각하면 될 것이다!


삭제 버튼 생성하기

요렇게 옆에 삭제 버튼을 생성해보자!
--> 각 할 일 항목에 삭제 버튼을 표시하고, 버튼 클릭 시 해당 항목을 삭제하는 기능을 구현하면 된다.


{toDoText} 옆에 <FontAwesomeIcon/> 에서 받아온 아이콘을 사용하려고 해서, <FontAwesomeIcon/>onClick() 이벤트를 주었다!
onClick={() => onClickDelete(toDoId)} : onClickDelete함수에 인자를 받기 위해 익명함수를 사용했다. 또한, onClick함수는 Board 컴포넌트에서 정의한 후, 전달하는역할이다.

Board.tsx 컴포넌트에서 확인해보자

onClickDelete 함수는 Card 컴포넌트에서 사용되며, 특정 id 값을 가진 할 일 항목을 삭제하는 기능을 수행한다.

  • 함수 선언: onClickDelete 함수는 id라는 하나의 매개변수를 받는다. 이 id는 삭제할 할 일 항목의 고유한 식별자이다.

  • 상태 업데이트: setToDos 함수의 콜백 함수 (allBoards) => { ... } 는 현재의 toDoState 상태를 받는다. 객체 형태로 구성되어 있고,각 보드(boardId) 마다 할 일 목록이 배열로 저장되어 있다.

  • 할 일 목록 필터링: [boardId]: [...allBoards[boardId].filter((toDo) => toDo.id !== id)] 부분은 선택한 보드(boardId)의 할 일 목록을 업데이트하는 부분인데,

  • allBoards[boardId] 는 현재 선택한 보드의 할 일 목록을 의미한다.

  • filter((toDo) => toDo.id !== id) 는 배열 메소드 filter를 사용하여 toDo.id가 id와 일치하지 않는 모든 항목들로 새 배열을 생성한다.
    --> 즉, id와 일치하지 않는 항목들만 남기고 나머지는 제거한다.

  • 새로운 상태 반환: setToDos 함수는 기존의 상태를 변경하지 않고 새로운 상태를 반환한다. 따라서 위 코드는 선택한 보드의 할 일 목록에서 id와 일치하는 항목을 제외한 나머지 항목들로 새로운 배열을 만들어 상태를 업데이트하는 역할을 한다.


onClickDelete를 정의한 후, <Card/> 컴포넌트에 props로 보내면 끝!


휴지통에 드래그 후 삭제시키기

위에서 만들었던 삭제 버튼 기능은 Board에게 넘겨주고,
Card 부분은 화면 하단에 있는 휴지통 그림에 드래그 하면 삭제되도록 코드를 작성해 보았다.

여기서, 생각해봐야할 것은

card는 이미 <draggable>에 속해있기 때문에, 드래그가 가능할 영역 즉 드롭할 영역에 휴지통 그림을 주면 되지 않을까? 라고 생각하면서 코드를 작성하기 시작했다!

이에 먼저 DeleteBoard 컴포넌트를 만들었다.

import { Droppable } from 'react-beautiful-dnd';
import styled from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';

const Container = styled.div`
  width: 80px;
  height: 80px;
  position: fixed; 
  right: 20px; 
  bottom: 20px; 
  display: flex;
  justify-content: center;
  align-items: center;
`;

const IconWrapper = styled.div`
  font-size: 80px; /* Adjust size as needed */
  color: ${(props) => props.theme.trash}; /* Change icon color */
  position: fixed;
`;

const DeleteBoard = () => {
  return (
    <Droppable droppableId="TRASH">
      {(provided) => (
        <Container ref={provided.innerRef} {...provided.droppableProps}>
          <IconWrapper>
            <FontAwesomeIcon icon={faTrashCan} />
          </IconWrapper>
          {provided.placeholder}
        </Container>
      )}
    </Droppable>
  );
};

export default DeleteBoard;

styled-component 부분을 제외하고는 코드는 간단하다!
<Droppable/> 를 사용해서, 드롭할 영역을 주었다.
현재 코드에서 Contanie에 드랍할 영역이 생겨, 그 안에 <FontAwesomeIcon icon={faTrashCan} />을 같은 크기로 주어, 휴지통에 드래그를 했을 때,
Container에 영역이 들어왔을 때, 삭제가 가능하도록 주었다.

여기서 <Droppable droppableId ="TRASH">를 주어

이렇게 onDragEnd함수에 실행할 코드를 작성해 주었따.
만약, destination.droppableId === 'TRASH'일 때,
즉, 드롭할 Id가 TRASH일 때, source.droppableTd에 해당하는 것을 제거하고, 새로운 배열을 만드는 것이다.

즉, 삭제할 수 있는 것이다.


DarkMode & LightMode

토글 버튼을 눌렀을 때, DarkMode & LightMode로 변경이 가능하도록 만들고 싶다!

이렇게 lightTheme와, darkTheme에게 적용할 색상을 각각 적용시키고,
atom에 기본 상태는 ligthmode를 적용시켜두었다.

수정 버튼!

이렇게 수정 버튼을 누르면, 위에 수정할 수 있는 폼이 나오는 것이다
처음에는 prompt를 사용했었는데 너어어어어어어무 이쁘지가 않아서 , 어떻게 할까 하다가 inpu창으로 그냥 안에 만들어버리자! 라고 생각해서 이렇게 만들었다!


<Card/> 컴포넌트 안에 Modal 컴포넌트를 따로 만들어서 넣어주었다.

Modal 컴포넌트
간단하게 input에 입력한 값이 변동될 수 있도록 return 값을 작성해 두었다.


submit() 함수를 살펴보자면,
input에서 받아온 값을 inputValue로 가져왔기 때문에,
newTodos 에 inputValue값을 저장해두고, newTodos가 null이거나 빈 값인지 확인한 후,
setTodos()로 상태 변경을 시도한다.
먼저, allBoards를 불러온 뒤, allBoards[boardId] 즉, boards들에서 선택된 boardId의 todo를 모두 불러온 뒤, id가 같은지 확인하고, 그 객체 안에 text를 newTodos로 변경하면 된다!

return 값에 { 이전 boards와, [boardId] : updatedTodos }를 저장하면서 바뀐 값을 저장해둔다!

또한, 수정 버튼을 눌렀을 때, 아이콘이 사라졌다가, input 입력이 끝나면 아이콘이 다시 생겨날 수 있도록 useState 를 사용해서 변경될 수 있도록 코드를 작성해 두었다!

여기서 모달 창 추가!

모달 창을 꾸며서 그냥 모달을 만드는게 더 깔끔할 것 같아서 모달 창을 만들어 보았다

import React, { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { toDoState } from '../atoms';
import styled from 'styled-components';

const ModalWrapper = styled.div`
  position: fixed; /* 화면 고정 */
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5); /* 배경 반투명 처리 */
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000; /* 다른 요소보다 위에 오도록 */
`;

const ModalContent = styled.div`
  background: ${(props) => props.theme.cardColor};
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
`;

const Input = styled.input`
  border-style: none;
  background-color: ${(props) => props.theme.cardColor};
  border-bottom: 1px solid ${(props) => props.theme.boardColor};
  color: ${(props) => props.theme.textColor};

  &:focus {
    outline: none;
  }
`;

const Button = styled.button`
  background-color: ${(props) => props.theme.boardColor};
  margin-left: 10px;
  color: ${(props) => props.theme.boardtextColor};
  border-radius: 20px;
  border: 1px solid ${(props) => props.theme.boardColor};
  font-size: 15px;
`;

interface IModal {
  toDoText: string;
  toDoId: number;
  boardId: string;
  onClose: () => void;
}

const Modal = ({ toDoText, toDoId, boardId, onClose }: IModal) => {
  const [inputValue, setValue] = useState(toDoText);
  const setTodos = useSetRecoilState(toDoState);

  const onChange = (e: React.FormEvent<HTMLInputElement>) => {
    setValue(e.currentTarget.value);
  };

  const onSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const newTodos = inputValue;
    if (newTodos !== null && newTodos.trim() !== '') {
      setTodos((allBoards) => {
        const updatedTodos = allBoards[boardId].map((todo) =>
          todo.id === toDoId ? { ...todo, text: newTodos } : todo
        );

        return {
          ...allBoards,
          [boardId]: updatedTodos,
        };
      });
      onClose();
      setValue('');
    }
  };

  return (
    <>
      <ModalWrapper onClick={onClose}>
        <ModalContent onClick={(e) => e.stopPropagation()}>
          <form onSubmit={onSubmit}>
            <Input type="text" value={inputValue} onChange={onChange} />
            <Button type="submit">확인</Button>
          </form>
        </ModalContent>
      </ModalWrapper>
    </>
  );
};

export default Modal;

코드가 길어져서 코드를 전체 공개를 한다!

e.stopPropagation()을 사용하는 이유는 모달 외부를 클릭했을 때 모달이 닫히는 기능을 유지하면서, 모달 내부를 클릭했을 때는 모달이 닫히지 않도록 하기 위해서이다. 그거 이외에는 css 부분 수정한것 밖에 없다!


요렇코롬 ! 모달창이 뜬다
깔끄롬하게!


진짜 진짜 마지막 allDeleteBtn 추가

싹다 삭제할 수 있는 기능을 구현하면 어떨까 싶어서

저 아래에 빗자루 아이콘을 추가하고, 그 버튼을 누르면, 모달창이 나와서 "정말 삭제 할거니?"하고 물어봐주고 ok를 누르면 삭제가 되도록!
cancel 를 누르면 삭제를 안하도록!

우선 저 부분은 DeleteBtn 컴포넌트이다.
저 컴포넌트에서 작업을 먼저 시작하도록 하겠다!

DeleteBoard 컴포넌트와 ModalAllDelete 컴포넌트는 함께 동작하여 사용자가 모든 할 일(To-Do)을 삭제할 수 있도록 한다.
DeleteBoard는 버튼을 통해 모달을 열고, ModalAllDelete는 삭제를 확인하고 처리할 수 있도록 한다.


간단하게 함수를 설명하자면,
onClickClearAll은 모든 할 일을 삭제하고 모달을 닫는 함수이다.
openAllClear와 closeAllClear는 모달을 열고 닫는 함수이다
onClickClearBtn은 클릭 이벤트를 처리하고 모달을 여는 함수이다.


빗자루 아이콘을 눌렀을 때, onClickClearBtn을 주어 모달을 열게 하고,
isClearOpen 즉, 모달이 열렸을 때, onClose(모달 닫기 함수), onConfirm(전체 삭제 함수)를 넘겨주었다.


이제 ModalAllDelete 컴포넌트에서 받아와서 나머지 기능을 구현해주면 된다!

Are you sure you want to delete everything?의 문구를 보여준뒤,
Ok를 누르면, handleConfirm 실행 --> onConfirm(전체 삭제), onClose(모달닫기)
cancel를 누르면, onClose 함수 실행 --> 모달창 닫기! 를 실행하면 된다!

이렇게 짠 코드를 실행해보면,

이렇게 멋있게 경고문도 알려주게 된다!


마무리

마지막 코드 챌린지를 하면서

휴지통으로 드래그해서 삭제하는 기능 만들기,
삭제 버튼으로 삭제 가능하도록 만들기,
darkMode, lightMode 버전 만들기 등등

하면서, 정말 삽질도 많이하고, 솔직히 지피티한테 물어보기도 하고 (많이!ㅋㅋ) 다른 사람들 코드도 보면서 이렇게 해야하는구나 힌트도 많이 얻었던 것 같다.

그래도 생각보다 예쁜(?) 결과물이 나온 것 같다!

서연이의 ToDoBoard 에 놀러오면 모든 것을 확인할 수 있다!

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

0개의 댓글