
๐ฏ KPT ํ๊ณ ๋ฅผ ํตํด ํ์ฌ ๋ฌธ์ ์ ๊ณผ ํด์ผํ ์ผ์ ํ์ ํ์ฌ ์ฝ๋๋ฅผ ๋ฆฌํฉํ ๋งํฉ๋๋ค.
KPT๋ ์ํํธ์จ์ด ๊ฐ๋ฐํ์์ ์ฃผ๋ก ์ฌ์ฉํ๋ ํ๊ณ ๋ฐฉ๋ฒ ์ค ํ๋๋ก, ํ๋ก์ ํธ๋ ์์
์ ๋์๋ณด๋ฉฐ ์ด๋ค ์ ์ด ์ข์๋์ง(KEEP), ์ด๋ค ๋ฌธ์ ์ ์ด ์์๋์ง(PROBLEM), ๊ทธ๋ฆฌ๊ณ ์์ผ๋ก ๋ฌด์์ ์๋(KRY)ํด๋ณผ์ง ์ ๋ฆฌํ๋ ๋ฐฉ๋ฒ๋ก ์
๋๋ค.
์ถ์ฒ: neuromagic
์์ผ๋ก๋ ์ ์งํ๊ณ ์ถ์ ๊ธ์ ์ ์ธ ์์์ ๋๋ค.
ํ์๊ฐ์ ~ ์ฃผ๋ฌธ๊น์ง ๊ฒฝํ
์์ฐ์ฑ ๊ณ ๋ ค
๋ฐ์ดํฐ ํ๋ฆ ๋ง์ถ๊ธฐ
๋ฌธ์ ์ ์ด๋ ๊ฐ์ ์ด ํ์ํ ๋ถ๋ถ์ ๋๋ค.
ํ ๋ง์ค์์ฒ ๋ฏธ์ ์ฉ
import alias๊ฐ ์์ฝ๋ค.
์ค๋ณต์ฝ๋๊ฐ ์๋ค.
css ์คํ์ผ๋ง ์ ๋ฆฌ
๋ค์ํ ui ํจํด์ ๋ค๋ฃจ์ง ๋ชปํจ
์๋ํด๋ณผ ๊ฐ์ ์์ด๋์ด์ ๋๋ค.
alias ์ ์ฉ
์ค๋ณต์ฝ๋ ์ ๊ฑฐ
useAuth ํ
react-query ์ ์ฉ
ํ ๋ง์ค์์ฒ ์ฌ๋ฐฐ์น
๋ค์ํ ui ํจํด
Alias๋ '๋ณ๋ช
', 'ํต์นญ'์ด๋ผ๋ ๋ป์ผ๋ก ๋ณต์กํ ์๋๊ฒฝ๋ก(../../../components/Example) ๋์ ์ ํ๋ก์ ํธ ๋ฃจํธ๋ฅผ ๊ธฐ์ค์ผ๋ก ์งง๊ณ ์๋ฏธ ์๋ ๊ฒฝ๋ก(@/components/Example)๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํด์ฃผ๋ ์ค์ ์
๋๋ค.
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๊ฐ ์๋์ผ๋ก ๋๊ธฐํ๋๊ธฐ ๋๋ฌธ์ ํธ๋ฆฌํด์ง๋๋ค.
ํ์ผ์ ๋ง๋ค์ด์ ํด๋น ๋ด์ฉ์ ๋ฃ์ด์ค๋๋ค.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
baseUrl: "." : ํ๋ก์ ํธ ๋ฃจํธ๋ฅผ ๊ธฐ์ค์ผ๋ก ์ค์ ํฉ๋๋ค.
@/* โ @๋ฅผ src ํด๋์ ๋ณ์นญ์ผ๋ก ์ง์ ํฉ๋๋ค. (ex: @/components/Box โ src/components/Box)
์ค์ ํ tsconfig.paths.json ํ์ผ์ ๋ฃ์ด์ค๋๋ค.
...
"extends": "./tsconfig.paths.json",
const cracoAlias = require('craco-alias');
module.exports = {
plugins: [
{
plugin: cracoAlias,
option: {
source: 'tsconfig',
baseUrl: '.',
tsConfigPath: 'tsconfig.paths.json',
debug: false,
},
},
],
};
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 ์์ฒญ ๋ฉ์๋๋ฅผ ํ๋์ ํจ์๋ก ๋ฌถ์ด 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);
};
์์ฃผ ์ฐ๋ ์ฝ๋ ์กฐ๊ฐ์ ๋ฏธ๋ฆฌ ์ ์ฅํด๋๊ณ , ๋จ์ถ์ด๋ก ๋น ๋ฅด๊ฒ ๋ถ๋ฌ์ค๋ ๊ธฐ๋ฅ์ ๋๋ค.
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 : ์ค๋ช
๋ก๊ทธ์ธ, ํ์๊ฐ์ , ๋น๋ฐ๋ฒํธ ์ด๊ธฐํ ๋ฑ ์ฌ์ฉ์ ์ธ์ฆ ๊ด๋ จ ๊ธฐ๋ฅ์ ํ ๊ณณ์ ์ ๋ฆฌํด ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๊ณ ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํฉ๋๋ค.
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,
};
};
useEffect์ useState๋ฅผ ์ฌ์ฉํด API ์์ฒญ์ ๊ด๋ฆฌํ๋ ๊ธฐ์กด ๋ฐฉ์์์ react-query๋ฅผ ์ฌ์ฉํ๋ฉด ์บ์ฑ, ์ํ๊ด๋ฆฌ, ๋ฆฌํฉํ ๋ง์ด ์์ํด์ง๋๋ค.
npm i @tanstack/react-query
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๋ก ์์ฒญ์ ํ๊ฒ ๋๋ฉด ์๋ฒ์ ์ ๋ณด๋ด๊ณ ๋ฐ๋ก ๋ณด์ฌ์ฃผ๊ฒ ๋ฉ๋๋ค.
๋น๋๊ธฐ ๋ฐ์ดํฐ๋ฅผ ๋ ์ฝ๊ฒ ๊ด๋ฆฌํ๊ณ , 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์ ๋ํด ์ตํ๋ค. ํญ์ ์บ์ฑ์ด ๋ญ์ง ๊ถ๊ธํ๋๋ฐ ์กฐ๊ธ์ ์๊ฒ๋ ๊ฒ ๊ฐ์ ๊ธฐ์๋ค. ใ ใ