[React+Spring] Tanstack-Query(React-Query) 를 이용한 상태 관리

HJ·2024년 2월 7일
0

React+Spring

목록 보기
11/11
post-thumbnail

아래 내용은 코드로 배우는 React with 스프링부트 API서버 강의와 함께 설명이 부족한 부분에 대해서 조사하여 추가한 내용입니다.


React Query


React Query 는 React 애플리케이션에서 데이터를 관리하고 처리하기 위한 라이브러리입니다. 주로 웹 애플리케이션에서 서버로부터 데이터를 가져오거나 업데이트하는 작업을 단순하고 효과적으로 처리할 수 있도록 도와줍니다.

1. Query

Query 는 데이터를 가져오는 작업을 나타내는 객체입니다. React Query 는 useQuery 훅을 통해 데이터를 가져오는 데 사용됩니다. 쿼리 객체에는 데이터를 가져오는 함수, 캐시 키 등이 포함됩니다.

2. Mutation

Mutation 은 데이터를 업데이트하거나 변경하는 작업을 나타내는 객체입니다. useMutation 훅을 사용하여 데이터를 업데이트하는 데 사용됩니다.

3. Infinite Queries

Infinite Queries 는 무한 스크롤 또는 페이징과 같은 상황에서 데이터를 가져오는 작업을 처리하기 위한 기능이 있습니다.




useQuery


useQuery 는 React Query 라이브러리에서 제공하는 훅 중 하나로, 비동기적으로 데이터를 가져오는 데( GET ) 사용됩니다. React Query는 데이터 가져오기, 캐싱, 상태 관리 및 갱신을 용이하게 하기 위해 사용합니다.

const {data, isFetching} = useQuery({
  queryKey : ['queryName', parameter],
  queryFn : () => {...},
  staleTime : 1000
});

1. queryKey

쿼리의 고유한 식별자로 사용되는 키입니다. 배열 형태로 제공되며, 첫 번째 요소는 쿼리 이름이나 키, 나머지 요소는 해당 쿼리에 필요한 매개변수 등을 나타냅니다. 이 키를 기반으로 데이터가 캐싱되고 업데이트됩니다.

2. queryFn

비동기 함수로, 실제로 데이터를 가져오는 함수를 제공합니다. 이 함수는 쿼리 키에 따라 호출되어 데이터를 반환합니다. React Query는 이 함수의 실행 결과를 자동으로 캐싱하고 관리합니다.

3. options

옵션 객체로, 쿼리에 대한 추가 설정을 제공할 수 있습니다. 예를 들어, staleTime 은 데이터가 만료되기 전까지 사용할 수 있는 시간을 설정합니다.



[ queryKey ]

useQuery 의 queryKey 는 React Query 에서 각 쿼리를 고유하게 식별하는 데 사용되는 키입니다. 이 키를 기반으로 캐싱되는 데이터가 구분되고, 데이터를 다시 가져올 때도 이 키를 사용하여 어떤 쿼리를 실행해야 하는지 식별합니다.

queryKey 는 배열 형태로 제공되며 첫 번째 요소는 쿼리의 식별자( 이름 또는 키 )이고, 나머지 요소는 해당 쿼리에 필요한 매개변수 등을 나타냅니다.

const { data, error, isFetching } = useQuery({
  queryKey : ['exampleQuery', useId]
});

'exampleQuery' 는 해당 쿼리의 이름 또는 식별자입니다. React Query 는 이를 기반으로 캐싱된 데이터를 관리합니다.

userId 는 이 쿼리에 필요한 추가 매개변수입니다. 쿼리를 실행할 때 이 매개변수를 사용하여 데이터를 가져옵니다.

이렇게 함으로써, 서로 다른 쿼리에 대해 다른 이름과 매개변수를 가진 queryKey 를 사용하여 각각의 쿼리를 식별하고 구분할 수 있습니다. 쉽게 말해서 파라미터로 지정된 값이 변경되면 React Query는 이를 새로운 쿼리로 처리하고 데이터를 다시 가져옵니다.



[ 상세 페이지에 적용 ]

1. 기존 방식

function ReadComponent({ itemId }) {

    const [item, setItem] = useState(initState);
    const [fetching, setFetching] = useState(false);
    ...
    useEffect(() => {
        setFetching(true);
        getItem(itemId).then(data => {
            setItem(data);
            setFetching(false);
            console.log("data = ", data);
        })
    }, [itemId]);
    ...
     return (
        {fetching ? <FetchingModal /> : <></>}
     );
}

기존에 ReadComponent 에서 useState 를 통해 데이터를 선언하고, useEffect 를 통해 비동기로 처리한 데이터를 변경해주었습니다.

또한 데이터를 가져오는 중인지를 확인하기 위해 fetching 을 선언하고 이를 상태로 관리하여 true 이면 FetchingModal 을 띄우도록 하였습니다.

React Query 의 useQuery 를 사용하여 위의 과정을 단순하게 처리할 수 있습니다.


2. 신규 방식

function ReadComponent({ itemId }) {
  // isFetching 이 fetching 대체
  // data 가 useState 대체
  // useEffect 에서 데이터를 가져오는 것을 queryFn 에 선언한 함수가 대체
    const {data, isFetching} = useQuery({
        queryKey : ['items', itemId],
        queryFn : () => getItem(itemId),
        staleTime : 1000 * 10
    });
    ...
    const item = data || initState;
    ...
    return (
      {isFetching ? <FetchingModal /> : <></>}
    );
}

data쿼리 결과로 얻은( API 호출 후 얻은 ) 데이터를 나타내는 변수입니다.

isFetching 은 현재 데이터를 가져오고 있는지 여부를 나타내는 변수입니다.

staleTime 는 데이터가 만료되기 전까지 사용할 수 있는 시간( ms )을 나타냅니다. 데이터가 만료되면 React Query 는 새로운 데이터를 가져와 갱신합니다.

데이터를 가져왔을 때 Fresh 상태이고, 지정한 시간이 지나면 Stale 이 됩니다. 데이터가 Fresh 상태일 때는 API 를 다시 호출하지 않지만, Stale 상태일 때는 다시 호출하게 됩니다.


참고> 다른 Fetching 처리

function ReadComponent({ itemId }) {
  const {data, isFetching} = useQuery({
        ...
  });

  if(isFetching) {
    return <FetchingModal />
  }
  ...
  const item = data;
  ...
  return (...);
}

return 내부에서 삼항 연산자를 사용해서 FetchingModal 을 띄우지 않고, useQueryisFetching 를 조건문으로 사용해서 FetchingModal 을 바로 반환할 수 있습니다.

이러한 방식을 사용하면 item 이라는 변수에 || 연산자를 사용하지 않고 바로 data 를 할당할 수 있습니다.




동일 페이지 호출


페이지에서 동일한 페이지를 눌렀을 때 API 를 다시 호출하지 않습니다. 쇼핑몰 같이 데이터가 실시간으로 변하지 않는 애플리케이션은 상관없지만 SNS 와 같이 실시간으로 데이터가 변경되는 애플리케이션에서는 다시 호출을 해야 합니다.

이전에는 useMove 훅에 refresh 라는 상태를 만들어서 동일한 페이지라도 호출하도록 하였는데 React Query 에서 하는 방법을 살펴보겠습니다.

[ Query Invalidate ]

Query Invalidate 는 쿼리로 보관하고 있는 데이터를 지우는, 무효화 시키는 것을 의미합니다. 주로 데이터 업데이트, 생성, 삭제 등의 이벤트 후에 새로운 데이터를 다시 불러와 UI를 업데이트하는 데 사용됩니다.

function ListComponent() {
    ...
    const queryClient = useQueryClient();
    const handleClickPage = (pageParam) => {
        if(pageParam.page === parseInt(page)) {
            queryClient.invalidateQueries("items/list");
        }
        moveToList(pageParam);
    }
    ...
    return (
        ...
        <PageComponent serverData={serverData} movePage={handleClickPage}></PageComponent>
    );

}

useQueryClient 훅을 호출하여 쿼리 클라이언트 객체를 가져옵니다. 이 객체를 통해 현재 애플리케이션의 쿼리 상태를 관리하고, 쿼리에 대한 여러 작업을 수행할 수 있습니다.

invalidateQueries특정 쿼리 또는 쿼리 그룹을 무효화하여 해당 데이터를 다시 불러오도록 유도합니다. 메서드를 호출할 때 식별자를 넘겨주면, 해당 식별자를 가진 쿼리를 무효화합니다. 무효화된 쿼리는 다음에 해당 쿼리에 대한 요청이 있을 때, 다시 서버에서 데이터를 가져와 갱신됩니다.



[ 상태와 StaleTime 이용 ]

function ListComponent() {
    const {data, isFetching, isError} = useQuery({
        queryKey : ['items/list', {page, size, refresh}],
        queryFn : () => getList({page, size}),
        staleTime : 1000 * 5
    });
}

이전에는 useMove 에 선언한 refresh 상태를 가져와서 useEffect 의 의존성 배열에 추가하여, 해당 값이 true, false 가 변경될 때마다 호출할 수 있도록 하였습니다.

위의 코드에서 refresh 를 queryKey 에 넘겨줌으로써 동일 페이지를 클릭해도 다시 호출하도록 설정하면서 staleTime 을 5초로 지정했기 때문에 처음 한 번은 다시 호출되지만 5초가 지나기 전까지는 다시 호출되지 않도록 합니다.

staleTime 으로 사용했을 때, 상품 등록을 한 후에 페이지가 이동되어도 아직 staleTime 이 남았다면 다시 불러오지 않기 때문에 추가된 데이터가 보이지 않는 경우도 발생합니다.
이런 경우에는 위의 invalidateQueries 를 사용하는 것이 더 좋습니다.




useMutation


useMutation 은 React Query 에서 제공하는 훅 중 하나로, 데이터를 변경하는 작업( 생성, 수정, 삭제 )을 처리하고 해당 작업의 결과를 업데이트할 때 사용됩니다. 주로 폼 제출, 사용자 입력에 대한 서버 측 변경 등에서 활용됩니다.

const mutation = useMutation({
  mutaionFn : () =>{...} // 실제 데이터 업데이트를 수행하는 비동기 함수
  }
);

// mutation 함수를 호출하여 데이터 변경 작업 실행
mutation.mutate(formData);

// 성공 시 true
if(mutation.isSuccess) {
  ...
}

// 처리 중일 때 true
if(mutation.isPending) {
  ...
}

1. mutaionFn

mutaionFn 은 데이터 변경 작업을 수행하고 Promise 를 반환해야 합니다.

2. Mutate 함수

mutate 함수는 실제로 데이터 변경 작업을 시작하는 함수입니다. 인자로 데이터를 전달하면, Mutation 함수가 실행됩니다.

3. isSuccess, isPending

생성된 mutation 객체에서 여러 속성을 사용할 수 있는데 isSuccuess 는 성공했는지를 나타내고, isPending 은 처리 중인지 상태를 나타냅니다.



[ 등록 페이지에 적용 ]

1. 기존 방식

function AddComponent() {
  const [fetching, setFetching] = useState(false);
  const [result, setResult] = useState(null);
  ...
  const handleClickSave = () => {
    if (window.confirm("저장하시겠습니까?")) {
      ...
      setFetching(true);
      ...
      addItem(formData).then(data => {
        setFetching(false);
        setResult(data.RESULT);
      });
    }
  }

  const closeModal = () => {
    setResult(null);
    moveToList();
  }

  return (
    {fetching ? <FetchingModal /> : <></>}
    {result ? <ResultModal
                title={'Success'}
                content={`${result}번 상품 등록 완료!`}
                callbackFn={closeModal} />
                : <></>}
    ...
  );
}

저장 버튼 클릭 시, formData 생성 후 직접 저장 API 를 호출합니다.

이때 처리 중인지를 표현하기 위해 fetching 상태를 만들고, 호출 전에 상태 변경, 호출 후에 상태가 변경되도록 하였습니다.

result 라는 상태를 만들어 성공했을 때 set 메서드를 이용하여 호출 후에 Modal 에 메세지를 띄우기 위해 결과값으로 변경하였고, 모달 창 닫을 때 다시 상태 변경을 하도록 하였습니다.


2. 신규 방식

function AddComponent() {
  const addMutation = useMutation({
    mutationFn: (item) => addItem(item),
  });
  const queryClient = useQueryClient();;
  ...
  const handleClickSave = () => {
    if (window.confirm("저장하시겠습니까?")) {
      addMutation.mutate(formData);
    }
  }

  const closeModal = () => {
    queryClient.invalidateQueries("items/list");
    moveToList();
  }

  return (
    {addMutation.isPending ? <FetchingModal /> : <></>}   {/* 저장이 처리 중이라면 처리 중이라는 모달을 띄운다 */}
    {addMutation.isSuccess ? <ResultModal
        title={'Success'}
        content={`${addMutation.data.RESULT}번 상품 등록 완료!`}
        callbackFn={closeModal} />
      : <></>}
    ...
  );
}

데이터를 신규 등록하기 위해 useMutation 을 사용해서 객체를 생성하는데 이때 API 호출하는 함수를 mutationFn 에 지정합니다. 저장 버튼을 누를 때 직접 함수를 호출하지 않고, mutate() 함수를 통해 호출하도록 합니다.

처리 중인지를 표현하기 위한 상태 대신, mutation 객체의 isPending 을 사용할 수 있고, 성공했을 때 결과 데이터를 넣는 result 에 isSuccess 를 사용할 수 있습니다.
이때 mutation객체.data 를 통해 전달 받은 데이터에 접근할 수 있습니다.

모달을 닫을 때 staleTime 이 지나지 않았어도 list 목록을 다시 호출하기 위해 invalidateQueries 를 사용해 저장된 값을 제거하고 다시 호출하도록 합니다.



[ 삭제 적용하기 ]

1. 기존 방식

function ReadComponent({ itemId }) {
  const [result, setResult] = useState(null);

  const handleClickDelete = () => {
    if(window.confirm("삭제하시겠습니까?")) {
      setResult(item.itemId);
      deleteItem(item.itemId);
    }
  }

  const closeModal = () => {
    setResult(null);    // null 로 변경하면 결과 Modal 이 사라진다
    moveToList();
  }

  return (
    {isFetching ? <FetchingModal /> : <></>}
    {result ? <ResultModal 
        title={'Success'} 
        content={`${result}번 상품이 삭제되었습니다`} 
        callbackFn={closeModal} />
      : <></>}
  );
}

ReadComponent 이기 때문에 이전에 적용한 useQuery 가 남아있는 형태입니다.

삭제 시에는 직접 API 를 호출하고, result 를 통해, 삭제 후 데이터를 가져와 ResultModal 에 띄우게 됩니다.

ResultModal 의 callbackFn 에 closeModal 을 두고 함수 내부에서 result 의 상태를 변경하고 list 로 이동하게 됩니다.


2. 신규 방식

function ReadComponent({ itemId }) {
  const delMutation = useMutation({
    mutationFn : (itemId) => deleteItem(itemId)
  });
  const queryClient = useQueryClient();

  const handleClickDelete = () => {
    if(window.confirm("삭제하시겠습니까?")) {
      delMutation.mutate(itemId);
    }
  }

  const closeModal = () => {
    if(delMutation.isSuccess) {
      queryClient.invalidateQueries(['items', itemId]);
      queryClient.invalidateQueries('items/list');
    }
    moveToList();
  }

  return(
    {isFetching || delMutation.isPending ? <FetchingModal /> : <></>}
    {delMutation.isSuccess ? <ResultModal 
        title={'Delete'}
        content={'삭제되었습니다'}
        callbackFn={closeModal}/>
    : <></>}
  );
}

삭제 시에는 useMutation 을 사용합니다. 이때 API 를 호출 시에 직접 호출하지 않고 mutate() 를 사용하여 호출하게 됩니다. 이때 처리 중인지 보여주기 위해 isPending 을 사용합니다.

ResultModal 을 띄우는 것도 result 상태에서 isSuccess 를 사용하게 되고, 삭제 후에 list 로 이동하는데 새로운 결과를 가져오기 위해 invalidateQueries 를 사용하여 쿼리의 데이터를 제거합니다.




수정 페이지에 적용


1. 기존 방식

function ModifyComponent({ itemId }) {
  const [result, setResult] = useState(null);
  const [fetching, setFetching] = useState(false);

  useEffect(() => {
    setFetching(true);
    getItem(itemId).then(data => {
        setItem(data);
        setFetching(false);
    })
  }, [itemId]);

  const handleClickSave = () => {
    ...
    setFetching(true);
    modifyItem(itemId, formData).then(() => {
        setFetching(false);
        setResult(itemId);
        setItem({...initState})
    })
  }

  const closeModal = () => {
    setResult(null);
    moveToRead(itemId);
  }

  return (
    {fetching ? <FetchingModal /> : <></>}
    {result ? <ResultModal
        title={'Success'}
        content={`${result}번 상품 수정 완료!`}
        callbackFn={closeModal} />
      : <></>}
  );
}

useEffect 를 통해 itemId 가 변경되면 데이터를 가져오는 API 를 호출하여 해당 상품 번호에 맞는 데이터를 불러오도록 합니다.

데이터 수정 전 fetching 상태를 변경하고, 버튼을 누르면 수정 API 를 호출하고 fetching 과 result 의 상태를 변경하도록 합니다.

ResultModal 에서 closeModal 이 호출되면 다시 result 상태를 변경하고 상세 페이지로 이동하도록 합니다.


2. 신규 방식

function ModifyComponent({ itemId }) {
  const queryClient = useQueryClient();

  // 수정 화면에서 데이터를 불러오는 역할
  const query = useQuery({
    queryKey : ['items', itemId],
    queryFn : () => getItem(itemId),
    staleTime : Infinity
  })

  // 수정 API 를 호출하는 역할
  const modifyMutation = useMutation({
    mutationFn : (item) => modifyItem(itemId, item)
  })

  // 호출 결과를 item 에 세팅
  useEffect(() => {
    if(query.isSuccess) {
        setItem(query.data);
    }
  }, [itemId, query.data, query.isSuccess])

  // 수정 후 저장 호출
  const handleClickSave = () => {
    modifyMutation.mutate(formData);
  }

  const closeModal = () => {
    if(modifyMutation.isSuccess) {
      queryClient.invalidateQueries(['items', itemId]);
      queryClient.invalidateQueries('items/list');
    }
    moveToRead(itemId);
  }

  return (
    {query.isFetching || modifyMutation.isPending ? <FetchingModal /> : <></>}
    {modifyMutation.isSuccess ? <ResultModal 
        title={'Modify'}
        content={'수정되었습니다'}
        callbackFn={closeModal}/>
      : <></>
      }
  );
}

수정 페이지로 들어갔을 때 데이터가 필요하기 때문에 useQuery 를 통해 데이터를 가져오도록 합니다.

이때 staleTime : Infinity 를 사용하여 데이터를 다시 가져오지 않고 계속 Fresh 한 상태를 유지하도록하고 useEffect 에서 item 의 상태를 변경하도록 합니다.

수정에서는 useMutation 이 필요하기 때문에 수정을 호출하는 함수를 가진 mutation 객체를 생성하고, 수정 버튼을 클릭하면 mutate() 를 호출하도록 합니다.

처리 상태와 처리 중인지를 나타내는 result 와 fetching 이 사라지고, isFetchingisSuccess 가 사용됩니다.

modal 이 뜬 이후에 닫기를 눌러 closeModal 이 호출되면 쿼리에 저장되어 있던 데이터를 invalidateQueries 를 이용해 초기화하고, 다시 가져올 수 있도록 합니다.

0개의 댓글