๐งญ Route
React Router๋ฅผ ์ฌ์ฉํด ํ์ด์ง๋ค์ ๋ผ์ฐํธ๋ฅผ ์์ฑํด์ฃผ๋ ค ํ๋ค.
์ค์น
`npm install react-router-dom @types/react-router-dom --save`
์ฌ์ฉ
// App.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/books",
element: <div>๋์ ๋ชฉ๋ก</div>
}
])
function App() {
return (
<BookStoreThemeProvider>
<Layout>
<RouterProvider router={router} />
</Layout>
</BookStoreThemeProvider>
);
}
์ฃผ์์ ๋ง๋ ํ๋ฉด์ด ์ ๋์ค๊ณ ์๋ค.
๋ง์ฝ ๋ผ์ฐํฐ์ ์ค์ ํด์ฃผ์ง ์์ ์ฃผ์๋ผ๋ฉด react router dom์ ๊ธฐ๋ณธ ์๋ฌํ๋ฉด์ด ๋ํ๋๋ค.
๊ณตํต์ ์ผ๋ก ์ฌ์ฉ๋ ์ด ์๋ฌ ํ๋ฉด๋ ๋ฐ๋ก ๋ง๋ค์ด์ฃผ๋ ค๊ณ ํ๋ค.
import { useRouteError } from "react-router-dom";
interface RouterError {
statusText?: string;
message?: string;
}
function Error() {
const error = useRouteError() as RouterError;
return (
<div>
<h1>์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</h1>
<p>๋ค์๊ณผ ๊ฐ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</p>
<p>{error.statusText || error.message}</p>
</div>
)
}
์ด๋ค ์๋ฌ๊ฐ ๋ฌ๋์ง ํจ๊ป ํ์ํด์ฃผ๊ธฐ ์ํด `useRouteError`๋ฅผ ์ฌ์ฉํ๋ค.
{
path: "/",
element: <Home />,
errorElement: <Error />
},
๋ฃจํธ ํ์ด์ง์ ์๋ฌ ํ๋ฉด์ ์ ์ฉํด์ฃผ๋ฉด ๋ชจ๋ ์์ธ ๊ฒฝ๋ก์ ๋ํด ์๋ฌ ํ๋ฉด์ด ๋ํ๋๊ฒ ๋๋ค.
๊ทธ๋ฐ๋ฐ ํ์ฌ ํค๋๋ฅผ ํตํด ์ฃผ์๊ฐ ๋ฌ๋ผ์ง ๋๋ง๋ค ํ๋ฉด์ด ๊น๋นก๊ฑฐ๋ฆฌ๋ ํ์์ด ์๋ค.
ํ์ด์ง ์ด๋์ด ์์ฐ์ค๋ฝ๊ฒ ๋ ์ ์๋๋ก ์์ ์ ํด์ฃผ๋ ค ํ๋ค.
<h1 className="logo">
<Link to="/">
<img src={logo} alt="book store" />
</Link>
</h1>
๊ธฐ์กด aํ๊ทธ๋ ๋ธ๋ผ์ฐ์ ๊ฐ ์ ์ฒด ํ์ด์ง๋ฅผ ์๋ก ๋ถ๋ฌ์ค๋ ๋ฐฉ์์ด๋ผ ์ด๋ํ ๋๋ง๋ค ์ ์ฒด๋ฅผ ๋ค์ ๋ ๋๋งํ์ฌ ๊น๋นก์์ด ์๊ฒผ๋ค.
๊ทธ๋์ ํ์ํ ์ปดํฌ๋ํธ๋ง ๊ต์ฒดํด์ฃผ๋ SPA๋ฐฉ์์ Linkํ๊ทธ๋ก ๊ต์ฒดํด์ฃผ์๋ค.
Linkํ๊ทธ๋ฅผ ์ฌ์ฉํ ๋์๋ `href` ๋์ `to`๋ฅผ ์ฌ์ฉํ๋ค.
const router = createBrowserRouter([
{
path: "/",
element: <Layout><Home /></Layout>,
errorElement: <Error />
},
{
path: "/books",
element: <Layout><div>๋์ ๋ชฉ๋ก</div></Layout>
}
])
function App() {
return (
<BookStoreThemeProvider>
<RouterProvider router={router} />
</BookStoreThemeProvider>
);
}
ํค๋์์ ๋ผ์ฐํฐ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด `RouterProvider`์์ ๋ค์ด๊ฐ์ผํ๊ธฐ ๋๋ฌธ์ `Layout`์ ๋ผ์ฐํฐ ์ค์ ํด์ฃผ๋ ๊ณณ์ผ๋ก ์ด๋์์ผฐ๋ค.
๐ ๋ชจ๋ธ ์ ์
์ถํ API ์์ฒญ์ ํ๋ฉฐ ๋ฐ์ ๋ฐ์ดํฐ๋ค์ ํ์ ์ ์ ์ํ๋ ๋ชจ๋ธ๋ค์ ์์ฑํ๋ค.
๋ํ์ ์ผ๋ก ๋์์ ๋ชจ๋ธ์ ๋ค์๊ณผ ๊ฐ๋ค.
export interface Book {
id: number;
title: string;
img: number;
category_id: number;
form: string;
isbn: string;
summary: string;
detail: string;
author: string;
pages: number;
contents: string;
price: number;
likes: number;
pubDate: string;
}
์ฌ๊ธฐ์ ๋์์ ์์ธ ์กฐํ์๋ ์นดํ ๊ณ ๋ฆฌ๋ช ๊ณผ ์ข์์ ์ฌ๋ถ๊ฐ ์ถ๊ฐ๋์ด์๋ค.
์ด๋ฐ ๊ฒฝ์ฐ ๊ฐ์ ์ ๋ณด๋ฅผ ๋ฐ๋ณตํด ์ ๊ธฐ๋ณด๋จ
export interface BookDetail extends Book {
categoryName: string;
liked: boolean
}
ํ์ฅ์ ์ฌ์ฉํด ์ถ๊ฐ์ ์ธ ์ ๋ณด๋ง ์ ๋ ฅํด์ฃผ๋ฉด ๊ฐ๋ตํ๊ฒ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.
๐ก API ํต์ ๋ชจ๋ ๊ตฌ์ฑ
๋ ์ด์ด๋ฅผ ๊ณ ๋ คํด ์ค๊ณํ๋ฉด
- ๋ ๋ ์์ญ์ ๊น๋ํ๊ฒ ์ ์ง ๊ฐ๋ฅ
- ํ ์ ํตํด ์ค๋ณต ์ฝ๋ ์ค์ด๊ณ ํ ์์์ ๋ฐ์ดํฐ ๊ฐ๊ณต ๋ก์ง์ ์ ๊ณต ๊ฐ๋ฅ
- Fetcher ์ญ์ ๋ถ๋ฆฌํด API๋ง๋ค ๋ฌ๋ผ์ง ์ ์๋ ์ค์ ์ด๋ ๋ณ๊ฒฝ์ฌํญ ๋ฑ์ ๋์ ๊ฐ๋ฅ
axios
- ๋ฐฑ์๋ API์ ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐ์ ์ ์๋๋ก ๋์์ฃผ๋ ๋๊ตฌ
- `npm i axios`
// http.ts
import axios, { AxiosRequestConfig } from 'axios';
const BASE_URL = 'http://localhost:9999';
const DEFAULT_TIMEOUT = 30000;
export const createClient = (config?: AxiosRequestConfig) => {
const axiosInstance = axios.create({
baseURL: BASE_URL,
timeout: DEFAULT_TIMEOUT,
headers: {
"content-type": "application/json",
},
withCredentials: true,
...config,
});
axiosInstance.interceptors.request.use(
(response) => {
return response;
},
(error) => {
return Promise.reject(error);
}
);
return axiosInstance;
};
export const httpClient = createClient();
API ์์ฒญ์ ๋ณด๋ผ ๋ ๊ณตํต์ผ๋ก ์ฌ์ฉํ ์ค์ ์ ๋ฏธ๋ฆฌ ์ ์ํ ํ์ผ
์ฌ๊ธฐ์ `config`๋ createClientํ ๋ BASE_URL์ด ๋ณ๊ฒฝ๋๊ฑฐ๋ ๊ธฐ์กด config๋ฅผ ์ค๋ฒ๋ผ์ด๋ํ ๋ ์ฌ์ฉ๋๋ค.
โ๏ธ ์นดํ ๊ณ ๋ฆฌ ์กฐํ API
// category.api.ts
export const fetchCategory = async () => {
const response = await httpClient.get<Category[]>('/category');
return response.data;
}
์์ ์ ์ํ httpClient๋ก '/category'๋ผ๋ ์ฃผ์๋ก GET์์ฒญ์ ํ๋ Fetcherํจ์์ด๋ค.
const [category, setCategory] = useState<Category[]>([]);
useEffect(() => {
fetchCategory().then((category) => {
setCategory(category);
});
}, []);
ํด๋น API ์๋ต ๋ฐ์ดํฐ๊ฐ ํ์ํ ํค๋์์ ํธ์ถํด์ฃผ์๋ค.
๊ทธ๋ฐ๋ฐ CORS์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
ํ๋ก ํธ ํฌํธ๋ 3000์ธ๋ฐ ๋ฐฑ์๋๋ 9999๋ก ์๋ก ๋ฌ๋ผ ๋ฐ์ํ ์๋ฌ์๋ค.
๋ฐ๋ผ์ ๋ฐฑ์๋ ์ฝ๋์ CORS ํ์ฉ ์ค์ ์ ํด์ฃผ์ด์ผ ํ๋ค.
๋จผ์ ๊ด๋ จ ์ค์ ์ ํด์ฃผ๊ธฐ ์ํด `npm install cors`๋ฅผ ์ค์นํด์ฃผ๊ณ
// app.js
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
์ ์ฝ๋๋ฅผ ์ถ๊ฐํด 3000ํฌํธ์ ๋ํด ํ์ฉ์ ํด์ค๋ค.
๊ทธ๋ผ ์ด์ 3000ํฌํธ์์๋ 9999๋ก ์์ฒญ์ด ์ ๋๋ค.
์ด๋ฒ์ ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์กฐํํ๋ API๋ฅผ ๋ค๋ฅธ ๊ณณ์์๋ ์ฌ์ฉํ ์ ์๋๋ก ํ ์ผ๋ก ์ ์ํ๋ ค๊ณ ํ๋ค.
export const useCategory = () => {
const [category, setCategory] = useState<Category[]>([]);
useEffect(() => {
fetchCategory().then((category) => {
if(!category) return;
const categoryWithAll = [
{
id: null,
name: "์ ์ฒด",
},
...category
];
setCategory(categoryWithAll);
});
}, []);
return { category };
};
ํค๋์์ APIํธ์ถํ๋ ์ฝ๋๋ฅผ ํ ์ผ๋ก ๊ฐ์ ธ์๋ค.
๋ฐํํ ๋์๋ ์ ์ฒด ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ถ๊ฐํ ํ ๋ฐํํด์ฃผ์๋ค.
const { category } = useCategory();
์ด์ ์นดํ ๊ณ ๋ฆฌ๊ฐ ํ์ํ ๊ณณ์์ `useCategory`ํ ์ผ๋ก ๋ฐ์์ ์ฌ์ฉํ๋ฉด ๋๋ค.
API๋ก ๋ฐ์์จ ์นดํ ๊ณ ๋ฆฌ์ ์ ์ฒด ์นดํ ๊ณ ๋ฆฌ๊น์ง ์ ๋์ค๊ณ ์๋ค.
โ๏ธ ํ์๊ฐ์
<form onSubmit={handleSubmit}>
<fieldset>
<InputText
placeholder="์ด๋ฉ์ผ"
inputType="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</fieldset>
<fieldset>
<InputText
placeholder="๋น๋ฐ๋ฒํธ"
inputType="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</fieldset>
<fieldset>
<Button type="submit" size="medium" schema="primary">
ํ์๊ฐ์
</Button>
</fieldset>
<div className="info">
<Link to="/reset">๋น๋ฐ๋ฒํธ ์ด๊ธฐํ</Link>
</div>
</form>
์ด๋ ๊ฒ ์์ฑํ๋ฉด ํ๋ ๊ฐ์๊ฐ ๋์ด๋จ์ ๋ฐ๋ผ ๊ด๋ฆฌํด์ผํ๋ ์ํ๊ฐ๊ณผ onChange๋ ๋์์ด ๋์ด๋๋ค.
๊ทธ๋ผ ์ํ๊ด๋ฆฌ๋ ์ด๋ ค์์ง๊ณ validation๋ ์ด๋ ค์ค์ง๊ฒ ๋๋ค.
์ด๋ฐ ํผ ๊ด๋ฆฌ๋ฅผ ๋์์ฃผ๋ React Hook Form์ด๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ค ํ๋ค.
React Hook Form - performant, flexible and extensible form library
Performant, flexible and extensible forms with easy-to-use validation.
react-hook-form.com
์ค์น
`npm install react-hook-form`
์ฌ์ฉ
import { useForm } from "react-hook-form";
interface SignupProps {
email: string;
password: string;
}
function Signup() {
const {
register,
handleSubmit,
formState: {errors}
} = useForm<SignupProps>();
const onSubmit = (data: SignupProps) => {
console.log(data);
};
return (
<>
<Title size='large'>ํ์๊ฐ์
</Title>
<SignupStyle>
<form onSubmit={handleSubmit(onSubmit)}>
<fieldset>
<InputText
placeholder="์ด๋ฉ์ผ"
inputType="email"
{...register("email", {required: true})}
/>
{errors.email && <p className="error-text">์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์.</p>}
</fieldset>
<fieldset>
<InputText
placeholder="๋น๋ฐ๋ฒํธ"
inputType="password"
{...register("password", {required: true})}
/>
{errors.password && <p className="error-text">๋น๋ฐ๋ฒํธ๋ฅผ์ ์
๋ ฅํด์ฃผ์ธ์.</p>}
</fieldset>
<fieldset>
<Button type="submit" size="medium" schema="primary">
ํ์๊ฐ์
</Button>
</fieldset>
<div className="info">
<Link to="/reset">๋น๋ฐ๋ฒํธ ์ด๊ธฐํ</Link>
</div>
</form>
</SignupStyle>
</>
);
}
์ ๋ ฅ์ ๋ฐ๋์ ํด์ผํ๋ค๋ ์์ฑ์ธ `required: true`๋ฅผ ์ค์ ํด์ ์ ๋ ฅํ์ง ์์์ ๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
์ด ์๋ฌ๋ฅผ ์ด์ฉํด ์ ๋ ฅํ์ง ์๊ณ ๋ฒํผ์ ๋๋ฅด๋ ๊ฒฝ์ฐ ๊ฐ์ ์ ๋ ฅํด๋ฌ๋ผ๋ ์๋ฌ ํ ์คํธ๊ฐ ๋ํ๋๋๋ก ํด์ฃผ์๋ค.
์ด์ ํ์๊ฐ์ ์ด ์๋ฃ๋๋ฉด ์ฑ๊ณตํ๋ค๋ alert์ฐฝ๊ณผ ํจ๊ป ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋์ด๊ฐ๋๋ก ํด์ฃผ๋ คํ๋ค.
export const signup = async(userData: SignupProps) => {
const response = await httpClient.post("/users/join", userData);
return response.data;
}
๋จผ์ ์ ๋ ฅ๋ฐ์ ๊ฐ์ ํ์๊ฐ์ API์ ๋ณด๋ด๊ณ
const onSubmit = (data: SignupProps) => {
signup(data).then((res) => {
// ์ฑ๊ณต
showAlert("ํ์๊ฐ์
์ด ์๋ฃ๋์์ต๋๋ค.");
navigate("/login");
})
};
์์ฒญ์ด ์ฑ๊ณตํ ๊ฒฝ์ฐ alert์ฐฝ์ ๋์ฐ๊ณ ๋ก๊ทธ์ธํ์ด์ง๋ก ์ด๋์ํค๋๋ก ํ๋ค.
export const useAlert = () => {
const showAlert = useCallback((massage: string) => {
window.alert(massage);
}, []);
return showAlert;
};
alert์ฐฝ์ ๊ฒฝ์ฐ ํ์ฅ์ฑ์ ์ํด ์ปค์คํ ํ ์ผ๋ก ์์ฑํ๋ค.
์์ง ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ๊ตฌํํ์ง ์์ ์ค๋ฅ ํ์ด์ง๊ฐ ๋์ค์ง๋ง ์ฃผ์๋ฅผ ๋ณด๋ฉด ์ ์ด๋๋์๋ค.
'๐๏ธ DevCourse > Frontend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[TIL] Week 13 ๋ฆฌ์กํธ ์ปดํฌ๋ํธ ์์ฑ ๊ธฐ์ด (0) | 2025.04.19 |
---|---|
[TIL] Week 13 ๋ฆฌ์กํธ ๋ ์ด์์ ๊ตฌ์ฑ๊ณผ ์คํ์ผ ์์คํ (0) | 2025.04.18 |
[TIL] Week 13 React ํ๋ก์ ํธ : CRA์ Vite (0) | 2025.04.17 |
[TIL] Week 13 ๋ฆฌ์กํธ๋ก Task ์์ฑ ์ฑ ๋ง๋ค๊ธฐ 4 (3) | 2025.04.16 |
[TIL] Week 13 ๋ฆฌ์กํธ๋ก Task ์์ฑ ์ฑ ๋ง๋ค๊ธฐ 3 (0) | 2025.04.15 |