const {
    data: { user },
  } = await supabase.auth.getUser();

  // TODO: /api로 시작하는 주소는 무시
  if (
    !user &&
    !request.nextUrl.pathname.startsWith("/api") &&
    !request.nextUrl.pathname.startsWith("/auth")
  ) {
    // no user, potentially respond by redirecting the user to the login page
    const url = request.nextUrl.clone();
    url.pathname = "/auth/login";
    return NextResponse.redirect(url);
  }

 

Supabase의 getUser 메서드를 사용하여 현재 인증된 사용자의 정보를 가져온다.

  • user 객체가 존재하지 않는 경우 (즉, 사용자가 인증되지 않은 경우)
  • 요청된 URL 경로가 /api로 시작하지 않는 경우
  • 요청된 URL 경로가 /auth로 시작하지 않는 경우

조건을 모두 만족하면, 사용자를 로그인 페이지로 리다이렉션한다. NextResponse.redirect(url) 메서드는 사용자를 /auth/login 경로로 리다이렉션 시킨다.

👾 에러

상태관리를 zustand 로 바꾸는 도중 빨간맛을 보았다. 

 

 

📁 zustand / 📑 auth.store.ts

임포트도 잘했는데 왜 그럴까 구글링해도 나와같은 오류는 없었다.. 한참 삽질하다가 설마~_~ 타입지정을 안해줘서 그런걸까? 하고 타입을 지정해보았다.

import create from "zustand";

export const useAuthStore = create((set) => ({
  email: "",
  password: "",
  nickname: "",
  favorite_artist: [],
  is_admin: false,
  error: {
    password: "",
    nickname: "",
  },

  setEmail: (email) => set({ email }),
  setPassword: (password) => set({ password }),
  setNickname: (nickname) => set({ nickname }),
  setFavoriteArtists: (artists) => set({ favorite_artist: artists }),
  setIsAdmin: (is_admin) => set({ is_admin }),
  setError: (error) =>
    set((state) => ({ error: { ...state.error, ...error } })),
}));

 

 

 

💡 해결

타입 지정은 안해줘서 생긴 오류인지 정확히는 모르겠지만 타입지정을 해주었더니 오류가 사라졌다.

export type AuthStore = {
  email: string;
  nickname: string;
  password: string;
  favorite_artist: string[];
  is_admin: boolean;
  error: {
    password: string;
    nickname: string;
  };

  setEmail: (email: string) => void;
  setPassword: (password: string) => void;
  setNickname: (nickname: string) => void;
  setFavoriteArtists: (favoriteArtists: string[]) => void;
  setIsAdmin: (is_admin: boolean) => void;
  setError: (error: Partial<AuthStore["error"]>) => void;
};

 

 

포켓몬 도감 웹 페이지를 리액트 쿼리로 서버 상태 관리를 했다. 그 중 헷갈리는 개념인 staleTime과 cacheTime을 내가 이해한대로 정리해보려고 한다. 

 


 

devtools

우선, 리액트 쿼리를 사용할 때 쿼리의 상태를 확인하기 위해 devtool을 먼저 설치하고 시작!!

yarn add @tanstack/react-query-devtools
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import React from "react";

const QueryProvider = ({ children }: React.PropsWithChildren) => {
  const queryClient = new QueryClient();
  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryDevtools initialIsOpen={false} /> //이 코드 추가!!
      {children}
    </QueryClientProvider>
  );
};

export default QueryProvider;

 

 

staleTime

staleTime은 언제까지 신선한 데이터를 유지할지 기간을 지정한다. 데이터 상태가 stale 이라면 이전 데이터는 캐싱되어있지만, 업데이트는 되지 않은 상태이다.(데이터를 새로 패칭해야 하는 상태) stale 단어 그대로 "탁한", "신선하지 않은" 상태이다.

 

staleTime은 기본적으로 0 으로 default value로 세팅 되어있다.

staleTime > 0으로 설정되면, staleTime 이후에도 이전 캐시 결과를 사용할 수 있다.

staleTime === 0으로 설정되면, 데이터가 한 번 "stale" 상태가 되면 다시 쿼리를 수행하여 업데이트된 데이터를 가져오고 받아오는 즉시 stale하다고 판단해서 캐싱 데이터와는 무관하게 계속 fetching을 수행한다.

데이터가 fresh한 상태일 때는 페이지를 이동했다가 돌아와도 다시 패치되지 않는다. 왜냐하면 이미 신선한 상태의 데이터를 다시 패치해서 업데이트해줄 필요가 없기때문!!

 

global 설정 변경

React Query v3부터는 QueryClient.setQueryDefaults를 통해 쿼리 키별로 기본값을 설정할 수 있다. 

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5000,
    },
  },
});

 

useQuery별 설정 변경

const { isPending ,isFetching, data: pokemon } = useQuery({
  queryKey: ["pokemon"],
  queryFn: () => fetchPokemonData(),
  staleTime: 5000,
});

 

아래 화면처럼 쿼리 상태를 확인 할 수 있다. 나는 5초동안 신선한 데이터를 유지하도록 설정(staleTime: 5000) 했기 때문에 5초 뒤 frech -> stale로 상태가 변경되는 것을 확인할 수 있다.

 

 

 

cacheTime(gcTime)

사용하지 않는 캐시 데이터를 언제까지 가지고 있을껀지 설정하는 옵션이다.

cacheTime은 기본적으로 5분(1000*60*5)으로 default value로 세팅 되어있다.

컴포넌트가 언마운트되거나 쿼리가 더 이상 필요하지 않을 때를 inactive 상태로 변경되며 캐시는 cacheTime 만큼 유지된다.

 

global 설정 변경

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 2000,
    },
  },
});

 

useQuery별 설정 변경

const { isPending ,isFetching, data: pokemon } = useQuery({
  queryKey: ["pokemon"],
  queryFn: () => fetchPokemonData(),
  gcTime: 5000,
});

 

 

 

 

프로젝트 적용

진행하고 있는 포켓몬 도감에서는 포켓몬 데이터가 변하는 값이 아니기 때문에 staleTime을 infinity로 설정해주었다. 그럼 계속해서 신선한 데이터를 가지고 있어서 페이지 이동을 해도 데이터를 리패칭하지 않는다.

  const {
    data: pokemonData,
    error,
    isPending,
    isFetching,
  } = useQuery<Pokemon[]>({
    queryKey: ["pokemonData"],
    queryFn: () => fetchPokemonData(),
    staleTime: Infinity,
  });

 

🔗 Local font 사용 공식문서 참조

 

Optimizing: Fonts | Next.js

Optimize your application's web fonts with the built-in `next/font` loaders.

nextjs.org

✨ next/font 를 사용하면 폰트를 최적으로 로드할 수 있다. 모든 글꼴 파일에 대한 자동 자체 호스팅이 내장되어 있어, 레이아웃 이동 없이 최적으로 글꼴을 로드 할 수 있다.

 

 

 

Next.js Local 폰트 적용

next/font/google

  • layout의 기본세팅은 inter font로 되어있다.
  • 구글 폰트의 CDN 서비스를 통해 폰트를 불러올 수 있다.
  • 서버사이드 렌더링(SSR)과 클라이언트사이드 렌더링(CSR)을 지원한다.

next/font/local

  • 웹 폰트 다운로드 시간을 절약하고, 오프라인 사용 가능성을 제공한다.
  • 이미지와 마찬가지로 클라이언트 측에서 파일을 다운로드할 필요가 없으므로 페이지 로드 속도를 향상시킬 수 있다.

 

 

 

📁 app / 📁 fonts 폴더 생성

위 경로대로 폴더를 생성하고 다운받은 font 파일을 넣어준다. localFont import 후에 body 태그 안에 {myFont.className} 이라고 써주면 body 태그 내에서 모든 폰트가 적용된다.

import type { Metadata } from "next";
import "./globals.css";
import QueryProvider from "./provider";
import localFont from "next/font/local";

export const metadata: Metadata = {
  title: "Pokémon",
  description: "포켓몬 도감 웹페이지 입니다.",
};

const myFont = localFont({
  src: "./fonts/DungGeunMo.ttf",
  weight: "400",
  style: "normal",
  display: "swap",
});

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ko">
      <body className={myFont.className}>
        <div className="flex flex-col justify-center items-center">
          <h1 className="text-center w-[300px] mx-auto my-10">
            <img src="/pokemon.png" alt="포켓몬 로고" className="w-[100%]" />
          </h1>
          <QueryProvider>{children}</QueryProvider>
        </div>
      </body>
    </html>
  );
}

 

 

 

id 값으로 동적라우팅

params를 props로 받아 구조분해 할당 후 사용할 수 있다. props는 types 폴더에 인터페이스로 따로 정의해 주었다.

const DetailPokemonPage = async ({ params }: Props) => {
  const { id } = params;
  const fetchPokemonData = async () => {
    try {
      const response = await axios.get(
        `http://localhost:3000/api/pokemons/${id}`
      );
      return response.data;
    } catch (error) {
      console.error("데이터를 가져오지 못했습니다.", error);
      return [];
    }
  };

 

사용방법 1)

export interface Props {
  params: {
    id: number;
  };
}

 

사용방법 2)

const DetailPokemonPage = async ({ params }: { params: { id: number; };) => {
  const { id } = params;
  };

 

 

 

 

Image 세팅

next에서 기본으로 제공해주는 Image 기능을 사용해보려고 한다. 그러기 위해선 필수로 next.config.mjs를 설정해주어야한다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "raw.githubusercontent.com",
      },
    ],
  },
};

export default nextConfig;

 

width, height, alt 속성을 꼭 채워주어야한다. 안그럼 오류가 나더라.. Next.js에서 제공하는 Image 태그를 사용하는 이유는 큰 이미지를 작은 이미지로 줄여주고, 웹사이트 용량이 줄어들기 때문이다. 

<Image
   src={pokemonData.sprites.front_default}
   width={150}
   height={150}
   alt="포켓몬 이미지"
/>

 

이미지 크기를 고정하고싶지 않을 때는 fill 속성을 사용하면 된다. 대신 부모태그에 position: relative 를 써주어야한다.

 

'Next.js' 카테고리의 다른 글

[NextJS] 넥스트JS 정리하기  (2) 2024.12.16
[Next.js] shadcn/ui와 Tailwind css 함께 사용해보기  (0) 2024.07.16
[Next.js] middleware.ts  (0) 2024.07.10
[Next.js] CSR, SSR, SSG, ISR 정리  (0) 2024.07.02

Next.js❓

Next.js는 풀스택 웹 어플리케이션을 구축하기 위한 React 프레임워크이다. 프론트 UI구성은 React로 하고, Next.js를 사용하여 추가 기능과 최적화를 수행한다. 내부적으로 Next.js는 번들링, 컴파일 등과 같이 React에 필요한 도구를 추상화하고 자동으로 구성한다. 이를 통해 구성에 시간을 낭비하는 대신 어플리케이션 구축에 집중할 수 있다.

💡 프레임워크?
개발자가 기능 구현에만 ‘딱’ 집중할 수 있도록 필요한 모든 프로그래밍적 재원을 지원하는 ‘기술의 조합’

 

제어의 역전(IoC : Inversion Of Control)

라이브러리는 개발자가 직접 제어하고 세팅한다. 반대로 프레임워크는 개발자가 직접 제어할 필요 없이 이미 정해진 기능을 적재적소에 사용만 해주면 된다.

 

 

 

 

✔️ Next.js 사용 이유

1. Full Stack

  • API Route를 지원하여 full stack 웹 개발이 가능

2. 다양한 렌더링 기법

  • 기존 SPA 라이브러리에서 사용하던 CSR에서 벗어나 SSR, ISR, SSG등을 가능케 함
  • Next.js는 코드스플리팅을 default로 지원

3. Data Fetching

  • 여러 옵션을 통해 한 번만 값을 가져올지, 일정 주기별로 가져올지, 지속적으로 계속 가져올지 결정할 수 있다.

4. 쉬운 배포

  • vercel로 일괄 배포

 

✔️ Next.js의 6가지 개발 원칙

  1. out-of-the-box functionality requiring no setup => 설정이 필요없다.
  2. JavaScript everywhere + all functions are written in JavaScript => 자바스크립트를 어디서나 사용할 수 있다.
  3. automatic code-splitting and server-rendering => 코드 스플리팅과 서버 렌더링이 자동화되어있다.
  4. configurable data-fetching => 다양한 방식으로 데이터를 가져올 수 있는 유연한 옵션을 제공한다.
  5. anticipating requests => 사용자가 원하는 것이 무엇인지를 먼저 예측 = 요구사항 예측
  6. simplifying deployment => 배포가 간단하다.

 


 

 

 

CSR(Client Side Rendering)

브라우저에서 JavaScript를 이용해 동적으로 페이지를 렌더링하는 방식

  • React의 CSR에서, 브라우저는 최소한의 HTML 파일과 페이지에 필요한 최소한의 JS 코드만을 다운로드 받는다.
  • 그 후 클라이언트는 JS를 사용하여 DOM을 업데이트하고, 페이지를 렌더링한다.
  • 애플리케이션이 처음 로드되었을 때, 사용자는 그들이 전체 페이지를 보기 전에 약간의 지연을 겪게 된다. 
    • root 라는 div 안에 번들링이 큰 components가 들어있기 때문에 로드되기까지 시간이 소요된다.
    • 이는 모든 JS 코드가 다운로드 되어 파싱되고, 실행되기 전까지 페이지가 렌더링되지 않기 때문이다.

장점

  • 사용자와의 상호작용이 빠르고 부드럽다.
  • 서버에게 추가적인 요청을 보낼 필요가 없기 때문에, 사용자 경험이 좋다.
  • 서버 부하가 적음

단점

  • 첫 페이지 로딩 시간(Time To View)이 길다.
  • JavaScript가 로딩되고 실행될 때까지 페이지가 비어있어 검색 엔진 최적화(SEO)에 불리하다.
CSR을 통해 SPA을 구축하는 라이브러리로 페이지의 전환 없이 한 페이지에서 바뀐 부분에 대해서만 
화면 전환이 이루어지기 때문에 사용자 경험을 향상시키는 데 유리하다. 
하지만 이런 React에도 단점이 존재하는데 CSR이기 때문에 서버에서 화면에 보여줄 수 있는 html파일을 보내주는 게 
아니라 처음에는 빈 html파일을 보여주고 그 이후에 필요한 resource들을 다운 받아서 보여주기 때문에 
1) 초기 로딩 속도가 느리다는 문제와 2) 처음에 빈 html을 보여주기 때문에 검색 엔진 최적화 측면에서 불리하다는 
문제가 있다.

 

 

✔️ pre-rendering

 

SSG(Static Side Generation)

서버에서 페이지를 렌더링하여 클라이언트에게 HTML을 전달하는 방식

  • 최초 빌드 시에 HTML이 생성되고 매 요청마다 HTML을 재사용 한다.
  • SSG에서 HTML은 next build 명령어를 사용할 때 생성
  • 그 후에는 CDN으로 캐시가 되어지고 요청마다 HTML을 재사용
  • 주로 about 페이지와 같이 정적으로 생성된 정보를 요청마다 동일한 정보로 반환하는 경우에 사용한다.
  • 사전에 미리 정적페이지를 여러개 만들어놓음 → 클라이언트가 홈페이지 요청을 하면, 서버에서는 이미 만들어져있는 사이트를 바로 제공 → 클라이언트는 표기만 함

장점

  • 첫 페이지 로딩 시간이 매우 짧아(TTV) 사용자가 빠르게 페이지를 볼 수 있다. 또한, SEO에 유리
  • CDN(Content Delivery Network) 캐싱 가능

단점

  • 정적인 데이터에만 사용할 수 있음
  • 사용자와의 상호작용이 서버와의 통신에 의존하므로, 클라이언트 사이드 렌더링보다 상호작용이 느릴 수 있다.
  • 계속해서 서버에 요청하기 때문에 서버 부하가 크다. 
  • 마이페이지처럼 데이터에 의존하여 화면을 그려주는 경우 사용 불가

 

 

ISR(Incremental Static Regeneration)

정적 페이지를 먼저 보여주고, 필요에 따라 서버에서 페이지를 재생성하는 방식

  • SSG에 포함되는 개념이며 SSG와의 차이는 설정한 시간마다 페이지를 새로 렌더링 한다는 점
  • SSG는 빌드 시에 페이지를 생성하기 때문에 데이터가 변경되면 다시 빌드를 해야하지만, ISR은 일정 시간마다 특정 페이지만 다시 빌드하여 페이지를 업데이트

장점

  • 정적 페이지를 먼저 제공하므로 사용자 경험이 좋으며, 콘텐츠가 변경되었을 때 서버에서 페이지를 재생성하므로 최신 상태 유지
  • CDN 캐싱 가능

단점

  • 동적인 콘텐츠를 다루기에 한계가 있을 수 있습니다. 실시간 페이지 X
  • 마이페이지처럼 데이터에 의존하여 화면을 그려주는 경우 사용 불가

코드 ➡️ revalidate

import React from 'react';

function HomePage({ data }) {
  return <div>{data}</div>;
}

export async function getStaticProps() {
  const res = await fetch('https://...'); // 외부 API 호출
  const data = await res.json();

  return { 
    props: { data },
    revalidate: 60, // 1초 후에 페이지 재생성
  };
}

export default HomePage;

 

 

 

SSR(Server Side Rendering)

빌드 시점에 모든 페이지를 미리 생성하여 서버 부하를 줄이는 방식

  • 클라이언트가 페이지 요청할 때마다 html을 생성한다.
  • 항상 최신 상태를 유지해야하는 웹 페이지나, 분석 차트 등 사용자의 요청마다 동적으로 페이지를 생성해서 다른 내용을 보여주어야 하는 경우에 사용

장점

  • 빠른 로딩 속도(TTV)와 높은 보안성을 제공
  • SEO 최적화
  • 실시간 데이터 사용
  • 마이페이지처럼 데이터에 의존한 페이지 구성 가능
  • CDN 캐싱 불가

단점

  • 사이트의 콘텐츠가 변경되면 전체 사이트를 다시 빌드하는 과정에서 시간이 오래 걸릴 수 있다. → 서버 과부하
  • 요청할 때 마다 페이지를 만들어야 함  서버 과부하

 

 

전체 비교

  CSR SSR SSG ISR
빌드시간 느림 빠름 빠름 빠름
SEO 나쁨 좋음 좋음 좋음
요청에 따른 응답시간 보통 길다 짧다 짧다
최신 정보 O O X O/X

 

 

1. 라이브러리 설치

yarn add react-redux
yarn add @reduxjs/toolkit

 

 

2. Redux Store 설정

📁 redux / 📁 store / 📑 store.ts

타입스크립트로 만든 프로젝트라 RootState를 추가해주었다 RootState타입이 스토어의 상태를 나타내는 타입으로 정의된다.

import { configureStore } from "@reduxjs/toolkit";
import todoSlice from "../slices/todoSlice";

export const store = configureStore({
  reducer: {
    todos: todoSlice,
  },
});

export type RootState = ReturnType<typeof store.getState>;

 

 

3.Slice 생성 및 액션 추가

📁 redux / 📁 slices / 📑 todoSlice.ts

todolist에 들어갈 내용은 id, content, isDone(완료여부)로 구성하였다. 따로 타입 파일을 만들어 인터페이스로 정의해주었다. 

import { createSlice } from "@reduxjs/toolkit";
import { TodosState } from "../../types/TodoType";

const initialState: TodosState = {
  todos: [],
};

const todoSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    addTodo: (state, action) => {
      state.todos = [...state.todos, action.payload];
    },
    deleteTodo: (state, action) => {
      state.todos = state.todos.filter((todo) => todo.id !== action.payload);
    },
    toggleTodo: (state, action) => {
      const todo = state.todos.find((todo) => todo.id === action.payload);
      todo ? (todo.isDone = !todo.isDone) : null;
    },
  },
});

export default todoSlice.reducer;
export const { addTodo, deleteTodo, toggleTodo } = todoSlice.actions;

 

3-1. 타입 정의

📁 types / 📑 TodoType.ts

export interface Todo {
  id: number;
  content: string;
  isDone: boolean;
}

export interface TodosState {
  todos: Todo[];
}

 

 

4. Store 통합

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { Provider } from "react-redux";
import { store } from "./redux/store/store.ts";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

 

 

5. 화면에 보여주기

import { useDispatch, useSelector } from "react-redux";
import { RootState } from "./redux/store/store";
import { useState } from "react";
import { addTodo, deleteTodo, toggleTodo } from "./redux/slices/todoSlice";

const App: React.FC = () => {
  const [content, setContent] = useState<string>("");
  const todos = useSelector((state: RootState) => state.todos.todos);
  const dispatch = useDispatch();

  const handleAdd = (e: React.MouseEvent<HTMLFormElement, MouseEvent>) => {
    e.preventDefault();
    const newTodo = {
      id: Date.now(),
      content,
      isDone: false,
    };
    dispatch(addTodo(newTodo));
    setContent("");
  };
  return (
    <>
      <form onSubmit={handleAdd}>
        <input
          type="text"
          value={content}
          onChange={(e) => setContent(e.target.value)}
        />
        <button type="submit">추가</button>
      </form>
      <ul>
        {todos.map((todo) => {
          return (
            <li key={todo.id}>
              <h2>{todo.content}</h2>
              <button onClick={() => dispatch(toggleTodo(todo.id))}>
                {todo.isDone ? "취소하기" : "완료하기"}
              </button>
              <button onClick={() => dispatch(deleteTodo(todo.id))}>
                삭제
              </button>
            </li>
          );
        })}
      </ul>
    </>
  );
};

export default App;

1. Query Cancellation

  • 대용량 fetching을 중간에 취소하거나 사용하지 않는 컴포넌트에서 fetching이 진행 중이면 자동으로 취소시켜 불필요한 네트워크 비용을 줄일 수 있다
  • queryFn 의 매개변수로 Abort Signal 을 받을 수 있고, 이를 이용해서 Query 취소 가능

 

 

✏️ 사용방법

  • QueryFunctionContext
  • queryFn 은 매개변수로 QueryFunctionContext 이란 객체를 받는다

원래 데이터를 서버로부터 가지고 올 때 받는 매개변수 받는 곳을 비워놓았었는데, 비워져있는게 아닌 Tastack query 에서는 이미 queryFnContext가 들어가 있다. 두번째 인자에 signal 을 넣어주면 GET 요청 시 abort signal 이 옵션으로 들어간 경우에만 unmount 시 자동으로 네트워크 취소가 된다.

export const getTodos = async (queryFnContext) => {
  const { queryKey, pageParam, signal, meta } = queryFnContext;
  const response = await axios.get("http://localhost:5000/todos", { signal });
  return response.data;
};

useQuery({
  queryKey: ["todos"],
  queryFn: getTodos,
})
// example: <div onClick={(event) => {}}

 

queryFnContext 안에는 몇가지 요소가 있는데 아래와 같다.

1. queryKey: 배열형태의 쿼리키
2. pageParam: useInfiniteQuery 사용 시 getNextPageParam 실행 시 적용
3. signal: AbortSignal 을 의미 (네트워크 요청을 중간에 중단시킬 수 있는 장치)
4. meta: query에 대한 정보를 추가적으로 메모를 남길 수 있는 string 필드

 

 

🚨 주의

  • 불필요한 네트워크 요청을 최소화 한다는 명분으로 단순하게 모든 GET 요청마다 Abort Signal 을 심는 것은 작업부하를 올리고 바람직하지 않다.
  • 동영상 다운로드 같은 대용량 fetching이 아닌 이상 대부분의 GET 요청은 빠르게 완료 및 캐싱처리되어 성능에 유의미한 영향을 끼치지 못한다.
  • 대용량 fetching 이 있는 경우 또는 Optimistic UI 를 구현할 때처럼 필요한 경우에만 적용하는 것을 권장

 

 

2. Optimistic Updates

  • 낙관적 업데이트, 네트워크 요청이 끝나기 전에 미리 UI 변경
  • 서버 요청이 정상적으로 잘 될거란 가정하에 UI 변경을 먼저하고, 서버 요청 하는 방식. 혹시라도 서버 요청이 실패하는 경우, UI 를 원상복구(revert / roll back)
  • 좋아요 기능 특징

 

✏️ 사용방법

실행 순서는 아래 주석과 같다.

1. onMutate

  • await queryClient.cancelQueries({ queryKey: ['todos'] }) : 쿼리를 불러오는 중이라면 사이드 이펙트를 막기위해 쿼리 취소를 해주어야한다.
  • const previousTodos = queryClient.getQueryData(['todos']) : 현재 가지고 있는 데이터를 저장해둔다.
  • queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) : 추가된 데이터 UI 갱신
  • return { previousTodos } : previousTodos는 onError에 context 로 들어간다.

2. mutationFn

3. onError

  • return { previousTodos } : previousTodos는 onError에 context 로 들어간다.
  • queryClient.setQueryData(['todos'], context.previousTodos) : 쿼리가 에러가 났을 때 기존에 저장된 데이터로 원복처리

4. onSettled

  • queryClient.invalidateQueries({ queryKey: ['todos'] }) : 쿼리 최신화
const queryClient = useQueryClient()

useMutation({
  // 2. 실행
  mutationFn: updateTodo,
  
  // 1. 실행
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] })
    const previousTodos = queryClient.getQueryData(['todos'])
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
    return { previousTodos }
  },
  
  // 3. 실행
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // 4. 실행
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

 

 

3. Prefetching

  • 페이지 이동 전에 이동할 페이지의 쿼리를 백그라운드에서 미리 호출 (prefetching)
  • 캐시 데이터가 있는 상태로 해당 페이지로 이동 시 로딩없이 바로 UI를 볼 수 있다

 

✏️ 사용방법

const prefetchTodos = async () => {
  // The results of this query will be cached like a normal query
  // prefetch 할 queryKey와 queryFn 은 이동할 페이지의 쿼리와 동일해야 적절합니다.
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
}

 

 

4. Paginated / Lagged Queries

  • 기존 UI를 유지하다가 서버로부터 새로운 데이터를 받아왔을 때 바꾸는 방식을 적용
  • useQuery의 옵션 중 keepPreviousDatatrue 로 바꾸면 데이터를 가져오기 전까진 기존 데이터를 가져와서 사용한다.

 

✏️ 사용방법

export default function MoviePagination () {
	const [page, setPage] = useState(1);
	const { data: movies, isPending } = useQuery ({
		queryKey: ["movies", page], 
		queryFn: fetchMovieData,
		select: ({ total_pages, results }) => ({
			total_pages, results,
		}),

		keepPreviousData: true,
});

 

🚨 주의

로딩 중임을 사용자에게 명시적으로 보여줘야할 때, 쿼리를 가져오는데 5초 이상이 걸린다면 사용자는 5초동안 갱신되지 않는 상태의 UI를 보게된다. 그럴때에는 로딩 중임을 보여주는 것이 좋다. 여러 상황에 맞게 처리해주는 것이 좋다.

 

 

5. Infinite Queries

  • Data Fetching 이 일어날 때 마다 기존 리스트 데이터에 Fetched Data 를 추가하고자 할 때 유용하게 사용할 수 있는 hook
  • 더보기 UI 또는 무한스크롤 UI 에 사용하기에 적합

 

실행 순서는 아래와 같다.

queryFn 실행 
→ 캐시 데이터 등록 { pages, pageParam }  
→ getNextPageParam 실행 (리턴된 NextPageParam는 훅 내부 메모리에 저장. 캐시에 저장X) 
→ (NextPageParam 이 undefined이 아니면) hasNextPage true로 상태변경 
→ fetchNextPage 실행 
→ queryFn 실행 (이 때 내부적으로 저장되어 있던 NextPageParam을 queryFn 의 매개변수로 넘겨줌)

 

✏️ 사용방법

const fetchProjects = async ({ pageParam = 0 }) => {
    const res = await fetch('/api/projects?cursor=' + pageParam)
    return res.json()
  }

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

 

 

👾 에러

부모 컨포넌트에서 자식 컴포넌트로 props를 전달해줄 때 마주한 타입스크립트 에러이다. 타입을 암시적인 형식이 아니라 명시적으로 선언해줘야 한다. 

 

 

 

💡 해결

 

props 로 받은 매개변수들을 인터페이스로 정의하여 타입을 지정해주었다. 

 

 

 

 

 


🔗 참고

 

바인딩 요소 'children'에 암시적으로 'any' 형식이 있습니다.

바인딩 요소 타입스크립트

velog.io

+ Recent posts