[달력 만들기 toy project] 날짜별 메모 CRUD ② : 메모 삭제, 수정 기능 추가

이민선(Jasmine)·2023년 5월 13일
1
post-thumbnail

저번 시간에는 날짜별로 메모를 추가하고, 또 내가 클릭한 날짜에 해당하는 메모만 필터링해서 보여주는 감동의 도가니 기능 구현을 해보았다.

오늘은 CRUD의 마지막을 장식할 삭제와 수정을 구현해보겠다.

우선 삭제는 간단하니 빠르게 구현하자.

ToDoCards.tsx

function ToDoCards({ currentDate }: ICurrentDate) {
  const dispatch = useDispatch();
  const [memo, setMemo] = useState("");
  const toDosList = useSelector((state: RootState) => state.toDos);
  
  // 삭제를 구현할 함수. onClick에 연결해 줄 것이다.
    const handleDelete = (id: number) => {
    const toDoToDelete = toDosList.find((toDo: ToDo) => toDo.id === id);
    if (toDoToDelete) {
      dispatch(deleteToDo(toDoToDelete));
    }
  };
  
  .
  .
  (중략)
  .
  .
        <Cards>
        {currentList.map((memo: ToDo) => (
          <Card key={memo.id}>
            {memo.text}
            <p>{`${memo.year}${memo.month + 1}${memo.date}`}</p>
            <Btn onClick={() => handleDelete(memo.id)}>삭제</Btn>
          </Card>
        ))}
      </Cards>
   .
   .
   (중략)
   .
   .

삭제 버튼을 만들고 onClick에 memo.id를 인자로 전달하는 callback함수를 연결해주었다.
삭제 버튼을 클릭하면 dispatch를 통해 deleteToDo 액션을 리듀서에 전달한다. 얍!

store.tsx

import { PayloadAction, configureStore, createSlice } from "@reduxjs/toolkit";

type ToDo = {
  id: number;
  year: number;
  month: number;
  date: number;
  text: string;
};

const initialToDosState: ToDo[] = [];

// slice reducer 객체
export const toDos = createSlice({
// 이름 하여 toDosReducer
  name: "toDosReducer",
 // 초기 상태는 위쪽에 선언했다.
  initialState: initialToDosState,
  reducers: {
   // 메모 추가 액션 메서드
    addToDo: (state: ToDo[], action: PayloadAction<ToDo>) => {
      return [action.payload, ...state];
    },
   // 메모 삭제 액션 메서드
    deleteToDo: (state: ToDo[], action: PayloadAction<ToDo>) => {
      return state.filter((toDo) => toDo.id !== action.payload.id);
    },
  // 메모 수정 액션 메서드
    updateToDo: (state: ToDo[], action: PayloadAction<ToDo>) => {
      return state.map((toDo) =>
        toDo.id === action.payload.id
          ? { ...toDo, text: action.payload.text }
          : toDo
      );
    },
  },
});

export const toDostore = configureStore({ reducer: toDos.reducer });
export const { addToDo, deleteToDo, updateToDo } = toDos.actions;

저번 시간에 다뤘던 메모 추가 액션 메서드인 addToDo도 보이고, 지금 다루려고 하는 deleteToDo 메서드도 보인다. updateToDo도 미리 만들어두었다.

삭제 메서드는 삭제할 메모의 id와 같은 객체 원소를 찾아서 filter한다.

리액트와 함께하는 행벅한 토요일 삭제됐어. 왜 벌써 11시야!!!!

아 그리고 sliceReducer의 경우 action 매개변수에 type을 PayloadAction<원소 타입>이렇게 제네릭으로 준다고 한다. 나 아직 (외워서 쓰고 있는데 깊게 몰라서 찝찝하고 모르는데 자꾸 나오는)제네릭 깊게 모른다.. 타입스크립트 스터디할 때 제네릭 열심히 봐야겠다. 후... 수민님 혹시라도 이 글을 보시게 된다면 저희 제네릭 화이팅!! ㅋㅋㅋㅋㅋㅋㅋ 제네릭 공부하고 달력 포스팅 다시 쭉 훑어보면서 제네릭 타입 놓친 것들 보면서 깨달음을 좀 얻어야겠다.

이제 수정 기능을 구현하자.
수정 기능을 구현할 때 항상 고민이 있었다.

UI를 어떻게 나타내야하지?!!

Firestore 메모장 토이 프로젝트할 때도 그렇고 이번에도 그렇고 처음에는 잘 모르겠어서 일단은 input에 new memo를 적고, 그 값을 update action의 인자로 전달했다.
물론 이렇게 하면 구현이 간단하긴 하다.

근데 너무 불친절하잖아!

실제로 코드스테이츠 동기분께도 이걸 보여드렸더니 수정 UI가 불편할 것 같다는 피드백을 받았다. 훔.. 고개를 끄덕이는 중.. 피드백 전격 수용합니다..!! 감사드립니다..!!!

근데.. 어떻게 구현하지..?

수정 버튼을 클릭하면 지금은 토요일이라는 문구가 input으로 바뀌고 그 input에 새로운 메모를 입력하고 완료버튼을 누를 수 있도록 구현해보는 쪽으로 추천을 받았다.

음.. 한번도 안해본건데 한번 도전해봐?? 오케이 나는 진격의 재스민. 피드백을 안고 도전하기로 결정!!!

일단 useState으로 수정버튼을 클릭한 상태인지 아닌지를 체크하는 isUpdateBtnClicked 상태를 하나 만들고, true면 저 문구를 input tag가 보여지도록 3항 연산자로 코드를 바꿔봐야겠다.

const [isUpdateBtnClicked, setIsUpdateBtnClicked] = useState(false);
.
.
(중략)
    <Cards>
        {currentList.map((memo: ToDo) => (
          <Card key={memo.id}>
            {isUpdateBtnClicked ? (
              <input placeholder='수정할 내용을 입력해주세요'></input>
            ) : (
              memo.text
            )}
            <p>{`${memo.year}${memo.month + 1}${memo.date}`}</p>
            <Btn onClick={() => handleUpdate(memo.id)}>
              {isUpdateBtnClicked ? "완료" : "수정"}
            </Btn>
            <Btn onClick={() => handleDelete(memo.id)}>삭제</Btn>
          </Card>

이렇게 하면 되지 않을까??..

input이 사라져 버리는데? ㅋㅋㅋㅋㅋㅋ
어디 간거야 대체?!!!

아 뭐야 handleUpdate에 로직 추가를 안했잖아.. ㅋㅋㅋㅋ

  const handleUpdate = (id: number) => {
  // 만약 isUpdatedBtnClicke가 false인 상태에서 클릭하면 값만 true로 변경하고 함수 종료
    if (!isUpdateBtnClicked) {
      setIsUpdateBtnClicked(true);
      return;
    }
    const updatedToDo = toDosList.find((toDo: ToDo) => toDo.id === id);
    if (updatedToDo) {
      dispatch(
        updateToDo({
          id,
          text: memo,
          year: currentDate.year,
          month: currentDate.month,
          date: currentDate.date,
        })
      );
    }
  };

와우 이제 잘 나온다!!!!

그럼 이제 새로운 내용을 입력했을 때 새로운 값을 action에 인자로 전달해야겠지??

// input에 value로 연결할 새로운 상태 선언
  const [updatedContent, setUpdatedContent] = useState("");
  
//  onChange 함수 선언
   const onChangeUpdatedContent = (
    event: React.ChangeEvent<HTMLInputElement>
  ) => {
    setUpdatedContent(event.currentTarget.value);
  };
  
// handleUpdate 함수 로직 변경
  const handleUpdate = (id: number, text: string) => {
    if (!isUpdateBtnClicked) {
      setIsUpdateBtnClicked(true);
      return;
    }
    const updatedToDo = toDosList.find((toDo: ToDo) => toDo.id === id);
    if (updatedToDo) {
      dispatch(
        updateToDo({
          id,
        // 이 부분만 변경하면 되는 것이었다.
          text: updatedContent,
          year: currentDate.year,
          month: currentDate.month,
          date: currentDate.date,
        })
      );
    }
    setIsUpdateBtnClicked(false);
  };
  
  //  버튼이 클릭된 상태면 input이 보이고, 다시 클릭해서 꺼지면 갱신된 memo를 보여준다.
         <Card key={memo.id}>
            {isUpdateBtnClicked ? (
              <input
                placeholder='수정할 내용을 입력해주세요'
                value={updatedContent}
                onChange={onChangeUpdatedContent}
              ></input>
            ) : (
              memo.text
            )}
            <p>{`${memo.year}${memo.month + 1}${memo.date}`}</p>
            <Btn onClick={() => handleUpdate(memo.id, updatedContent)}>
            // 수정 상태일 경우 버튼의 문구는 "완료"가 되도록 변경
              {isUpdateBtnClicked ? "완료" : "수정"}
            </Btn>
            <Btn onClick={() => handleDelete(memo.id)}>삭제</Btn>
          </Card>

오오 수정도 잘 된다!!
잘 된 줄 알았는데..
근데 문제가 있다 ㅋㅋㅋㅋ
수정 모드에서 메모를 삭제하고 새로운 메모를 추가하면 아까 수정 상태 그대로 남아있다.


잉??

아무래도 수정 모드에서는 삭제 버튼이 메모를 취소하는 버튼이 되도록 로직을 또 변경해야겠다. 도전! 근데 어떻게 해야 하지??!!

  1. handleDelete 함수 내부에 isUpdateBtnClicked가 true일 경우 false로 바꾸고 함수 종료(return)
  2. 수정 모드일 때 문구를 취소버튼으로 변경

이제 수정 모드는 구현이 된 것 같은데.. 같았는데.. 근데 문제가 많이 생겼다. 문제를 해결하려면 정의하자.

[버그 🐛] 수정 input에 기존 메모가 뜨지 않고 비어있는 input이 떠서 사실상 수정이 아님.
[버그 🐛] 메모를 2개 이상 추가하고 수정 버튼을 누르면 다른 메모들도 같이 수정 모드가 되어버린다.

음.. 수정 로직을 전부 바꿔야 할 것 같은데...? ㅋㅋㅋㅋㅋㅋ
일단은 12시가 넘었으니 자고 내일 하자ㅜㅜ

기상!!!!!!!!!

수정 버그 고치기 대작전 다시 시작.

targetId라는 상태를 만들고 targetId와 같은 경우만 input이 뜨도록 고쳐보자.

// targetId 상태 선언
  const [targetId, setTargetId] = useState(0);

// setTargetId에 인자로 들어온 id로 설정
  const handleUpdate = (id: number, text: string) => {
    if (!isUpdateBtnClicked) {
      setIsUpdateBtnClicked(true);
      // 인자로 들어온 id를 전달하자!!!!
      setTargetId(id);
      console.log(targetId);
      return;
    }
    const updatedToDo = toDosList.find((toDo: ToDo) => toDo.id === id);
    if (updatedToDo) {
      dispatch(
        updateToDo({
          id,
          text: updatedContent,
          year: currentDate.year,
          month: currentDate.month,
          date: currentDate.date,
        })
      );
    }
    setIsUpdateBtnClicked(false);
  };

// targetId로 조건이 새롭게 분기되었으므로 화면에 반영하자.
return (
    <CardsWrapper>
      <h1>{`${currentDate.year}${currentDate.month + 1}${
        currentDate.date
      }`}</h1>
      <Input value={memo} onChange={onChange}></Input>
      <AddBtn onClick={handleAddMemo}>추가</AddBtn>
      <Cards>
        {currentList.map((memo: ToDo) => (
          <Card key={memo.id}>
            // targetId와 같을 때만 input이 뜨도록 구현
            {isUpdateBtnClicked && memo.id === targetId ? (
              <input
                placeholder='수정할 내용을 입력해주세요'
                onChange={onChangeUpdatedContent}
              ></input>
            ) : (
              memo.text
            )}
            <p>{`${memo.year}${memo.month + 1}${memo.date}`}</p>
            <Btn onClick={() => handleUpdate(memo.id, updatedContent)}>
              // targetId와 같은 카드만 수정 문구가 완료 문구로 변경되도록 함
              {isUpdateBtnClicked && memo.id === targetId ? "완료" : "수정"}
            </Btn>
            <Btn onClick={() => handleDelete(memo.id)}>
              {" "}
             // targetId와 같은 카드만 취소 문구가 삭제 문구로 변경되도록 함
              {isUpdateBtnClicked && memo.id === targetId ? "취소" : "삭제"}
            </Btn>
          </Card>
        ))}
      </Cards>
    </CardsWrapper>
  );
}

오오 이제 됐나??!!
어제 정의했던 문제를 다시 보자.

음 ,, 그런데 버그가 추가되었다 .. ㅋㅋㅋㅋㅋㅋㅋㅋ

[버그 🐛] 수정 input에 기존 메모가 뜨지 않고 비어있는 input이 떠서 사실상 수정이 아님.
[버그 🐛] 메모1에서 컨텐츠를 수정하고 메모2의 수정버튼을 누르면 메모2의 컨텐츠가 수정된다.
//-----------------------------------------------------------------------
[해결 ✅] 메모를 2개 이상 추가하고 수정 버튼을 누르면 다른 메모들도 같이 수정 모드가 되어버린다.

후.. 하나씩 또 차근차근 해결해보자. 할 수 있다!!!

3번 문제의 경우 update 함수의 로직이 클릭한 수정 버튼을 가지고 있는 id를 store에 payload로 전달한다.
그렇다면, store에 전달하는 id와 targetId와 다르다면?

아아 됐다됐다!

if (id !== targetId) return;

handleUpdate 함수에 로직을 추가하면 되는 거였어!!

이제 메모1을 수정하고 메모2의 수정버튼을 눌러도 아무런 일이 일어나지 않는다.

[버그 🐛] 수정 input에 기존 메모가 뜨지 않고 비어있는 input이 떠서 사실상 수정이 아님.
//---------------------------------------------------------------------
[해결 ✅] 메모를 2개 이상 추가하고 수정 버튼을 누르면 다른 메모들도 같이 수정 모드가 되어버린다.
[해결 ✅]메모1에서 컨텐츠를 수정하고 메모2의 수정버튼을 누르면 메모2의 컨텐츠가 수정된다.

이제 수정 모드 답게 원래 메모가 수정 input에 뜨도록 구현해보자.

와 해결됐다 !!!!!

handleUpdate함수 내부에 매개변수로 받은 text를 이용하여

setUpdatedContent(text)

를 추가하고

input의 value를

value={updatedContent)

로 추가하고

onClick에서 콜백함수 내부의 인자를 updatedContent가 아니라 toDo.text로 고쳤다.

<Btn onClick={() => handleUpdate(toDo.id, toDo.text)}>

후 이제 수정 된다..

[해결 ✅]수정 input에 기존 메모가 뜨지 않고 비어있는 input이 떠서 사실상 수정이 아님.
[해결 ✅] 메모를 2개 이상 추가하고 수정 버튼을 누르면 다른 메모들도 같이 수정 모드가 되어버린다.
[해결 ✅]메모1에서 컨텐츠를 수정하고 메모2의 수정버튼을 누르면 메모2의 컨텐츠가 수정된다.

드디어 CRUD 완료!!!!!
으아 뿌듯한데 배고프다 ㅠㅠㅠㅠ
오늘 저녁은 파스타🥰

profile
기록에 진심인 개발자 🌿

0개의 댓글