👾 에러

상태관리를 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' 카테고리의 다른 글

[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

 

 

 

타입스크립트 사용이유❓


 

타입스크립트란 

TypeScript는 자바스크립트의 상위 집합(superset)으로, 자바스크립트 문법에 타입 체크를 위한 문법들이 추가된것

 

  1. 보다 신뢰할 수 있는 코드: 타입 체크를 통해 런타임 오류를 줄이고, 코드의 예측 가능성을 높인다.
  2. 규모 확장성: 코드베이스가 커질수록 타입 시스템 덕분에 안전하고 효율적으로 관리가 가능하다. (faster safely)
  3. 개발 도구 지원: 강력한 코드 자동 완성을 지원하여 생산성을 높인다.

 

✔️ JavaScript의 단점 상쇄

  • 실행 시간에 결정되는 변수 타입 -> 컴파일 시간에 변수의 타입을 체크
  • 약한 타입 체크 -> VS Code에 코드를 입력하는 순간 에러 메시지 발생
  • 너무나도 물렁물렁한 객체 -> 정의되지 않은 프로퍼티를 연산하여 NaN이 되는 현상을 미연에 방지

✔️ 향상되는 생산성

✔️ 높아지는 안전성

  • TypeScript 코드는 안정성과 가독성이 높아져 개발 및 유지 보수 과정에서 시간을 절약
  • 정적 언어에 익숙한 프로그래머들과의 협업도 더 빠르고 원활히 할 수 있음

 

 

 

1.Boolean

  • 2가지의 상태(켜짐/꺼짐, 유효함/유효하지 않음)를 표현하고 싶은 경우
  • boolean 타입은 참(true) 또는 거짓(false)
  • 조건문, 비교 연산 등에서 주로 사용

 

✏️ 사용예시

function isValidPassword(password: string): boolean {
  return password.length >= 8;
}

const password = "q1w2e3r4!";
const valid = isValidPassword(password);

if (valid) {
  console.log("유효한 패스워드입니다!");
} else {
  console.log("유효하지 않은 패스워드입니다!");
}

 

 

 

2.Number

  • TypeScript에서 사용하는 모든 숫자
  • 정수는 short, int, long
  • 실수는 float, double
  • 모든 수치 연산에 사용되는 값은 number 타입으로 명시

 

✏️ 사용예시

function calculateArea(radius: number): number {
  return Math.PI * radius * radius;
}

const radius = 5;
const area = calculateArea(radius);
console.log(`반지름이 ${radius}인 원의 넓이: ${area}`);

 

 

3.String

  • 텍스트 데이터
  • 텍스트와 텍스트가 합쳐져야 되는 경우
  • 텍스트에서 특정 문자열을 찾아야 하는 경우

 

✏️ 사용예시

function greet(name: string): string {
  return `안녕, ${name}!`;
}

const name = "Spartan";
const greeting = greet(name);
console.log(greeting);

 

 

4.Array

  • 기본타입에 []가 붙은 형태의 타입

 

✏️ 사용예시

function calculateSum(numbers: number[]): number {
  let sum: number = 0;
  for (let i = 0; i < numbers.length; i++) {
    sum += numbers[i];
  }
  return sum;
}

const testScores: number[] = [90, 85, 78, 92, 88];
const sumScore = calculateSum(testScores);
console.log(`점수의 총합: ${sumScore}`);

 

 

 

5. Tuple

  • 서로 다른 타입의 원소를 순서에 맞게 가질 수 있는 특수한 형태의 배열
  • 배열은 number[], string[] 처럼 같은 타입의 원소만

 

✏️ 사용예시

const person: [string, number, boolean] = ['Spartan', 25, false];
const person2: [string, number, boolean] = [25, 'Spartan', false]; // 오류!

 

 

6. enum

  • 명확하게 관련된 상수 값들을 그룹화하고자 할 때 사용
  • 열거형 데이터 타입
  • enum 안에 있는 각 요소는 값이 설정되어 있지 않으면 기본적으로 숫자 0으로 시작
  • enum 안에 있는 요소에는 number 혹은 string타입의 값만을 할당

 

✏️ 사용예시

enum UserRole {
  ADMIN = "ADMIN",
  EDITOR = "EDITOR",
  USER = "USER",
}

enum UserLevel {
  NOT_OPERATOR, // 0
  OPERATOR // 1
}

function checkPermission(userRole: UserRole, userLevel: UserLevel): void {
  if (userLevel === UserLevel.NOT_OPERATOR) {
    console.log('당신은 일반 사용자 레벨이에요');
  } else {
    console.log('당신은 운영자 레벨이군요');
  } 

  if (userRole === UserRole.ADMIN) {
    console.log("당신은 어드민이군요");
  } else if (userRole === UserRole.EDITOR) {
    console.log("당신은 에디터에요");
  } else {
    console.log("당신은 사용자군요");
  }
}

const userRole: UserRole = UserRole.EDITOR;
const userLevel: UserLevel = UserLevel.NOT_OPERATOR;
checkPermission(userRole, userLevel);

 

 

7. any

  • TypeScript에서 any 타입은 모든 타입의 슈퍼 타입
  • 어떤 타입의 값이든 저장할 수 있다는 의미

 

✏️ 사용예시

let anything: any;
anything = 5; // 최초에는 숫자를 넣었지만
anything = 'Hello'; // 문자열도 들어가고요
anything = { id: 1, name: 'John' }; // JSON도 들어가네요

 

 

8. unknown

  • any 타입과 비슷한 역할을 하지만 더 안전한 방식으로 동작
  • unknown 타입의 변수에도 모든 타입의 값을 저장
  • 다른 타입의 변수에 할당하려면 명시적으로 타입을 확인
  • unkwown 타입의 변수를 다른 곳에서 사용하려면 타입 단언을 통해 타입 보장을 하여 사용
  • 재할당이 일어나지 않으면 타입 안전성이 보장이 되지 않음

 

✏️ 사용예시

let unknownValue: unknown = '나는 문자열이지롱!';
console.log(unknownValue); // 나는 문자열이지롱!

let stringValue: string;
stringValue = unknownValue; // 에러 발생! unknownValue가 string임이 보장이 안되기 때문!
stringValue = unknownValue as string; // Type Assertion(타입 단언)
console.log(stringValue);
  • typeof 키워드를 이용하여 타입 체크를 미리한 후 unknown 타입의 변수를 string 타입의 변수에 할당
let unknownValue: unknown = '나는 문자열이지롱!';
let stringValue: string;

if (typeof unknownValue === 'string') {
  stringValue = unknownValue;
  console.log('unknownValue는 문자열이네요~');
} else {
  console.log('unknownValue는 문자열이 아니었습니다~');
}

 

 

9. union

  • 여러 타입 중 하나를 가질 수 있는 변수를 선언할 때 사용
  • | 연산자를 사용하여 여러 타입을 결합하여 표현
  • TypeScript를 쓰면서 여러 타입을 하나의 변수로 해결하겠다는 생각은 가급적 지양

 

✏️ 사용예시

type StringOrNumber = string | number; // 원한다면 | boolean 이런식으로 타입 추가가 가능해요!

function processValue(value: StringOrNumber) {
  if (typeof value === 'string') {
    // value는 여기서 string 타입으로 간주됩니다.
    console.log('String value:', value);
  } else if (typeof value === 'number') {
    // value는 여기서 number 타입으로 간주되구요!
    console.log('Number value:', value);
  }
}

processValue('Hello');
processValue(42);

+ Recent posts