포켓몬 도감 웹 페이지를 리액트 쿼리로 서버 상태 관리를 했다. 그 중 헷갈리는 개념인 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,
  });

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,
  })

 

 

createBrowserRouter

createBrowserRouter는 react router v6.4부터 사용할 수 있다.

 

 

 

라우터 생성

createBrowserRouter()

createBrowserRouter()에 라우팅 할 path element로 작성할 수 있다. children 속성으로 배열에 중첩된 라우터(Nested Router)를 추가해 줄 수 있다.

import { createBrowserRouter } from "react-router-dom";
import Login from "../pages/Login";
import Join from "../pages/Join";
import Layout from "../layout/Layout";
import Main from "../pages/Main";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        path: "/",
        element: <Main />,
      },
      {
        path: "/login",
        element: <Login />,
      },
      {
        path: "/join",
        element: <Join />,
      },
    ],
  },
]);

 

 

 

 

App.jsx

최상단에서 <RouterProvider> 를 import한다.
RouterRrovider에는 router={} props를 필수로 넣어야하고, Router.jsx에서 createBrowserRouter 함수로 생성한 router를 넘겨준다.

import React from "react";
import { RouterProvider } from "react-router-dom";
import { router } from "./router/Router";

const App = () => {
  return <RouterProvider router={router} />;
};

export default App;

 

기존의 상태관리 라이브러리인 Redux는 제공하는 기능과 연계된 미들웨어 등 매우 강력한 퍼포먼스를 자랑하지만, 설정과 사용법이 복잡했다.  Zustand는 상태관리 본연의 기능에 집중하여 위와 같은 복잡성을 줄이고, 보다 간단하고 직관적인 상태관리 기능을 제공한다는 장점이 있다. 

📌 공식문서
단순화된 Flux 패턴을 사용하는 작고(small) 빠르고(fast) 확장가능한(scalable) 상태관리 솔루션이며, 
Hooks에 기반으로하는 간편한 API가 존재

 

 

 

 

설치

yarn add zustand

 

해당 경로로 파일 생성 후 세팅 초기값을 설정하고, 1씩 증가하는 로직과 초기화하는 로직을 만들었다.

// src > zustand > bearsStore.js
import { create } from "zustand";

const useBearsStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

export default useBearsStore;

 

 

 

사용

다른 컴포넌트에서 사용하는 방법은 useState, useEffect와 같은 기본 훅을 사용하는 것처럼 간편하게 사용할 수 있다.

// src > App.jsx
import "./App.css";
import useBearsStore from "./zustand/bearsStore";

function App() {
  const bears = useBearsStore((state) => state.bears);
  const increasePopulation = useBearsStore((state) => state.increasePopulation);
  return (
    <div>
      <h1>{bears} around here ...</h1>
      <button onClick={increasePopulation}>one up</button>
    </div>
  );
}
export default App;

심화프로젝트 사전 기획 중 스타일링은 어떤 라이브러리를 사용할껀지 논의하다가 강민 튜터님께서 next.js를 쓰면 Tailwind css는 자연스럽게 쓰는 추세라고 말씀해주셨다. 거진 고정적이라고.. 그래서 이번 프로젝트 때 테일윈드를 사용해보기로 합의했다. 한번도 써본적 없어서 좀 부담스럽지만 요새 계속 쓰는 추세라고하니 사용해보는게 좋을 것 같다.

 

 

 

TailWind CSS

성능 이슈와 클래스 네임 충돌 이슈를 줄이고, 유연하고 직관적인 스타일링을 제공한다. 공식 문서에서는 

유틸리티 퍼스트(Utility-First) CSS 프레임워크로, 빠르고 쉽게 스타일링을 적용할 수 있는 클래스를 제공한다 고 쓰여져있다.

📌 유틸리티 클래스란?
유틸리티 클래스는 특정 스타일 속성을 나타내는 짧고 간단한 `CSS 클래스`로, 
HTML 요소에 직접 적용하여 빠르고 쉽게 스타일링을 할 수 있습니다.
Tailwind CSS는 이러한 유틸리티 클래스를 대량으로 제공하여, 
별도의 CSS 작성 없이도 다양한 스타일을 구현할 수 있게 합니다. 
예를 들어,`bg-blue-500`은 배경색을 파란색으로, `p-4`는 패딩을 설정하는 유틸리티 클래스입니다.

 

 

 

성능

  • Tailwind CSS는 불필요한 스타일을 제거하고, 필요한 부분만 스타일을 적용하는 방식으로 성능 최적화
  • 사용하지 않는 CSS를 제거하여, 최종 빌드 파일 크기를 최소화
  • CSS 파일의 크기를 줄이고, 애플리케이션 로딩 속도를 개선
  • Tailwind CSS는 React의 JSX 문법과 함께 사용할 수 있어, 스타일링 간편
  • Tailwind 설정 파일(tailwind.config.js)을 통해 색상, 폰트, 스페이싱 등 다양한 설정을 커스터마이징
  • 다양한 설정 옵션을 제공하여, 프로젝트의 요구사항에 맞는 스타일링을 손쉽게 적용
📌 Purging CSS?
`Purging CSS`는 사용하지 않는 CSS를 제거하여 최종 빌드 파일 크기를 줄이는 과정을 의미합니다. 
Tailwind CSS는 이를 자동으로 처리해줍니다.

 

 

 

 

설치 및 기본 사용법

yarn add tailwindcss postcss autoprefixer

 

초기화 파일 생성

yarn tailwind init -p

tailwind.config.js

 

 

 

 

 

 

 

 

 

 

 

 

 

 

tailwind.config.js 수정

src 하위 파일 중 확장자가 .js, .jsx, .ts, .tsx인 파일을 대상으로 한다.

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

 

 

index.css

css에 적용하기

@tailwind base;
@tailwind components;
@tailwind utilities;

 

 

 

 

컴포넌트에 써먹기

inline-style

return (
    <div style={{display:"flex" flexDirection:"column" gap:"8px" marginBottom:"1.25rem"}} >
      {sortedPlace.map((place) => {
        return <PlaceItem key={place.id} place={place} />;
      })}
    </div>
  );

 

 

Tailwind CSS

return (
    <div className="flex flex-col gap-8 mb-20">
      {sortedPlace.map((place) => {
        return <PlaceItem key={place.id} place={place} />;
      })}
    </div>
  );

 

 

 

무조건 껐다 키기!!!!!!!!! 껐다 켜야지 적용된다 적용안된다고 울지마세요 엉엉 난 울었으니까..

이번 개인 프로젝트때 주로 사용했던 Tanstack Query에 대해 알아보자. 리액트는 정말 훅이 많다..고 항상 느낀다.. 이번 TIL은 리액트 쿼리에 대해 깊이는 아니지만 내가 이해한만큼 써보려고 한다.

 

 

Tanstack Query❓

💡 fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리

 

서버 상태를 관리하기 위한 라이브러리로, 데이터를 패칭하고 캐싱, 동기화, 무효화 등의 기능을 제공한다. 복잡하고 장황한 코드가 필요하지 않고 리액트 컴포넌트 내부에서 간단하고 직관적으로 API를 사용할 수 있으며 이전보다 비동기 로직을 간편하게 작성하고 유지보수성을 높일 수 있다.

 

 

 

 

 

사용하는 이유

비동기 로직의 복잡성 해결 필요성

기존의 방식은 Fetching 코드를 작성하고 데이터를 담아 둘 상태(useState) 생성, useEffect를 이용해 컴포넌트 마운트시 데이터를 Fetching 한 뒤 상태에 저장하였다. 리액트 쿼리를 사용하면 해당 로직의 복잡함을 해결할 수 있다.

 

서버 상태 관리의 어려움

서버 상태는 클라이언트 상태와 달리 캐싱, 동기화, 재검증 등 관리해야 할 요소가 많아 기존 방법으론 관리가 어려움.

 

 

 

 

주요기능

  • 데이터 캐싱: DB서버에 여러번 동일한 데이터를 요청하지 않고 캐싱하여 데이터를 빠르게 가져온다.
  • 자동 리페칭: 데이터가 변경되었을 때 자동으로 리페칭하여 최신 상태를 유지한다.
  • 쿼리 무효화: 특정 이벤트가 발생했을 때 쿼리를 무효화하고 데이터를 다시 가져온다.

 

데이터를 가져오고, 수정하고, 리프레시한다. 무조건 외우기!!

📌 get / Modify / refresh

get = useQuery
Modify = useMutation
refresh = invalidateQueries

 

 

 

 

라이브러리 설치

yarn add @tanstack/react-query

 

리액트 쿼리를 사용하려면 백엔드 데이터 베이스가 필요해서 json-server로 임시 데이터 베이스를 만들어준다.

yarn add json-server

 

 

 

단축명령어 설정

 

📂package.json에서 아래와 같이 설정해준다. 그러면 yarn json 입력시 서버가 켜진다.

{
  "scripts": {
	"json": "json-server --watch db.json --port 5000",
  }
}

 

단축 명령어 없이 서버를 키는 명령어는 아래와 같다.

json-server --watch db.json

 

 

 

 

db.json

📂src > 📄db.json

{
  "expenses": [
    {
      "id": "f70630fd-852d-4566-a830-9d318c22046a",
      "date": "2024-06-18",
      "item": "📚",
      "amount": 50000,
      "desc": "리액트",
      "month": 5,
      "user": "wnswns"
    },
  ]
}

 

 

 

 

QueryClient 생성

useQuery를 사용할 곳에 쿼리 클라이언트를 생성해준 뒤 Provider를 적용해준다. 이렇게하면 전역에서 사용할 수 있다.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

 

 

 

 

데이터 가져오기 : useQuery()

useQuery에 쿼리 키비동기 함수를 인자로 받아 데이터를 가져오고, 로딩 상태, 오류 상태, 그리고 데이터를 반환한다.

여기서 쿼리키는 배열로 들어간다. 

useQuery({ queryKey: [쿼리키], queryFn: 비동기 함수 })

 

실무에서 자주 사용하는 쿼리키 이름

💡 꼭 알아야 하는 것
 - 다른 API는 다른 쿼리키를 사용한다. (ex. 목록 API와 상세 API는 다른 쿼리키를 사용한다)
 - 목록 페이지에선 `["posts"]` or `["movies"]` or `["todos"]`이렇게 쓰는 경우가 많음.
 - 상세 페이지에서는 `["posts", id]` 이렇게 쓰는 경우가 많음.
 - 페이지네이션의 쿼리키는 `["posts", page]` 이렇게 쓰는 경우가 많음

 

 

 

 

서버에서 데이터를 가져오는 API 함수를 만들어보자. 

import axios from "axios";

const JSON_SERVER_HOST = "http://localhost:4000";

export const getExpense = async () => {
  try {
    const response = await axios.get(`${JSON_SERVER_HOST}/expenses`);
    return response.data;
  } catch (error) {
    console.error(error);
    alert("데이터를 가져오지 못했습니다.");
  }
};

 

 

위에서 만든 API 함수를 useQuery에 적용해보자. 아래 코드로 적용하면 데이터 가져오는 로직 완성.

useQuery({ queryKey: ["expense"], queryFn: getExpense })

 

위 코드를 아래 코드처럼 쓸 수 있다.

  • data : 화면에 보여줄 데이터 (queryFn의 return 값)
  • isLoading : 데이터 로딩 여부 (true, false)
  • error : 데이터 로딩 중 발생한 에러 데이터
const { data, isLoading, error } = useQuery({
  queryKey: ["expense"],
  queryFn: getExpense,
});

 

데이터를 받아오기 전 로딩중으로 뜨고, 데이터를 가져오지 못할 경우 예외 처리를 해줄 수 있다.

  if (isLoading) {
    return <div>로딩중입니다.</div>;
  }

  if (error) {
    return <div>데이터를 불러오지 못했습니다.</div>;
  }

 

 

 

 

데이터 추가, 수정, 삭제하기 : useMutation()

export const postExpense = async (newExes) => {
  try {
    const response = await axios.post(`${JSON_SERVER_HOST}/expenses`, newExes);
    return response.data;
  } catch (error) {
    console.error(error);
    alert("데이터를 생성하지 못했습니다.");
  }
};

 

 

다른 컴포넌트에서 리액트 쿼리를 써야할 경우 ✔️ useQueryClient() 훅을 사용해줘야한다. invalidateQueries 속성을 사용해주기 위해선 필수이다. 생성 후 데이터를 다시 가져와야하기 때문에 invalidateQueries를 사용해줬다. 기존에 있던(데이터를 생성하기전) 데이터는 무효화시키고 최신 상태(데이터가 추가된)를 가져온다

  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: postExpense,
    onSuccess: () => {
      queryClient.invalidateQueries(["expenses"]);
    },
  });

 

 

새로운 데이터가 추가되는 함수에 mutation.mutate 사용해주면 추가하는 로직도 완성이다. 차례대로 수정과 삭제도 같은 방법으로 하면 된다!

const onInsert = useCallback(
    (date, item, amount, desc) => {
      const newExes = {
        id: uuidv4(),
        date,
        item,
        amount,
        desc,
        month: selectedMonth,
        user: userInfo.id,
      };

      mutation.mutate(newExes);
    },
    [mutation, userInfo]
  );

 

Thunder Client ❓

 

Thunder Client 는 VSCode에서 사용할 수 있는 HTTP 클라이언트 확장 프로그램 중 하나로 VSCode 내에서 HTTP 요청을 생성하고 테스트할 수 있다.

 

 

 

⚡️확장 프로그램 설치

 

 

⚡️확장 프로그램 사용방법

New Request

설치를 완료하고 나면 왼쪽 툴에 번개 모양 아이콘이 생긴다. 방금 설치한 Thunder Client다. 번개 아이콘을 누른 후 New Request 버튼을 차례대로 누른다.

 

 

간단한 HTTP 요청

GET 외부로부터 데이터 가져오기
POST 데이터 생성
PUT 데이터 업데이트
PATCH 데이터 일부 업데이트
DELETE 데이터 삭제

리액트 첫번째 팀프로젝트가 시작됐다. supabase를 사용해서 뉴스피드를 만드는 프로젝트이다. supabase는 처음 사용해보는데 오늘 강의 들은 내용으로 정리해보고자한다.

 


 

 

 

 

Supabase❓

Supabase는 Firebase의 대안으로 나온 오픈 소스이다. Firebase와 달리 Supabase는 PostgreSQL 관계형 데이터베이스를 기반으로 관련된 다양한 기능을 제공해준다. 아래 주요 특징에 따라 가장 큰 난관인 CRUD 기능과 유저 기능을 손쉽게 해결할 수 있다.

 

Database: 확장성이 뛰어난 PostgreSQL 데이터베이스를 활용한다

Authentication: 손쉽게 소셜 로그인을 활성화할 수 있다.

Edge Functions: 서버를 배포하거나 확장하지 않고도 사용자 지정 코드를 쉽게 작성할 수 있다.

Storage: 모든 유형의 디지털 콘텐츠를 저장 및 제공한다.

 

 

 

 

Supabase 설치

yarn add @supabase/supabase-js

 

회원가입 후 프로젝트를 생성하고 데이터 베이스를 설정

https://supabase.com/dashboard/projects

 

Dashboard | Supabase

 

supabase.com

 

Project Setting -> API 에서 Project URL과 Key값을 확인할 수 있다. 

 

supabase.js 파일을 만들어 주고 위에 확인한 url과 키값을  넣어준다.

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = 'URL'; 
const supabaseKey = import.meta.env.VITE_SUPABASE_KEY; 
const supabase = createClient(supabaseUrl, supabaseKey);

 

.env.local 파일 생성

VITE_SUPABASE_KEY=본인_supabase_키

 

 

 

 

인증/인가 구현하기

  • 인증
    • 증명하다
    • 내가 이 서비스의 회원이라는 것을 증명하다.
    • ex) 회원가입 & 로그인
  • 인가
    • 허가하다
    • 조회하기, 쓰기, 삭제하기, 수정하기 등을 할 수 있는 사람인지 확인하고 허가하는 것
    • ex) 로그인 후 글쓰기 가능
    • ex) 관리자 계정 → 회원관리 가능

 

로그인 구현

const { data, error } = await supabase.auth.signInWithPassword({
  email: 'example@email.com',
  password: 'example-password',
})
const signInUser = async (e) => {
    e.preventDefault();
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });
    setUser(data.user);
  };
  
  ...
  return
  <button onClick={signInUser}>로그인</button>

 

 

 

로그아웃 구현

const signOutUser = async (e) => {
    e.preventDefault();
    const { data, error } = await supabase.auth.signOut();
    console.log("signout: ", { data, error });
    setUser(null);
  };
  
  
  ...
  } else {
    return (
      <div>
        <p>{user.email}</p>
        <button onClick={signOutUser}>로그아웃</button>
      </div>
    );
  }

 

 

회원가입 구현

const { data, error } = await supabase.auth.signUp({
  email: 'example@email.com',
  password: 'example-password',
})
const signUpNewUser = async (e) => {
    e.preventDefault();
    const { data, error } = await supabase.auth.signUp({
      email,
      password,
    });
    setUser(data.user);
  }

 

 

 

 

 

onAuthStateChange 사용하기

Listen, subscribe → “계속 지켜본다”라는 뜻이다. useEffect에 넣어야 하는 이유는 컴포넌트 렌더링 시 한 번만 등록하면 되기 때문이다. 넣지 않으면 리렌더링마다 계속 CCTV 추가가 된다. 하나만 있으면 되는데 계속 추가되면 성능 안좋아질 수 있다

useEffect(() => {
    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, session) => {
      if (session) {
        setUser(session.user);
      } else {
        setUser(null);
      }
    });

    return () => subscription.unsubscribe();
  }, []);

+ Recent posts