탠스택 쿼리를 사용하며 뮤테이션 이후 쿼리를 좀 더 유려하게 무효화할 수 없을까 고민하던 중, 쿼리 자동 무효화 기능을 도입하게 되었습니다.
도입 과정과 경험을 공유해 보고자 합니다.
쿼리와 뮤테이션
우선 무효화를 이야기하기 전에, 탠스택 쿼리의 쿼리와 뮤테이션에 대해 간단히 설명하겠습니다.
쿼리 (Query)
우리는 웹 애플리케이션을 개발할 때, 서버로부터 데이터(자원) 등을 가져와 화면에 표시해야 하는 경우가 정말 많습니다.
예를 들면, 사용자 목록, 사용자의 상세 정보를 가져와 표시하는 등이 있습니다.
이와 같이 서버에서 데이터를 가져올 때 사용하는 것이 쿼리로, 고유한 키에 연결된 비동기 데이터에 대한 선언적 의존성입니다.
탠스택 쿼리에선 이를 useQuery 훅을 통해 쿼리를 선언하고, QueryClient를 통해 쿼리를 가져오거나 무효화할 수 있습니다.
쿼리는 주로 데이터를 읽는(Read) 작업을 수행합니다.
뮤테이션 (Mutation)
뮤테이션은 쿼리와는 다른 목적을 갖는 비동기 작업입니다.
읽는 것이 주목적이었던 쿼리와는 달리, 뮤테이션은 데이터를 변경하는 작업을 수행합니다.
예를 들면, 사용자 정보를 업데이트하거나 삭제하는 등의 작업이 있습니다.
탠스택 쿼리에선 이를 useMutation 훅을 통해 뮤테이션을 선언하고 실행할 수 있습니다.
뮤테이션은 주로 데이터를 변경하는(Update) 작업을 수행합니다.
두 작업을 표로 정리하면 다음과 같습니다:
구분
쿼리(Query)
뮤테이션(Mutation)
주요 목적
데이터 읽기(Read)
데이터 변경(Update)
사용 훅
useQuery
useMutation
주요 기능
서버로부터 데이터 가져오기, 데이터 캐싱
데이터 생성/수정/삭제
실행 방식
선언적 의존성을 통한 자동 실행
명시적 함수 호출을 통한 실행
관리 도구
QueryClient를 통한 쿼리 관리
useMutation 훅을 통한 직접 실행
사용 예시
사용자 목록 조회, 상세 정보 조회
사용자 정보 업데이트, 삭제
뮤테이션 작업이 성공적으로 완료될 경우, onSuccess 콜백을 통해 성공 시 동작을 정의할 수 있습니다.
onSettled 콜백을 통해 성공/실패 여부와 상관없이 동작을 정의할 수도 있습니다.
쿼리 무효화 (Query Invalidation)
뮤테이션이 성공했다면, 쿼리에 영향을 줄 가능성이 높습니다.
만약 데이터의 변경이 발생했는데 유저의 화면에 반영되지 않는다면, 사용자 경험은 떨어질 것이고 애플리케이션의 신뢰도가 떨어질 수 있습니다.
예를 들어, 게시물의 좋아요를 눌렀을 때, 좋아요 수가 증가했지만 화면에는 반영되지 않는다면 사용자는 혼란스러울 것입니다. 🫨
이런 경우 탠스택 쿼리가 내부적으로 알아서 리페치 처리를 해주면 좋겠지만, 탠스택 쿼리는 개발자가 이때 어떻게 자원를 처리할지 결정할 수 있도록 설계되어 있습니다.
리페치를 싫어하는 개발자도 있을 수 있고, 뮤테이션의 변경된 데이터를 반환받아 캐시를 업데이트하고 싶을 수도 있습니다.
그렇다면 뮤테이션 이후 어떻게 쿼리를 무효화 시켜 리페치를 실행할 수 있을까요?
앞서 언급했듯이, onSuccess 콜백을 통해 뮤테이션 성공 시 동작을 정의할 수 있습니다.
이를 통해 뮤테이션 성공 시 쿼리를 무효화 시킬 수 있습니다. 이때 우린 QueryClient 인스턴스를 통해 쿼리를 무효화 시킬 수 있습니다.
우선 useQueryClient 훅을 통해 컴포넌트에서 가장 가까운 QueryClient 인스턴스를 가져옵니다.
가져온 인스턴스의 메서드인 invalidateQueries를 통해 쿼리를 무효화 시킬 수 있습니다.
무효화를 자동화하기 위해선 MutationCache를 확장하여 뮤테이션 성공 시 자동으로 쿼리를 무효화 시키는 기능을 추가해야 합니다.
MutationCache
뮤테이션을 위한 저장소로 일반적인 뮤테이션과 동일하게 onSuccess, onSettled, onError 콜백을 가집니다.
애플리케이션 내에서 오직 하나만 존재합니다. -> 모든 뮤테이션에 정의한 콜백 함수들이 실행됩니다.
일반적으로 암묵적으로 생성됩니다.
MutationCache를 사용하기 위해선 다음과 같이 QueryClient의 mutationCache 프로퍼티를 통해 접근해야 합니다.
queryClient.ts
import { QueryClient, MutationCache } from "@tanstack/react-query";const queryClient = new QueryClient({ mutationCache: new MutationCache({ onSuccess, onError, onSettled, }),});
이제 본격적으로 MutationCache를 확장하여 뮤테이션 성공 시 자동으로 쿼리를 무효화 시키는 방법을 알아보겠습니다.
1. 모든 쿼리 무효화
모든 쿼리를 무효화 시키는 방법은 다음과 같습니다:
queryClient.ts
const queryClient = new QueryClient({ mutationCache: new MutationCache({ onSuccess: () => queryClient.invalidateQueries(), }),});
단 3줄의 코드 추가로 뮤테이션 성공 시 모든 쿼리를 무효화 시킬 수 있습니다.
별도의 무효화 로직을 각 컴포넌트 내에서 작성할 필요가 없습니다.
또한 useQueryClient훅을 사용해 QueryClient 인스턴스를 가져올 필요도 없습니다.
뮤테이션 성공 시 자동으로 활성 상태인 쿼리들이 갱신되어 데이터의 일관성을 유지할 수 있습니다.
하지만 이 방식은 모든 쿼리를 무효화 시키기 때문에, 불필요한 리페치가 발생할 수 있습니다.
그렇다면 특정 쿼리만 무효화 시키는 방법은 무엇일까요?
2. 특정 쿼리 무효화
모든 쿼리를 무효화하는 것은 짧은 코드로 간단하게 구현할 수 있지만, 유저가 바우처를 발급했을 시 유저 프로필 정보도 무효화되는 것은 굳이라고 생각이 들었습니다.
특정 쿼리만 무효화 시키기 위해선 뮤테이션에 대한 정보를 통해 어떤 쿼리를 무효화 시킬지 결정해야 했습니다.
mutationKey를 통한 쿼리 무효화
mutationKey의 특징은 다음과 같습니다:
뮤테이션을 식별하는 고유한 키
뮤테이션을 선언할 때 useMutation 훅의 mutationKey 옵션을 통해 정의할 수 있습니다.
뮤테이션 성공 시 MutationCache의 onSuccess 콜백에서 mutationKey를 통해 쿼리를 무효화 시킬 수 있습니다.
위의 예시는 바우처를 발급하는 뮤테이션을 선언하고, mutationKey를 통해 뮤테이션을 식별합니다.
뮤테이션 성공 시 mutation.options.mutationKey를 통해 queryKey와 일치하는 쿼리를 무효화 시킵니다.
만약 mutationKey가 없는 뮤테이션의 경우, 모든 쿼리가 무효화됩니다.
모든 쿼리의 무효화가 필요가 없다고 판단한 저는 다른 방법을 찾아보았습니다.
meta 옵션을 통한 쿼리 무효화
meta는 개발자 즉 사용자가 쿼리나 뮤테이션에 추가적인 정보를 저장할 수 있는 옵션입니다.
이 옵션을 통해 뮤테이션에 태그를 추가할 수 있습니다. 추가된 태그들은 무효화의 대상이 되는 쿼리를 결정할 때 사용될 수 있습니다.
이때 predicate 함수를 사용해 특정 조건에 맞는 뮤테이션만 선택적으로 처리할 수 있도록 해야 합니다.
함수 내에선 matchQuery 함수를 통해 쿼리가 조건에 맞는지 확인할 수 있습니다.
기존에는 콜백 함수 내에서 인스턴스 메서드를 직접 실행하는 명령형 방식을 사용했습니다. 하지만 meta 옵션을 도입하여 쿼리 무효화를 선언적으로 정의할 수 있게 개선했고, 이를 통해 코드 중복을 크게 줄일 수 있었습니다.
특히 mutationCache와 meta를 활용하여 시스템 전반에 걸쳐 일관된 방식으로 쿼리 무효화를 구현할 수 있게 되었습니다.
결론
탠스택 쿼리의 자동 무효화 도입을 통해 여러 긍정적인 변화를 이끌어낼 수 있었습니다.
무효화 로직을 선언적으로 정의하고 중앙에서 관리함으로써 코드의 가독성과 유지 보수성이 향상되었으며, 결과적으로 개발 생산성과 코드 품질 또한 높일 수 있게되었습니다.