React-Query

김명주·2023년 4월 20일
0

React-Query

react-query는 서버의 값을 클라이언트에 가져오거나, 캐싱, 값 업데이트, 에러핸들링 등 비동기 과정을 더욱 편하게 하는데 사용된다.

React-Query의 장점

  1. Clinet Side Data와 Server Side Data 분리
  2. 비동기 과정을 선언적으로 관리할 수 있다.
  3. 캐싱
    • 캐싱이란, 저장한다는 의미이다.
    • 오랜시간이 걸리는 작업의 결과를 저장해서 시간과 비용을 필요로 회피하는 기법을 의미
    • 캐시한 데이터는 주로 파일, 메모리, 데이터베이스에 저장한다.
    • 파일은 가장 기본적으로 고려되는 캐시 저장 장소이다. 저렴하지만, 캐쉬 데이터를 여러 시스템에서 공유하기가 어렵다는 점과 메모리 대비 느리다는 점을 들 수 있다. 또한 캐쉬 메커니즘을 직접 구현해야 하는 어려움이 있다.
    • 메모리는 네트워크를 통해서 접근 하는 기능을 지원하기 때문에 단일 캐쉬에 대해서 여러 머신에서 엑세스 할 수 있다는 장점이 있다. 무엇보다도 큰 장점은 파일 보다 훨씬 빠르게 데이터를 처리 할 수 있다는 점이다. 단점은 비싸다.
    • 데이터베이스도 데이터를 캐쉬하기에 좋은 공간이다. 자체적인 보안 시스템을 갖추고 있고, 네트웍을 통해서 접근 할 수 있기 때문에 캐슁 데이터를 공유할 수 있는 장점도 있다. 메모리 보다 느린 것이 단점이다.
  4. React Hooks 과 사용 방법이 비슷하여 다루기 쉽다.
  5. get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행
  6. 데이터가 오래 되었다고 판단되면 다시 get

useQuery

  1. 데이터를 get하기 위해 사용하는 api. post나 update는 useMutation을 사용한다.
  2. 첫번째 파라미터로 unique Key가 들어가고, 두번째 파라미터로 비동기 함수(api호출 함수)가 들어간다.
  3. return 값은 api의 성공, 실패여부, api return 값을 포함한 객체
  4. seQuery는 비동기로 작동합니다. 즉, 한 컴포넌트에 여러개의 useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는 것이 아닌 두개의 useQuery가 동시에 실행
  5. enabled를 사용하면 useQuery를 동기적으로 사용 가능

예시

function Todos() {
  const { status, data, error } = useQuery("todos", fetchTodoList);

  // isLoading, isError로 할 필요 없이 status로도 가능하다
  if (status === "loading") {
    return <span>Loading...</span>;
  }

  if (status === "error") {
    return <span>Error: {error.message}</span>;
  }

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

useQuery 동기적으로 실행하기

  1. enabled를 사용하면 useQuery를 동기적으로 사용 가능하다
  2. useQuery의 3번째 인자로 옵션값이 들어가는데 그 옵션의 enabled에 값을 넣으면 그 값이 true일때 useQuery를 실행
const { data: todoList, error, isFetching } = useQuery("todos", fetchTodoList);
const { data: nextTodo, error, isFetching } = useQuery(
  "nextTodos",
  fetchNextTodoList,
  {
    enabled: !!todoList // true가 되면 fetchNextTodoList를 실행한다
  }
);

그렇다면 여러개의 useQuery를 한번에 할 수는 없을까?

이럴 때 useQueries를 사용하면 여러개의 useQuery를 하나로 묶을 수 있다.
useQueries를 사용하면 promise.all과 마찬가지로 하나의 배열에 각 쿼리에 대한 상태 값이 객체로 들어온다.

const usersQuery = useQuery("users", fetchUsers);
const teamsQuery = useQuery("teams", fetchTeams);
const projectsQuery = useQuery("projects", fetchProjects);

// 어짜피 세 함수 모두 비동기로 실행하는데, 세 변수를 개발자는 다 기억해야하고 세 변수에 대한 로딩, 성공, 실패처리를 모두 해야한다.
 // useQueries를 사용하면 여러개의 쿼리를 한번에 받아올 수 있다.
const result = useQueries([
  {
    queryKey: ["users"],
    queryFn: () => api.fetchUsers()
  },
  {
    queryKey: ["teams"],
    queryFn: () => api.fetchUsers()
  }
]);

useMutation

값을 바꿀때 사용하는 api. return 값은 useQuery와 동일

mutationFn

mutationFn은 mutation Function으로 promise 처리가 이루어지는 함수, 다른 말로는 axios를 이용해 서버에 API를 요청하는 부분

const editMutation = useMutation(editProfileImg)

mutate

mutate는 useMutation을 이용해 작성한 내용들이 실행될 수 있도록 도와주는 trigger 역할
즉, useMutation을 정의 해둔 뒤 이벤트가 발생되었을 때 mutate를 사용해주면 된다.

onSuccess, onError, onSettled

요청이 성공, 실패했을 경우에 실행할 함수를 선언할 수 있다.
onSettled를 통해서도 가능하다.

const savePerson = useMutation((person: Iperson) => axios.post('http://localhost:8080/savePerson', person), {
    onSuccess: () => { // 요청이 성공한 경우
        console.log('onSuccess');
    },
    onError: (error) => { // 요청에 에러가 발생된 경우
        console.log('onError');
    },
    onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
        console.log('onSettled');
    }
});

아래는 프로젝트에서 사용한 프로필 수정 예제다

const queryClient = useQueryClient()
  const imgRef: any = useRef<HTMLInputElement | null>(null)
  const [alertMessage, setAlertMessage] = useState('')
  const [showCustomAlert, setShowCustomAlert] = useState<boolean>(false)
  const [imgUrl, setImgUrl] = useState<string | ArrayBuffer | null>()
  const [imgFile, setImgFile] = useState<File>()
  const [newNickname, setNewNickname] = useState('')
  
const editMutation = useMutation(editProfileImg, {
    onSuccess: () => {
      queryClient.invalidateQueries()
    },
  })
  const changeMutation = useMutation(changeNickname, {
    onSuccess: () => {
      queryClient.invalidateQueries()
    },
  })
  
  const onChangeImageHandler = () => {
    const reader = new FileReader()
    const file = imgRef.current.files[0]
    if (file.size > 5 * 1024 * 1024) {
      setAlertMessage('파일 크기는 최대 5MB 입니다')
      setShowCustomAlert(true)
      setImgUrl(null)
    } else {
      reader.readAsDataURL(file)
      reader.onloadend = () => {
        setImgUrl(reader.result)
        setImgFile(file)
      }
    }
  }
  const onSubmitImageHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (!imgFile) {
      setAlertMessage('먼저 변경할 프로필 이미지를 등록해 주세요.')
      setShowCustomAlert(true)
    } else {
      e.preventDefault()
      const data = new FormData()
      data.append('image', imgFile as File)
      editMutation.mutate(data)
      onSetLocalStorageHandler('img', imgUrl)
      setAlertMessage('변경되었습니다.')
      setShowCustomAlert(true)
    }
  }
  const onChangeNicknameHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault()
    let regExp: RegExp = /^[^\s]{2,8}$/
    if (regExp.test(newNickname) && newNickname !== '') {
      checkNickname(newNickname)
        .then(() => {
          changeMutation.mutate(newNickname)
          setAlertMessage('변경되었습니다.')
          setShowCustomAlert(true)
          setNewNickname('')
          onSetLocalStorageHandler('nickname', newNickname)
        })
        .catch((error) => {
          setAlertMessage(error.response.data.message)
        })
    } else if (regExp.test(newNickname) === false) {
      setAlertMessage('닉네임은 2자리 이상, 8자리 이하여야 합니다.')
      setShowCustomAlert(true)
    }
  }
  if (profLoading) return <p></p>
  return (
    <>
      <CustomAlert 
        showAlert={showCustomAlert}
        onHide={() => setShowCustomAlert(false)}
        message={alertMessage}
        loginState={false}
      />
      <MyPageContentsContainer>
        <ExternalContainer>
          <MyPageEditContainer>
            <label htmlFor="fileinput">
              <EditDiv>
                <BsPlusLg />
              </EditDiv>
            </label>
            <MyPageEditImg
              src={
                !imgUrl
                  ? profData.profileUrl
                    ? profData.profileUrl
                    : baseProifle
                  : imgUrl
              }
              alt="이미지"
            />

            <MyPageImgEditInput
              id="fileinput"
              ref={imgRef}
              type="file"
              accept="image/*"
              onChange={onChangeImageHandler}
            />
            <MyPageImgBtnWrap>
              <MyPageInputBtn onClick={onSubmitImageHandler}>
                수정하기
              </MyPageInputBtn>
            </MyPageImgBtnWrap>
            <MyPageInputContainer>
              <form>
                <MyPageInputLabel>닉네임</MyPageInputLabel>
                <MyPageInput
                  type="text"
                  value={newNickname}
                  placeholder="닉네임을 입력하세요"
                  onChange={(e) => setNewNickname(e.target.value)}
                />
                <MyPageEditBtnTwo
                  type="submit"
                  onClick={onChangeNicknameHandler}
                >
                  닉네임 변경
                </MyPageEditBtnTwo>
              </form>
            </MyPageInputContainer>
          </MyPageEditContainer>
        </ExternalContainer>
      </MyPageContentsContainer>
    </>
  )
}

invalidateQueries

invalidateQueries는 useQuery에서 사용되는 queryKey의 유효성을 제거해주는 목적으로 사용된다.
그리고 queryKey의 유효성을 제거해주는 이유는 서버로부터 다시 데이터를 조회해오기 위함
useQuery에는 staleTime과 cacheTime이 존재한다. 정해진 시간이 지나지 않으면, 새로운 데이터가 추가되어도 useQuery는 변동 없이 동일한 데이터를 보여준다.
사용자 입장에서는 데이터 생성이 제대로 되었는지에 대한 파악이 힘들기 때문에 혼란을 겪을 수 있게 된다.
이걸 해결하기 위해 사용하는 invalidateQueries이다.
데이터를 저장할 때 invalidateQueries를 이용해 useQuery가 가지고 있던 queryKey의 유효성을 제거해주면 캐싱되어있는 데이터를 화면에 보여주지 않고 서버에 새롭게 데이터를 요청

const editMutation = useMutation(editProfileImg, {
    onSuccess: () => {
      queryClient.invalidateQueries()
    },
  })
  const changeMutation = useMutation(changeNickname, {
    onSuccess: () => {
      queryClient.invalidateQueries()
    },
  })
profile
개발자를 향해 달리는 사람

0개의 댓글