๋ค๋ก๊ฐ๊ธฐ
-- ํ ํฐ ๋ง๋ฃ์์ ํํฌ ๊ธฐ๋ก --
ํ๋ก ํธ์๋๋ฅผ ํ๋ค๋ณด๋ฉด, ๊ทธ ์ด๋ค api๋ณด๋ค๋ ๋๋ฅผ ๊ฐ์ฅ ๊ดด๋กญํ๋๊ฒ ๋ฑ์ฅํ๋ค
๋ฐ๋ก ํ ํฐ ๋ง๋ฃ(401)
์ด๋ฒ ๊ธ์
โ์ฒ์์ ๋์ถฉ ํ์ฌ ์ฝ๋ ctrl c + ctrl v ํ๋ค๊ฐ ํผ๋์ ๊ฒฐ๊ตญ ํ๋ก์ ํธ ๊ฐ์์๊ณ ๋ค์ ์ฒ์๋ถํฐ axios ์ธํฐ์
ํฐ ์ค๊ณํ๊ฒ ๋ ์ด์ผ๊ธฐโ
1. ์์์ ํญ์ **๋ณต๋ถ**์ด์๋ค.
์์งํ ๋งํ๋ฉด ์ธํฐ์ ํฐ ๋ง๋ค ์ฌ์ ๋ณด๋ค๋ ํ๋ฉด ๊ตฌ์ฑํ๊ณ ์ถ์ ๋ง์ด ์กฐ๊ธํ๋ค.
๋ฐฑ์คํผ์ค๋ฅผ ๋ง์ด๊ทธ๋ ์ด์ ํ๋ผ๋ ์ง์๋ฅผ ๋ฐ์๊ณ , ์ธํฐ์ ํฐ ๊ทธ๊น์ด๊ฑฐ ๊ทธ๋ฅ ์ฌ์ฉํ๋๊ฑฐ ์ฐ๋ฉด ๋๋๊ฑฐ ์๋? ๋น์ฐํ ์ด๋ ๊ฒ ์์ํ๋ค.
๋๋ ๊ทธ๋ฅ ์๋ ์ฝ๋ ๊ฐ์ ธ์ ๋ถ์๊ณ , ์๋ฌด ์๊ฐ ์์ด ์งํํ๊ณ ์์๋ค.
๊ทผ๋ฐ ์ฝ๋๋ฆฌ๋ทฐ์์ ๋ฉํ ๋์ด ํ ๋ง์
๊ธฐ์กด๊ฑฐ ๊ฐ์ ธ๋ค์ฐ์ง ๋ง์๊ณ ์ฒ์๋ถํฐ ๋จธ๋ฆฌ ๋ฐ์๊ฐ๋ฉด์ ๋ง๋ค์ด๋ณด์์ฃ .
๋ค..? ๋ค์์..?
๊ทธ๋ ๊ฒ ๋๋ ๋ค์ 401์ ๋ง์๋ค.
๊ทธ๋์ ์ง์ง๋ก ์ธํฐ์ ํฐ๊ฐ ์ด๋ค ๊ตฌ์กฐ๋ก ์๋ํด์ผ ํ๋์ง, ํ ํฐ ๋ก์ง์ ์ด๋ป๊ฒ ๊ตฌ์ฑํด์ผ ํ๋์ง ๊ธฐ๋ณธ๋ถํฐ ๋ค์ ๋ฏ์ด๋ณด๊ธฐ๋ก ํ๋ค.
์ฌ์ค ์ด๋ฒ ์ธํด ๋ฉด์ ์์ ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ์ ๊ดํ ์ง๋ฌธ์ด ์์๊ณ , (ํ ๋ฉํ ๋์ด ๋ด ๋ฉด์ ๊ด์ด์ จ๋ค)
ํ ํ์ด์ง์์ ์ฌ๋ฌ ์์ฒญ์ด ๋์์ 401 ์๋ฌ๊ฐ ๋์ค๋ฉด ์ด๋ป๊ฒ ํด๊ฒฐํ๋์ง์ ๋ํด ์ฌ์ญค๋ณด์ จ๋ค.
๋๋ ๊ทธ๊ฒ๊น์ง๋ ์๊ฐ์ ํด๋ณธ ์ ์ด ์์ด์ ์ ๋ชจ๋ฅด๊ฒ ๋ค๊ณ ๋ต ํ์๊ณ ?? ๋๊ณ ๋๊ณ ๋ง์ ๋จ์๋ค.
๊ทธ๋์ ๊ธฐ์กด ํ๋ก์ ํธ axios ์ธํฐ์ ํฐ ๋ณด๊ณ โ์ค ์ด๋ ๊ฒ ๋ง๋๋๊ตฌ๋. ์ค๊ณ๊ฐ ์ ๋์ด์๊ตฌ๋โ ํ๊ณ ๊ทธ๋๋ก ๊ฐ๋ค ์ผ๋ ๋ง์์ด ํฐ ๊ฒ ๊ฐ๋ค.
2. ์ฒซ ์ฝ์ง: 401 ํด๊ฒฐํ๋ ค๋ค 403 ๋ง๊ธฐ
๋๋ ๋น ๋ฅด๊ฒ at ๋ง๋ฃ ๊ฐ์ง๋ฅผ ๋ง๋ค๊ณ , refresh api๋ ๋ถ์๋ค.
๊ทธ๋ฆฌ๊ณ ์ข ๊ธฐ๋ค๋ ค์ ๋ง๋ฃ๋์์ ๋ ๋์ ์๋๋ฆฌ์ค
- ์ค๋๋ at๋ก api ํธ์ถ
- ์๋ฒ๊ฐ 401 ๋์ง
- ์ธํฐ์ ํฐ๊ฐ refresh ํธ์ถ
- ์ ํ ํฐ์ผ๋ก ์์ฒญ ์ฌ์๋
โฆ ๋์ด์ผ ํ๋๋ฐ
- ๋ง๋ฃ๋ at๋ก api ํธ์ถ -> 401 err
- ์๋์ ์ผ๋ก refresh api ํธ์ถ -> 403 err
์๋ ์ ์ฌ๋ฐ๊ธ ํ๋ฒ๋ ๋ชปํ๋๋ฐ ์ 403 ๋์ด?
์ด ์ ๋ชฝ๊ฐ์ ํ์์ผ๋ก ํ๋ฃจ์ข ์ผ ์ฝ์ง๋ง ํ๋ค.
์ฌ๋ฐ๊ธ ์๋ง๋ค alert๋ ๋์๋ณด๊ณ , Authorization ํค๋๋ ๋ค์ ๋ถ์ฌ๋ณด๊ณ , ์ธํฐ์ ํฐ ๋ฐ๊นฅ์์ refresh ํธ์ถ ์ฑ๊ณตํ๋๊ฒ๋ ํ์ธํ๋๋ฐ,,
์์ธ์ด ๊ฑ ํ๋นํ์.
at์ rt์ ์ ํจ๊ธฐ๊ฐ์ด ๊ฐ์๋ ๊ฒ์!!! ์!๋ฟ!์ธ!
๊ธฐ์กด ์ธํฐ์ ํฐ๋ ์ด๊ฑฐ ์ฒ๋ฆฌ๊ฐ ์๋์ด์๋๋ผ!
![]()
๋๋ฌด ๋ชจ๋ฅด๊ฒ ์ด์ jwt.io์์ token ๊น๋ณด๊ณ ์์๋ค.
at๊ฐ ๋ง๋ฃ๋์์ ๋ rt๋ ๋ง๋ฃ๊ฐ๋์ด refresh๋ฅผ ํ ์ ์๋ ์๊ฐ์ด ์์๋ ๊ฒ์ด๋ค.
accessToken ๋ง๋ฃ โ refresh ์์ฒญ ์๋ ๊ทผ๋ฐ refreshToken๋ ์ด๋ฏธ ๋ง๋ฃ โ 403
๊ทธ๋์ ๋น์ฐํ 403์ด ๋๋ ๊ตฌ์กฐ์๋ค.
๊ทผ๋ฐ ๋ฐฑ์๋๋ฅผ ๋ฐ๊ฟ๋ฌ๋ผ๊ณ ํ ์๊ฐ ์๋ ์ํฉ
์ด๊ฑธ ๊ณ๊ธฐ๋ก ์ค๋๋ง์ ์กฐ๊ธ ๋จธ๋ฆฌ๋ฅผ ๊ตด๋ ค๋ณด์๋ค.
3. ์๋ ์ฌ๋ฐ๊ธ? ๊ทธ๊ฑด ์ข ์๋๊ฒ๊ฐ์์.
์ฒ์์๋ ์ด๋ฐ ์์ ์๋ ๋ก์ง์ ์๊ฐํ๋ค.
์ฌ์ฉ์๊ฐ ๋ญ ํ๋ at ๋ง๋ฃ 5๋ถ์ ์ ์๋์ผ๋ก refresh ํ์
๊ทผ๋ฐ ์ด ์๊ฐ์ ๋ง์๋๋ ธ๋๋ ๋ฉํ ๋์ด ๋ง์ํ์ จ๋ค.
์ฌ์ฉ์๊ฐ ์๋ฌด ํ๋๋ ์ํ๋๋ฐ ์์์ ์ฌ๋ฐ๊ธ ํ๋๊ฑด ์๋ ๊ฒ ๊ฐ์์!
๋ง๋ง์. ์๋ฌด๋๋ ๋ฐฑ์คํผ์ค๋ค๋ณด๋๊น ๋ณด์์ ์ธ ๋ถ๋ถ๋ ๊ณ ๋ ค๋ฅผ ํ์ด์ผํ๋ค.
๊ทธ๋์ ๋ฐฉํฅ์ ๋ฐ๊ฟจ๋ค.
์ฌ์ฉ์๊ฐ ์ค์ api ์์ฒญ ์ ๋ง๋ฃ ์๊ฐ์ด 5๋ถ ์ดํ๋ผ๋ฉด ๊ทธ๋ refresh ํ์
์ฆ ํ์ฑ ์ฌ์ฉ์๋ง ์ฌ๋ฐ๊ธ โ ๋นํ์ฑ ์ฌ์ฉ์ ์ธ์ ์ ์ข ๋ฃ
// ๋ง๋ฃ 5๋ถ ์ดํ + ์์ง ์๋ ๊ฐฑ์ ์ํ ๊ฒฝ์ฐ
if (diff <= 5 * 60_000 && !hasRefreshOnce) {
// ์ด๋ฏธ ๋ค๋ฅธ ์์ฒญ์ด ์ฌ๋ฐ๊ธ ์ค์ด๋ฉด โ Queue์ push
if (isRefreshing) {
return new Promise((resolve, reject) => {
queue.push({ resolve, reject, config })
})
}
isRefreshing = true
try {
const res = await axios.post<ApiResponse<string>>(
`refresh api url`,
{ headers: { Accept: 'application/json' }, withCredentials: true }
)
if (res.data.code !== '0000') {
throw new Error('refresh ์คํจ')
}
const newAT = res.data.result
setAccessTokenState(newAT)
setRefreshOnce(true)
config.headers.Authorization = newAT
processQueue(null, newAT)
return config
} catch (err) {
processQueue(err, undefined)
authReset()
reset()
window.location.assign('/')
return Promise.reject(err)
} finally {
isRefreshing = false
}
4. ๋์ ์์ฒญ ๋ฌธ์ โ Pending Queue ๋์
๋ค์ ๋ฌธ์ ๋ ์ด๊ฑฐ์๋ค. ์ด๊ฑฐ๋ ๋ด๊ฐ ๋ฉด์ ๋ ์ง๋ฌธ์ ๋ฐ๊ณ ๋๋ต์ ๋ชปํ๋ ๊ทธ ๋ฌธ์ ์ด๊ธฐ๋ ํ๋ฐ
- A API ์์ฒญ ์ โ ํ ํฐ ๋ง๋ฃ โ refresh ์์
- ๊ทธ ์ฌ์ด์ B API ์์ฒญ โ ๋ ํ ํฐ ๋ง๋ฃ โ refresh ๋ ํธ์ถ
์ด๋ฌ๋ฉด refresh api๊ฐ ๋์์ ์ฌ๋ฌ๋ฒ ํธ์ถ๋๋ฉฐ ๊ฒฝํฉ์ด ๋ฐ์ํ๋ค.
๊ทธ๋์ ํ์์ ์ผ๋ก refresh ์ค์ ๋ค๋ฅธ ์์ฒญ์ ๋ฉ์ถฐ์ ์ค์ธ์ฐ๋ ๋ก์ง์ด ํ์ํ์๋ค.
๊ทธ๊ฒ์ด ๋ฐ๋ก pending queue
๋ด๊ฐ ์๋ฃ๊ตฌ์กฐ ์์
์์ ๋ฃ๊ณ ์ฝํ
ํ๋๋ง ์ฐ๋ ํ๋ฅผ ์ง์ง ์จ๋ณด๋ ๋ ์ด ์ค๋ค๋
let isRefreshing = false
let queue: PendingReq[] = []
const processQueue = (error: unknown | null, token?: string) => {
queue.forEach(({ resolve, reject, config }) => {
if (error) return reject(error)
if (token) config.headers.Authorization = token
resolve(api.request(config))
})
queue = []
}
์์ฝํ์๋ฉด
- refresh ์ค์ด๋ฉด โ ์์ฒญ์ queue ์ ์์๋
- refresh ๋๋๋ฉด โ queue์ ์๋ ์์ฒญ๋ค์ ์ ํ ํฐ์ผ๋ก ์ฌ์์ฒญ
์ด๋ ๊ฒ ํ๋ฉด ๋์ ์ฌ๋ฐ๊ธ ์์ฒญ ๋ฌธ์ ๋ ํด๊ฒฐ๋๋ค.
5. ์ธ์ ๋ง๋ฃ ๊ฒฝ๊ณ ๊ฐ ์ฌ๋ฌ ๋ฒ ๋จ๋ ๋ฌธ์
์ ๊ทผ๋ฐ ์ฐ๋์ด ์ฐ์ด๋ผ๋๋ ์๋ฒ ๋ ์คํธ๊ฐ ๋์ด
refresh ์คํจ(401/403) โ ๋ก๊ทธ์์ ์ฒ๋ฆฌํ ๋ ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๊ฐ ์์๋ค.
alert("์ธ์
์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์")
๊ทผ๋ฐ api ์ฌ๋ฌ๊ฐ๊ฐ ๋์์ ํฐ์ง๋ฉด์ alert์ด ์ฌ๋ฌ๊ฐ๊ฐ ๋ด๋๊ฒ์! alert ํ์ธ ๋ฒํผ ์ฐํํจ
๊ทธ๋์ ๋ก๊ทธ์์์ด ํ ๋ฒ๋ง ์คํ๋๊ฒ ํ๊ธฐ ์ํด์ zustand์ isLoggingOut state๋ฅผ ์ถ๊ฐํ์ฌ ํด๊ฒฐํ์๋ค.
const { isLoggingOut, setIsLoggingOut, authReset } = useAuthStore.getState()
const { reset } = useUserStore.getState()
if (isLoggingOut) {
return Promise.reject(error)
}
if (status === 401 || status === 403) {
setIsLoggingOut(true)
authReset()
reset()
alert('์ธ์
์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์')
window.location.assign('/')
}
๊ทผ๋ฐ ์ isLoggingOut ์ด๊ธฐํ๊ฐ ์๋ผ์? ๋ฌธ์ ๋ฐ์
๋๋ฒ์งธ ์ธ์ ๋ง๋ฃ ๋ก๊ทธ์์ ๋, 401 ๋ด๋๋ฐ ๋ก๊ทธ์์์ด ์๋๋๋ผ
๊ทธ๊ฑด ๋ด๊ฐ isLoggingOut ์ํ๊ฐ์ zustand persist ๋ฏธ๋ค์จ์ด์ ๋ฃ์ด๋จ๊ธฐ ๋๋ฌธ์ด์๋ค. ๊ทธ๋์ persist partialize ์ค์ ํด์ ํด๊ฒฐํ์๋ค. ใ ใ ๋ฐ๋ณด
6. ์ต์ข Axios ์ธํฐ์ ํฐ ์ฝ๋
์ฝ์ง ๋์ ๋ง๋ค์ด์ง ์ธํฐ์ ํฐ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ๋ค.
- ํ ํฐ ๋ง๋ฃ 5๋ถ ์ดํ โ refresh
- refresh ๋์ ๋ค๋ฅธ ์์ฒญ์ pending queue
- refresh ์คํจ โ ๋ก๊ทธ์์
- alert ์ค๋ณต ๋ฐฉ์ง โ isLoggingOut
- accessToken, refreshToken ์ํ๋ zustand์์ ๊ด๋ฆฌ
- Authorization ํค๋ ์๋ ์ฃผ์
import { useAuthStore } from '@/entities/user/store/auth.store'
import { useUserStore } from '@/entities/user/store/user.store'
import axios, {
AxiosError,
type AxiosInstance,
type AxiosRequestConfig,
type AxiosResponse,
type InternalAxiosRequestConfig,
} from 'axios'
import type { ApiResponse } from './api.type'
// --------------------
// axios ๊ธฐ๋ณธ ๊ฐ์ฒด
// --------------------
const BASE_URL = 'example url'
export const api: AxiosInstance = axios.create({
baseURL: BASE_URL,
withCredentials: true,
headers: {
Accept: 'application/json',
},
})
// --------------------
// ์ฌ๋ฐ๊ธ ํ ๋ก์ง
// --------------------
let isRefreshing = false
type PendingReq = {
resolve: (
value: InternalAxiosRequestConfig<any> | PromiseLike<InternalAxiosRequestConfig<any>>
) => void
reject: (error?: unknown) => void
config: InternalAxiosRequestConfig
}
let queue: PendingReq[] = []
const processQueue = (error: unknown | null, token?: string) => {
queue.forEach(({ resolve, reject, config }) => {
if (error) {
return reject(error)
}
if (token) {
config.headers = config.headers ?? {}
config.headers.Authorization = token
}
resolve(api.request(config))
})
queue = []
}
// --------------------
// ์์ฒญ ์ธํฐ์
ํฐ
// --------------------
api.interceptors.request.use(
async (config) => {
const {
sessionExpireAt,
hasRefreshOnce,
refreshTokenState,
accessTokenState,
setAccessTokenState,
setRefreshOnce,
authReset,
} = useAuthStore.getState()
const { user, reset } = useUserStore.getState()
if (accessTokenState) {
config.headers.Authorization = accessTokenState
}
if (!sessionExpireAt || !refreshTokenState) return config
const diff = Number(sessionExpireAt) - Date.now()
// ๋ง๋ฃ 5๋ถ ์ดํ + ์์ง ์๋ ๊ฐฑ์ ์ํ ๊ฒฝ์ฐ
if (diff <= 5 * 60_000 && !hasRefreshOnce) {
// ์ด๋ฏธ ๋ค๋ฅธ ์์ฒญ์ด ์ฌ๋ฐ๊ธ ์ค์ด๋ฉด โ Queue์ push
if (isRefreshing) {
return new Promise((resolve, reject) => {
queue.push({ resolve, reject, config })
})
}
isRefreshing = true
try {
const res = await axios.post<ApiResponse<string>>(
`refresh api url`,
{ headers: { Accept: 'application/json' }, withCredentials: true }
)
if (res.data.code !== '0000') {
throw new Error('refresh ์คํจ')
}
const newAT = res.data.result
setAccessTokenState(newAT)
setRefreshOnce(true)
config.headers.Authorization = newAT
processQueue(null, newAT)
return config
} catch (err) {
processQueue(err, undefined)
authReset()
reset()
window.location.assign('/')
return Promise.reject(err)
} finally {
isRefreshing = false
}
}
return config
},
(error) => Promise.reject(error)
)
// --------------------
// ์๋ต ์ธํฐ์
ํฐ
// --------------------
api.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError) => {
const status = error.response?.status
const { isLoggingOut, setIsLoggingOut, authReset } = useAuthStore.getState()
const { reset } = useUserStore.getState()
if (isLoggingOut) {
return Promise.reject(error)
}
if (status === 401 || status === 403) {
setIsLoggingOut(true)
authReset()
reset()
alert('์ธ์
์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์')
window.location.assign('/')
}
return Promise.reject(error)
}
)
// --------------------
// api wrapper
// --------------------
export const get = async <TResponse>(url: string, params?: any, config?: AxiosRequestConfig) => {
const res = await api.get<TResponse>(url, { params, ...config })
return res.data
}
export const post = async <TResponse>(url: string, data?: any, config?: AxiosRequestConfig) => {
const res = await api.post<TResponse>(url, data, config)
return res.data
}
export const put = async <TResponse>(url: string, data?: any, config?: AxiosRequestConfig) => {
const res = await api.put<TResponse>(url, data, config)
return res.data
}
export const del = async <TResponse>(url: string, params?: any, config?: AxiosRequestConfig) => {
const res = await api.delete<TResponse>(url, { params, ...config })
return res.data
}
export default { get, post, put, del }
7. ์ ๋ฆฌํ์๋ฉด..
์ด๋ฒ์ ์ธํฐ์ ํฐ๋ฅผ ์ฒ์๋ถํฐ ์ค๊ณํ๋ฉด์ ๋๋ ์ :
- ํ ํฐ ๊ตฌ์กฐ๋ฅผ ์ดํดํด์ผ ํ๋ค
- ์ธ์ ๋ง๋ฃ UX๊น์ง ๊ณ ๋ คํด์ผ ํ๋ค
- ๋์ ์์ฒญ ๋ฌธ์ ๊น์ง ์ก์์ผ ํ๋ค
- ์ํ๊ด๋ฆฌ๊น์ง ์ฐ๊ณํด์ผ ํ๋ค
์ธํฐ์ ํฐ๋ ๊ทธ๋ฅ ํ ๋ ๊ฐ๊ฐ ์๋๋ผ ํ๋ก์ ํธ์ ์ ์ฒด ์ธ์ฆ ํ๋ฆ์ ๋ด๋นํ๋ ์ค์ํ ๋ถ๋ถ์์ ๊นจ๋ฌ์๋ค.
๊ทธ๋ฆฌ๊ณ ๋ฌด์๋ณด๋ค:
์ ๋โฆ ์๊ฐ ์ํ๊ณ ์ฝ๋ ๋ณต๋ถ๋ถํฐ ์์ํ์ง ๋ง์.
![]()
ํ๋ก ํธ์๋ ์นดํ ๊ณ ๋ฆฌ์ ๊ด๋ จ๋ ์ต์ ๊ธ
๐งจ ์ฐ๋นํํ Axios ์ธํฐ์ ํฐ ๋ง๋ค๊ธฐ
@tanstack/react-query Optimistic Updates
ํผํฌ๋ฏผ ๋ธ๋ฃธ์ ์ด๋ป๊ฒ ๊ฝ๊ธธ๐ธ์ ๋จ๊ธธ๊น?โจโจ
Next.js ๋ง์ด๊ทธ๋ ์ด์ : ์๋ฒ ์ปดํฌ๋ํธ(SSC)์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ(CSC) ์ ํํ๊ธฐ
์ต๊ณ ์ ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ์ฐพ์์....