
리액트 쿼리(Tanstack Query)는 서버 데이터 관리를 쉽게 만들어주는 라이브러리이다.
리액트 쿼리는 유니크 키가 변경되면 서버로 재요청을 보내준다.
//groupId가 변경되면 서버로 재요청을 보낸다.
useQuery(['notification', groupId], () => getNotificationInfo(groupId))
그런데 unique key는 primitive type이 아닌 reference type을 사용해도
실제 데이터가 변경되지 않는 이상 재요청을 보내지 않는다.
아래와 같이 말이다.
//Array에 담아도 groupId가 변경되지 않는 이상 재요청 보내지 않음
useQuery(['notification', [groupId]], () => getNotificationInfo(groupId))
그런데 어떻게 이 unique key가 변경되는 것을 알 수 있을까?
조금만 생각해보면 일단 메모리 주소를 보고 데이터의 변경을 아는 것은 아닐 것이다.
왜냐하면 리액트는 랜더링 될 때마다, 새로운 메모리 주소에 할당되기 때문이다.
그렇다면 내부적으로 데이터를 하나하나 비교하고 있다는 것이다.
확인해보니 내부적으로 객체를 문자열로 변환하는 JSON.stringify()를 사용하고 있었다.
그런데 의문이 들었다.
JSON.stringify()는 객체를 문자열로 변경하는데
{a: "a", b: "b"}를 변경하는 것과 {b: "b", a: "a"} 를 변경하는 것은
데이터는 같지만 순서가 다르기 때문에 다른 문자열이 나올 것이다.

그렇다면 어떻게 똑같이 만드는 걸까?
👉 코드 보러가기
아래 코드는 React Query 내부에 선언되어 있는 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,
)
}
솔직히 말해서 JSON.stringify에 두번째 인자가 있다는 것 조차 몰랐다.
그리고 보통 자바스크립트에서 콜백함수를 전달할 때에는 (값, 인덱스) => {}형태로 전달하는데
이 두 번째 인자는 조금 달라보였다.
👉MDN 보러가기
MDN에JSON.stringify()는 아래와 같이 정의되어 있다.
JSON.stringify() 메서드는 JavaScript 값이나 객체를 JSON 문자열로 변환합니다.
선택적으로, replacer를 함수로 전달할 경우 변환 전 값을 변형할 수 있고,
배열로 전달할 경우 지정한 속성만 결과에 포함합니다.
JSON.stringify()에 파라미터로 객체나 문자를 넣으면 JSON 문자열로 반환한다.
JSON.stringify(value[, replacer[, space]])
추가적으로 두 개의 파라미터가 더 존재하는데 각각을 replacer와 space라고 한다.
replacer는 문자열화 동작 방식을 변경하는 함수이다.
이 값이 null이거나 제공되지 않으면 객체의 모든 속성들이 JSON 문자열 결과에 포함된다.
배열이 replace로 들어갈 경우 배열에 포함되는 key값만이 반환되고,
함수가 들어갈 경우 (key, value) => {} 의 형태로 사용할 수 있다.
export function hashKey(queryKey) {
return JSON.stringify(queryKey, (key, value) => // <<key, value의 형태
isPlainObject(val)
? Object.keys(val)
.sort()
.reduce((result, key) => {
result[key] = val[key]
return result
}, {} as any)
: val,
)
}
replacer의 반환값은 value가 되어야 한다.
만약에 어떤 특정 key값을 반환하고 싶지 않다면 undefined를 반환하면 된다.
({a: undefined} 객체를 변환하면 "{}"이 반환된다.)
즉, React Query의 객체 순서가 바뀌어도 같은 값이라고 판단할 수 있는 이유는
JSON.stringify의replacer를 이용해 Object의 key를 정렬한 후 반환해서
가능한 것이었다.
글을 작성하다 보니 갑자기 궁금해졌다.
JSON.stringify는 내부적으로 어떻게 동작해서 string으로 직렬화를 할 수 있는 걸까?
그래서 직접 구현해봤다.
function stringify(obj) {
if (obj === null) {
return "null";
}
if (typeof obj === "number" || typeof obj === "boolean") {
return obj.toString();
}
if (typeof obj === "string") {
return '"' + obj + '"';
}
if (Array.isArray(obj)) {
// 배열의 경우
let result = "[";
for (let i = 0; i < obj.length; i++) {
if (i > 0) {
result += ",";
}
result += stringify(obj[i]);
}
result += "]";
return result;
}
if (typeof obj === "object") {
// 객체의 경우
let result = "{";
let keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = stringify(obj[key]);
if (value !== undefined && typeof value !== "function") {
if (i > 0) {
result += ",";
}
result += '"' + key + '":' + value;
}
}
result += "}";
return result;
}
// 그 외의 경우 (함수 등)
return undefined;
}
실제 구현사항과 다를 수는 있지만 javascript로 구현하려면 재귀함수 외에는 없는 거 같다(아닐 수도..)
구현을 하고 나니 갑자기 아차하는 생각이 든다.
객체의 데이터를 전부 확인한다고?
기존에 react query의 쿼리 키로 그냥 사용하던 객체를 던져줬다.
심지어 Date객체를 사용한 부분도 날짜만 전달하는 것이 아닌 new Date('2023-11-11')과 같이 Date객체를 통째로 넣어줬다.
그런데 Date에는 엄청나게 많은 메서드들이 있지 않은가?
메서드가 함수이기에 반환되진 hashKey에 저장되진 않아도 순회는 한다는 것 아닌가?
확인해보았다.
console.time("stringify");
JSON.stringify(new Date("2023-10-10"));
console.timeEnd("stringify");
console.time("stringify2");
JSON.stringify("2023-10-10");
console.timeEnd("stringify2");

결과는 사진과 같았다.예상과 동일하게 속도 차이가 엄청났다.
평균적으로 300~500배가 차이난 것을 확인할 수 있었다.
사실 컴퓨터가 처리하는 속도가 빠르기 때문에 체감을 느낄 수는 없지만
간단한 수정만으로 프로젝트를 더 좋게 만들 수 있는데 변경하지 않을 이유가 없다.
쿼리 키에는 꼭 변경되는 데이터만 전달하자..! 😅 (Date 같은 객체 금지 ❌)