TIL

2024.04.05 TIL #Tanstack_Query #prefetch #dehydration

inz1234 2024. 5. 3. 03:04

📌 Task TODOLIST

- [x] 불필요한 서버 요청 줄이기

- [x] TTV 줄이기

 

 


🚨  TroubleShotting

TTV를 줄이고자 초기에 가장 최신 메세지 10개만 가져오도록 했던 야무진 소망은.. 거꾸로 돌아가고 있었다.

 

불필요한 서버요청과 TTV를 줄이자.

 

Why(이유)?

- 초기에 allMsgs(최신 10개의 메세지)를 서버에서 호출 후, 가장 최상단 CSR 컴포넌트 InitChat.tsx에 props로 받아서  setQueryData()로 초기 메세지를 호출함으로써 -> 호기롭게 TTV를 줄이고자 했으나,

다른 곳에서 useMsgsQuery() (= useSuspenseQuery)를 호출하고 있어서, 아무리 최상단 컴포넌트에서 set을 한들

다른 곳에서 useSuspenseQuery로 DB에 새로운 메세지를 요청하기 때문에 

화면을 focus out 했다가 돌아오면 setQueryData가 무산되고 전체 메세지가 렌더링 되는 이슈가 있었다.

- 당시에는 나름 해결해보고자,

"아 그럼 InitChat.tsx가 최상단 컴포넌트이니까 제일 먼저 useSuspenseQuery를 호출하면 다른 곳에서 호출하는 SuspenseQuery는 InitChat.tsx에서 호출한 데이터를 가져다 쓰겠구나!" 

하고 InitChat.tsx에서 호출했었다.

- 그런데, 그러면 서버에서도 최근 10개를 호출해서 클라이언트에 넘기고, 클라이언트(InitChat.tsx)에서도 useMsgsQuery()를 호출해버리는 꼴로 서버에 두 번 호출하는 게 되는데 이게 무슨 뭐 피하려다가 똥을 밟은...상황이란말인가  

 

How(과정)?

(1) 1차 시행착오 

ok. 그러면 서버에서 넘겨주는 최신 10개 메세지를 캐시데이터로 쓰고, Inichat.tsx를 포함한 messages를 필요로 하는 모든 컴포넌트에서 useMsgsQuery() 대신 -> getQueryData()로 다 바꿔보자.

그런 다음, 메세지가 새로 추가되면(= payload가 추가되면)

1) invalidateQueries를 하고,

2) 그럼 DB에 데이터가 업데이트 될 테니

3) 그걸 다시 getQueryData를 한 뒤, 유저가 이전에 보던대로 렌더링 하기 위해서 가공해서 setQueryData()를 해야겠다!
또 야무진 계획을 짜본 결과,

async (payload) => {
            if (payload) {
             1) await queryClient.invalidateQueries({ queryKey: [MSGS_QUERY_KEY, chatRoomId] });
             2) const includingNew: Message[] | undefined = queryClient.getQueryData([MSGS_QUERY_KEY, chatRoomId]);
              const lastIdx =
                messages?.length && includingNew?.map((i) => i.message_id).indexOf(messages[0].message_id);
              messages &&
                includingNew &&
             3) (await queryClient.setQueryData(
                  [MSGS_QUERY_KEY, chatRoomId],
                  [...includingNew].slice(lastIdx ?? 0, includingNew.length)
                ));

invalidateQueries를 함에도 devTools로 확인한 캐시데이터가 업데이트 되지 않는 현상 발생..

=> 이로써 알게된 점: invalidateQueries는 useQuery/useSuspenseQuery가 없이는 캐시데이터를 무효화하고 새로운 데이터를 받아오지 못 한다..

(2) 2차 시행착오

- 그렇담 invalidateQueries를 안쓰고(useSuspenseQuery를 어떻게든 안쓰겠다는 이상한 고집...) 그냥 fetch하는 함수를 날 것으로 써볼테다!

 async (payload) => {
            if (payload) {
              const includingNew = await fetchMsgs(chatRoomId) --> 날것의 fetch
              console.log("includingNew =>", includingNew)
              const lastIdx =
                messages?.length && includingNew?.map((i) => i.message_id).indexOf(messages[0].message_id);
              messages &&
                includingNew &&
                (await queryClient.setQueryData(
                  [MSGS_QUERY_KEY, chatRoomId],
                  [...includingNew].slice(lastIdx ?? 0, includingNew.length)
                ));
             const newArr =  messages &&
                includingNew &&
                (await queryClient.setQueryData(
                  [MSGS_QUERY_KEY, chatRoomId],
                  [...includingNew].slice(lastIdx ?? 0, includingNew.length)
                ));
                console.log("newArr =>", newArr)
            }
          }
        )

 50을 새로 추가하면 이번에는 devTools의 캐시데이터와 setQueryData한 반환값을 콘솔로 찍어보니 둘 다 잘 업데이트 됨!

하지만, re-렌더링이 되지 않음ㅜㅜ

원인은 위에 useQuery를 사용하던 것들을 -> 모두 getQueryData()로 바꾸었다고 했는데, getQueryData()는 변경된 캐시데이터를 감지 하지 못하여 re-렌더링을 일으키지 못하기 때문이었다..

 

=> 이 시행착오들로 배운 점: invalidateQuries와 setQueryData, getQueryData 모두 useQuery/UseSuspenseQuery 없이 단독으로는 캐시데이터를 아예 업데이트 하지 못하거나, 업데이트 해도 re-렌더링을 일으키지 못하여 무용지물이 된다..  

 

what(결과)

그러다 문득.. 예전에 prefetch로 dehydration 해서 클라이언트 컴포넌트로 넘겼던 것이 생각났다.

- 그렇지만 사실 하기 전에도 의심은 됐다.
아니 이것도 서버에서 prefetch 하지만 어쨌든 클라이언트 컴포넌트에서 useSuspenseQuery로 부르면, 두 번 호출되는 거 아니야?

- 결과는 아니었다. prefetch + dehydration 해서 클라이언트 컴포넌트의 hydration 전에 데이터를 넘긴 뒤, 컴포넌트에서 useSuspense를 해도 prefetch한 결과(최신 10개 데이터)가 DB의 전체 데이터로 갈아끼워지지 않고,

useSuspenseQuery의 반환값이 온전히 최신 10개의 데이터로 잘 반환되었기 때문이다.

하여 나의 최종 코드는..

const ChatPage = async ({ params }: { params: { chatroom_id: string } }) => {
  const chatRoomId = params.chatroom_id;
  const supabase = serverSupabase();
  const {
    data: { user }
  } = await supabase.auth.getUser();

  const prefetchMsgs = async () => {
    const { from, to } = getFromTo(0, ITEM_INTERVAL);
    const { data: allMsgs, error } = await supabase
      .from('messages')
      .select('*')
      .eq('chatting_room_id', chatRoomId)
      .range(from, to)
      .order('created_at', { ascending: false });
    if (error || !allMsgs) {
      console.error(error.message);
    } else {
      return allMsgs;
    }
  };
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({ -----> (1)
    queryKey: [MSGS_QUERY_KEY, chatRoomId],
    queryFn: prefetchMsgs
  });

  return (
    <main>
      <HydrationBoundary state={dehydrate(queryClient)}> ----> (2)
        <Suspense fallback={<ChatLoading />}>  
          <div className="relative flex flex-row">
            <InitChat user={user} chatRoomId={chatRoomId} />
            <div className="flex lg:flex-row w-full max-sm:flex-col justify-center mx-auto">
              <section className="lg:flex lg:max-w-96 max-sm:absolute max-sm:z-50 max-sm:bg-white ">
                <SideBar chatRoomId={chatRoomId} />
              </section>
              <section className="w-full max-w-xl max-h-[calc(100vh-90px)] min-h-[36rem]  relative">
                <div className="absolute top-0 left-0">
                  <SideBarButton />
                </div>
                <div className="h-full border rounded-md flex flex-col relative ">
                  <ChatHeader chatRoomId={chatRoomId} />
                  <Suspense>
                    <ChatList user={user} chatRoomId={chatRoomId} />
                    <ChatInput />
                  </Suspense>
                </div>
              </section>
            </div>
          </div>
        </Suspense>
      </HydrationBoundary>
    </main>
  );
};

 

chatPage > InitChat.tsx
const InitChat = ({ user, chatRoomId }: { user: User | null; chatRoomId: string }) => {
  const allMsgs = useMsgsQuery(chatRoomId);