250x250
๋ฐ˜์‘ํ˜•
arkhyeon
arkhyeon
arkhyeon
์ „์ฒด ๋ฐฉ๋ฌธ์ž
์˜ค๋Š˜
์–ด์ œ
  • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (88)
    • Spring (5)
    • Java (4)
    • React (25)
      • TypeScript (6)
      • JavaScript (1)
      • Jest (9)
    • NEXT (8)
    • SQL (1)
    • React native (1)
    • CSS (3)
    • Web (1)
    • Git (3)
    • ETC (6)
    • ๋น…๋ฐ์ดํ„ฐDB (8)
    • Docker (4)
    • Tool (1)

๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

  • ํ™ˆ
  • ํƒœ๊ทธ
  • ๋ฐฉ๋ช…๋ก

๊ณต์ง€์‚ฌํ•ญ

์ธ๊ธฐ ๊ธ€

ํƒœ๊ทธ

  • usetransition
  • react19
  • docker tomcat
  • Spring WebSocket
  • javasciprt websocket
  • react
  • websocket server
  • kudu
  • react jest
  • WSS
  • react usetransition
  • react spring websocket
  • HIVE
  • node WebSocket
  • react loading
  • jest
  • react typescript
  • react websocket
  • websocket
  • javascript wss

์ตœ๊ทผ ๋Œ“๊ธ€

์ตœ๊ทผ ๊ธ€

ํ‹ฐ์Šคํ† ๋ฆฌ

hELLO ยท Designed By ์ •์ƒ์šฐ.
arkhyeon

arkhyeon

React/TypeScript

React Axios TypeScript with JWT Boilerplate

2024. 3. 14. 14:49
728x90
๋ฐ˜์‘ํ˜•
 ๐Ÿ’ก Axios ๋ฉ”์†Œ๋“œ๋“ค์˜ ํƒ€์ž… ์„ค์ •๊ณผ ์ธ์Šคํ„ด์Šค ๊ธฐ๋ณธ ์„ค์ • ๊ทธ๋ฆฌ๊ณ  ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ์žฌ๋ฐœ๊ธ‰ ๋กœ์ง์— ๋Œ€ํ•ด ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

Axios Type Setting

interface CustomInstance extends AxiosInstance {
  get<T>(...params: Parameters<AxiosInstance['get']>): Promise<T>;
  delete<T>(...params: Parameters<AxiosInstance['delete']>): Promise<T>;
  post<T>(...params: Parameters<AxiosInstance['post']>): Promise<T>;
  put<T>(...params: Parameters<AxiosInstance['put']>): Promise<T>;
  patch<T>(...params: Parameters<AxiosInstance['patch']>): Promise<T>;
}

Create Axios

์ƒˆ๋กœ์šด Axios ์ธ์Šคํ„ด์Šค๋ฅผ client ์ด๋ฆ„์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ํ˜ธ์ถœ๋˜๋Š” URL ์•ž์— ๊ณ ์ •์ ์œผ๋กœ ๋ถ™๋Š” BaseURL ์„ค์ •ํ•ด์ค€๋‹ค. ๋˜ํ•œ ๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด ์ž๊ฒฉ ์ฆ๋ช…์„ ํฌํ•จํ•˜๋Š” withCredentials ์˜ต์…˜์„ ์ผœ์ฃผ์–ด ์ฟ ํ‚ค, HTTP ์ธ์ฆ ํ—ค๋” ์ฆ๋ช… ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

const BASEURL = ${BASEURL};
const client: CustomInstance = axios.create();
client.defaults.baseURL = BASEURL;
client.defaults.withCredentials = true;

Axios Interceptors

Request Interceptors

์ฟ ํ‚ค์— accessToken์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด Request Header์— ์•ก์„ธ์Šค ํ† ํฐ์„ ๋„ฃ์–ด์„œ ๋ณด๋‚ด์ค€๋‹ค. ์—†๋‹ค๋ฉด ๋ฐฑ๋‹จ์—์„œ 401 unauthorized ์—๋Ÿฌ๋ฅผ ์‘๋‹ตํ•  ๊ฒƒ์ด๋‹ค.

Response Interceptors

Axios Response์— ๋Œ€ํ•ด ๋กœ๊ทธ๋ฅผ ์ฐ๋Š”๋‹ค. ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ๋ชจ๋‘ reject ์‹œํ‚ค๊ณ  401 ์—๋Ÿฌ๋Š” ๋กœ๊ทธ์•„์›ƒ์„ ์ง„ํ–‰์‹œํ‚จ๋‹ค.

// Request
client.interceptors.request.use(
  function (config: InternalAxiosRequestConfig) {
    const accessToken = getCookie('accessToken');

    if (accessToken) {
      config.headers['Authorization'] = 'Bearer ' + accessToken;
    }

    return config;
  },
  function (error) {
    return Promise.reject(error);
  },
);

client.interceptors.response.use(
  function (response) {
    const url = response.config.url;

    console.log(
      `%cURL Info : ${url}-------------------------------------`,
      'background: #000; color: #bada55',
    );
    if (response.config.method !== 'get') {
      console.log(response.config.data);
    }
    console.log(response.data);
    console.log(
      '%c------------------------------------------------------------',
      'background: #000; color: #bada55',
    );

    if (response.data === null) {
      return response;
    }

    return response.data;
  },
  async error => {
    errorLog(error);
    if (error.response.status !== 401 || error.config.sent) {
      return Promise.reject(error);
    }

    error.config.sent = true;
    return await unAuthProcess(error).catch(() => logout());
  },
);

export { client };

Example

client.post("board/update")
  .then(res => console.log(res))
  .catch(err => console.log(err));

Login Axios with JWT Token

๋กœ๊ทธ์ธ ์‹œ ์‚ฌ์šฉํ•  Axios ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์˜€๋‹ค. ID, PWD๋ฅผ ๋„˜๊ธฐ๋ฉด AccessToken, RefreshToken ์ „๋‹ฌ์„ ๋ฐ›๊ณ  ์ฟ ํ‚ค์— ์ €์žฅ ํ›„ ํ•„์š”ํ•  ๋•Œ ๋งˆ๋‹ค ๊บผ๋‚ด ์“ด๋‹ค.

    loginAxiospost('user/login', { userId, password })
loginAxios.interceptors.response.use(
  function (response) {
    setCookie('accessToken', response.data.accessToken);
    setCookie('refreshToken', response.data.refreshToken);

    if (response.config.url === '/logout') {
      return '';
    }

    return response.data;
  },
  error => {
    errorLog(error);
    return Promise.reject(error);
  },
);

export { loginAxios };

Refresh Token ํ† ํฐ ์žฌ๋ฐœ๊ธ‰

Access Token ๋งŒ๋ฃŒ๊ฐ€ ๋˜๋ฉด ๊ธฐ์กด API ์š”์ฒญ์„ ์ €์žฅํ•ด๋‘๊ณ  Refresh Token์œผ๋กœ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰์„ ์ง„ํ–‰ํ•œ๋‹ค. ์ง„ํ–‰์ค‘ ๋“ค์–ด์˜ค๋Š” API ์š”์ฒญ์— ๋Œ€ํ•ด์„œ๋Š” ๊ตฌ๋… ์‹œ์Šคํ…œ์„ ์ด์šฉํ•ด ๋ฐฐ์—ด์— ๋‹ด์•„๋‘์—ˆ๋‹ค๊ฐ€ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰์ด ์„ฑ๊ณตํ•˜๋ฉด ๊ทธ๋•Œ ๋ชจ๋“  API ์š”์ฒญ์„ ์‹คํ–‰ํ•œ๋‹ค.

// ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์ค‘ ๋“ค์–ด์˜ค๋Š” API ์š”์ฒญ์„ ๊ตฌ๋…ํ•  ๋ณ€์ˆ˜
let subscribers: Array<() => void> = [];
// ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์ค‘ ๋‹ค๋ฅธ API๊ฐ€ ๋“ค์–ด์˜ค๋Š”์ง€ ์•„๋‹Œ์ง€ ํŒ๋‹จํ•˜๋Š” ๋ณ€์ˆ˜
let lock = false;

const unAuthProcess = async ({config}: {config: InternalAxiosRequestConfig;})
: Promise<void | AxiosResponse> => {
	//ํ† ํฐ ๋งŒ๋ฃŒ๋กœ ์‘๋‹ต ๋ชป๋ฐ›์€ API ์š”์ฒญ ์ €์žฅ
  const originalRequest = <InternalAxiosRequestConfig>config;

	// ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์ค‘ ๋‹ค๋ฅธ API๊ฐ€ ๋จผ์ € ๋“ค์–ด์™”๋‹ค๋ฉด ๋‹ค์Œ์— ๋“ค์–ด์˜จ API ์š”์ฒญ์€ ๊ตฌ๋…
  if (lock) {
    return onSubscribe(() => client(originalRequest));
  }

  lock = true;
  await reissueAccessToken();
  // ํ† ํฐ ๋งŒ๋ฃŒ๋กœ ์‘๋‹ต ๋ชป๋ฐ›์€ API ์žฌ์‹คํ–‰
  return client(config);
};
// ์•ก์„ธ์Šค ํ† ํฐ ๋„ฃ์—ˆ๋˜ ์ž๋ฆฌ์— ๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ ๋„ฃ์–ด์„œ ์š”์ฒญ
tokenAxios.interceptors.request.use(
  function (config) {
    config.headers['Authorization'] = 'Bearer ' + getCookie('refreshToken');
    return config;
  },
  error => {
    return Promise.reject(error);
  },
);

// ๊ตฌ๋… ๋ณ€์ˆ˜์— ํ† ํฐ ๋งŒ๋ฃŒ๋กœ ์‘๋‹ต ๋ชป๋ฐ›์€ API ์š”์ฒญ ๊ตฌ๋…
function onSubscribe(cb: () => void) {
  subscribers.push(cb);
}

// ํ† ํฐ ๋งŒ๋ฃŒ๋กœ ์‘๋‹ต ๋ชป๋ฐ›์€ API ์š”์ฒญ ์‹คํ–‰
function onPublish() {
  subscribers.forEach(cb => cb());
}

const reissueAccessToken = async (): Promise<string | void> => {
  try {
	  //๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ์œผ๋กœ ์•ก์„ธ์Šค ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ API ์š”์ฒญ
    const { data } = await tokenAxios.post<TokenType>('user/reissue');
    lock = false;
    onPublish();
    subscribers = [];
    setCookie('accessToken', data.accessToken);

    return data.accessToken;
  } catch (e) {
    subscribers = [];
    // ์‹คํŒจ ์‹œ ๋กœ๊ทธ์•„์›ƒ ์ง„ํ–‰
    logout();
  }
};

function logout() {
  window.location.href = '/login';
  lock = false;
  removeCookie('accessToken');
  removeCookie('refreshToken');
}

์›๋ณธ ์ฝ”๋“œ

import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { getCookie, removeCookie, setCookie } from './Cookie';

interface CustomInstance extends AxiosInstance {
  get<T>(...params: Parameters<AxiosInstance['get']>): Promise<T>;
  delete<T>(...params: Parameters<AxiosInstance['delete']>): Promise<T>;
  post<T>(...params: Parameters<AxiosInstance['post']>): Promise<T>;
  put<T>(...params: Parameters<AxiosInstance['put']>): Promise<T>;
  patch<T>(...params: Parameters<AxiosInstance['patch']>): Promise<T>;
}

export interface GetResponse {
  responseMessage?: string;
  statusCode?: number;
}

const BASEURL = ${BASEURL};
const client: CustomInstance = axios.create();
client.defaults.baseURL = BASEURL;
client.defaults.withCredentials = true;

client.interceptors.request.use(
  function (config: InternalAxiosRequestConfig) {
    const accessToken = getCookie('accessToken');

    if (accessToken) {
      config.headers['Authorization'] = 'Bearer ' + accessToken;
    }

    return config;
  },
  function (error) {
    return Promise.reject(error);
  },
);

client.interceptors.response.use(
  function (response) {
    const url = response.config.url;
    console.log(response);
    console.log(
      `%cURL Info : ${url}-------------------------------------`,
      'background: #000; color: #bada55',
    );
    if (response.config.method !== 'get') {
      console.log(response.config.data);
    }
    console.log(response.data);
    console.log(
      '%c------------------------------------------------------------',
      'background: #000; color: #bada55',
    );

    if (response.data === null) {
      return response;
    }

    return response.data;
  },
  async error => {
    errorLog(error);
    if (error.response.status !== 401 || error.config.sent) {
      return Promise.reject(error);
    }

    error.config.sent = true;
    return await unAuthProcess(error).catch(() => logout());
  },
);

export { client };

const loginAxios: CustomInstance = axios.create();

loginAxios.defaults.baseURL = BASEURL;
loginAxios.defaults.withCredentials = true;

loginAxios.interceptors.response.use(
  function (response) {
    setCookie('accessToken', response.data.accessToken);
    setCookie('refreshToken', response.data.refreshToken);

    if (response.config.url === '/logout') {
      return '';
    }

    return response.data;
  },
  error => {
    errorLog(error);
    return Promise.reject(error);
  },
);

export { loginAxios };

const errorLog = (error: AxiosError | Error) => {
  if (axios.isAxiosError(error)) {
    const { message, config, response } = error;

    console.log(message);
    console.log(config?.url);
    console.log(response);
  }

  if (axios.isAxiosError(error)) {
    console.log(
      `%cURL Info : ${error?.config?.url}-------------------------------------`,
      'background: #000; color: #2CD4A8',
    );
    if (error.response) {
      console.log(error.response);
      console.log(
        `%cError Code : ${error?.response.status} ${error.response.statusText}\nError Msg  : ${error.response.data.responseMessage}`,
        'background: #000; color: #2CD4A8',
      );
    } else if (error.request) {
      console.log(error.request.statusText);
      console.log(error);
      console.log(
        `%cError Code : ${error.request.status} ${
          error.request.statusText || error.message
        }\nError Msg  :`,
        'background: #000; color: #2CD4A8',
      );
    }
  } else {
    console.log(error);
  }
};

const tokenAxios: CustomInstance = axios.create();

tokenAxios.defaults.baseURL = BASEURL;
tokenAxios.defaults.withCredentials = true;

let subscribers: Array<() => void> = [];
let lock = false;

tokenAxios.interceptors.request.use(
  function (config) {
    config.headers['Authorization'] = 'Bearer ' + getCookie('refreshToken');
    return config;
  },
  error => {
    return Promise.reject(error);
  },
);

function onSubscribe(cb: () => void) {
  subscribers.push(cb);
}

function onPublish() {
  subscribers.forEach(cb => cb());
}

const unAuthProcess = async ({
  config,
}: {
  config: InternalAxiosRequestConfig;
}): Promise<void | AxiosResponse> => {
  const originalRequest = <InternalAxiosRequestConfig>config;

  if (lock) {
    return onSubscribe(() => client(originalRequest));
  }

  lock = true;
  await reissueAccessToken();
  return client(config);
};

interface TokenType extends GetResponse {
  data: { accessToken: string; refreshToken?: string };
}

const reissueAccessToken = async (): Promise<string | void> => {
  try {
    const { data } = await tokenAxios.post<TokenType>('user/reissue');
    lock = false;
    onPublish();
    subscribers = [];
    setCookie('accessToken', data.accessToken);

    return data.accessToken;
  } catch (e) {
    subscribers = [];
    logout();
  }
};

function logout() {
  window.location.href = '/login';
  lock = false;
  removeCookie('accessToken');
  removeCookie('refreshToken');
}
728x90
๋ฐ˜์‘ํ˜•

'React > TypeScript' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

React Typescript Interface Tuple Generic  (0) 2024.03.25
React RichTextEditor Image Upload - reactQuill  (0) 2024.03.18
React TypeScript useInfiniteQuery Infinite Scroll  (0) 2024.03.15
React TypeScript Zustand Infinite Scroll with IntersectionObserver  (0) 2024.03.14
React TypeSciprt - Community Project  (0) 2024.03.13
    'React/TypeScript' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
    • React RichTextEditor Image Upload - reactQuill
    • React TypeScript useInfiniteQuery Infinite Scroll
    • React TypeScript Zustand Infinite Scroll with IntersectionObserver
    • React TypeSciprt - Community Project
    arkhyeon
    arkhyeon

    ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”