๐ŸŽฏ KPT ํšŒ๊ณ ๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ๋ฌธ์ œ์ ๊ณผ ํ•ด์•ผํ•  ์ผ์„ ํŒŒ์•…ํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“— Today I Learned

KPT ํšŒ๊ณ  (Keep, Problem, Try)

KPT๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœํŒ€์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ํšŒ๊ณ  ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜๋กœ, ํ”„๋กœ์ ํŠธ๋‚˜ ์ž‘์—…์„ ๋Œ์•„๋ณด๋ฉฐ ์–ด๋–ค ์ ์ด ์ข‹์•˜๋Š”์ง€(KEEP), ์–ด๋–ค ๋ฌธ์ œ์ ์ด ์žˆ์—ˆ๋Š”์ง€(PROBLEM), ๊ทธ๋ฆฌ๊ณ  ์•ž์œผ๋กœ ๋ฌด์—‡์„ ์‹œ๋„(KRY)ํ•ด๋ณผ์ง€ ์ •๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•๋ก ์ž…๋‹ˆ๋‹ค.
์ถœ์ฒ˜: neuromagic

KEEP

์•ž์œผ๋กœ๋„ ์œ ์ง€ํ•˜๊ณ  ์‹ถ์€ ๊ธ์ •์ ์ธ ์š”์†Œ์ž…๋‹ˆ๋‹ค.

  • ํšŒ์›๊ฐ€์ž… ~ ์ฃผ๋ฌธ๊นŒ์ง€ ๊ฒฝํ—˜

  • ์ƒ์‚ฐ์„ฑ ๊ณ ๋ ค

  • ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๋งž์ถ”๊ธฐ


PROBLEM

๋ฌธ์ œ์ ์ด๋‚˜ ๊ฐœ์„ ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.

  • ํ…Œ๋งˆ์Šค์œ„์ฒ˜ ๋ฏธ์ ์šฉ

  • import alias๊ฐ€ ์•„์‰ฝ๋‹ค.

  • ์ค‘๋ณต์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค.

  • css ์Šคํƒ€์ผ๋ง ์ •๋ฆฌ

  • ๋‹ค์–‘ํ•œ ui ํŒจํ„ด์„ ๋‹ค๋ฃจ์ง€ ๋ชปํ•จ


TRY

์‹œ๋„ํ•ด๋ณผ ๊ฐœ์„  ์•„์ด๋””์–ด์ž…๋‹ˆ๋‹ค.

  • alias ์ ์šฉ

  • ์ค‘๋ณต์ฝ”๋“œ ์ œ๊ฑฐ

  • useAuth ํ›…

  • react-query ์ ์šฉ

  • ํ…Œ๋งˆ์Šค์œ„์ฒ˜ ์žฌ๋ฐฐ์น˜

  • ๋‹ค์–‘ํ•œ ui ํŒจํ„ด




Alias ์„ค์ •ํ•˜๊ธฐ

Alias๋Š” '๋ณ„๋ช…', 'ํ†ต์นญ'์ด๋ผ๋Š” ๋œป์œผ๋กœ ๋ณต์žกํ•œ ์ƒ๋Œ€๊ฒฝ๋กœ(../../../components/Example) ๋Œ€์‹ ์— ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์งง๊ณ  ์˜๋ฏธ ์žˆ๋Š” ๊ฒฝ๋กœ(@/components/Example)๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์„ค์ •์ž…๋‹ˆ๋‹ค.


CRACO

CRACO(Create React App Configuration Override)๋Š” Create React App(CRA) ํ”„๋กœ์ ํŠธ์—์„œ ๊ธฐ๋ณธ ์ œ๊ณต๋˜๋Š” ์„ค์ •๋“ค์„ eject ํ•˜์ง€ ์•Š๊ณ ๋„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.

๐Ÿค” ์™œ craco๋ฅผ ์‚ฌ์šฉํ• ๋ผ?

CRA(Create React App)๋Š” Webpack ์„ค์ •์„ ์ง์ ‘ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์—, react-scripts๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ alias ์„ค์ •์ด ์ œํ•œ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด CRACO๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด webpack, babel, eslint ๋“ฑ์˜ ์„ค์ •์„ ์ง์ ‘ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ ๋„ ์‰ฝ๊ฒŒ ์˜ค๋ฒ„๋ผ์ด๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์„ค์น˜ ๋ฐฉ๋ฒ•

npm i -D @craco/craco craco-alias
  • @craco/craco : CRA์˜ ์ˆจ๊ฒจ์ง„ ์„ค์ •์„ ์ง์ ‘ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋„๊ตฌ

  • craco-alias : ๊ฒฝ๋กœ alias ์„ค์ •์„ ์‰ฝ๊ฒŒ ํ•ด์ฃผ๋Š” craco์šฉ ํ”Œ๋Ÿฌ๊ทธ์ธ

๐Ÿค” craco-alias๋Š” ์–ด๋–ค ์ผ์„ ๋„์™€์ค„๊นŒ?

craco-alias ์—†์ด alias ์„ค์ •์„ ํ•˜๋ ค๊ณ  ํ•˜๋ฉด webpack๊ณผ tsconfig ์–‘์ชฝ ๋‹ค ์ˆ˜๋™ ์„ค์ •ํ•ด์•ผ ํ•˜๊ณ , ์‹ค์ˆ˜๋‚˜ ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค. craco-alias๊ฐ€ ์žˆ์œผ๋ฉด, tsconfig.paths.json์— ํ•œ ๋ฒˆ๋งŒ ์„ค์ •ํ•˜๋ฉด webpack๊ณผ tsconfig๊ฐ€ ์ž๋™์œผ๋กœ ๋™๊ธฐํ™”๋˜๊ธฐ ๋•Œ๋ฌธ์— ํŽธ๋ฆฌํ•ด์ง‘๋‹ˆ๋‹ค.


tsconfig.path.json

ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ ํ•ด๋‹น ๋‚ด์šฉ์„ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
  • baseUrl: "." : ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • @/* โ†’ @๋ฅผ src ํด๋”์˜ ๋ณ„์นญ์œผ๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. (ex: @/components/Box โ†’ src/components/Box)


tsconfig.json

์„ค์ •ํ•œ tsconfig.paths.json ํŒŒ์ผ์„ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.

...
  "extends": "./tsconfig.paths.json",

craco.config.js

const cracoAlias = require('craco-alias');

module.exports = {
  plugins: [
    {
      plugin: cracoAlias,
      option: {
        source: 'tsconfig',
        baseUrl: '.',
        tsConfigPath: 'tsconfig.paths.json',
        debug: false,
      },
    },
  ],
};

package.json

scripts๋ฅผ craco ๊ธฐ๋ฐ˜์œผ๋กœ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.

...
  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    ...
  }



์ค‘๋ณต ์ œ๊ฑฐ

Layout๊ณผ Error๋ฅผ ๊ฐ ํŽ˜์ด์ง€๋งˆ๋‹ค ์ค‘๋ณตํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด route ๋ฐฐ์—ด์„ map()์œผ๋กœ ์ˆœํšŒํ•˜๋ฉฐ wrapping ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

const routeList = [
  {
    path: '/',
    element: <Home />,
  },
  {
    path: '/books',
    element: <Books />,
  },
  {
    path: '/books/:bookId',
    element: <BookDetail />,
  },
  {
    path: '/cart',
    element: <Cart />,
  },
  {
    path: '/order',
    element: <Order />,
  },
  {
    path: '/orderlist',
    element: <OrderList />,
  },
  {
    path: '/signup',
    element: <Signup />,
  },
  {
    path: '/reset',
    element: <ResetPassword />,
  },
  {
    path: '/login',
    element: <Login />,
  },
];

const newRouteList = routeList.map((item) => {
  return {
    ...item,
    element: <Layout>{item.element}</Layout>,
    errorElement: <Error />,
  };
});

const router = createBrowserRouter(newRouteList);

HTTP Request Handler

๋ฐ˜๋ณต๋˜๋Š” HTTP ์š”์ฒญ ๋ฉ”์„œ๋“œ๋ฅผ ํ•˜๋‚˜์˜ ํ•จ์ˆ˜๋กœ ๋ฌถ์–ด method, url, payload๋งŒ ๋„˜๊ธฐ๋ฉด ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

// http.ts
type RequestMethod = 'get' | 'post' | 'put' | 'delete';

export const requestHandler = async <T>(
  method: RequestMethod,
  url: string,
  payload?: T
) => {
  let response;

  switch (method) {
    case 'post':
      response = await httpClient.post(url, payload);
      break;
    case 'get':
      response = await httpClient.get(url);
      break;
    case 'put':
      response = await httpClient.put(url, payload);
      break;
    case 'delete':
      response = await httpClient.delete(url);
      break;
  }

  return response.data;
};

์‚ฌ์šฉ ์˜ˆ์‹œ

/orders๋กœ post ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค.

import { requestHandler } from './http';

export const order = async (orderData: OrderSheet) => {
  return await requestHandler<OrderSheet>('post', '/orders', orderData);
};



snippet

์ž์ฃผ ์“ฐ๋Š” ์ฝ”๋“œ ์กฐ๊ฐ์„ ๋ฏธ๋ฆฌ ์ €์žฅํ•ด๋‘๊ณ , ๋‹จ์ถ•์–ด๋กœ ๋น ๋ฅด๊ฒŒ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.

์„ค์น˜ ๋ฐ ์‚ฌ์šฉ๋ฐฉ๋ฒ•

1๏ธโƒฃSnippet Generator extension์„ ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.

import styled from 'styled-components';

export default function ${TM_FILENAME_BASE}() {
  return <Styled${TM_FILENAME_BASE}>${1:๋‚ด์šฉ}</Styled${TM_FILENAME_BASE}>;
}

const Styled${TM_FILENAME_BASE} = styled.div`
  ${2:}
`;
  • ${TM_FILENAME_BASE} โ†’ ์ง€๊ธˆ ํŒŒ์ผ๋ช…(ํ™•์žฅ์ž ์ œ์™ธ)์„ ์ž๋™์œผ๋กœ ์‚ฝ์ž…ํ•ด์ค๋‹ˆ๋‹ค.

  • ${1:๋‚ด์šฉ} โ†’ ์ฒซ ๋ฒˆ์งธ ์ปค์„œ ํฌ์ง€์…˜์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

  • ${2:} โ†’ ๋‘ ๋ฒˆ์งธ ์ปค์„œ ํฌ์ง€์…˜์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.


2๏ธโƒฃ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ ๋’ค์— ์ „์ฒด ๋“œ๋ž˜๊ทธ๋ฅผ ํ•˜์—ฌ ์šฐํด๋ฆญํ•œ ๋’ค Generate Snippet์„ ๋ˆŒ๋Ÿฌ์ค๋‹ˆ๋‹ค.

3๏ธโƒฃ์ฝ”๋“œ์— ํ•ด๋‹น๋˜๋Š” ์–ธ์–ด๋ฅผ ์„ ํƒํ•ด์ค๋‹ˆ๋‹ค.


4๏ธโƒฃprefix(๋‹จ์ถ•์–ด)๋ฅผ ์ ์–ด์ฃผ๋ฉด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.


์ƒ์„ฑ๋œ snippet ํ…œํ”Œ๋ฆฟ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

{
  "Styled Component Template": {
    "prefix": "scomp",
    "body": [
      "import styled from 'styled-components';",
      "",
      "export default function ${TM_FILENAME_BASE}() {",
      "  return <Styled${TM_FILENAME_BASE}>${1:๋‚ด์šฉ}</Styled${TM_FILENAME_BASE}>;",
      "}",
      "",
      "const Styled${TM_FILENAME_BASE} = styled.div`",
      "  ${2:}",
      "`;"
    ],
    "description": "Styled-component base template with file name"
  }
}
  • prefix : ํ˜ธ์ถœ ํ‚ค์›Œ๋“œ

  • body : ์‹ค์ œ ์ฝ”๋“œ ํ…œํ”Œ๋ฆฟ

  • description : ์„ค๋ช…




useAuth.ts

๋กœ๊ทธ์ธ, ํšŒ์›๊ฐ€์ž…, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ดˆ๊ธฐํ™” ๋“ฑ ์‚ฌ์šฉ์ž ์ธ์ฆ ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ํ•œ ๊ณณ์— ์ •๋ฆฌํ•ด ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ด๊ณ  ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

import { login, resetPassword, resetRequest, signup } from '@/api/auth.api';
import { LoginProps } from '@/pages/Login';
import { useAuthStore } from '@/store/authStore';
import { useNavigate } from 'react-router-dom';
import { useAlert } from './useAlert';
import { SignUpProps } from '@/pages/Signup';
import { useState } from 'react';

export const useAuth = () => {
  const { storeLogin, storeLogout, isLoggedIn } = useAuthStore();
  const navigate = useNavigate();
  const { showAlert } = useAlert();

  const userLogin = (data: LoginProps) => {
    login(data).then(
      (res) => {
        storeLogin(res.token);
        showAlert('๋กœ๊ทธ์ธ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
        navigate('/');
      },
      (error) => {
        showAlert('๋กœ๊ทธ์ธ์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.');
      }
    );
  };

  const userSignup = (data: SignUpProps) => {
    signup(data).then((data) => {
      showAlert('ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
      navigate('/login');
    });
  };

  const userResetPassword = (data: SignUpProps) => {
    resetPassword(data).then(() => {
      showAlert('๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
      navigate('/login');
    });
  };

  const [resetRequested, setResetRequested] = useState(false);

  const userResetRequest = (data: SignUpProps) => {
    resetRequest(data).then(() => {
      setResetRequested(true);
    });
  };

  return {
    userLogin,
    userSignup,
    userResetPassword,
    userResetRequest,
    resetRequested,
  };
};



react-query

useEffect์™€ useState๋ฅผ ์‚ฌ์šฉํ•ด API ์š”์ฒญ์„ ๊ด€๋ฆฌํ•˜๋˜ ๊ธฐ์กด ๋ฐฉ์‹์—์„œ react-query๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์บ์‹ฑ, ์ƒํƒœ๊ด€๋ฆฌ, ๋ฆฌํŒฉํ† ๋ง์ด ์ˆ˜์›”ํ•ด์ง‘๋‹ˆ๋‹ค.

์„ค์น˜ ๋ฐฉ๋ฒ•

npm i @tanstack/react-query

App.ts ์„ค์ •

QueryClientProvider๋กœ ๊ฐ์‹ธ์•ผ ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <BookStoreThemeProvider>
        <ThemeSwitcher />
        <RouterProvider router={router} />
      </BookStoreThemeProvider>
    </QueryClientProvider>
  );
}

๊ธฐ์กด ๋ฐฉ์‹

๊ธฐ์กด์—๋Š” useState + useEffect๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋กœ๋”ฉ/์—๋Ÿฌ ์ƒํƒœ ์ง์ ‘ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

  • ์บ์‹ฑ์ด ์—†์–ด ํ™”๋ฉด ์ด๋™ ์‹œ๋งˆ๋‹ค ์žฌ์š”์ฒญ์„ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

  • ์ค‘๋ณต ์ฝ”๋“œ๊ฐ€ ๋งŽ์•„์ง‘๋‹ˆ๋‹ค.

  const [books, setBooks] = useState<Book[]>([]);
  const [pagination, setPagination] = useState<Pagination>({
    totalCount: 0,
    currentPage: 1,
  });

  const [isEmpty, setIsEmpty] = useState(true);

  useEffect(() => {
    const params = new URLSearchParams(location.search);

    fetchBooks({
      category_id: params.get(QUERYSTRING.CATEGORY_ID)
        ? Number(params.get(QUERYSTRING.CATEGORY_ID))
        : undefined,
      news: params.get(QUERYSTRING.NEWS) ? true : undefined,
      currentPage: params.get(QUERYSTRING.PAGE)
        ? Number(params.get(QUERYSTRING.PAGE))
        : 1,
      limit: LIMIT,
    }).then((res) => {
      setBooks(res.books);
      setPagination(res.pagination);
      setIsEmpty(res.books.length === 0);
    });
  }, [location.search]);

๐Ÿค” ์บ์‹ฑ(Caching)์ด ๋ญ์ง€..?

ํ•œ ๋ฒˆ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ด๋’€๋‹ค๊ฐ€, ๋‹ค์Œ์— ๋˜ ํ•„์š”ํ•  ๋•Œ ๋น ๋ฅด๊ฒŒ ๊บผ๋‚ด ์“ฐ๋Š” ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค.
์บ์‹ฑ์ด ์—†์œผ๋ฉด ์•„๋ฌด๋ฆฌ ์ด๋ฏธ ๋ณธ ํŽ˜์ด์ง€๋ผ๋„ ๋‹ค์‹œ fetch๋ฅผ ํ•˜๋Š” ๋ถˆํ•„์š”ํ•œ ์ž‘์—…์„ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

React Query๋Š” ํ•œ ๋ฒˆ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ JavaScript ๋ฉ”๋ชจ๋ฆฌ(Heap)์— ์ €์žฅํ•ด๋‘๊ณ  ๊ฐ™์€ queryKey๋กœ ์š”์ฒญ์„ ํ•˜๊ฒŒ ๋˜๋ฉด ์„œ๋ฒ„์— ์•ˆ ๋ณด๋‚ด๊ณ  ๋ฐ”๋กœ ๋ณด์—ฌ์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


React Query ๋ฐฉ์‹

๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋” ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ , isLoading, isError ๋“ฑ ์ƒํƒœ๋„ ํ•จ๊ป˜ ์ œ๊ณต๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  const { data: booksData, isLoading: isBooksLoading } = useQuery({
    queryKey: ['books', location.search],
    queryFn: () =>
      fetchBooks({
        category_id: params.get(QUERYSTRING.CATEGORY_ID)
          ? Number(params.get(QUERYSTRING.CATEGORY_ID))
          : undefined,
        news: params.get(QUERYSTRING.NEWS) ? true : undefined,
        currentPage: params.get(QUERYSTRING.PAGE)
          ? Number(params.get(QUERYSTRING.PAGE))
          : 1,
        limit: LIMIT,
      }),
  });
  • queryKey : ์ฟผ๋ฆฌ ๊ตฌ๋ถ„์„ ์œ„ํ•œ ์‹๋ณ„์ž

  • queryFn : ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜




โœ๏ธ ํšŒ๊ณ 

์บ์‹ฑ์— ๋Œ€ํ•œ ๊ฐœ๋…์„ ์ตํžˆ๊ณ  ๋ถˆํ•„์š”ํ•œ fetch์™€ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์ƒ๊ธด react-query์— ๋Œ€ํ•ด ์ตํ˜”๋‹ค. ํ•ญ์ƒ ์บ์‹ฑ์ด ๋ญ”์ง€ ๊ถ๊ธˆํ–ˆ๋Š”๋ฐ ์กฐ๊ธˆ์€ ์•Œ๊ฒŒ๋œ ๊ฒƒ ๊ฐ™์•„ ๊ธฐ์˜๋‹ค. ใ…Žใ…Ž

profile
๐ŸŒฑ๊ฐœ๋ฐœ ๊ธฐ๋ก์žฅ

0๊ฐœ์˜ ๋Œ“๊ธ€