티스토리 뷰

문제상황

처음 테이블 목록은 1위 타이타닉, 2위 헤어질 결심, 3위 세 얼간이이다.
데이터를 수정하면 랭킹순서대로 정렬되지 않는다. (드래그앤드롭으로 순위를 변경하는 것은 문제없이 잘 된다.)
데이터 수정 후 내가 지정한 랭킹(1위, 2위, 3위) 순서가 아니라, 데이터베이스에 데이터를 추가한 시간 순서대로(2위 헤어질 결심, 1위 타이타닉, 3위 세 얼간이) 정렬이 되고 있다.

🩹 초기 구현

리스트 데이터를 수정하면 파이어베이스의 snapshot기능으로 자동으로 업데이트된 데이터를 가져온다. 이 때 가져온 쿼리데이터로 likes상태를 업데이트 해준다. 데이터베이스에는 데이터가 생성된 시간 순서대로 정렬이 되어있고, 이 순서대로 쿼리데이터를 가져오며, likes상태(배열)에도 이 순서대로 저장된다.

한편, 테이블 리스트를 렌더링할 때 이 likes상태(배열) 순서대로 렌더링된다.
처음 테이블 컴포넌트를 렌더링할 때에는 useEffect(..., [ranking])에서 likes가 ranking 순서대로 정렬된 후 렌더링하지만, 리스트 데이터를 수정할 때에는 순위(ranking)가 변경되지않으므로 likes를 정렬하는 로직이 실행되지 않는다.

요약하면, 리스트 데이터를 수정하면 likes상태가 갱신되지만 ranking대로 정렬은 되지 않아, 데이터베이스에 저장되어있는 순서대로 리렌더링이 되는 것이다.

다음은 해당 부분의 코드이다.

// Table.tsx
// 마지막줄 setNewRanking(copyRanking)으로 드래그앤드롭마다 데이터베이스의 ranking이 갱신된다.

const setNewRanking = async (newRanking: IRanking, likeId?: ILike["id"]) => {
  const rankingDoc = doc(dbService, currentCategory, `ranking_${loggedInUser?.uid}`);
  if (likeId) {
    await updateDoc(rankingDoc, { ...newRanking, [likeId]: deleteField() });
  } else {
    await updateDoc(rankingDoc, newRanking);
  }
};

const onDragEnd = ({ destination, source, draggableId }: DropResult) => {
    if (!destination) return;
    const copyRanking = Object.assign({}, ranking);
    if (destination.index < source.index) {
      Object.keys(copyRanking).forEach((likeId) => {
        copyRanking[likeId] >= destination.index + 1 &&
          copyRanking[likeId] < source.index + 1 &&
          (copyRanking[likeId] = copyRanking[likeId] + 1);
      });
      copyRanking[draggableId] = destination.index + 1;
    } else if (destination.index > source.index) {
      Object.keys(copyRanking).forEach((songId) => {
        copyRanking[songId] > source.index + 1 &&
          copyRanking[songId] <= destination.index + 1 &&
          (copyRanking[songId] = copyRanking[songId] - 1);
      });
      copyRanking[draggableId] = destination.index + 1;
    }
    setNewRanking(copyRanking);
  };
// MyLike.tsx

// onSnapShot을 통해 데이터베이스 변경사항이 있으면 실시간으로 가져온다. 자동으로 리렌더링은 되지 않는다.
// 리렌더링을 위해 likes과 ranking의 스냅샷을 각각 상태로 저장한다.
useEffect(() => {
    const q = query(collection(dbService, currentCategory), where("creatorId", "==", loggedInUser?.uid));
    onSnapshot(q, (querySnapshot) => {
      const likesDB = [] as ILike[];
      querySnapshot.forEach((doc) => {
        likesDB.push({ ...(doc.data() as ILike) });
      });
      setLikes(likesDB);
    });
  }, [currentCategory]);

  useEffect(() => {
    onSnapshot(doc(dbService, currentCategory, `ranking_${loggedInUser?.uid}`), (doc) => {
      setRanking({ ...doc.data() });
    });
  }, [currentCategory]);


// 드래그앤드롭을 할 경우 likes데이터는 그대로이고 순위만 변경되기 때문에 ranking스냅샷만 새로 가져오고, 
// 갱신된 ranking상태를 가지고 기존의 likes상태를 재정렬한다.
  useEffect(() => {
    const orderedLikes = likes.slice();
    orderedLikes.sort((a, b) => ranking[a.id] - ranking[b.id]);
    setLikes(orderedLikes);
  }, [ranking]);

Table컴포넌트에서 드래그앤드롭마다 setNewRanking이 실행되어 데이터베이스의 ranking이 갱신되고, MyLike컴포넌트에서 snapshot을 통해 갱신된 ranking데이터를 가져와서 ranking상태를 갱신한다. ranking상태가 갱신되었으니 useEffect가 실행되고 likes상태를 ranking대로 정렬하여 likes상태도 갱신한다. 그리고 갱신된 likes상태와 map을 이용하여 Table컴포넌트를 리렌더링한다.

 

🔨 수정

리스트 데이터를 업데이트한 후 likes데이터를 새로 가져오면 랭킹 순서대로 정렬하는 로직을 추가한다.

  const [rareLikes, setRareLikes] = useState<ILike[]>([]);
  const [likes, setLikes] = useRecoilState(likesAtom);
  
  useEffect(() => {
    const q = query(collection(dbService, currentCategory), where("creatorId", "==", loggedInUser?.uid));
    onSnapshot(q, (querySnapshot) => {
      const likesDB = [] as ILike[];
      querySnapshot.forEach((doc) => {
        likesDB.push({ ...(doc.data() as ILike) });
      });
      setRareLikes(likesDB); // 지역상태 rareLikes에 저장
    });
  }, [currentCategory]);
  
  useEffect(() => {
    const orderedLikes = rareLikes.slice();
    orderedLikes.sort((a, b) => ranking[a.id] - ranking[b.id]);
    setLikes(orderedLikes); // rareLikes가 갱신될 때마다 랭킹순서대로 정렬하여 전역상태 likes에 저장
  }, [rareLikes]);

초기 구현은 파이어베이스에서 스냅샷으로 가져온 likesDB를 바로 전역상태 likes로 저장하고 프로젝트 전체에서 이 전역상태를 사용하였다.
하단의 실패사례1,2 이후 스냅샷으로 가져온 레어데이터를 임시로 저장할 지역상태를 추가하고 실제 사용할, 랭킹순서대로 정렬된 데이터를 전역상태로 관리하는 것이 좋겠다고 생각했다. 그래서 레어데이터는 useState로 rareLikes에 저장하고, 사용자가 리스트데이터를 수정하면 스냅샷을 통해 rareLikes상태가 갱신이 될 것이고, rareLikes상태가 갱신되면 useEffect를 통해 rareLikes를 랭킹순서대로 정렬한 뒤 이 배열로 likes상태가 갱신된다. 

문제가 해결되었다. 리스트데이터를 수정하면 수정된 데이터만 바뀌고, 리스트도 랭킹 순서대로 정렬되어 그대로 유지된다.

 

실패사례1

useEffect(() => {
    const q = query(collection(dbService, currentCategory), where("creatorId", "==", loggedInUser?.uid));
    onSnapshot(q, (querySnapshot) => {
      const likesDB = [] as ILike[];
      querySnapshot.forEach((doc) => {
        likesDB.push({ ...(doc.data() as ILike) });
      });
      likesDB.sort((a, b) => ranking[a.id] - ranking[b.id]); // 랭킹순으로 배열 정렬
      setLikes(likesDB);
    });
  }, [currentCategory]);

리스트데이터가 수정되면 자동으로 업데이트된 likes데이터를 가져오는 snapshot부분이다. likes데이터를 가져와 likesDB 배열에 저장한 뒤 likesDB를 랭킹대로 정렬해주는 sort메서드를 추가해주었다. 그러나 작동하지 않았다. 앞뒤로 console.log를 찍어보았는데 변화가 없었고 문제도 해결되지 않았다. 공식문서에 파이어베이스의 onSnapshot을 사용할 때에 정렬하기 위해서 orderBy("필드명")을 사용하라고 나와있는 것을 보니 sort가 작용하지 않는 것 같다.

 

실패사례2

나는 cloud firestore 데이터베이스를 이용하고 있는데 정렬은 orderby 메서드만 제공한다. orderby메서드를 이용할 때에는 각 문서들(리스트 문서들)의 공통 필드가 있어야 한다. 그러나 데이터 구조상 랭킹만 저장해서 관리하는 문서를 따로 두어서 리스트 문서들에는 공통적인 ranking필드가 없기 때문에 orderby메서드를 사용할 수 없다. (프로젝트 처음에는 리스트 문서들에 각각 ranking필드를 넣고 리스트의 랭킹을 각각 저장하였다가, 예를 들어 100위를 1위로 변경하면 순위가 그 사이인 데이터들까지 모두 랭킹이 변하고, 총 100번 요청을 해서 서버를 많이 사용하게 되는 문제가 있었다. 결과적으로 파이어베이스 무료 사용량을 초과하여 과금이 되었다.)

firebase에서 제공하는 실시간 데이터베이스 제품을 이용했다면 orderByValue메서드를 사용해서 다른 문서의 데이터를 참조하여 정렬이 가능해보이는데, 나는 cloud firestore 제품을 이용했기 때문에 데이터구조를 바꾸지 않는 이상 orderby메서드로는 불가능하다.

 

🙌 github commit 🙌

 

fix: add logic for sorting likes after updating likes DB · hahagarden/project-machine@d2f0710

Showing 1 changed file with 8 additions and 1 deletion.

github.com

 

반응형
댓글