카테고리 없음

[React] 성냥퍼즐 웹 서비스 만들기 15일차 (React Query)

seungho-dev 2025. 1. 8. 18:13

오랜만에 하는 포스팅이다.

드디어 최소기능 MVP 성냥퍼즐 웹 서비스를 완성하였다. 🎉

 

이제 배포하기 전에 서버비용을 줄여보고자 캐싱을 위한 React Query 설정을 

마지막으로 서비스 홍보하는 방식이나 유지보수 부분을 포스팅해 봐야겠다.

배포는 아직 정해지진 않았지만 cloudflare pages + aws ec2 + aws rds로 생각해두고 있다.

 


개인적인 공부를 위해 작성하는 블로그입니다. 혹시라도 잘못되거나 부족한 부분이 있다면 댓글로 알려주시면 감사하겠습니다.

 

 

보다시피 기능은 어느 정도 완성이 되었는데 계속해서 손봐야 할게 실시간으로 생기고 있다..

계속 수정하다가는 배포까지 못할 것 같아 이 정도에서 마무리하기로 했다.

 

💁🏻 React Query

React Query(TanStack Query)는 서버로부터 데이터 가져오기, 데이터 캐싱, 동기화 및 업데이트 처리 등 데어터를 쉽고 효율적으로 관리할 수 있는 라이브러리이다.

 

npm i @tanstack/react-query

 

import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import Router from './routes/index'
import "./index.css"

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5분
      gcTime: 60 * 60 * 1000, // 1시간
      retry: 2, // 2번 재시도
      refetchOnWindowFocus: false, // 페이지 이동 시 데이터 재요청
    }
  },
})

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <Router />
    </QueryClientProvider>
  </React.StrictMode>
)

 

main.js에 Router를 QueryClientProvider로 애플리케이션 전체에서 React Query의 기능을 사용할 수 있습니다.

queries에 대한 각 설정값에 대해서 간단하게 알아보면 

1. staleTime: 데이터가 신선(fresh)하다고 간주되는 시간 설정

2. gcTime: 페이지에서 사용되지 않는 비활성 쿼리가 메모리에 유지되는 시간

3. retry: 기본값은 3, 쿼리 실패 시 재시도 횟수

4. refetchOnWindowFocus: 기본값은 true, 다른 탭/창을 보다가 다시 앱으로 focus가 되었을 때 자동으로 다시 가져오는 설정이다. 우리 서비스는 그렇게 실시간성이 중요하지 않아 false로 해주었다.

 

우선 Puzzle을 불러오는 부분만 React Query로 관리해 보자

import { useQuery } from '@tanstack/react-query';
import { fetchAllPuzzles, fetchPuzzleById } from '../api/api-puzzle';

// 모든 퍼즐 가져오기
export const useAllPuzzles = () => {
  return useQuery({
    queryKey: ['puzzles'],
    queryFn: fetchAllPuzzles,
    staleTime: 1000 * 60 * 10, // 10분
    gcTime: 1000 * 60 * 20, // 20분
  });
};

// 특정 퍼즐 가져오기
export const usePuzzleById = (puzzleId) => {
  return useQuery({
    queryKey: ['puzzle', puzzleId],
    queryFn: () => fetchPuzzleById(puzzleId),
    staleTime: 1000 * 60 * 10, // 10분
    gcTime: 1000 * 60 * 20, // 20분
  });
};

커스텀 훅으로 만들어주고 home에서 사용해 보았다. isLoading과 isError 상태를 가져와서 fallback 처리를 해줄 수 있다.

import PuzzleCard from '../../components/PuzzleCard'
import { useAllPuzzles } from '../../hooks/usePuzzle';

export default function Home() {
  const { 
    data: puzzles,
    isLoading,
    isError,
    error
  } = useAllPuzzles();
  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error: {error.message}</div>;


  return (
    <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
        {puzzles.map(puzzle => (
          <PuzzleCard key={puzzle.id} puzzle={puzzle} />
        ))}
      </div>
    </div>
  )
}

그럼 위와 같이 초반에 puzzles를 불러온 후 마이페이지를 갔다가 다시 홈으로 왔을 때 다시 fetch가 일어나지 않는 걸 확인할 수 있다.

 

👨🏻‍💻 퍼즐 업데이트 후 리패치

캐싱된 퍼즐데이터가 퍼즐을 업데이트 시 다시 가져오기 위해 쿼리를 무효화하는 훅을 만들어 업데이트 로직에 넣어준다.

// 퍼즐 업데이트 후 리패치를 위한 훅
export function useInvalidatePuzzles() {
  const queryClient = useQueryClient()
  
  const invalidatePuzzles = async () => {
    // 'puzzles' 쿼리를 무효화하고 리패치
    await queryClient.invalidateQueries({ queryKey: ['puzzles'] })
  }
  return { invalidatePuzzles }
}
import { useInvalidatePuzzles } from '../hooks/usePuzzle';
...(생략)

export default function CreatePuzzleCanvas() {
  const navigate = useNavigate();
  const { invalidatePuzzles } = useInvalidatePuzzles();
  ...(생략)
    // 퍼즐 제출
  const handleSubmit = async () => {
    if (puzzleCreateCount <= 0) {
      alert('퍼즐 생성 횟수가 부족합니다.');
      return;
    }

...(생략)

    try {
      await createPuzzle(puzzleData);
      setPuzzleCreateCount(prev => prev - 1);
      invalidatePuzzles();
      alert(`퍼즐이 성공적으로 생성되었습니다! (craft coin: ${puzzleCreateCount - 1})`);
      navigate('/');
    } catch (error) {
      console.error('퍼즐 생성 실패:', error);
      alert(error.message || '퍼즐 생성에 실패했습니다.');
    }
  };