React Query

2024. 3. 3. 16:43·React

❓React Query

- 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리

- 컴포넌트 내부에서 간단하고 직관적으로 API 사용 가능

- 데이터 캐싱(데이터의 복사본 저장) -> 동일한 데이터에 대한 반복적인 비동기 데이터 호출 방지 -> 서버에 대한 부하를 줄임

- 최신데이터(fresh) 기존의 데이터(stale)

- Client 데이터는 상태 관리 라이브러리가 관리, Server 데이터는 리액트 쿼리가 관리


 

❇️  App.tsx or index.tsx 파일에서 QueryClientProvider를 사용해서 모든 컴포넌트를 감싸기

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import { QueryClient, QueryClientProvider } from 'react-query'
import { BrowserRouter } from 'react-router-dom'

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </QueryClientProvider>
  </React.StrictMode>,
)

❇️ useQuery

import { useQuery } from 'react-query'
import axios from 'axios'

export const ReactQuery = () => {
  const { isLoading, data, isError, error } = useQuery('get-product', () => {
    return axios.get(
      'https://mypocketbase.fly.dev/api/collections/products/records',
    )
  })

  if (isLoading) return <>Loading...</>
  if (isError) return <>{error.message}</>

  return (
    <>
      <div className='text-4xl'>ReactQuery</div>

      <ul className='list-disc p-4'>
        {data &&
          data.data?.items?.map(product => (
            <li key={product.id}>
              {product.name} / {product.price}
            </li>
          ))}
      </ul>
    </>
  )
}

useQuery 훅 첫 번째 인자(문자열) 'get-product' 부분은 queryKey임! 

이 queryKey로 구분해서 data fetching 수행! -> 이 이름으로 캐시 저장

그래서 다시 라우팅하면 캐시 내용 돌려보내줌 -> 로딩x 

그러면 실제 api값이 변경된다면? -> 백그라운드에서 조용히 api 호출을 해서 캐시에 있는 데이터를 최신화시킴(isFetching)

두 번째 인자엔 콜백 함수 -> data fetching을 위한 axios 코드를 넣음

다음과 같이 함수를 따로 만들 수도 있음 -> 간결해

const fetchProducts = () => {
  return axios.get(
    "https://mypocketbase.fly.dev/api/collections/products/records"
  );
};

export const ReactQuery = () => {
  const { isLoading, data, isError, error } = useQuery("get-product", fetchProducts);

  if (isLoading) return <>Loading...</>;
  if (isError) return <>{error.message}</>;
...
...
}

isLoading, data, isError, error -> 기본적으로 제공해주는 변수

❇️ Devtools 컴포넌트 추가

import { Routes, Route } from 'react-router-dom'
import { Layout } from './components/Layout'
import { Home } from './components/Home'
import { AxiosQuery } from './AxiosQuery'
import { ReactQuery } from './ReactQuery'
import { ReactQueryDevtools } from 'react-query/devtools'

function App() {
  return (
    <>
      <Routes>
        <Route path='/' element={<Layout />}>
          <Route index element={<Home />} />
          <Route path='/axios-query' element={<AxiosQuery />} />
          <Route path='/react-query' element={<ReactQuery />} />
        </Route>
      </Routes>
      <ReactQueryDevtools initialIsOpen={false} position='bottom-right' />
    </>
  )
}

export default App

❇️cache

캐시의 기본 유지 시간은? 5분! -> 가장 좋은 시간...굳이 수정할 필요 x

캐시의 기본 시간 수정가능! ex) 5000->5초

const { isLoading, isFetching, data, isError, error } = useQuery(
  'get-product',
  fetchProducts,
  {
    cacheTime: 5000,
  },
)

❇️stale

fresh <-> stale

isFetching주기는 staleTime으로 지정 가능!

const { isLoading, isFetching, data, isError, error } = useQuery(
  'get-product',
  fetchProducts,
  { staleTime: 60000 },
)

이렇게 하면 60초마다 데이터 갱신됨

리액트 쿼리의 캐시 상태 (inactive -> fetching -> fresh -> stale)

staleTime의 기본 값은? 0 -> 잘 조절해서 무분별한 api 호출이 일어나지 않도록 하자!

❇️refetch

컴포넌트가 라우팅 될 때마다 fetching이 일어나는 이유 -> refetchOnMount 항목이 true로 기본값 세팅 되어있기 때문!

const { isLoading, isFetching, data, isError, error } = useQuery(
  'get-product',
  fetchProducts,
  { staleTime: 3000, refetchOnMount: false },
)

false로 하면 Mount 됐을 때 fetching 작업이 일어나지 않음

추가로 refetchOnWindowFocus 도 있는데 브라우저 활성화 유무에 따라서 refetch 방식 결정

❇️polling

polling: 주기적으로 데이터를 가져오는 프로세스

refetchInterval 값은 기본값이 false!

-> refetch가 유저의 행위(refetchOnMount & refetchOnWindowFocus)에 의해 의존한다는 뜻!

 refetchIntervals는 유저의 행위와는 별도로 polling작업을 할 수 있음!

const { isLoading, isFetching, data, isError, error } = useQuery(
  'get-product',
  fetchProducts,
  {
    refetchInterval: 2000,
    refetchIntervalInBackground: true,
  },
)

-> refetchInterval로 2초마다 fetching이 일어나도록 함 

+ refetchIntervalInBackground를 기본값인 false에서 true로 변경함으로써 앱이 비활성화되었어도 fetching 작업 계속 일어남

주기적으로 데이터가 변경되는 앱에서 매우 유용하다!

❇️enabled

리액트 컴포넌트 마운트시 자동으로 useQuery 시작됨 -> enabled 기본 세팅이 true이기 때문!

그럼 유저의 요청에 의해 데이터를 가져와야 할 때는?

  const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
    "get-product",
    fetchProducts,
    {
      enabled: false,
    }
  );

  if (isLoading || isFetching) return <>Loading...</>;
  if (isError) return <>{error.message}</>;

return (
  <>
  <div className="text-4xl">ReactQuery</div>
      <button
        onClick={refetch}
        className="py-2 px-4 border bg-slate-100 rounded-md"
      >
        fetch data
      </button>
      <ul className="p-4 list-disc">
</>
)

enabled를 false로 해놓으면 원하는 상황에 useQuery가 시작되게 할 수 있음!

기본 제공해주는 refetch 콜백함수를 이용하면 됨!

❇️커스텀 콜백 함수

useQuery 훅에서 성공 또는 실패했을 경우 커스텀 콜백 지정 가능!

const fetchProducts = () => {
  return axios.get(
    "https://mypocketbase.fly.dev/api/collections/products/records"
  );
};


export const ReactQuery = () => {

  const onSuccess = (data) => {
    console.log("데이터 가져오기 후 사이드 이펙트 수행", data);
  };

  const onError = (error) => {
    console.log("오류 발생 후 사이드 이펙트 수행", error);
  };

  const { isLoading, isFetching, data, isError, error } = useQuery(
    "get-product",
    fetchProducts,
    {
      onSuccess, //onSuccess: onSuceess
      onError,
    }
  );

  console.log({ isLoading, isFetching });
...
...
...
}

onSuccess 콜백 -> 데이터 활용

onError 콜백 -> 오류 처리

❇️데이터 변환

백엔드 - 프론트 간 데이터 형식의 차이가 있는 경우... -> select 옵션 이용!

const { isLoading, isFetching, data, isError, error } = useQuery(
  'get-product',
  fetchProducts,
  {
    onSuccess: onSuccess,
    onError: onError,
    select: data => {
      const productName = data.data?.items.map(p => p.name)
      return productName
    },
  },
)

+

<>
  <div className='text-4xl'>ReactQuery</div>

  <ul className='list-disc p-4'>
    {/* {data &&
          data.data?.items?.map((product) => (
            <li key={product.id}>
              {product.name} / {product.price}
            </li>
          ))} */}
    {data && data.map(productName => <li key={productName}>{productName}</li>)}
  </ul>
</>

filter 메소드도 사용 가능

select: data => {
  const productName = data.data?.items
    .filter(p => parseInt(p.price) <= 9)
    .map(p => p.name)
  return productName
}

❇️커스텀 훅으로 만들어 재사용

예) src > hooks > useProductName.ts

import { useQuery } from "react-query";
import axios from "axios";

const fetchProducts = () => {
  return axios.get(
    "https://mypocketbase.fly.dev/api/collections/products/records"
  );
};

export const useProductName = (onSuccess, onError) => {
  return useQuery("get-product", fetchProducts, {
    onSuccess: onSuccess,
    onError: onError,
    select: (data) => {
      const productName = data.data?.items
        .filter((p) => parseInt(p.price) <= 9)
        .map((p) => p.name);
      return productName;
    },
  });
};

사용

import { useProductName  } from "./hooks/useProductName";

export const ReactQuery = () => {
  const onSuccess = (data) => {
    console.log("데이터 가져오기 후 사이드 이펙트 수행", data);
  };

  const onError = (error) => {
    console.log("오류 발생 후 사이드 이펙트 수행", error);
  };

  const { isLoading, isFetching, data, isError, error } = useProductName(
    onSuccess,
    onError
  );

...
...
...
}

❇️id로 특정 항목만 가져오기

보통 상세페이지 불러올 때 사용할 쿼리임!

import { useQuery } from 'react-query'
import axios from 'axios'

const fetchProductDetails = productId => {
  return axios.get(
    `https://mypocketbase.fly.dev/api/collections/products/records/${productId}`,
  )
}

export const useProductId = productId => {
  return useQuery(['product-id', productId], () =>
    fetchProductDetails(productId),
  )
}

배열의 첫 번째 인자엔 query 이름, 두 번째 인자엔 다이나믹으로 변하는 변수를 넣어줌( ['product-id', productId] )

❇️병렬 쿼리

하나의 컴포넌트에서 여러 api를 호출해야 할 때?

-> useQuery를 두 번 호출하는 것만으로도 간단히 처리 가능!

import { useQuery } from "react-query";
import axios from "axios";

const fetchProducts = () => {
  return axios.get(
    "https://mypocketbase.fly.dev/api/collections/products/records"
  );
};

const fetchUsers = () => {
  return axios.get(
    "https://mypocketbase.fly.dev/api/collections/users/records"
  );
};

export const ParallelQuery = () => {
  useQuery("parallel-get-product", fetchProducts);
  useQuery("parallel-get-users", fetchUsers);
  return <div>ParallelQuery</div>;
};

그러면 쿼리가 두 개인데 데이터를 어떻게 구분? -> 별칭 사용

  const {
    data: productsData,
    isLoading: productsLoading,
    isError: productsError,
  } = useQuery("parallel-get-product", fetchProducts);

❇️동적 병렬 쿼리

여러 개의 값이 있는 배열이 전달될 경우엔 어떻게 useQuery를 수행? -> useQuerise 함수 사용

import { useQueries } from 'react-query'
import axios from 'axios'

const fetchProducts = productId => {
  return axios.get(
    `https://mypocketbase.fly.dev/api/collections/products/records/${productId}`,
  )
}
export const DynamicParallelQueries = ({ productIds }) => {
  console.log(productIds)
  const results = useQueries(
    productIds.map(id => {
      return {
        queryKey: ['get-product', id],
        queryFn: () => fetchProducts(id),
      }
    }),
  )

  console.log({ results })

  return <div>DynamicParallelQueries</div>
}

❇️initial query data

전체 -> 상세로 갈 때 각각 쿼리 작업 하지 말고 캐시로부터 가져와서 쓰면 네트워크 사용량 절약 가능!

-> initialData 옵션과 queryClient를 사용하면 구현할 수 있음

import { useQuery, useQueryClient } from "react-query";
import axios from "axios";

const fetchProductDetails = (productId) => {
  return axios.get(
    `https://mypocketbase.fly.dev/api/collections/products/records/${productId}`
  );
};

export const useProductId = (productId) => {
  const queryClient = useQueryClient();
  return useQuery(
    ["product-id", productId],
    () => fetchProductDetails(productId),
    {
      initialData: () => {
        const product = queryClient
          .getQueryData("get-product")
          ?.data?.items.find((p) => p.id === productId);

        if (product) {
          return {
            data: product,
          };
        } else return undefined;
      },
    }
  );
};

주의점!

1. 찾은 데이터를 data: product 같은 형식으로 사용해야 함(ReactQueryDetails 컴포넌트에서 data.data로 사용해서)

2. 바로 원하는 값을 찾지 못했을 경우 undefined를 리턴해야 함(그래야 해당 쿼리 수행x)

❇️ useQuery의 강력한 기능?!

캐시 된 데이터를 화면에 먼저 보여주고 -> fetching을 해서 기존 데이터와 같으면 UI를 바꾸지 않고, 다르면 그제서야 UI를 바꿈

데이터 로딩 중엔 화면의 Layout이 깨짐 -> 스켈레톤 UI 사용하는 이유...

const { data, isLoading, isFetching } = useQuery(
  ['get-paginated', pageNumber],
  () => fetchProducts(pageNumber),
  {
    keepPreviousData: true,
  },
)

 

다음과 같이 keepPreviousData: true 옵션을 주면 됨!

❇️ useInfiniteQuery

무한 스크롤 구현하기 위한 함수!

페이지네이션 구현 코드로 살펴보자

import { Fragment } from 'react'
import { useInfiniteQuery } from 'react-query'
import axios from 'axios'

const fetchProducts = (page) => {
  return axios.get(
    `https://mypocketbase.fly.dev/api/collections/products/records/?perPage=4&page=${page}`,
  )
}

export const PaginatedQuery = () => {
  const {ƒ
    data,
    isLoading,
    isFetching,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery(
    ['get-paginated'],
    ({ pageParam = 1 }) => fetchProducts(pageParam),
    {
      getNextPageParam: (_lastPage, pages) => {
        if (pages.length < 3) {
          return pages.length + 1
        } else return undefined
      },
    },
  )
  if (isLoading) return <div>Loading...</div>

  return (
    <>
      <div className='text-4xl'>ReactQuery</div>

      {data &&
        data.pages?.map((group, i) => (
          <Fragment key={i}>
            {group &&
              group?.data.items.map(p => <div key={p.id}>{p.name}</div>)}
          </Fragment>
        ))}
      <div className='space-x-4'>
        <button
          className='border'
          onClick={fetchNextPage}
          disabled={!hasNextPage}
        >
          Load More
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
    </>
  )
}

fetchProducts 함수에서 page라는 함수 인자를 받고, pageParam = 1 로 디폴트 값을 1로 지정해서 넘겨줌

주의!

useInfiniteQuery는 queryKey를 꼭 배열로 제공해 줘야됨.

마지막 옵션이 getNextPageParam 콜백함수 -> 다음 페이지의 pageParam 값 제공

꼭 undefined를 리턴하는 코드도 작성해야함!

useInfiniteQuery는 페이지 정보를 가져다주고, 실제 그 pages에는 해당 페이지가 있고, group이란 항목이 있음 -> map 메서드를 두 번 써야됨

다음 페이지는 fetchNextPage 콜백 함수 사용

❇️ 무한 스크롤 구현(scrollHeight, scrollTop, clientHeight)

useEffect(() => {
    let fetching = false;
    const handleScroll = async (e) => {
      const { scrollHeight, scrollTop, clientHeight } =
        e.target.scrollingElement;
      if (!fetching && scrollHeight - scrollTop <= clientHeight * 1.2) {
        fetching = true;
        if (hasNextPage) await fetchNextPage();
        fetching = false;
      }
    };
    document.addEventListener("scroll", handleScroll);
    return () => {
      document.removeEventListener("scroll", handleScroll);
    };
  }, [fetchNextPage, hasNextPage]);

❇️ 무한 스크롤 구현2(Intersection Observer)

https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

 

Intersection Observer API - Web APIs | MDN

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

developer.mozilla.org

import { Fragment, useEffect, useRef, useCallback } from "react";
import { useInfiniteQuery } from "react-query";
import axios from "axios";

const fetchProducts = (page) => {
  return axios.get(
    `https://mypocketbase.fly.dev/api/collections/products/records/?perPage=4&page=${page}`
  );
};

export const PaginatedQuery2 = () => {
  const observerElem = useRef(null);

  const { data, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage } =
    useInfiniteQuery(
      ["get-paginated"],
      ({ pageParam = 1 }) => fetchProducts(pageParam),
      {
        getNextPageParam: (_lastPage, pages) => {
          if (pages.length < 3) {
            return pages.length + 1;
          } else return undefined;
        },
      }
    );

  const handleObserver = useCallback(
    (entries) => {
      const [target] = entries;
      if (target.isIntersecting && hasNextPage) {
        fetchNextPage();
      }
    },
    [fetchNextPage, hasNextPage]
  );

  useEffect(() => {
    const element = observerElem.current;

    let options = {
      root: null,
      rootMargin: "0px",
      threshold: 1,
    };

    const observer = new IntersectionObserver(handleObserver, options);
    if (element) observer.observe(element);
    return () => {
      if (element) observer.unobserve(element);
    };
  }, [fetchNextPage, hasNextPage, handleObserver]);

  if (isLoading) return <div>Loading...</div>;

  return (
    <>
      <div className="text-4xl">ReactQuery</div>

      {data &&
        data.pages?.map((group, i) => (
          <Fragment key={i}>
            {group &&
              group?.data.items.map((p) => <div key={p.id}>{p.name}</div>)}
          </Fragment>
        ))}
      <div className="loader" ref={observerElem}>
        {isFetchingNextPage && hasNextPage ? "Loading..." : "No search left"}
      </div>
    </>
  );
};

useEffect가 useRef보다 먼저 실행되는 것을 생각하며 element가 있는지 if문으로 체크하자

❇️ useMutaion

데이터를 post 할 때 사용하는 훅이 useMutaion

import { useState } from "react";
...
...
...

export const ReactQuery = () => {
  const [name, setName] = useState("");
  const [price, setPrice] = useState(0);

  ...
  ...
  ...
  const handleCreate = () => {
    console.log({ name, price });
  };

  return (
    <>
      <div className="text-4xl">ReactQuery</div>
      <div className="space-x-2">
        <input
          className="border"
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <input
          className="border"
          type="number"
          value={price}
          onChange={(e) => setPrice(Number(e.target.value))}
        />
        <button className="border" onClick={handleCreate}>
          Create
        </button>
      </div>
      ...
      ...
    </>
  );
}

커스텀 useMutaion 훅 생성하기

import { useQuery, useMutation } from "react-query";

...
...
...

const addProduct = (product) => {
  return axios.post(
    "http://127.0.0.1:8090/api/collections/products/records", product
  );
};

...
...
...
export const useAddProduct = () => {
  return useMutation(addProduct)
}

커스텀 훅 사용하기

import { useProductName, useAddProduct } from './hooks/useProductName'
...
...

const { mutate: addProduct } = useAddProduct()

const handleCreate = () => {
  console.log({ name, price })
  const data = { name, price }
  addProduct(data)
}

useAddProduct 훅에서 useMutation 훅을 받는데 거기서 mutate라는 함수만 받음

-> mutate 함수의 이름을 addProduct라고 alias 형태로 새로 지정해줌

-> handleCreate 함수에서 addProduct 함수를 실행시키면 됨!

근데 fetching 로딩 시간 걸림... ->  POST 액션 후 바로 쿼리 갱신시키고 싶으면? -> queryKey를 이용해서 invalidate 하면 됨!

import { useQuery, useMutation, useQueryClient } from 'react-query'

export const useAddProduct = () => {
  const queryClient = useQueryClient()
  return useMutation(addProduct, {
    onSuccess: () => {
      queryClient.invalidateQueries('get-product')
    },
  })
}

 

useQueryClient를 이용해서 메모리상의 queryClient 객체를 얻어서 바로 invalidateQueries 함수를 실행

-> invalidateQueries 함수는 queryKey의 쿼리를 즉시 무효화 시키고 다시 fetching 시킴

-> post 액션 후에 시간지체 없이 바로 화면에 업데이트 된 정보 보임!

하지만 위와 같은 방법 말고 메모리의 쿼리에 직접 데이터를 업데이트하여 useQuery가 fetching을 하지 않도록 만들 수 있음! (post 이후 get이 무조건 일어나는데 이 불필요한 get을 없애는 것)

export const useAddProduct = () => {
  const queryClient = useQueryClient()
  return useMutation(addProduct, {
    onSuccess: data => {
      // queryClient.invalidateQueries("get-product");
      queryClient.setQueryData('get-product', oldProductData => {
        return {
          ...oldProductData,
          data: [...oldProductData.data.items, data.data],
        }
      })
    },
  })
}

❇️Optimistic Updates

실제 post 액션은 백그라운드로 실행되는 것 -> 가장 이상적인 post 액션! = Optimistic Updates

import { v4 as uuid } from 'uuid';
...
...
...
export const useAddProduct = () => {
  const queryClient = useQueryClient();
  return useMutation(addProduct, {
    // onSuccess: (data) => {
    //   // queryClient.invalidateQueries("get-product");
    //   queryClient.setQueryData("get-product", (oldProductData) => {
    //     return {
    //       ...oldProductData,
    //       data: [...oldProductData, data.data],
    //     };
    //   });
    // },
    onMutate: async (newProduct) => {
      await queryClient.cancelQueries("get-product");
      const previousProductData = queryClient.getQueryData("get-product");
      queryClient.setQueryData("get-product", (oldProductData) => {
        return {
          ...oldProductData,
          data: [...oldProductData.data.items, { id: uuid(), ...newProduct }],
        };
      });
      return {
        previousProductData,
      };
    },
  });
};

주의: 본인이 사용하는 DB가 리턴하는 JSON 데이터의 형식을 꼭 확인하고 숙지해야 함

작동 원리 : 'get-product' 쿼리를 먼저 취소 ->  예전 'get-product' 데이터를 previousProductData에 불러옴 -> setQueryData를 이용해서 직접 데이터를 수작업으로 쿼리에 업데이트

+ uuid는 id가 랜덤 텍스트일 경우 작성하기 위한 패키지

previousProductData 를 리턴해 주는 이유: onError 항목에서 처리하기 위함!

-> 실제 POST 액션이 에러가 나서 취소가 됐을 때 원상복구하기 위함

previousProductData로 원상복구(setQueryData)

onError 인자들: 1. error 객체 2. 사용자가 입력한 데이터 3. query 컨텍스트 

onError: (_error, _product, context) => {
  queryClient.setQueryData('get-product', context.previousProductData)
}

onSettled이 실행되는 조건 -> post 액션이 에러 or 성공

get-product 쿼리를 무효화시켜서 쿼리를 다시 fetching 하도록 하면 됨

 

onSettled: () => {
  queryClient.invalidateQueries('get-product')
},

완성 코드

export const useAddProduct = () => {
  const queryClient = useQueryClient()
  return useMutation(addProduct, {
    onMutate: async (newProduct) => {
      await queryClient.cancelQueries("get-product");
      const previousProductData = queryClient.getQueryData("get-product");
      queryClient.setQueryData("get-product", (oldProductData) => {
        return {
          ...oldProductData,
          data: [...oldProductData.data.items, { id: uuid(), ...newProduct }],
        };
      });
      return {
        previousProductData,
      };
    },
    onError: (_error, _product, context) => {
      queryClient.setQueryData('get-product', context.previousProductData)
    },
    onSettled: () => {
      queryClient.invalidateQueries('get-product')
    },
  })
}

출처: https://mycodings.fly.dev/blog

 

드리프트의 myCodings

드리프트의 myCodings.fly.dev!

mycodings.fly.dev

 

저작자표시 (새창열림)

'React' 카테고리의 다른 글

[React-Query]prefetchQuery  (0) 2025.02.25
useEffect의 의존성 배열  (0) 2024.03.10
useRef  (0) 2024.03.10
리액트 개념 공부  (0) 2024.02.09
리액트 상태 관리 라이브러리  (0) 2024.01.06
'React' 카테고리의 다른 글
  • useEffect의 의존성 배열
  • useRef
  • 리액트 개념 공부
  • 리액트 상태 관리 라이브러리
버그잡는고양이발
버그잡는고양이발
주니어 개발자입니다!
  • 버그잡는고양이발
    지극히평범한개발블로그
    버그잡는고양이발
  • 전체
    오늘
    어제
    • 분류 전체보기 (381)
      • React (16)
      • Next.js (5)
      • Javascript (5)
      • Typescript (4)
      • Node.js (2)
      • Cs (16)
      • 트러블 슈팅 (5)
      • Html (1)
      • Css (3)
      • Django (0)
      • vue (0)
      • Java (1)
      • Python (0)
      • 독서 (1)
      • 기타 (3)
      • 백준 (192)
      • swea (31)
      • 프로그래머스 (30)
      • 이코테 (4)
      • 99클럽 코테 스터디 (30)
      • ssafy (31)
      • IT기사 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 인기 글

  • 태그

    99클럽
    항해99
    Til
    코딩테스트준비
    개발자취업
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
버그잡는고양이발
React Query
상단으로

티스토리툴바