react-query의 infinity 속성 채택 및 데이터 동기화 전략

pengooseDev·2023년 4월 2일
2

기존의 방식

user가 참여한 project 리스트는 페이지가 렌더링 될 때마다 axios를 통해 받아와 state로 관리되는 방식이었다.


바꾸자!

현재 로직을 걷어내고 react-query를 이용하여 user가 참여한 project 데이터를 caching하기로 했다. 또한, cacheTime과 staleTime 전부 Infinity를 적용하고자 했다.

근거는 다음과 같았다.

  1. 유저가 참여한 프로젝트는 자주 변하는 값이 아니다.
  2. 유저가 참여한 프로젝트는 타인에 의해 변경되는 경우는 오직 "kick"당했을 경우이다.
  3. 서버비는 최소화하자..

useQuery 설정

import styled from 'styled-components';
import { SIDE_NAV, TOP_NAV } from 'constants/layout';
import { NavLink, AddProject, Config } from 'components';
import { ProjectsLink } from 'types';
import { getUserProjects } from 'api';
import { useQuery } from 'react-query';

export function SideNav() {
  const { data: fetchData } = useQuery('userProjects', getUserProjects, {
    staleTime: Infinity,
    cacheTime: Infinity,
  });

  return (
    <Wrapper>
      <TopWrapper>
        {fetchData?.data?.map((project: ProjectsLink) => {
          const { id } = project;

          return <NavLink key={id} data={project} />;
        })}
      </TopWrapper>
      <BottomWrapper>
        <AddProject />
        <Config />
      </BottomWrapper>
    </Wrapper>
  );
}

뭐 사실 설명할 부분이 없다.
기술이 있고 근거와 유인이 있었을 뿐이다.
그저 신경써야할 부분이라면 한 가지.

user가 프로젝트를 생성하거나 삭제한다면 invalidateQueries로 캐시값만 날려주면 된다.


user에 의해 데이터가 변하는 경우

보통 useMutation을 사용할 때, 데이터가 변한다.
해당 요청에 성공할 경우 invalidateQueries로 현재 캐싱된 값을 삭제하면 된다. (react-query가 똑똑해서 자동으로 새 데이터를 받아온다)


export function CreateProject({ closeModal }: CreateProjectProps) {
  const [thumbnail, setThumbnail] = useState<File | null>(null);
  const queryClient = useQueryClient();
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<TitleForm>({
    mode: 'onChange',
  });

  const { mutate } = useMutation(createProject, {
    onSuccess: () => {
      queryClient.invalidateQueries('userProjects'); // 캐시값 삭제!
      closeModal();
      sendToast.success('create the project!');
    },
    onError: () => {
      sendToast.success('failed to create project!');
    },
  });

  const handleThumbnailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { files } = e.target;
    if (files && files[0]) setThumbnail(files[0]);
  };

  const onValid = async (data: TitleForm) => {
    const formData = new FormData();
    const dataField = {
      projectTitle: data.projectTitle,
    };
    const jsonDataField = JSON.stringify(dataField);
    const blobDataField = new Blob([jsonDataField], {
      type: 'application/json',
    });

    formData.append('data', blobDataField);
    if (thumbnail) formData.append('thumbnail', thumbnail);

    mutate(formData);
  };

  return (
    <ModalContainer>
      <Title>Create Project</Title>
      <CreateForm onSubmit={handleSubmit(onValid)}>
        <FileInput
          hidden
          id="imgInput"
          type="file"
          accept="image/png, image/gif, image/jpeg, image/webp"
          onChange={handleThumbnailChange}
        />
        <BottomWrapper>
          <TextWrapper>
            <Content>Project name</Content>
            {errors.projectTitle && (
              <Errorspan>{errors.projectTitle.message}</Errorspan>
            )}
          </TextWrapper>
          <InputContainer>
            <Input
              type="text"
              placeholder="Enter your ProjectName"
              {...register('projectTitle', {
                required: 'Please enter your projectTitle!',
                maxLength: {
                  value: 20,
                  message: 'Requires shorter than 20',
                },
              })}
            />
            <Button>Create</Button>
          </InputContainer>
        </BottomWrapper>
        <Hr />
        {thumbnail ? (
          <ThumbnailLabel htmlFor="imgInput">
            <ThumbnailPreview src={URL.createObjectURL(thumbnail)} />
          </ThumbnailLabel>
        ) : (
          <ThumbnailLabel htmlFor="imgInput">
            <Add size={50} />
          </ThumbnailLabel>
        )}
      </CreateForm>
      <Text>If you have invite code?</Text>
      <InviteCodeButton>Enter invite code</InviteCodeButton>
    </ModalContainer>
  );
}

엥? 그럼 kick에 대한 결과는 어떻게 관리하시려구요?

물론, 강퇴를 당하더라도(프로젝트에 참여하고 있지 않더라도) 캐싱된 값에 의해 해당 프로젝트가 렌더링된다.

하지만, project의 data를 가져오는 API에 해결책이 존재한다.
참여하지 않은 프로젝트에 접근할 경우 BE에서 403 Error를 던져준다.
해당 APi를 담당하는 axios instance의 interceptor에서 403 Error가 발생했을 때, 캐싱 되어있는 캐시값을 invalidateQueries로 날려주고 toast를 띄우도록 분기처리만 하면 된다.

BE의 Cost 최소화클라이언트에게 빠른 렌더링까지 제공할 수 있는 재밌는 고민이었다.

0개의 댓글