리액트 북마크 개발하기 (supabase + optimistic update 활용)

seonik kim·2023년 8월 27일

리액트 북마크 개발하기

supabase + 리액트쿼리의 낙관적 업데이트(optimistic update)를 적용하여 리액트에서 북마크 기능을 개발하는 과정을 공유합니다.

개발 과정

  1. supabase bookMark api 코드 생성
    : Create, Read, Delete 3가지 추가
  2. BookMark.tsx 컴포넌트 생성
    : useParams, loginUser Auth는 적용된 상태
  3. optimistic update 적용
  4. 회고

1. supabase bookMark api 코드 생성

처음 북마크를 개발할 때는 useCreateBookMarkMutationuseHook으로 분리해 사용하지 않아서 BookMark.tsx 컴포넌트에서 가독성이 떨어지는 단점이 있었습니다.

BookMark.tsxuseMutation을 직접 사용하는 것과 useHook으로 분리하여 사용하는 방법 중 더 효율(?)적인 방향인지 판단하기 어려워 가독성에 중점을 두고 코드를 개선했습니다.

혹시 더 좋은 방향에 대해 아시는 분이 있다면 댓글로 남겨주시면 감사하겠습니다 :)

import { useMutation, useQueryClient } from '@tanstack/react-query';
import supabase from '../supabase';
import { BookMarkType } from '../supabase/database.types';

// 전체 북마크 데이터 조회
export const fetchBookmark = async () => {
  const { data } = await supabase.from('bookmark').select('*');
  return data;
};

// 해당 게시물(튜터) 북마크 데이터만 조회
export const matchBookMark = async (tutorId: string) => {
  const { data } = await supabase.from('bookmark').select().match({ tutor_id: tutorId });

  return data;
};

// 북마크 추가
export const createBookMark = async (bookMark: BookMarkType) => {
  await supabase.from('bookmark').insert(bookMark).select();
};

// 북마크 삭제
export const deleteBookMark = async (tutorId?: string) => {
  await supabase.from('bookmark').delete().match({ tutor_id: tutorId });
};

// 북마크 추가( 리액트쿼리 useMutation 적용 )
export const useCreateBookMarkMutation = () => {
  const queryClient = useQueryClient();

  const mutation = useMutation(createBookMark, {
    onSuccess: () => {
      queryClient.invalidateQueries(['matchBookMark']);
    },
  });

  return mutation;
};

// 북마크 삭제( 리액트쿼리 useMutation 적용 )
export const useDeleteBookMarkMutation = () => {
  const queryClient = useQueryClient();

  const mutation = useMutation(deleteBookMark, {
    onSuccess: () => {
      queryClient.invalidateQueries(['matchBookMark']);
    },
  });

  return mutation;
};

2. BookMark.tsx 컴포넌트 생성

matchBookMark를 통해 supabase에서 select를 할 때 특정 게시물의 id값이 일치하는 데이터만 조회하여 supabase와의 통신속도를 개선하고자 했습니다.


import * as S from './BookMark.styled';
import { useQuery } from '@tanstack/react-query';
import { matchBookMark, useCreateBookMarkMutation, useDeleteBookMarkMutation } from '../../api/bookmark';
import { BookMarkType } from '../../supabase/database.types';
import { useSelector } from 'react-redux';
import { RootState } from '../../redux/config/configStore';
import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';

const BookMark = () => {
  const { id } = useParams();
  if (!id) return;

  const { data: bookMarkList, isError, error } = useQuery(['matchBookMark'], () => matchBookMark(id));
  const [isBookMark, setIsBookMark] = useState(false);

  const loginUser = useSelector((state: RootState) => state.user.user);

  const bookMarkCreateMutation = useCreateBookMarkMutation();
  const bookMarkDeleteMutation = useDeleteBookMarkMutation();

  useEffect(() => {
    if (bookMarkList?.length === 0) {
      setIsBookMark(false);
    } else {
      setIsBookMark(true);
    }
  }, [bookMarkList]);

  const handleToggleBookMark = async () => {
    const newBookMark: BookMarkType = {
      tutor_id: id || '',
      user_id: loginUser?.id || '',
    };

    if (isBookMark === false) {
      bookMarkCreateMutation.mutate(newBookMark);
    } else {
      bookMarkDeleteMutation.mutate(id);
    }
  };

  if (isError) {
    return console.log('supabase Error', error);
  }

  return (
    <div>
      <button onClick={() => handleToggleBookMark()}> {isBookMark === false ? `북마크` : `북마크 해제`} </button>
    </div>
  );
};

export default BookMark;

3. optimistic update 적용

낙관적업데이트(optimistic update)란?

: 데이터를 서버로 부터 성공적으로 업데이트 하기전에 UI를 업데이트하여 사용자에게 빠른 피드백을 제공하여 좋은 UX를 제공할 수 있습니다.


// 북마크 추가( 리액트쿼리 useMutation 적용 )
export const useCreateBookMarkMutation = () => {
  const queryClient = useQueryClient();

  const mutation = useMutation(createBookMark, {
    onMutate: async (newBookMark) => {
      await queryClient.cancelQueries(['matchBookMark']);

      const previousBookMark = queryClient.getQueryData(['matchBookMark']);
      queryClient.setQueriesData(['matchBookMark'], (old: any) => [...old, newBookMark]);

      return { previousBookMark };
    },

    onError: (error, newBookMark, context) => {
      console.log(error);
      queryClient.setQueriesData(['matchBookMark'], context?.previousBookMark);
    },

    onSettled: () => {
      queryClient.invalidateQueries(['matchBookMark']);
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['matchBookMark'] });
    },
  });

  return mutation;
};

// 북마크 삭제( 리액트쿼리 useMutation 적용 )
export const useDeleteBookMarkMutation = () => {
  const queryClient = useQueryClient();

  const mutation = useMutation(deleteBookMark, {
    onMutate: async (newBookMark) => {
      await queryClient.cancelQueries(['matchBookMark']);

      const previousBookMark = queryClient.getQueryData(['matchBookMark']);
      queryClient.setQueriesData(['matchBookMark'], (old: any) => [...old, newBookMark]);

      return { previousBookMark };
    },

    onError: (error, newBookMark, context) => {
      console.log(error);
      queryClient.setQueriesData(['matchBookMark'], context?.previousBookMark);
    },

    onSettled: () => {
      queryClient.invalidateQueries(['matchBookMark']);
    },
  });

  return mutation;
};

리액트쿼리 공식 문서 참조


4. 회고

간단한 북마크 기능이지만 코드의 가독성과 재사용성, 통신속도 등 고려할게 정말 많다는 걸 경험할 수 있었습니다. 처음 optimistic update를 적용하면서 사용자의 ux에 긍정적인 영향을 줄 수 있게 되어 기쁘고 더 좋은 코드를 작성할 수 있도록 부족한 부분을 공부할 생각입니다.

개선필요사항 :
1. 사용자가 로그인 시 본인이 북마크를 누른 게시물만 북마크 또는 북마크해제가 보여지도록 수정이 필요합니다

profile
김선익

0개의 댓글