기존 Api호출 Fetch들이 나누어져있었다.
그거를 한개로 통합해야될 필요성이 있어서 한개로 합치게되었다.
먼저 예시로 어떻게 수정이되었는지 기록용으로 남긴다.
로그인부분으로 예를 들겠다.
OTP로 로그인시
const resp = await api.call(`auth`, `mfa`, otp.value);
이렇게 요청을 하는데
✅
/* Start: OTP입력 */
if (!dialogsStore.alertFlag) {
if (isNil(otp.value.otp)) {
pushNoti(`error`, `MGS.OTPINPUT`); /* OTP를 입력해 주세요 */
return;
}
loadingStore.increaseLoadingState();
const resp = await api.call(`auth`, `mfa`, otp.value);
if (resp.status) {
userClaim.value = await api.call(`auth`, `getUserClaim`);
await userStore.setPersonalizationByCommonFetch();
await systemConfigStore.setSystemConfig();
pushNoti(`success`, `MSG.LOGIN`); /*로그인 되었습니다*/
if (typeof window !== 'undefined') {
localStorage.setItem(
'platform-user-claim',
JSON.stringify(userClaim.value)
);
// loginStepStatus.value = 'id';
}
navigateTo('/');
} else {
pushNoti(`error`, `MSG.OTPFAIL`); /*OTP 인증에 실패하였습니다.*/
loadingStore.resetLoadingState();
}
}
}
/* End: OTP입력 */
useCommonFetch.ts
import type { ApiInstance } from '~/plugins/api';
import { useRouter } from 'vue-router';
export default () => {
const {
$api,
$i18n: { t },
} = useNuxtApp();
// const localPath = useLocalePath()
const notificationsStore = useNotificationsStore();
const loadingStore = useLoadingStore();
const dialogStore = useDialogsStore();
const router = useRouter();
// get client cookies in server environment
// if process.server === fales, cookies = {}
const cookies = useRequestHeaders(['cookie']);
let accessToken: string | null;
if (typeof window !== 'undefined') {
accessToken = localStorage.getItem('platform-dfocus-access-token');
}
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
const commonFetch = async function <
T extends keyof ApiInstance,
U extends FunctionKeys<ApiInstance[T]>,
>(
module: T,
method: U,
...args: ApiInstance[T][U] extends (...args: [any, ...infer P]) => any ? P : []
): Promise<ApiInstance[T][U] extends (...args: any[]) => Promise<infer R> ? R : void> {
const firstArg = args[0] as { keepLogin?: boolean };
const keepLogin = firstArg?.keepLogin ? 'Y' : null;
//UserClaim이 없는 경우 실행하지 않음
if (
![
'getUserClaim',
'getPersonalization',
'login',
'sendEmailToAdmin',
'findId',
'findPassword',
].includes(method.toString())
) {
const storedData = localStorage.getItem('platform-user-claim');
if (!storedData || !JSON.parse(storedData)) {
console.log('>> storedData', storedData);
console.log('>> UserClaim is null Aborting Request!!!');
loadingStore.resetLoadingState();
return Promise.reject(new Error('UserClaim is null'));
}
}
try {
const setHeaders = {
...cookies,
...{ dfocus_access_token: accessToken },
};
const resp = await ($api[module][method] as Function)(
{
headers: setHeaders,
},
...args
);
if (!accessToken) {
const getToken = resp.headers?.get('dfocus_access_token');
if (getToken) {
localStorage.setItem('platform-dfocus-access-token', getToken);
}
}
if (method == 'logout') {
accessToken = null;
}
if (resp._data) {
const data = resp._data;
// data.status > resp.status 변경(data에 status가 없는 경우가 있음)
if (!data.status) {
notificationsStore.pushNotification({
type: `error`,
text: data.resultMsg,
});
}
return resp._data.map || resp._data.list || resp._data.result || resp?._data;
}
return resp;
} catch (error: any) {
loadingStore.resetLoadingState();
if (error.response && error.response.status === 401) {
// notificationsStore.pushNotification({
// type: `error`,
// text: t(
// "MSG.SESSION.EXPIRE2" /*세션이 만료되었습니다. 다시 로그인하세요.*/
// ),
// });
localStorage.removeItem('platform-user-claim');
localStorage.removeItem('platform-dfocus-access-token');
dialogStore.openDialog(
{
text: t(
'MSG.SESSION.EXPIRE' /*세션이 만료되었습니다. 로그인 페이지로 이동하시겠습니까?*/
),
},
async (result: any) => {
if (result) {
router.push({
path: '/login',
});
}
}
);
} else {
notificationsStore.pushNotification({
type: `error`,
text: `${error}`,
});
// await navigateTo(`login`);
// navigateTo 호출 후 store reset됨 ??
// 일단 navigateTo 대신 router.push 사용
// router.push(`login`);
// throw error 안 하면, useAsyncData client단에서 retry ??
}
throw error;
}
};
return { commonFetch };
};
기존 패치 로직이다
그런데 저런 패치들이 각각상황별로 패치들이 엄청 많았다
// API 모듈 호출
const callApiModule = async <T extends keyof ApiInstance, U extends keyof ApiInstance[T]>(
moduleKey: T,
method: U,
...args: any[]
) => {
// 예외 API는 UserClaim 없어도 호출 가능
const exceptionMethods = ['getUserClaim', 'getPersonalization', 'login', 'mfa'];
if (!exceptionMethods.includes(String(method)) && !checkUserClaim()) {
loadingStore.resetLoadingState();
return Promise.reject();
}
const moduleFn = $api[moduleKey][method] as Function;
const resp = await moduleFn(
{
headers: {
...cookies,
...(getAccessToken() ? { [CONST.HEADER_KEYS.ACCESS]: getAccessToken() } : {}),
},
},
...args
);
// MFA 처리 시 token 저장
if (method === 'mfa') {
const newToken = resp.headers?.get(CONST.HEADER_KEYS.ACCESS);
if (newToken) setAccessToken(newToken);
}
// UserClaim 저장 (예: getUserClaim 호출 시)
if (method === 'getUserClaim') {
const userClaimData = resp?._data?.result || resp?._data;
if (userClaimData) {
localStorage.setItem('platform-user-claim', JSON.stringify(userClaimData));
}
}
const newAccessToken = resp.headers?.get(CONST.HEADER_KEYS.ACCESS);
const newRefreshToken = resp.headers?.get(CONST.HEADER_KEYS.REFRESH);
if (newAccessToken) setAccessToken(newAccessToken);
if (newRefreshToken) setRefreshToken(newRefreshToken);
if (method === 'logout') {
clearTokens();
}
return resp?._data?.map || resp?._data?.list || resp?._data?.result || resp?._data || resp;
};
이렇게 처리를하게되어서 모든 모듈을 통해서 api를 호출하는건
api.call(); 로 통일시켜서 호출시키게할수있게바꾸었다.
물론 get post put delete도 따로만들어서
전체코드를 기록하겠다 그래서 호출할때는 api.get()이런식으로호출해서 하드코딩을 할 이유가없어지고
다양한 api fetch들을 하나로 합칠수 있게되었다.
/*
* Copyright (c) 2024. Dfocus Co., Ltd . ALL RIGHT RESERVED.
*
* API 호출 공통
* @author : 윤성호
* @date : 2025.11.07
*
* @change history
*******************************************************************************
** 2025.11.07 윤성호 - 최초 생성
*******************************************************************************
*/
import type { ApiInstance } from '~/plugins/api';
import { useRouter } from 'vue-router';
import { ofetch } from 'ofetch';
export default function useApiFetch() {
const { $api }: { $api: ApiInstance } = useNuxtApp();
const { $i18n: { t }, $config } = useNuxtApp();
const { CONST } = useConstants(t);
const notificationsStore = useNotificationsStore();
const loadingStore = useLoadingStore();
const dialogStore = useDialogsStore();
const router = useRouter();
const cookies = useRequestHeaders(['cookie']);
const REFRESH_URL = `${$config.public.baseURL}/v2/api/common/token/refresh`;
// 토큰 가져오기
const getAccessToken = () => localStorage.getItem(CONST.TOKEN_KEYS.ACCESS);
const getRefreshToken = () => localStorage.getItem(CONST.TOKEN_KEYS.REFRESH);
// 토큰 저장
const setAccessToken = (token: string) => localStorage.setItem(CONST.TOKEN_KEYS.ACCESS, token);
const setRefreshToken = (token: string) => localStorage.setItem(CONST.TOKEN_KEYS.REFRESH, token);
// 토큰 제거 (로그아웃 시)
const clearTokens = () => {
localStorage.removeItem(CONST.TOKEN_KEYS.ACCESS);
localStorage.removeItem(CONST.TOKEN_KEYS.REFRESH);
localStorage.removeItem('platform-user-claim');
};
// JWT 한글 파싱
const parseJwt = (token: string) => {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
} catch {
return null;
}
};
// JWT 만료 체크
const isTokenExpired = (token: string | null): boolean => {
if (!token) return true;
try {
const payload = parseJwt(token);
console.log('payload === >>', payload);
if (!payload?.exp) return true;
return payload.exp < Date.now() / 1000;
} catch {
return true;
}
};
// AccessToken 갱신
const refreshAccessToken = async (): Promise<string | null> => {
const refreshToken = getRefreshToken();
console.log('refreshAccessToken ==>>>' , refreshToken);
if (!refreshToken) throw new Error('No refresh token found');
try {
const resp = await ofetch.raw(REFRESH_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[CONST.HEADER_KEYS.REFRESH]: refreshToken,
},
});
const newAccessToken = resp.headers.get(CONST.HEADER_KEYS.ACCESS);
const newRefreshToken = resp.headers.get(CONST.HEADER_KEYS.REFRESH);
if (newAccessToken) setAccessToken(newAccessToken);
if (newRefreshToken) setRefreshToken(newRefreshToken);
return newAccessToken;
} catch (err) {
console.error('Failed to refresh token', err);
clearTokens();
throw err;
}
};
// UserClaim 체크
const checkUserClaim = (): boolean => {
const storedData = localStorage.getItem('platform-user-claim');
return !!storedData && !!JSON.parse(storedData);
};
// 공통 요청
const request = async (
url: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
data?: any,
options?: { fileFlag?: boolean; skipLoading?: boolean; keepLogin?: boolean; userClaimCheck?: boolean }
) => {
const skipLoading = options?.skipLoading || url.includes('/common') || url.includes('/public');
if (!skipLoading) loadingStore.increaseLoadingState();
let accessToken = getAccessToken();
let refreshToken = getRefreshToken();
// 유저 클레임 검증
if (options?.userClaimCheck && !checkUserClaim()) {
loadingStore.resetLoadingState();
return Promise.reject(new Error('UserClaim is null'));
}
// accessToken이 없거나 만료된 경우 → refresh 시도
if (!accessToken || isTokenExpired(accessToken)) {
try {
// refreshToken이 존재하면 refresh 시도
if (refreshToken) {
const newAccessToken = await refreshAccessToken();
if (newAccessToken) {
accessToken = newAccessToken; // 🔹 로컬 변수에 갱신된 토큰 반영
}
} else {
throw new Error('No refresh token');
}
} catch (err) {
// refresh 실패 → 세션 만료 처리
if (!skipLoading) loadingStore.resetLoadingState();
clearTokens();
dialogStore.openDialog({ text: t('MSG.SESSION.EXPIRE') }, () => router.push('/login'));
return;
}
}
try {
const headers: Record<string, string> = {};
// 파일 업로드/일반 요청 헤더 처리
let bodyData = data;
if (options?.fileFlag && data instanceof FormData) {
// multipart/form-data인 경우 자동 설정되므로 Content-Type 제거
delete headers['Content-Type'];
} else if (!options?.fileFlag) {
headers['Content-Type'] = 'application/json';
bodyData = JSON.stringify(data);
}
// 공통 헤더 설정
if (cookies?.cookie) headers['cookie'] = cookies.cookie;
if (accessToken) headers[CONST.HEADER_KEYS.ACCESS] = accessToken;
if (options?.keepLogin) headers[CONST.HEADER_KEYS.KEEP_LOGIN] = 'Y';
//실제 요청
const resp = await ofetch(url, {
method,
headers,
body: method !== 'GET' ? bodyData : undefined,
params: method === 'GET' ? data : undefined,
onResponse({ response }) {
// 백엔드가 새 토큰을 내려주는 경우 갱신
const newAccessToken = response.headers.get(CONST.HEADER_KEYS.ACCESS);
const newRefreshToken = response.headers.get(CONST.HEADER_KEYS.REFRESH);
if (newAccessToken) setAccessToken(newAccessToken);
if (newRefreshToken) setRefreshToken(newRefreshToken);
},
});
if (!skipLoading) loadingStore.resetLoadingState();
return resp?._data?.map || resp?._data?.list || resp?._data?.result || resp?._data || resp;
} catch (error: any) {
if (!skipLoading) loadingStore.resetLoadingState();
const status = error?.response?.status;
// 상태코드별 처리
if (status === 401) {
clearTokens();
dialogStore.openDialog({ text: t('MSG.SESSION.EXPIRE') }, () => router.push('/login'));
} else if (status === 403) {
notificationsStore.pushNotification({
type: 'error',
text: t('MSG.FUNC.NOT.AUTH')
});
} else if (status === 413) {
notificationsStore.pushNotification({
type: 'error',
text: t('MSG.FILE.LARGE'),
});
} else {
notificationsStore.pushNotification({
type: 'error',
text: `${error}`
});
}
throw error;
}
};
// API 모듈 호출
const callApiModule = async <T extends keyof ApiInstance, U extends keyof ApiInstance[T]>(
moduleKey: T,
method: U,
...args: any[]
) => {
// 예외 API는 UserClaim 없어도 호출 가능
const exceptionMethods = ['getUserClaim', 'getPersonalization', 'login', 'mfa'];
if (!exceptionMethods.includes(String(method)) && !checkUserClaim()) {
loadingStore.resetLoadingState();
return Promise.reject();
}
const moduleFn = $api[moduleKey][method] as Function;
const resp = await moduleFn(
{
headers: {
...cookies,
...(getAccessToken() ? { [CONST.HEADER_KEYS.ACCESS]: getAccessToken() } : {}),
},
},
...args
);
// MFA 처리 시 token 저장
if (method === 'mfa') {
const newToken = resp.headers?.get(CONST.HEADER_KEYS.ACCESS);
if (newToken) setAccessToken(newToken);
}
// UserClaim 저장 (예: getUserClaim 호출 시)
if (method === 'getUserClaim') {
const userClaimData = resp?._data?.result || resp?._data;
if (userClaimData) {
localStorage.setItem('platform-user-claim', JSON.stringify(userClaimData));
}
}
const newAccessToken = resp.headers?.get(CONST.HEADER_KEYS.ACCESS);
const newRefreshToken = resp.headers?.get(CONST.HEADER_KEYS.REFRESH);
if (newAccessToken) setAccessToken(newAccessToken);
if (newRefreshToken) setRefreshToken(newRefreshToken);
if (method === 'logout') {
clearTokens();
}
return resp?._data?.map || resp?._data?.list || resp?._data?.result || resp?._data || resp;
};
return {
get: (url: string, params?: any, options?: any) => request(url, 'GET', params, options),
post: (url: string, body?: any, options?: any) => request(url, 'POST', body, options),
put: (url: string, body?: any, options?: any) => request(url, 'PUT', body, options),
delete: (url: string, params?: any, options?: any) => request(url, 'DELETE', params, options),
call: callApiModule,
};
}'Front-end > Javascript' 카테고리의 다른 글
| [TypeScript] 옵셔널 프로퍼티(optional property) (0) | 2025.11.04 |
|---|---|
| [TypeScript] 핵심 고급 문법 - 제네릭 / keyof연산자 / Mapped Types (매핑된 타입) / Conditional Types (조건부 타입) (0) | 2025.11.04 |
| [Javascript] Object.assign() (0) | 2024.02.27 |
| [Javascript] 자바스크립트 비동기 처리와 콜백 함수 (0) | 2024.02.21 |