본문 바로가기

프론트엔드

react query로 데이터 전송하기- mutation, 쿼리 무효화

const { mutate, isPending, isError, error } = useMutation({
  	// mutationKey: 전송은 응답 데이터를 캐시 처리하지 않기 때문에 필수로 필요한 것은 아님
	mutationFn: createNewEvent,
})
// 여기서 받아오는 mutate함수로 이 함수를 언제 실행할지 지정할 수 있다.

Mutation을 사용하여 Post요청하기

  • useQuery는 데이터를 가져올 때 사용
  • useMutation는 데이터를 전송할 때 사용

useMutation 사용법

useMutation 사용

const { mutate, isPending, isError, error } = useMutation({
  	// mutationKey: 전송은 응답 데이터를 캐시 처리하지 않기 때문에 필수로 필요한 것은 아님
	mutationFn: createNewEvent,
})
// 여기서 받아오는 mutate함수로 이 함수를 언제 실행할지 지정할 수 있다.

 

mutation 함수

export async function createNewEvent(eventData) {
	const response = await fetch(`http://localhost:3000/events`, {
    	methode: 'POST',
      	body: JSON.stringify(eventData),
      	headers: {
        	'Content-Type': 'application/json'
        }
    })
    
    if(!response.ok) {
    	const error = new Error('');
      	error.code = response.status;
      	error.info = await response.json();
      	throw error;
    }
  
  const { event } = await response.json();
  
  return event;
}

 

데이터 전송

function handleSubmit(formData) {
	mutate({ event: formData })
}

 

mutate 성공 시 동작 및 쿼리 무효화

요청이 완료되는걸 기다린 후 성공여부에 따른 동작 처리

const { mutate, isPending, isError, error } = useMutation({
    mutationFn: createNewEvent,
      onSuccess: () => {
        navigate('/events')
    }
})

전송 완료 후 최신 데이터 받아오기

  • 해당 데이터를 불러오는 useQuery에서 이전에 가져온 데이터가 만료됐다고 표시하고 다시 가져오도록 트리거해야함
    👉🏽 리액트 쿼리에서 제공하는 메소드를 이용해 하나 이상의 쿼리를 무효화한다.

쿼리 무효화하기

  • queryClient로 얻은 객체로 쿼리를 무효화한다.
  • 루트파일이 아닌 공통으로 사용하는 파일(여기에서는 api.js파일)로 queryClient를 옮긴 후 export하고, QueryClientProvider에는 import하여 queryClient를 넣어준다.
  • useMutation에서 onSuccess할 경우 queryClient를 무효화한다.
    const { mutate, isPending, isError, error } = useMutation({
      mutationFn: createNewEvent,
        onSuccess: () => {
            queryClient.invalidateQueries({queryKey: ['events']}); 
            // ⭐️쿼리 무효화
            // 배열의 key가 포함된 모든 쿼리를 무효화한다.
          navigate('/events')
      }
    })
    👉🏽 현재 화면에 표시된 컴포넌트와 관련된 쿼리가 실행된 경우 특정 데이터가 오래됐으니 만료로 표시하고 즉시 다시가져와야한다고 리액트쿼리에 알려줌
  • queryClient.invalidateQueries({queryKey: ['events'], exact: true}) exact: true 설정을 넣으면, 쿼리키가 정확히 일치하는 쿼리만 무효화된다.

무효화 후 자동 다시 가져오기 비활성화

예를 들어, 특정 게시글의 상세페이지에서 '삭제'버튼을 클릭하여 해당 게시글을 삭제했을 경우, 쿼리 무효화가 되어있으면 그 게시글을 새로 불러오도록 트리거가 되고 404에러가 발생한다.
이를 막기 위해 쿼리 무효화 후 자동 트리거가 되지 않도록 막을 수 있다.

const { mutate, 
           isPending: isPendingDeletion, 
           isError: isErrorDeletion, 
        error: deleteError 
      } = useMutation({
        mutationFn: deleteEvent,
          onSuccess: () => {
              queryClient.invalidateQueries({
                  queryKey: ['events'],
                  refetchType: 'none', // ⭐️invalidateQueries를 호출할 때 기존 쿼리가 자동으로 트리거되지 않도록 함. 다음번 요청될 때 다시 실행됨
            }); 
            navigate('/events')
        }
    })

mutate를 사용하여 데이터 업데이트 (PUT 요청하기)

수정화면에서 기존 상세 데이터 가져오기


아래 쿼리는 상세보기 화면에서 사용한 쿼리와 queryKey가 완전히 동일하기 때문에 캐싱된 데이터를 사용하게 되어 데이터를 즉각적으로 표시할 수 있다.

const { data, isPending, isError, error } = userQuery({
  	queryKey: ['events', params.id],
	queryFn: ({signal}) => fetchEvent({ signal, id: params.id }),

update 함수

export async function updateEvent({ id, event }) {
	const response = await fetch(`http://localhost:3000/events/${id}`, {
    	methode: 'PUT',
      	body: JSON.stringify({ event }),
      	headers: {
        	'Content-Type': 'application/json'
        }
    });
    
    if(!response.ok) {
    	const error = new Error('');
      	error.code = response.status;
      	error.info = await response.json();
      	throw error;
    }
  
  const { event } = await response.json();
  
  return event;
}

 

mutation

const { mutate, isPending, isError, deleteError } = useMutation({
		mutationFn: updateEvent,
  		// onSuccess: () => {
      		// queryClient.invalidateQueries({
        	  //	queryKey: ['events'],
	        // }); 
    		// navigate(`/details/${id}`)
    	// }
  		// ⭐️ onSuccess는 사용하지 않음! #낙관적 업데이트 참조
	})

 

mutate 실행

function handleSubmit(formData) {
	mutate({ id: params.id, event: formData })
  	navigate('../'); //이전 페이지(상세 페이지)로 이동
}

function handleClose(){
	navigate('../');
}

 

낙관적 업데이트

  • 특정 데이터를 업데이트할 경우, 업데이트 내용이 서버로부터 성공적으로 업데이트 되기 전에 UI에 즉각적으로 반영하는 것으로, 백엔드의 응답을 기다리지 않아 사용자에게 빠른 피드백을 제공할 수 있는 것이 낙관적 업데이트이다.
  • 낙관적 업데이트는 요청이 실패하면 실행된 업데이트를 롤백하는 로직을 포함해야 한다.
  • 이는 react query로 비교적 구현하기 쉬운 패턴이자 접근방식이다.

onMutate 옵션 사용

  • onMutate의 함수는 mutate를 호출하는 즉시 실행된다. (mutate의 프로세스가 완료되어 응답 받기 전에 실행됨)
  • onMutate에서 query client를 통해 리액트 쿼리에 의해 캐시된 데이터를 업데이트한다. setQueryData()
    (* query client를 통해 리액트 쿼리와 상호작용하며 쿼리무효화를 지시하거나 캐시된 데이터 변경을 지시할 수 있다)
  • onMutate는 mutationFn에 연결된 함수의 파라미터를 인자로 전달 받는다.

 

1. setQueryData() - 캐시 데이터 수정

  • 응답을 기다리지 않고 값을 내부적으로 수정하게됨
  • 첫번째 파라미터는 쿼리키, 두번째 파라미터는 새로운 데이터이다.
const { mutate, isPending, isError, deleteError } = useMutation({
		mutationFn: updateEvent,
  		onMutate: (data) => {
          	const newEvent = data.event
        	queryClient.setQueryData(['events', params.id], newEvent); // ⭐️
        }
	})

function handleSubmit(formData) {
	mutate({ id: params.id, event: formData })
  	navigate('../'); //이전 페이지(상세 페이지)로 이동
}

 

2. cancelQueries() - 쿼리 취소

  • 특정 키의 모든 활성 쿼리를 취소한다.
  • 쿼리를 취소하려는 쿼리키를 전달한다.
  • mutation을 취소하는 것이 아니라 useQuery로 트리거된 쿼리만 취소한다.
    👉🏽 해당 쿼리의 응답데이터와 낙관적으로 업데이트된 쿼리 데이터가 충돌하지 않게 됨
const { mutate, isPending, isError, deleteError } = useMutation({
        mutationFn: updateEvent,
          onMutate: async (data) => {
              const newEvent = data.event

            await queryClient.cancelQueries({queryKey: ['events', params.id]}) // ⭐️ 
            queryClient.setQueryData(['events', params.id], newEvent);
        }
    })

 

3. 롤백 처리

  • 업데이트 실패시 낙관적 업데이트 롤백 처리
  • 업데이트 이전 데이터 보관해두기
const { mutate, isPending, isError, deleteError } = useMutation({
		mutationFn: updateEvent,
  		onMutate: async (data) => {
          	const newEvent = data.event
            
            await queryClient.cancelQueries({queryKey: ['events', params.id]}) 
          	const previosEvent = queryClient.getQueryData(['events', params.id]); // ⭐️ 업데이트 이전 데이터 가져오기 
        	queryClient.setQueryData(['events', params.id], newEvent);
          
          	return { previosEvent } // ⭐️ 여기서 return 하는 객체가 context가 됨
        },
  		onError: (error, data, context) => {
            // 에러일 경우 롤백
        	// ⭐️ context에 previosEvent가 포함
          	queryClient.setQueryData(['events', params.id], context.previosEvent);
        }
})

 

4. 데이터 최신화 유지

  • onSettled옵션은 성공 여부와 상관없이 mutation이 완료될 때마다 호출된다.
    👉🏽 mutation의 성공, 실패 여부 상관없이 항상 쿼리 무효화를 하여 최신 데이터를 유지한다.
    👉🏽 낙관적 업데이트에 실패하여 롤백하더라도 이 mutation이 완료될 때마다 백엔드에서 최신 데이터를 가져왔는지 확인할 수 있다.
    const { mutate, isPending, isError, deleteError } = useMutation({
          mutationFn: updateEvent,
            onMutate: async (data) => {
                ...
                return { previosEvent }
          },
            onError: (error, data, context) => {
                queryClient.setQueryData(['events', params.id], context.previosEvent);
          },
            onSettled: () => {
               // ⭐️ 쿼리 무효화로 최신 데이터 유지
              queryClient.invalidateQueries(['events', params.id])
          }
      })

    쿼리 키를 쿼리 함수 입력으로 사용

    아래 쿼리에서는 쿼리키와 쿼리함수에서 동일한 id값을 사용하고 있다.
    const { data, isPending, isError, error } = userQuery({
        queryKey: ['events', {id: params.id}],
      	queryFn: ({signal}) => fetchEvent({ signal, id: params.id }),

게다가 queryFn에서 기본적으로 전달하는 객체에서는 queryKey도 포함하고 있다.

👉🏽 queryKey의 id값을 queryFn에서 재사용하면 됨

 

const { data, isPending, isError, error } = userQuery({
     queryKey: ['events', {id: params.id}],
   queryFn: ({signal, queryKey}) => fetchEvent({ signal, ...queryKey[1] }),