이번 시간에는 delete 요청을 구현해보자. 버튼만 똑 누르면 내가 원하는 데이터가 화면에는 물론 DB에서도 삭제될 수 있도록 구현할 것이다. getDoc, addDoc 메서드를 사용해보았고, 이번 시간에는 deleteDoc 메서드를 사용해볼 것이다.

그런데.. 다시 보니까 완전 바보 같은 실수를 했좌나...
생각해보니 delete 버튼이 save 옆에 있었다. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 현재 작성 중인 데이터가 아니라 이미 DB에 저장된 데이터를 삭제하는 기능을 추가해야 논리적으로 맞는거지 ㅠㅠㅠㅠ 엉뚱한 위치였다. ㅎㅎㅎ 이걸 삭제 구현하려고 할 때 되서야 깨닫다닠ㅋㅋㅋㅋ 왕초보다운 실수 .. ㅋㅋㅋㅋㅋㅋ

그래서 원래 save button 옆에 말도 안되게 붙어있던 delete 버튼을 지우고 get 요청으로 불러온 각 데이터의 오른쪽에 삭제 버튼(X 표시)이 있도록 다시 만들어주었다. ㅎㅎㅎㅎ

이제 각 데이터 오른쪽에 X 표시가 생겼다. X 표시를 클릭하면 화면에서는 물론이고 DB에서도 해당 데이터가 아예 사라질 것이다.

소심하게 타코 옆에 있는 X 표시를 눌러보자..

호오... ! 타코를 삭제했더니 타코가 사라졌다..!!!
뭔가 오른쪽에 시뻘건 게 있지만 잠시 뒤로 하고 Firestore로 가보니 역시 타코가 잘 사라져 있다!!

삭제는 잘 된 것 같은데.. Warning이 뜨고 있는데 뭐라는 지 좀 봐볼까??

Warning: Each child in a list should have a unique "key" prop.

문제: 각 항목에 key를 분명 줬는데 key가 있어야 한다고 경고가 뜸

오잉 이거 map으로 데이터 뿌려줄 때 각 데이터에 key가 없을 때 나오는 경고인데? 나는 key를 지정했는뎁쇼??

      <ul>
        {memos.map((memo) => {
          return (
            <MemoWrapper>
              <li  key={memo.id}>{memo.value}</li>
              <DeleteBtn onClick={() => handleDeleteMemo(memo.docRef)}>
                X
              </DeleteBtn>
            </MemoWrapper>
          );
        })}
      </ul>

예전에 각 데이터를 Link로 감쌌을 때에도 이런 비슷한 일이 있었다.

https://velog.io/@jasmine0714/error-Warning-Each-child-in-a-list-should-have-a-unique-key-prop

해결: key 속성을 각 항목의 가장 상위 태그인 MemoWrapper로 이동

key={memo.id}를 li 태그에서 MemoWrapper로 옮겨주었다.
이렇게 하니 더 이상 경고 메시지가 뜨지 않는다.

이 2가지의 경험으로 미루어보아, ul 태그 내에서 각 항목의 가장 상위 태그에 key를 달아주어야만 이러한 경고가 뜨지 않는 듯하다.

이제 소스코드를 리뷰해보자.

Memo 컴포넌트 소스코드 전체 리뷰

import { useState } from "react";
import styled from "styled-components";
import {
  collection,
  getDocs,
  addDoc,
  DocumentReference,
  DocumentData,
  deleteDoc,
} from "firebase/firestore";
import { db } from "./service/firebase";

// styled-component 생략

interface IMemo {
  id: string;
  value: string;
  docRef: DocumentReference<DocumentData>;
}

function Memo() {
  const [memos, setMemos] = useState<IMemo[]>([]);
  const [newMemo, setNewMemo] = useState("");

// handleGetMemo 함수 일부 수정 (memoList에 doc.ref도 함께 push되도록 함)
  const handleGetMemo = async () => {
    let querySnapshot = await getDocs(collection(db, "learn_firebase"));
    const memoList: IMemo[] = [];
    querySnapshot.docs.forEach((doc) => {
      for (let key in doc.data()) {
        memoList.push({ id: key, value: doc.data()[key], docRef: doc.ref });
        console.log(doc.ref); // ⭐️ 아래 설명 있음
      }
    });
    setMemos(memoList);
  };

// handleSaveMemo 생략

// handleDeleteMemo 구현
  const handleDeleteMemo = async (docRef: DocumentReference<DocumentData>) => {
    await deleteDoc(docRef);
    setMemos(memos.filter((memo) => memo.docRef !== docRef));
  };
  
  return (
    <MemoContainer>
      <Title>메모장</Title>
      <MemoTextarea
        value={newMemo}
        onChange={(event) => {
          console.log(event.target.value);
          setNewMemo(event.target.value as any);
        }}
      ></MemoTextarea>
      <BtnContainer>
        <Btn onClick={handleSaveMemo}>save</Btn>
      </BtnContainer>
      <MemoListBtn onClick={handleGetMemo}>나의 메모 보기</MemoListBtn>
      <ul>
        {memos.map((memo) => {
          return (
            <MemoWrapper>
              <li key={memo.id}>{memo.value}</li>
              <DeleteBtn onClick={() => handleDeleteMemo(memo.docRef)}>
                X
              </DeleteBtn>
            </MemoWrapper>
          );
        })}
      </ul>
    </MemoContainer>
  );
}
export default Memo;

⭐️ 삭제할 때 가장 핵심적인 사항은 doc.ref를 이용하는 것이다.
우선 GET 요청할 때부터 doc.ref를 사용해서, 추후 삭제할 때 doc.ref로 해당 참조를 사용하여 문서를 찾을 수 있도록 해야 한다.

doc.ref는 Firebase Store에서 문서의 참조를 나타내는 속성이다.
GET요청할 때 각 데이터의 doc.ref를 console에 출력해보면

이렇게 각 데이터의 속성이 출력되는 것을 볼 수 있다.

memoList에 { id: key, value: doc.data()[key] }까지 push했었는데, 이제 docRef라는 속성도 함께 push할 것이다.

memoList.push({ id: key, value: doc.data()[key], docRef: doc.ref });

물론 이렇게 하면 memos라는 state의 interface에도 docRef를 추가해주어야 한다.

interface IMemo {
  id: string;
  value: string;
  docRef: DocumentReference<DocumentData>;
}

docRef라는 속성의 타입을 지정해야 하는데, DocumentReference라는 타입은 Firestore의 문서에 대한 참조를 나타내는 타입이라고 한다. 어렵다;; 그냥 string, number 같이 평소에 잘 알던 타입이 아니라, 특정 상황에서 사용해야 하는 타입이 정해져 있다는 게 익숙하지 않은 포인트이다.

여튼 성공적으로 doc.ref라는 속성을 포함하여 GET 요청을 했고, interface도 지정했으니, 이를 활용하여 DELETE 요청을 하면 된다.

const handleDeleteMemo = async (docRef: DocumentReference<DocumentData>) => {
    await deleteDoc(docRef);
    setMemos(memos.filter((memo) => memo.docRef !== docRef));
  };

GET 요청 시 추가했던 docRef라는 속성을 handleDeleteMemo의 인자로 전달하여 (DocumentReference라는 타입도 표기함) deleteDoc메서드를 호출 시 해당 인자를 전달할 수 있다. 이 때 DB에서 문서가 삭제되고 난 후, memos라는 state에서도 docRef에 해당되지 않는 memo들만 filter로 추려서 보여주면 데이터 삭제된 바가 화면에 성공적으로 반영되어 재렌더링 된다.

그리고 또 한 가지 중요한 것!

<DeleteBtn onClick={() => handleDeleteMemo(memo.docRef)}>

이렇게 콜백함수를 사용하여 handleDeleteMemo를 등록해야한다는 것이다.

만약 콜백함수를 사용하지 않고 <DeleteBtn onClick={handleDeleteMemo(memo.docRef)}> 이렇게 나타내버리면 DeleteBtn이 렌더링될 때마다 handleDeleteMemo가 실행된다. 함수 뒤에 memo.docRef를 인자로 전달하면서 함수를 호출하기 때문이지! 그래서 버튼을 클릭할 때만 함수를 호출하게끔 구현하려면 반드시 콜백함수를 사용해야 한다.

어려웠지만 잘 해냈으!!! 다음 시간에는 UPDATE 요청을 해보자. 지금까지 GET, POST, DELETE 구현을 완료했으니 이제 하나 남은거야 벌써!! 얼른 UPDATE도 마무리하고 리팩토링을 조금 거친 후에 또 새로운 토이 프로젝트를 시작하고 싶다 ㅎㅎㅎ 쟤스민 화이팅!!

profile
기록에 진심인 개발자 🌿

0개의 댓글