
export const postSigninThunk = createAsyncThunk('postSigninThunk', postSignin)
const signinSlice = createSlice({
name: "signin",
initialState: initialState,
reducers: {
signin: (state, action) => {
console.log("signin state: "+state, action);
const email = action.payload.username
const result = {email:email}
cookies.set("member", JSON.stringify(result), {path:"/", maxAge: 60*60*24*7}) // stringify: ์๋ฐ์ ๊ฐ์ฒด๋ฅผ ๋ฌธ์์ด๋ก ๋ณ๊ฒฝํ๊ธฐ ์ํด
return result
},
signout: (state, action) => {
console.log("signout state: "+state, action);
return {...initialState}
},
},
extraReducers: builder => { // ์ํ๋ฅผ ์ง์ ๊ด๋ฆฌํ ํ์๊ฐ ์์ด์ extraReducers๋ฅผ ์ฌ์ฉ / ๋น๋๊ธฐ์ํ๋ฅผ ์ ๊ฒฝ์ธํ์๊ฐ ์๋ค.
builder
.addCase(postSigninThunk.fulfilled, (state,action) => { // fulfilled๋ ๋ค ๋์๋ค๋ฉด ์ด๋ผ๋๋ป
console.log("postSigninThunk.fulfilled")
const result = action.payload
return result
})
.addCase(postSigninThunk.pending, (state,action) => { // pending์ ๋ก๋ฉ์ค ์ด๋ผ๋ ๋ป
console.log("postSigninThunk.pending")
})
}
})
๐ export๋ฅผ ๋ณด๋ฉด postSigninThunk๋ผ๋ ์ด๋ฆ์ผ๋ก postSignin์ ์ฌ์ฉํ๊ฒ ๋ค๋ ๋ด์ฉ์ด ์๋๋ฐ, postSignin์ api์๋ฒ์์ ์ฌ์ฉ๋๋ ๋ก๊ทธ์ธ๋ฉ์๋ ์ด๋ฆ์ด๋ค.
๐ ๊ธฐ์กด์ reducers์๋์ ์์ฑ๋๋ extraReucers๋ ํ์ฌ ์์ฑ๋ reducers์ ๋ค๋ฅด๊ฒ API์๋ฒ๋ฅผ ํตํ ๋น๋๊ธฐ ์์ ์ ์งํํ ๋ ์ฌ์ฉ๋๋๊ฒ์ผ๋ก ๋น๋๊ธฐ์์ ํน์ฑ์ ์๊ฐ์ฐจ๊ฐ์์ผ๋ฏ๋ก ์์ฒญ์๋ต ๋ฐ ์์ฒญ๋๊ธฐ์ ๊ฐ์ ํํ์ ํ ์์์ผ๋ฏ๋ก ์๊ฐ์ ์ผ๋ก ์ข๋ค
๐ ํ์ฌ ์ฝ๋์์๋ postSigninThunk๋ APIํจ์๋ฅผ ํธ์ถํ์ฌ ์๋ฒ์ ๋ก๊ทธ์ธ ์์ฒญ์ ๋ณด๋ด๋ ๋น๋๊ธฐ์์ ์ด๋ฏ๋ก extraReducers๋ฅผ ์ฌ์ฉ์ค์ด๋ค
๐ fulfilled : ๋ก๊ทธ์ธ ์์ฒญ์ฑ๊ณต์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ํ์ ์ ์ฅ
const result = action.payload // ๊ฒฐ๊ณผ๊ฐ์ ์ํ์์ ์ฅ
return result
๐ pending : ์์ฒญ์ด ์งํ์ค์ผ๋ ๋ก๋ฉ ์ํ ์ฒ๋ฆฌ
console.log("postSigninThunk.pending") // ๋ก๋ฉ์ค
import {useAppDispatch, useAppSelector} from "./rtk.ts";
import {ISigninParam} from "../types/member.ts";
import {postSigninThunk, signin, signout} from "../slices/signinSlice.ts";
import {Cookies} from "react-cookie";
const cookies = new Cookies();
const loadCookie = () => {
const memberCookie = cookies.get("member",{path:'/'}) // ์ฟ ํค๋ฅผ ๊ฐ์ ธ์์
console.log("Cookie",memberCookie)
return memberCookie; //์ฟ ํค๋ฅผ ๋ฆฌํดํด์ฃผ๋๊ฒ
}
const useSignin = () => {
const dispatch = useAppDispatch();
let member = useAppSelector(state => state.signin) // email์ ์ํ
if(!member.email){ // ์๋ก๊ณ ์นจํด์ ํ์ฌ member์ email๊ฐ์ด ์์๋ ๋ฆฌํด๋ ์ฟ ํค๊ฐ์ด member์ ๋ฐ๊ฒ๋๋๊ฒ.
member = loadCookie()
}
const doSignin = (param:ISigninParam) => {
dispatch(postSigninThunk(param)).unwrap().then( data => {
console.log(data)
cookies.set("member",data,{path:"/"})
})
}
const doSignout = () => {
dispatch(signout(null))
cookies.remove("member",{path:'/'})
}
return{member, doSignin, doSignout}
}
export default useSignin
๐ ์ด๊ณณ์์ ์ค์ ์ผ๋ก ๋ด์ผํ ์ ์ doSignin์ธ๋ฐ, ๊ฒฐ๊ตญ api์๋ฒ์์๋ ์ ๋๋ก๋ username๊ณผ pw๋ฅผ ์ฌ์ฉํ๋ค๋ ๋ง์ด ๋๋๊ฒ์ด๊ณ , unwrap์ ์ด์ฉํด์ ๋ก๊ทธ์ธ ์ฑ๊ณต ๋ฐ ์คํจ์ ๋ํ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ์ฝ๊ฒ ํ ์ ์๋ค.
import jwtAxios from "../util/jwtUtil.ts";
const host:string = 'http://localhost:8090/api/products'
export const postAdd = async (formData: FormData): Promise<number> => {
const res = await jwtAxios.post(`${host}/`, formData)
return Number(res.data.result)
}
export const getList = async ( page:number = 1, size:number = 10) => {
const res = await jwtAxios.get(`${host}/list?page=${page}&size=${size}`)
return res.data
}
๐ ์ผ๋ฐ์ ์ธ axios๋ฅผ ์ฌ์ฉํ์์ง๋ง, product์์ ๋ถํฐ๋ token์ ์ด์ฉํ ์ธ์ฆ๊ด๋ จ์๋น์ค๊ฐ ๋ค์ด๊ฐ๋ฏ๋ก jwt(token)์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํจ - ํ ํฐ์๋์ถ๊ฐ, ๊ฐฑ์ ์ฒ๋ฆฌ๋ฑ
๐ ํด๋น ํ ํฐ๋ค์ ๋ํ ์ดํด๊ฐ ํ์ํ๋ฏ๋ก ์ค๋ช ๋ถํฐ ์งํํ๊ฒ ๋ค. accessToken์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ธฐ์ํด ํ์ํ ์ ์ฅ๊ถ ๊ฐ์ ์ญํ ์ ํ๋ค. ํ์ง๋ง ์ด ํ ํฐ์ ๋ง๋ฃ์๊ฐ์ด 10๋ถ์ผ๋ก ์งง๊ธฐ ๋๋ฌธ์ ๋ง๋ฃ๊ฐ ๋๋ฉด ์ฌ ๋ฐ๊ธ์ ๋ฐ์์ผํ๋๋ฐ, ๊ด๋ จ๋ ๋๋ค๋ฅธ ์ธ์ฆ์ ๋ฐ๋๊ฒ์ ๋ํ ๋ฒ๊ฑฐ๋ก์ ๋๋ฌธ์ AccessToken์ด ๋ฐ๊ธ์ด ๋๋๋์์ ๋ง๋ฃ๊ธฐ๊ฐ์ด ๊ธด RefreshToken์ ๊ฐ์ด ๋ฐ๊ธํด์ค๋ค
๐ ๊ทธ๋ ๊ฒ AccessToken์ด ๋ง๋ฃ๋๊ฒ ๋๋ฉด AccessToken๊ณผ RefreshToken์ ๊ฐ์ ธ๊ฐ์๋, ์๋ก์ด Access/Refresh Token์ ์ฃผ๋ ์คํ์ผ์ด๋ค.
import {IMember, ISigninParam} from "../types/member.ts";
import axios from "axios";
const host:string = 'http://localhost:8090/api/member'
export const postSignin = async (param:ISigninParam):Promise<IMember> => { //id,pw๊ฐ์ ์ฃผ๋ฉด email๊ฐ return
const res = await axios.post(`${host}/login`,
param,
{headers: {'Content-Type': 'application/x-www-form-urlencoded'}})
return res.data
}
export const refreshRequest = async (accessToken:string, refreshToken:string):Promise<IMember> => { // access ๋ง๋ฃ๋ผ์ access,refresh์ค์ผํจ
const res = await axios.get(`${host}/refresh?refreshToken=${refreshToken}`, {
headers: {'Authorization ': `Bearer ${accessToken}`},
})
console.log(res.data)
return res.data
}
๐ API์์ postSignin์์๋ ํด๋น๋๋ username, pw๋ฅผ ์ฃผ๊ฒ๋๋ฉด ๊ฐ token๋ค์ ๋ฐ๊ธํด์ฃผ๋์ญํ ์ํ๊ณ
๐ refreshRequest์์๋ ๋ง๋ฃ๋ accessToken๊ณผ ๋น์์ ๋ฐ์ refreshToken์ ์ฃผ๊ฒ๋๋ฉด ์๋ก์ด access,refresh Token์ ๋ฐ๊ธํด์ฃผ๋ ์ญํ ์ ํ๋ค.
const doSignin = (param:ISigninParam) => {
dispatch(postSigninThunk(param)).unwrap().then( data => {
cookies.set("member",data,{path:"/"})
})
console.log(cookies)
}
๐ ๊ทธ๋ฌ๋ฉด useSignin์์ api์๋ฒ์ ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ํ ํฐ๋ค์ cookie์ ๋ฃ์ด์ค๋ค.
import axios, {AxiosResponse, InternalAxiosRequestConfig} from "axios";
import {Cookies} from "react-cookie";
import {refreshRequest} from "../api/memberAPI.ts";
const cookies = new Cookies();
const jwtAxios = axios.create() // jwt๋ก์ง์ ์ฌ์ฉํ๋ค.
const beforeReq = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { // ์์ฒญ์ด ์๋ฒ์ ๋ณด๋ด์ง๊ธฐ ์ ์ ํธ์ถ
console.log("beforeReq============")
const memberCookie = cookies.get("member", {path:"/"}) // ์ฟ ํค์ฝ๊ธฐ
if(!memberCookie) { // ์ฟ ํค๊ฐ ์๋ค๋ฉด ์์ฒญ์ ๊ฑฐ๋ถํ๋ค.
return Promise.reject("cookie is not found")
}
console.log(config)
const accessToken = memberCookie.accessToken // signinSlice์์ ๋ฐ๊ธ๋ ์ฟ ํค
// ์ฟ ํค๊ฐ ์ด ๋๊ตฐ๋ฐ์์ ๋ฐ๊ธ์ด๋๋๋ฐ signinSlice๋ ๋ก๊ทธ์ธ์์ฒญํ์ ์ฌ์ฉ์ ์ ๋ณด์ ์ฅ์ฉ - ํ์ง๋ง extrareducers๋ฅผ ์ฌ์ฉํ๋ฏ๋ก reducers์ cookie๋ ์ฌ์ฉ๋์ง์์
// useSignin์ฟ ํค๋ ์๋ก๊ณ ์นจํด์ ์ฟ ํค๊ฐ ์ฌ๋ผ์ง๋ ์ฟ ํค์ ๋ณด ์ฝ์ด์ค๋ ๋ณต์์ญํ ๊ทผ๋ฐ ๋๊ฐ๋ค ๊ฐ์ member์ฟ ํค
console.log("at",accessToken)
if(accessToken){
config.headers.Authorization = `Bearer ${accessToken}`
}
return config
}
const failReq = (error:any) => { // ์์ฒญ์๋ฌ ๋ฐ์์
console.log("failReq=============")
return Promise.reject(error)
}
const beforeRes = async (res: AxiosResponse): Promise<AxiosResponse> => { // ์๋ต ๋ณด๋ด๊ธฐ์ , 200ok
console.log("beforeRes===============")
const data = res.data // res์๋ list๊ฐ์ ๋ฐ์ดํฐ ๊ฐ์ด ๋ค์ด๊ฐ๋๋ฐ, ํ ํฐ์๊ฐ์ด ๋ง๋ฃ๋๋ฉด error_access token์ด๋ผ๋ ๋ฉ์ธ์ง๊ฐ ๋ค์ด๊ฐ์๋ค.
if(data.error === 'ERROR_ACCESS_TOKEN') {
console.log("access token์ ๋ฌธ์ ๊ฐ ์์ผ๋ฏ๋ก refresh ํ์")
const memberCookie = cookies.get("member", {path: "/"})
const {accessToken, refreshToken} = memberCookie
const refreshResult = await refreshRequest(accessToken, refreshToken) // api์๋ฒ์ refreshRequest๋ฅผ ํตํด์ ์๋ก์ด ํ ํฐ๋ค์ ๋ฐ๊ธฐ
console.log("refresh", refreshResult)
memberCookie.accessToken = refreshResult.accessToken // ์๋ก๋ฐ์ ์ฟ ํค๋ค์ ๋ฃ๊ธฐ
memberCookie.refreshToken = refreshResult.refreshToken
cookies.set("member", memberCookie, {path: "/", maxAge: 60 * 60 * 24 * 7})
const originalRequest = res.config
originalRequest.headers.Authorization = `Bearer ${accessToken}`
return await axios(originalRequest)
}
return res
}
const failRes = (error:any) => {
console.log("failRes=============")
return Promise.reject({response: {msg:error.message}})
}
jwtAxios.interceptors.request.use(beforeReq, failReq)
jwtAxios.interceptors.response.use(beforeRes, failRes)
export default jwtAxios
๐ ํด๋น์ฝ๋๋ฅผ ๋ถ์ํด๋ณด์๋ฉด jwtUtil์ ์ด 4๊ฐ์ง๋ก ๋ถ๋ฆฌ๋์ด์๋ค.
beforeReq : ์์ฒญ์ด ์๋ฒ์ ๋ณด๋ด์ง๊ธฐ ์ ์ ํธ์ถ์ด๋๋ค
๐ ์ฃผ๋ก ์ฟ ํค๊ฐ ์๋์ง ํ์ธํ์ฌ ์ฟ ํค๊ฐ ์๋ค๋ฉด ์ฟ ํค๋ด๋ถ์ accessToken์ ํ์ธํด์, config.header๋ด๋ถ์ accessToken์ ๋ฃ์ด์ฃผ๋ ์ญํ
failReq : ์์ฒญ์ ํ๋์ค์ ์๋ฌ๊ฐ ๋ฐ์ํ ๋ ํธ์ถ์ด๋๋ค
beforeRes : ์๋ต์ด ํด๋ผ์ด์ธํธ๋ก ๋ณด๋ด์ง๊ธฐ ์ ์ ํธ์ถ๋๋ค.
๐ ์ฃผ๋ก accessToken์ด ๋ง๋ฃ๋์ด์ api์๋ฒ์ ์๋ refresh๊ด๋ จ ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ access/refreshToken์ ์ ๋ฌํ๋ฉด ์๋ก์ด ํ ํฐ๋ค์ ๋ฐ๊ฒ๋๊ณ , ์๋ก์ด ํ ํฐ๋ค์ ์ฟ ํค์ ๋ค์ ๋ฃ์ด์ฃผ๊ณ , Req์ ๋ง์ง๋ง์ ํ์๋ config.header๋ด๋ถ์ accesToken์ ๋ฃ์ด์ฃผ๋ ์ญํ
faliRes : ์๋ต ์ฒ๋ฆฌ ์ค์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ํธ์ถ๋๋ค.
๐ ์ํ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
๐ npm i @tanstack/react-query
๐ npm i @tanstack/react-query-devtools
import { createRoot } from 'react-dom/client'
import './index.css'
import {RouterProvider} from "react-router-dom";
import mainRouter from "./router/mainRouter.tsx";
import {Provider} from "react-redux";
import projectStore from "./store.ts";
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
const queryClient = new QueryClient();
createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<Provider store={projectStore}>
<RouterProvider router={mainRouter}></RouterProvider>
</Provider>
<ReactQueryDevtools initialIsOpen={true}></ReactQueryDevtools>
</QueryClientProvider>
)
๐ queryClient ๊ฐ์ฒด์์ฑํด์ฃผ๊ณ , ReactQueryDevtools ํ๊ทธ์, QueryClientProvider ํ๊ทธ๋ฅผ main.tsx์ ํฌํจ์์ผ์ค๋ค.
import {useQuery} from "@tanstack/react-query";
import {getList} from "../../api/ProductAPI.ts";
function ProductListPage() {
const {} = useQuery({
queryKey: ['product','list',1],
queryFn: () => getList(1,10),
staleTime: 1000*10
})
return (
<div>
<div>Product List page</div>
</div>
);
}
export default ProductListPage;
๐ ํด๋น ์ฝ๋๋ถํฐ ์ค๋ช ์ ํ์๋ฉด, useQuery๋ด๋ถ์
queryKey : ๋ฐฐ์ด๋ก ๋์ค๋ฅผ ๋ํ๋ธ๊ฒ์ด๋ค. ํ์ฌ tsx๋ ProductListPage์ผ๋ก product / list / 1๋ฒํ์ด์ง ๋ฅผ ๋ํ๋
queryFn : API์๋ฒ์ getList๋ฉ์๋์ 1,10์ ๋งค๊ฐ๋ณ์๋ฅผ ๋ฃ์ ๊ฐ์ ํธ์ถํ๋ค
staleTime : ์ ์ ๋ ํ์ 10์ด๊น์ง๋ ์ด ๋ฐ์ดํฐ๊ฐ ์ ์ ํ๋ค๊ณ ํ๋จ๋์ด ํด๋น ํ์ด์ง ๋ฐ apiํธ์ถ์ ํ์ง ์์ง๋ง, 10์ด๊ฐ ์ง๋ ์ดํ์๋ ์๋์ผ๋ก ๋ฆฌ๋ก๋๋๋ค.
import {useQuery, useQueryClient} from "@tanstack/react-query";
import {getList} from "../../api/ProductAPI.ts";
import LoadingComponent from "../../common/LoadingComponent.tsx";
import {useSearchParams} from "react-router-dom";
function ProductListPage() {
const [query,setQuery] = useSearchParams()
const page: number = Number(query.get("page")) || 1
const {isFetching} = useQuery({
queryKey: ['product/list', page],
queryFn: () => getList(page,10),
staleTime: 1000*10
})
const queryClient = useQueryClient()
const changePage = (pageNum: number) => {
if(page === pageNum){
queryClient.invalidateQueries('product/list')
}
query.set("page", String(pageNum))
setQuery(query)
}
return (
<>
<ul className='text-3xl'>
<li className='text-3xl b-2 m-2 p-2 bg-blue-500'
onClick={()=> changePage(1)}>1</li>
<li className='text-3xl b-2 m-2 p-2 bg-blue-500'
onClick={()=> changePage(2)}>2</li>
<li className='text-3xl b-2 m-2 p-2 bg-blue-500'
onClick={()=> changePage(3)}>3</li>
</ul>
{isFetching&& <LoadingComponent/>}
<div>
<div>Product List page</div>
</div>
</>
);
}
export default ProductListPage;
๐ ํ์ฌ์ฝ๋์์๋ getList๋ฉ์๋๋ฅผ api์๋ฒ์์ ํธ์ถํ๋๋ฐ, page์ ๋ฐ๋ฅธ ํธ์ถ์ด ๊ฐ๋ฅํ ์ํ์ด๋ค. useSearchParams๋ฅผ ์ด์ฉํ์ฌ url์ page๊ฐ์ ๋ถ๋ฌ์ ์ํ๊ด๋ฆฌ๋ฅผ ํ๊ณ , changPage๋ฉ์๋๋ฅผ ์ด์ฉํ์ฌ ํด๋ฆญ์ ํ์ด์ง๊ฐ ๋ณ๊ฒฝ๋๋ค. ๋ํ useQueryClient๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด ํ์ฌํ์ด์ง์ ๋ด๊ฐ ์ ํํ ํ์ด์ง๊ฐ ๋์ผํ ๋ url์ด ๋์ผํ๋๋ผ๋ ๋ฆฌ๋ก๋ ๋๋๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํจํํ๋ค.
๐ ๋ํ isFeaching ์ ํตํด ํ์ฌ ๋ฉ์๋๊ฐ ์คํ๋๊ณ ์์๋ LoadingComponent๋ฅผ ์ถ๋ ฅํ ์์๋๋ก ๊ตฌ์กฐํ ํ ์ ์๋ค.
POST - http://localhost:8090/api/member/login
body - (x-www-from-urlencoded)๋ก
key: username / value: user1@aaa.com
key: password / value: 1111
send
accessToken / refreshToken์ ๋ฐ์ ์ ์๋ค.
GET - http://localhost:8090/api/products/list
Headers
key: Authorization / value: Bearer (accessToken)
send
๊ฒฐ๊ณผ๊ฐ ๋ฐ์์์๋ค
GET - http://localhost:8090/api/member/refresh?(refreshToken)
Headers
Key: Authorization / value: Bearer (๋ง๋ฃ๋ accessToken)
Body
Key:refreshToken