
๐ฏ ๋ฆฌ์กํธ ๋ผ์ฐํฐ(React Router)๋ฅผ ํตํด์ ๊ฒฝ๋ก๋ฅผ ์ค์ ํ๊ณ , API ํต์ ๊ณผ ๋ฐ์ดํฐ ๋ ์ด์ด๋ฅผ ํตํด ํ์๊ฐ์ ํ์ด์ง๋ฅผ ์ ์ํฉ๋๋ค.
React Router๋ ํ ์ฅ์ ์นํ์ด์ง ์( SPA )์์ ์ฌ์ฉ์๊ฐ ์ด๋ํ๋ ๊ฒ์ฒ๋ผ ๋ณด์ด๊ฒ ํ๋ฉด๋ง ๋ฐ๊ฟ์ฃผ๋๋ผ์ฐํ
๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค.

๐ค ์ React Router๋ฅผ ์ฌ์ฉํ๋ ๊ฑธ๊น?
React๋ ๊ธฐ๋ณธ์ ์ผ๋ก SPA ๊ตฌ์กฐ๋ผ ํ์ด์ง๋ฅผ ์ด๋ํ๋๋ผ๋ ์ ์ฒด ํ์ด์ง๋ฅผ ์๋ก๊ณ ์นจํ์ง ์์ต๋๋ค. ๊ทธ๋์
URL์ ๋ฐ๋ผ ๋ค๋ฅธ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๋URL๋ผ์ฐํ ์ ์ง์ํ์ง ์์ต๋๋ค.์ฌ์ฉ์ ๊ฒฝํ์ด๋ SEO, ๊ณต์ ๊ธฐ๋ฅ์์ ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์๊ธฐ ๋๋ฌธ์ ๋ผ์ฐํ ๊ธฐ๋ฅ์ ์ง์ํด์ฃผ๋ ๊ฒ์ด ๐"React Router" ์ ๋๋ค.
์ด๋ฏธ ๋ฆฌ์กํธ ํ ํ๋ฆฟ์ ๋ง๋ ์ํ์ด๋ฏ๋ก ๋๋ฒ์งธ ๋ฐฉ๋ฒ์ผ๋ก ์ค์นํด์ฃผ์์ต๋๋ค.
1๏ธโฃ ๊ณต์๋ฌธ์ ์ค์น ๋ฐฉ๋ฒ( React ํ ํ๋ฆฟ์ router ํฌํจ๋ ๋ฐฉ๋ฒ )
npx create-react-router@latest my-react-router-app
2๏ธโฃ React Router DOM ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋จ๋ ์ค์น ๋ฐฉ๋ฒ( react-router-dom / @types/react-router-dom )
npm install react-router-dom
npm install --save-dev @types/react-router-dom
๐ค
react-router/react-router-dom/react-router-native์ ์ฐจ์ด๊ฐ ๋ญ๊น?
ํจํค์ง ์ด๋ฆ ์ค๋ช react-router๋ผ์ฐํฐ์ ๊ธฐ๋ณธ ํต์ฌ (๋ผ์ฐํ ๋ก์ง๋ง ์์) react-router-dom๋ธ๋ผ์ฐ์ ์ฉ DOM ๋ผ์ฐํฐ (์น) react-router-nativeReact Native ์ ์ฉ ๋ผ์ฐํฐ (๋ชจ๋ฐ์ผ)
์ฌ์ฉ์ ํ๋ฉด์์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๊ณ ๊ด๋ฆฌํ์ฌ, ์ต์ข ์ ์ผ๋ก ์๋ฒ๋ก๋ถํฐ ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ํ์ํ๋ ์ ์ฒด ํ๋ฆ ๊ตฌ์กฐ์ ๋๋ค.
โโโโโโโโโโโโโโโโโโ
โ [1] View ๐ โ
โ Header โ โ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ง๋ ํ๋ฉด
โโโโโโฌโโโโโโโโโโโโ
โ ํ๋ฉด์ด ๋ฐ ๋ ๋ฐ์ดํฐ ํ์!
โผ
โโโโโโโโโโโโโโโโโโ
โ [2] Hook ๐ โ
โ useCategory() โ โ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ ์ปค์คํ
ํ
โโโโโโฌโโโโโโโโโโโโ
โ ๋ด๋ถ์์ Query ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํธ์ถ!
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ[3] Query Library โ๏ธโ
โ useQuery โ โ ์บ์ฑ/๋ก๋ฉ/์๋ฌ ๊ด๋ฆฌ ๋์ฐ๋ฏธ
โโโโโโฌโโโโโโโโโโโโโโโโโโ
โ Fetcher ํธ์ถ (์ง์ง ์์ฒญํจ์)
โผ
โโโโโโโโโโโโโโโโโโโโ
โ [4] Fetcher ๐ฌ โ
โ fetchCategory()โ โ fetch๋ก API ํธ์ถ
โโโโโโฌโโโโโโโโโโโโโโ
โ ๋ฐฑ์๋ํํ
๋ฐ์ดํฐ ์์ฒญ!
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ [5] API Server ๐ง โ
โ '/categories' โ โ ์ค์ ๋ฐ์ดํฐ ์๋ต (JSON)
โโโโโโโโโโโโโโโโโโโโโโโ
React์์ ํผ์ ์ฝ๊ฒ ๋ค๋ฃฐ ์ ์๊ฒ ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
npm install react-hook-form
import { useForm } from 'react-hook-form';
function SimpleForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => {
console.log(data); // { email: '...', password: '...' }
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
placeholder="์ด๋ฉ์ผ"
{...register('email', { required: true })}
/>
{errors.email && <span>์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์</span>}
<input
placeholder="๋น๋ฐ๋ฒํธ"
type="password"
{...register('password', { required: true })}
/>
{errors.password && <span>๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์</span>}
<button type="submit">์ ์ถ</button>
</form>
);
}
register : input์ ํผ์ ๋ฑ๋กํฉ๋๋ค. (์
๋ ฅ๊ฐ ์ถ์ + ์ ํจ์ฑ ๊ฒ์ฌ ํฌํจ)
handleSubmit : ํผ ์ ์ถ ์ ์คํ๋๋ ํจ์๋ฅผ ์ธ์๋ก ๋ฐ๋ ํจ์๋ก ๋ด๋ถ์ ์ผ๋ก ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํต๊ณผํ๋ฉด onSubmit์ด ์คํ๋ฉ๋๋ค.
formState.errors : ๊ฐ input์ ์๋ฌ ์ํ๋ฅผ ๊ฐ์ฒด๋ก ๊ด๋ฆฌํฉ๋๋ค.
{...register('email', { required: true })}
'email'์ด๋ผ๋ key ๊ฐ์ ์ถ์ ํด์ค โ ๋์ค์ onSubmit(data)์์ data.email๋ก ์ ๊ทผํฉ๋๋ค.ํด๋ผ์ด์ธํธ ์ธก์์ ์๋ฒ๋ก HTTP ์์ฒญ์ ์ฝ๊ฒ ๋ณด๋ด๊ณ ์๋ต์ ์ฒ๋ฆฌํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค. fetch API์ ๊ธฐ๋ฅ์ ํ์ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, ๋น๋๊ธฐ ์์ฒญ์ ๋ณด๋ด๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ ๋ฐ ํธ๋ฆฌํ ์ฅ์ ์ ๊ฐ์ง๊ณ ์์ต๋๋ค.
npm install axios
//http.ts
import axios, { AxiosRequestConfig } from 'axios';
import { getToken, removeToken } from '../store/authStore';
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',
Authorization: getToken() ? getToken() : '',
},
withCredentials: true, // ์ฟ ํค ํฌํจ ์ฌ๋ถ
...config, // ์ธ๋ถ ์ปค์คํฐ๋ง์ด์ง ํ์ฉ
});
axiosInstance.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers.Authorization = `${token}`;
}
return config;
});
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response.status === 401) {
removeToken();
window.location.href = '/login';
return;
}
return Promise.reject(error);
}
);
return axiosInstance;
};
export const httpClient = createClient();
BASE_URL : ๊ธฐ๋ณธ ์์ฒญ URL์ ๋ํ๋
๋๋ค.
DEFAULT_TIMEOUT : ์์ฒญ์ ๊ธฐ๋ค๋ฆด ์ ์๋ ์ต๋ ์๊ฐ(ms)์ ๋ํ๋
๋๋ค. ์์ฒญ์ด 30์ด ์์ ์ ๋๋๋ฉด timeout ๋ฉ๋๋ค. ๋ฌดํ ๋ก๋ฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์ฌ์ฉ๋ฉ๋๋ค.
axios.create()๋ฅผ ํตํด ์ปค์คํ
ํด๋ผ์ด์ธํธ ๊ฐ์ฒด(axiosInstance)๋ฅผ ์์ฑํด์ค๋๋ค.
headers: { 'Content-Type': 'application/json' } : JSON ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๊ฒ ๋ค๊ณ ๋ช
์ํด์ค๋๋ค.
Authorization: getToken() ? getToken() : '' : ๋ก๊ทธ์ธํ์ผ๋ฉด ํ ํฐ์ ๋ฃ๊ณ , ์์ผ๋ฉด ๋น ๋ฌธ์์ด๋ก ๋ด๋ณด๋
๋๋ค.
withCredentials: true : ์ด ์ค์ ์ด ์์ผ๋ฉด, ์ฟ ํค ๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ ํจ๊ป ๋ณด๋ด์ค๋๋ค.
์ธ๋ถ์์ ์ถ๊ฐ ์ค์ ์ ๋๊ธฐ๋ฉด ...config๋ก ๋ณํฉํด์ค๋๋ค.
axiosInstance.interceptors.request.use() : ์์ฒญ ์ธํฐ์
ํฐ๋ก, ๋งค ์์ฒญ ์ ์ ํ ํฐ์ด ์๋์ง ์ฒดํฌํ๊ณ , ์๋ค๋ฉด Authorization ํค๋์ ๋ถ์ฌ์ค๋๋ค. ๋ก๊ทธ์ธ ์ํ ๋ณํ์ ๋ฐ๋ฅธ ํ ํฐ ๋๊ธฐํ๊ฐ ํ์คํด์ง๋๋ค.
axiosInstance.interceptors.response.use() : ์๋ต ์ธํฐ์
ํฐ๋ก, ์๋ต ์ฒ๋ฆฌ ์ค ์๋ฌ๊ฐ 401 Unauthorized๋ฉด ํ ํฐ์ ์ญ์ ํ๊ณ , /login์ผ๋ก ๋ณด๋
๋๋ค.
// category.api.ts
import { Category } from '../models/category.model';
import { httpClient } from './http';
export const fetchCategory = async () => {
const response = await httpClient.get<Category[]>('/categories');
return response.data;
};
// cart.api.ts
interface AddCartParams {
bookId: number;
quantity: number;
}
export const addCart = async (params: AddCartParams) => {
const response = await httpClient.post(`/carts`, params);
return response.data;
};
/categories ๊ฒฝ๋ก๋ก GET ์์ฒญ์ ๋ณด๋ด๋ ์์์
๋๋ค.
httpClient๋ ์ค์ ํ axiosInstance๋ฅผ ๋ถ๋ฌ์ต๋๋ค.
response.data๋ Axios๊ฐ ์๋ต์ ๋ฐ์ ๋ ์๋์ผ๋ก .data์ ์๋ต ๋ณธ๋ฌธ์ ๋ด์์ฃผ๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ๋ฆฌํดํด์ค๋๋ค.
/carts ๊ฒฝ๋ก๋ก POST ์์ฒญ์ ๋ณด๋ด์ ์ฅ๋ฐ๊ตฌ๋์ ์ฑ
์ ์ถ๊ฐํฉ๋๋ค.
params๋ { bookId, quantity } ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๋ ๊ฐ์ฒด์
๋๋ค.
๐ค ๊ธฐ์กด์ ์์ฑํ๋ ๋ฐฑ์๋ ์ฝ๋๋ฅผ ์ด๋ป๊ฒ ๋ถ๋ฌ์์ผ ํ์ง?
์๋ฒ์ ๊ฐ๋ ์ด ํท๊ฐ๋ ค ํ๋ก์ ํธ ๋ด๋ถ์ ์๊ฑฐ๋ ์๋ฒ ์ฝ๋๋ฅผ ๋ด ์ปดํจํฐ๋ก ๊ฐ์ ธ์์ผ ํ๋ค๊ณ ์๊ฐํ์ง๋ง, ๊ตณ์ด ๊ทธ๋ด ํ์ ์์ด ์ฃผ์(URL)๋ง ์๊ณ ์๋ฒ๊ฐ ์ ๊ตฌ๋๋์ด ์๋ค๋ฉด ์ด๋์๋ ์ฌ์ฉํ ์ ์์์ต๋๋ค.
CORS๋ ๋ค๋ฅธ ์ถ์ฒ(origin)์ ์๋ ์๋ฒ์ ๋ธ๋ผ์ฐ์ ๊ฐ ์์ฒญํ ๋, ๋ณด์์ ์ํด ๋ง์๋๋ ๊ฒ(SOP : Same-Origin Policy)์ ์๋ฒ๊ฐ ๋ช ์์ ์ผ๋ก ํ์ฉํด์ ํต์ ์ ๊ฐ๋ฅํ๊ฒ ํด์ฃผ๋ ๋ฐฉ์์ ๋๋ค.
origin): ํ๋กํ ์ฝ(Protocol) + ํธ์คํธ(Host) + ํฌํธ ๋ฒํธ(Port number)
์ถ์ฒ: CORS(๊ต์ฐจ ์ถ์ฒ ๋ฆฌ์์ค ๊ณต์ ) / tosspayments
ํ์ฌ ํ๋ก ํธ์ ๋ฐฑ์๋์ ํฌํธ ๋ฒํธ๊ฐ ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ cors ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด CORS ์ค์ ์ ํด์ค๋๋ค.
const cors = require('cors');
app.use(
cors({
origin: true, // ๋ฐฐํฌ ์์๋ ๋๋ฉ์ธ์ ์
๋ ฅํด์ฃผ๋ ๊ฒ์ด ์ข์
credentials: true,
})
);
origin: true : ์์ฒญ์ด ์จ ๋๋ฉ์ธ์ด ๋ฌด์์ด๋ ํ์ฉํด์ค๋๋ค.
credentials: true : ์์ฒญ์ ์ฟ ํค๋ ์ธ์ฆ ์ ๋ณด๋ฅผ ํฌํจํ ์ ์๋๋ก ํด์ฃผ๋ ์ต์
์
๋๋ค.
โจ ํ๋ก ํธ์์๋
axios๋fetch์withCredentials: true๋ฅผ ๋ฃ์ด์ค์ผ ๊ฐ์ด ๋์ํฉ๋๋ค.
์๋ฒ๋ ์ผ์ ธ์๋ค๋ฉด ์ด๋์๋ ์ด๋ฆฐ๋ค๋ ๊ฑธ ๊ฐ๋ ์ ์ผ๋ก๋ง ์ดํดํ๊ณ ์๋ค๊ฐ ๋ง์ ํ ํด๋์์ ์ฝ๋๋ง ์ ๋ค๋ณด๋ ํ๋ก์ ํธ ํ์ผ๋ก ๋ฐฑ์๋ ์ฝ๋๊น์ง ๋ค ์ฎ๊ฒจ์ ๋ฒ๋ฆด ๋ป ํ๋ค..๐ ํ์๋ค์ ๋์๋ ๋ฐ๊ณ , ๊ฐ๋ ์ ๋ฆฌ๋ฅผ ํ๋ ํ์คํ ์๋ฒ์ ๊ฐ๋ ์ ๋ํด ํ์คํ ์ ๋ฆฝํ ๊ฒ ๊ฐ๋ค.