개요
tanstack query는 유니크한 query key를 통해서 쿼리를 관리합니다. 그런데 primitive type이 아닌 reference type을 사용해도 실제 데이터가 변경되지 않는 이상 재요청을 보내지 않습니다.
useQuery({ queryKey: ['user', [userId] ], ...})const a = [1, 2, 3];
const b = [1, 2, 3];
console.log(a === b); // ?자바스크립트를 공부한 사람이라면 위 코드에서 false가 로그로 찍힌다는 것을 알 수 있습니다.
리액트의 렌더링 때마다 새로운 배열값이 queryKey에 할당될 것입니다.
매번 새로운 값이 들어오는 셈이니 당연히 매번 데이터가 변경, 새로운 키라고 인식하겠죠.
하지만 useQuery를 한 번이라도 사용했다면, 당연히 그렇게 동작하지 않는다는 것을 알 수 있습니다.
이를 통해 tanstack query가 내부적으로 queryKey에 할당된 값의 메모리 주소를 그대로 비교해서 데이터의 변경을 캐치하는 것은 아님을 예상할 수 있겠죠.
무엇인가 이를 위한 특별한 로직이 있는 것입니다.
hashKey 함수
정답은 hashKey 라는 간단한 함수에 있었습니다.
hashKey함수를 포함하여 내부적으로 사용하는 함수의 코드까지 가져와봤습니다.
/**
* Default query & mutation keys hash function.
* Hashes the value into a stable hash.
*/
export function hashKey(queryKey: QueryKey | MutationKey): string {
return JSON.stringify(queryKey, (_, val) =>
isPlainObject(val)
? Object.keys(val)
.sort()
.reduce((result, key) => {
result[key] = val[key];
return result;
}, {} as any)
: val,
);
}
function hasObjectPrototype(o: any): boolean {
return Object.prototype.toString.call(o) === '[object Object]';
}
// Copied from: https://github.com/jonschlinkert/is-plain-object
export function isPlainObject(o: any): o is object {
if (!hasObjectPrototype(o)) {
return false;
}
// If has no constructor
const ctor = o.constructor;
if (ctor === undefined) {
return true;
}
// If has modified prototype
const prot = ctor.prototype;
if (!hasObjectPrototype(prot)) {
return false;
}
// If constructor does not have an Object-specific method
if (!prot.hasOwnProperty('isPrototypeOf')) {
return false;
}
// Handles Objects created by Object.create(<arbitrary prototype>)
if (Object.getPrototypeOf(o) !== Object.prototype) {
return false;
}
// Most likely a plain Object
return true;
}JSON.stringify()의 두 번째 인자 활용
hashKey 함수의 코드를 보면 JSON.stringify 메서드의 두 번째 인자를 활용한 것을 볼 수 있습니다.
mdn에 따르면, JSON.stringify의 문법은 다음과 같습니다.
let json = JSON.stringify(value, [replacer, space]);value인코딩 하려는 값replacerJSON으로 인코딩 하길 원하는 프로퍼티가 담긴 배열. 또는 매핑 함수function(key, value)space서식 변경 목적으로 사용할 공백 문자 수
두 번째 인자에 대한 좀 더 자세한 설명을 하자면 다음과 같습니다.
- 두 번째 인자는 함수나 혹은 JSON화 하려는 객체의 속성들을 배열로 넘겨야 한다.
- JSON으로 인코딩 하길 원하는 프로퍼티가 담긴 배열. 또는 매핑 함수
function(key, value)형태가 될 수 있다. - 재귀적으로 처리하기 때문에 중첩된 프로퍼티도 처리 가능
다시 hashKey함수 코드로 돌아가 보겠습니다.
/**
* Default query & mutation keys hash function.
* Hashes the value into a stable hash.
*/
export function hashKey(queryKey: QueryKey | MutationKey): string {
return JSON.stringify(queryKey, (_, val) =>
isPlainObject(val)
? Object.keys(val)
.sort()
.reduce((result, key) => {
result[key] = val[key];
return result;
}, {} as any)
: val,
);
}두 번째 인자로 콜백함수를 넣어준 것을 알 수 있습니다.
그리고 sort를 통해 plainObject라면 정렬을 하는 코드가 있네요.
이를 통해 우리가 만약 아래와 같이 두 가지 방식으로 queryKey를 지정하여 stringified 되었다고 해도 정렬이 되어 동일하게 취급될 수 있음을 알 수 있습니다.
useQuery({ queryKey: ['user', { some: 1, something: 2} ], ...})useQuery({ queryKey: ['user', { something: 2, some: 1 } ], ...})결론
- queryKey의 비교는 stringified된 string type 단에서 비교된다.
- 내부적으로
hashKey라는 함수를 사용하는데JSON.stringifiy메서드의 두 번째 인자 replacer를 활용하여 정렬을 통해 정확한 키의 비교를 수행한다.