2024년 11월 19일
회고 하는 이유
주요 학습 주제
타입과 모델
데이터 흐름
컴포넌트 작성
css 스타일링
커스텀 훅
생산성
상대 경로를 이용하면 규모가 커지는 프로젝트일수록 유지보수가 힘듦 (depths가 계속 깊어짐, path를 변경한다던지 하나의 폴더로 묶을때 문제가 생길 수 있음) → 절대 경로 이용
CRACO (CreateReactAppConfigurationOverride) 사용
CreateReactApp 설정을 override함 → 그 중 하나인 alias 를 이용해 절대 경로를 사용할 것임
설치
설정
tsconfig.paths.json 파일 생성
// tsconfig.paths.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
tsconfig.json 파일 수정
// tsconfig.paths.json
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"extends": "./tsconfig.paths.json",
"include": [
"src", "craco.config.js"
]
}
craco alias 파일 생성
// craco.config.js
const cracoAlias = require("craco-alias");
module.exports = {
plugins: [
{
plugin: cracoAlias,
options: {
source: "tsconfig",
baseUrl: ".",
tsConfigPath: "tsconfig.paths.json",
debug: false,
},
},
],
};
package.json 수정
// pacakge.json
... 생략 ...
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"type-check": "tsc --noEmit --skipLibCheck"
},
... 생략 ...
craco 설정 하면서 궁금했던 것
적용해보기
// components / books / BookItems.tsx
import styled from 'styled-components'
import { Book } from '@/models/book.model';
import { getImgSrc } from '@/utils/image';
import { formatNumber } from '@/utils/format';
import { FaHeart } from "react-icons/fa";
import { ViewMode } from '@/components/books/BooksViewSwitcher';
import { Link } from 'react-router-dom';
... 생략 ...
// App.tsx
... 생략 ...
const routeList = [
{
path: "/",
element: <Home />
},
{
path: "/books",
element: <Books />
},
{
path: "/signup",
element: <Signup />
},
{
path: "/reset",
element: <ResetPassword />
},
{
path: "/login",
element: <Login />
},
{
path: "/books/:bookId",
element: <BookDetail />
}
,{
path: "/carts",
element: <Cart />
},
{
path: "/order",
element: <Order />
},
{
path: "/orderlist",
element: <OrderList />
}
];
const router = createBrowserRouter(
routeList.map((item)=>{
return {
...item,
element: <Layout>{item.element}</Layout>,
errorElement: <Error />
}
})
);
... 생략 ...
// api / 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;
}
// api / order.api.ts
import { requestHandler } from './http';
export const order = async (orderData: OrderSheet) => {
return await requestHandler<OrderSheet>('post', '/orders', orderData);
}
export const fetchOrders = async () => {
return await requestHandler('get', '/orders');
}
export const fetchOrder = async (orderId: number) => {
return await requestHandler('get', '/orders/${orderId}');
}
import styled from "styled-components";
const $1 = () => {
return (
<$1Style>
<h1>$1</h1>
</$1Style>
);
}
const $1Style = styled.div``;
export default $1;
// hooks / useAuth.ts
import { LoginProps } from '@/pages/Login';
import { useAuthStore } from '../store/authStore';
import { useNavigate } from 'react-router-dom';
import { useAlert } from './useAlert';
import { login } from '@/api/auth.api';
export const useAuth = () => {
const navigate = useNavigate();
const {showAlert} = useAlert();
// 상태
const { storeLogin, storeLogout, isloggedIn } = useAuthStore();
// 메소드
const userLogin = (data:LoginProps) => {
login(data).then((res) => {
// 상태 변화
storeLogin(res.token);
showAlert("로그인 완료되었습니다.");
// window.location.href = "/";
navigate("/");
}, (error) => {
showAlert("로그인이 실패했습니다.");
});
}
// 리턴
return { userLogin }
}
// pages / Login.tsx
... 생략 ...
export interface LoginProps {
email : string;
password : string;
}
const Login = () => {
const { userLogin } = useAuth();
... 생략 ...
const onSubmit = (data: LoginProps) => {
userLogin(data);
};
return (
<>
<Title size="large">로그인</Title>
<SignupStyle>
<form onSubmit={handleSubmit(onSubmit)}>
... 생략 ...
// hooks / useAuth.tsx
... 생략 ...
const userSignup = (data:SignupProps) => {
signUp(data).then((res) => {
// 성공
showAlert('회원 가입이 완료되었습니다.');
navigate("/login");
})
}
// 리턴
return { userLogin, userSignup }
}
// pages / Signup.tsx
... 생략 ...
const Signup = () => {
const { userSignup } = useAuth();
const { register, handleSubmit, formState: {errors} } = useForm<SignupProps>();
const onSubmit = (data: SignupProps) => {
userSignup(data);
};
... 생략 ...
// hooks / useAuth.ts
... 생략 ...
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 }
}
// pages / ResetPassword.tsx
... 생략 ...
const ResetPassword = () => {
const { userResetPassword, userResetRequest, resetRequested } = useAuth();
... 생략 ...
const onSubmit = (data: SignupProps) => {
// if(resetRequested) {
// userResetPassword(data);
// } else {
// userResetRequest(data);
// }
resetRequested ? userResetPassword(data) : userResetRequest(data);
};
... 생략 ...
eact-query: 서버의 state 라이브러리
자동으로 데이터를 동기화 해주는 역할을 함
설치
사용
queryClient 작성
// api / queryClient.ts
import { QueryClient } from 'react-query';
export const queryClient = new QueryClient();
queryClientProvider 넣어주기
// App.tsx
... 생략 ...
import { QueryClientProvider } from 'react-query';
import { queryClient } from './api/queryClient';
... 생략 ...
function App() {
return(
<QueryClientProvider client={queryClient}>
<BookStoreThemeProvider>
<ThemeSwitcher />
<RouterProvider router={router} />
</BookStoreThemeProvider>
</QueryClientProvider>
);
}
export default App;
react-query 사용해서 데이터 사용하기
// hooks / useBooks.ts
... 생략 ...
export const useBooks = () => {
const location = useLocation();
const params = new URLSearchParams(location.search);
const { data: booksData, isLoading: isBooksLoading } = useQuery(["books", 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,
})
);
... 생략 ...
return {
books: booksData?.books,
// 받아오는 데이터가 undefined일 수 있기 때문에 옵셔널 체이닝으로 써줘야함
pagination: booksData?.pagination,
isEmpty: booksData?.books.length === 0,
isBooksLoading
} ;
};
// pages / Books.tsx
... 생략 ...
const Books = () => {
const { books, pagination, isEmpty, isBooksLoading } = useBooks();
console.log(isBooksLoading);
return (
<>
<Title size="large">도서 검색 결과</Title>
<BooksStyle>
<div className="filter">
<BooksFilter />
<BooksViewSwitcher />
</div>
{ !isEmpty && books && <BooksList books={books} /> }
// books가 undfined인 경우 방어
{ isEmpty && <BooksEmpty /> }
{ !isEmpty && pagination && <Pagination pagination={pagination} /> }
</BooksStyle>
</>
)
}
... 생략 ...
옵셔널 체이닝(?.), 널 병합 연산자 (??)까먹지 말기 : https://velog.io/@wlwl99/JavaScript-널-병합-연산자-옵셔널-체이닝
// components / common / Loading.tsx
import styled from 'styled-components';
import { FaSpinner } from 'react-icons/fa';
const Loading = () => {
return (
<LoadingStyle>
<FaSpinner />
</LoadingStyle>
);
}
const LoadingStyle = styled.div`
padding: 40px 0;
text-align: center;
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
svg {
width: 70px;
height: 70px;
fill: #ccc;
animation: rotate 1s linear infinite;
}
`;
export default Loading;
// pages / Books.tsx
... 생략 ...
const Books = () => {
const { books, pagination, isEmpty, isBooksLoading } = useBooks();
return (
<>
<Title size="large">도서 검색 결과</Title>
<BooksStyle>
<div className="filter">
<BooksFilter />
<BooksViewSwitcher />
</div>
{ isBooksLoading && <Loading /> }
{ !isEmpty && books && <BooksList books={books} /> }
{ isEmpty && <BooksEmpty /> }
{ !isEmpty && pagination && <Pagination pagination={pagination} /> }
</BooksStyle>
</>
)
}
... 생략 ...
// pages / Books.tsx
... 생략 ...
const Books = () => {
const { books, pagination, isEmpty, isBooksLoading } = useBooks();
if (isEmpty) {
return <BooksEmpty />
}
if (!books || !pagination || isBooksLoading) {
return <Loading />
}
return (
<>
<Title size="large">도서 검색 결과</Title>
<BooksStyle>
<div className="filter">
<BooksFilter />
<BooksViewSwitcher />
</div>
<BooksList books={books} />
<Pagination pagination={pagination} />
</BooksStyle>
</>
)
}
... 생략 ...