리액트 쿼리(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 같은 객체 금지 ❌)