ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Socket-io, Tanstack-Query 사용해 커스텀 훅 만들기
    WIL 2024. 3. 4. 00:00

     

    기술 도입의 이유

    진행 중이었던 사이드 프로젝트의 중요 기능은 사용자가 직접 장르의 카테고리와 아이디어를 작성해 서버에 보내면

    서버에서는 AI 프롬포트 서버를 통해 gpt가 만든 이야기 내용을 받아와 실시간으로 클라이언트에게 보내줘야 했습니다.

    목적은 실시간으로 스토리를 제공하여 사용자가 스토리를 기다리는 경험을 최소화하는 것이 목적이었습니다.

     

    그러기 위해서는 Socket-IO를 사용해 실시간 통신을 이용하기로 하였고,

    여기서 Tanstack-query를 이용해 서버의 상태 처리하는 건 어떨까라는 생각이 들었습니다.

     

    Tanstack-query를 이용해 편리한 에러, 로딩 처리 또는 Tanstack-query에서 제공하는 여러 옵션들을 사용할 수 있다면

    유저에게 더 좋은 로딩, 에러 처리나 작업자에게도 재사용성이 높은 커스텀 훅을 만들 수 있다고 생각하여 개발을 하게 되었습니다.


     

    socket-io, tanstack-query에 자세한 사용법은 기재하지 않았습니다.

     

    queryFn에 사용할 SocketFn 만들기

     

    Tanstack-query중 useQuery를 사용하기 위해서는 필수 인자중 하나인 queryFn이 필요합니다.

    해당 queryFn은 promise를 반환하는 함수여야 하고,

    반환된 Promise는 데이터를 해결하거나 오류를 발생시켜야 하는 함수여야 합니다.

     

    Tanstack-query 공식 문서

    A query function can be literally any function that returns a promise. The promise that is returned should either resolve the data or throw an error.

     

    그렇다면 SocketFn을 queryFn 인자에 사용할 수 있게 Promise 함수로 만들어야 했습니다.

    import { Socket } from 'socket.io-client';
    
    export const requestSocketFn = (eventName: string, socket: Socket, timeout = 150_000) => {
      const socketPromise = new Promise((resolve, reject) => {
        socket.on(eventName, (res) => {
          if (res?.error) return reject(res?.error);
          return resolve(res);
        });
        socket.on('exception', (_) => {
          return reject({ state: 'UNAUTHORIZED', message: '인증이 만료되었습니다.' });
        });
      });
    
      const timeoutPromise = new Promise(async (resolve, reject) => {
        await new Promise((resolve) => setTimeout(resolve, timeout));
        return reject('TIMEOUT');
      });
    
      return Promise.race([socketPromise, timeoutPromise]);
    };

     

    위 코드에서 socketPromise 함수는 socket을 통해 받아온 데이터가 error가 있을때 또는 인증이 만료되었을 때 reject를 리턴하고

    정상적으로 데이터를 받아 왔을시에는 resolve로 리턴을 합니다.

     

    timeoutPromise 함수socket에 연결 자체를 실패 했을시에 예외 처리로 timeout으로 설정한 시간이 되었을때 reject로 리턴합니다.

     

    requestSocketFn에 리턴을 Promise.race를 사용하여 socketPromise 또는 timeoutPromise 함수 중 가장 먼저 완료된 Promise 객체를 반환하게 만들었습니다.

     

    이렇게 queryFn 대신 사용 할 수 있는 requestSocketFn을 개발하였습니다.


    Tanstack-Query 커스텀 훅 만들기

    useQuery에 사용할 수 있는 requestSocketFn을 만들었으니 Tanstack-query를 사용한 커스텀 훅을 만들었습니다.

    import { QueryKey, useQuery, useQueryClient } from '@tanstack/react-query';
    import { requestSocketFn } from 'app.modules/util/requestSocketFn';
    import useStoreUser from 'app.hooks/useStoreUser';
    
    export const useSocketQueryFn = (queryKey: string[], options = {}) => {
      const queryClient = useQueryClient();
      const { socket } = useStoreUser();
      const [eventKey, listenEventKey, ...extraKeys] = queryKey;
      const eventName = listenEventKey ?? eventKey;
    
      return useQuery<any, any, any, QueryKey>(queryKey, () => requestSocketFn(eventName, socket), {
        ...options,
        onSettled: async (data: any) => {
          if (data?.isFinish === false) {
            await queryClient.invalidateQueries(queryKey);
          }
        },
      });
    };

     

    여기서 onSettled 옵션이 추가 되어 있는 이유는 아래 트러블 슈팅에서 자세하게 다루겠습니다.

     

    위 코드에서는 queryKey를 받아 서버와 약속한 socket의 eventName을 정하고 queryKey를 설정합니다.

    useQuery인자에 만들어놓은 requestSocketFn과 queryKey를 사용하여 커스텀 훅을 만들었습니다.


    사용법과 사용 결과

    사용하는곳에 아래와 같이 사용할 수 있습니다. 

      const { data: bookData, isLoading } = useSocketQueryFn(['bookUpdate', 'bookResponse', bookId], {
        onError: (error) => {
          if (error?.state === 'UNAUTHORIZED') {
            message.error('로그인이 필요한 기능입니다.');
            return router.push(`/auth/login?redirect=${window.location.href}`);
          }
          message.error('스토리 업데이트에 실패하였습니다.');
        },
      });

     

    아래처럼 socket을 통해 정상적으로 실시간으로 데이터를 받아와 지고 query가 저장된 것을 볼 수 있습니다.

    Socket을 이용한 실시간 통신
    Tanstack-query에 저장

     

    그리고 Tanstack-Query로 서버 상태를 관리함으로서 Socket을 통신 중에 로딩 및 에러 처리를 할 수 있게 되었습니다.

     

    서버에서 아직 통신중인걸 알려주는 로딩 처리
    비로그인, 인증 에러 예외 처리


    트러블 슈팅

    Socket 통신 로딩처리

     

    Socket 통신중인걸 로딩으로 처리를 하기 위해 isLoading을 사용하면 될 것으로 예상하여 시도해봤지만

    한번 로딩 후 로딩이 동작하지 않은 걸 확인했었다.

     

    그 이유로는 isLoading은 데이터가 캐싱이 되어 있냐 없냐의 기준이다 보니 socket을 한번 통신한 후에는 캐싱이 되어 있어

    이후의 통신은 로딩 처리를 하지 않는 것이 원인이었다.

     

    그렇다면 처음 로딩을 한 후 계속 요청을 받는 것이니 isFetching을 사용하는 것이 적합하다고 생각했지만,

    어떻게 해야 할지에 대한 고민이 있었다.

     

    Tanstack-query 문서를 보던 중 onSettled 옵션을 보았고 해당 옵션의 기능은 아래와 같았다.

    쿼리가 성공적으로 가져오거나 오류가 발생할 때마다 실행되며 데이터 또는 오류를 전달합니다.

    해당 옵션을 보고 생각한 것은 쿼리를 가져오고 invalidateQueries를 사용해

    캐싱된 쿼리를 invalidate를 시켜 stale 상태로 만들어 refetching 시켜 isFetching을 사용해 로딩 처리를 하는것이였다.
    그러기 위해 socket으로 받아오는 데이터중 해당 데이터가 마지막 데이터인것인지에 대해 알아야 했고

    백엔드분과 소통을 하여 socket을 통해 받아오는 데이터중 isFinish 값으로 마지막인지 아닌지에 대한 여부를 받기로 했다.

     

    테스트를 해봤을 때 원하는 대로 마지막 데이터가 아닐 시에는 쿼리가 fetching 상태로 변하는 걸 확인하였고, 

    로딩 처리에 성공하였다.

     

    또 하나의 장점은 Tanstack-query에 제공하는 APIuseIsFetching으로 

    로딩이 필요한 컴포넌트까지 props를 내려 사용하는 것이 아닌 필요한 곳에서 useIsFetching 사용해 로딩처리가 가능했다.

     

    쿼리의 상태가 fetching 상태일때 로딩 처리

     


    실제 사용은 아래 링크에서 사용해 볼 수 있습니다.
     

    Kidztales - 학습 AI 이야기

    상상력과 창의력을 자극하는 Kidztales에서 영단어가 자동으로 습득되는 나만의 영어 동화책, 실생활 속의 예시를 짚어주는 재미있는 학습책을 만들어보세요.

    tale.fit

Designed by Tistory.