JS, TS 코드 조각 모음 (메모용)
JS, TS 코드 조각 모음 (메모용)

JS, TS 코드 조각 모음 (메모용)

작성자
2skydev2skydev
카테고리
Typescript
태그
react
frontend
backend

AxiosRefreshTokenAdapter

  • Axios 인스턴스에 인터셉터를 추가하여, 응답 상태가 에러라면 토큰을 갱신하고 재요청합니다.
  • SimpleEventEmitter가 필요합니다. (아래에 구현해둔 코드 있습니다)
import { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'; import { SimpleEventEmitter, TListener, TUnsubscribe, } from '@/utils/simpleEventEmitter'; export interface IAxiosRequestConfigWithRefreshToken extends AxiosRequestConfig { refreshToken?: string | null; } export interface IAxiosErrorWithRefreshToken extends AxiosError { config: IAxiosRequestConfigWithRefreshToken; } export interface IAxiosRefreshTokenAdapterOptions { /** * 토큰 갱신 요청을 보내는 함수입니다. * * @description * 토큰 갱신 요청 후 응답으로 받은 엑세스 토큰을 반환합니다. */ refreshTokenFetcher: ( axiosError: IAxiosErrorWithRefreshToken ) => Promise<string>; /** * AxiosError 발생 시, 토큰 갱신이 필요한지 여부를 반환합니다. * * @description * 토큰 갱신이 필요한지 여부를 반환합니다. */ tryRequestRefreshAccessTokenCondition: ( axiosError: IAxiosErrorWithRefreshToken ) => boolean; /** * AxiosError 발생 시, 에러를 반환할지 여부를 반환합니다. * * @description * 에러를 반환할지 여부를 반환합니다. * 보통 토큰 갱신이 실패했을 경우로 설정합니다. * * @example * ```ts * (axiosError) => err.config.url === '/refresh-access-token' * ``` */ failedRequestRefreshAccessTokenCondition: ( axiosError: IAxiosErrorWithRefreshToken ) => boolean; } /** * Refresh Token을 구현하기 위한 Axios 어댑터입니다. * * @description * Axios 인스턴스에 인터셉터를 추가하여, 응답 상태가 에러라면 토큰을 갱신하고 재요청합니다. */ export class AxiosRefreshTokenAdapter { private readonly CACHE_MAX_AGE = 1000; private cachedAccessToken: null | string = null; private queue: Array<[(token: string) => void, () => void]> = []; private isFetching = false; private eventEmitter = new SimpleEventEmitter(); constructor( private axiosInstance: AxiosInstance, private options: IAxiosRefreshTokenAdapterOptions ) { this.axiosInstance.interceptors.response.use( undefined, this.handleResponseReject.bind(this) ); } /** * 응답 상태가 에러라면 토큰을 갱신하고 재요청합니다. */ private async handleResponseReject(err: AxiosError) { const isTry = this.options.tryRequestRefreshAccessTokenCondition(err); const isFailed = this.options.failedRequestRefreshAccessTokenCondition(err); if (isFailed || !isTry) { return Promise.reject(err); } try { const accessToken = await this.getRefreshedAccessToken(err); err.config.headers.Authorization = `Bearer ${accessToken}`; return this.axiosInstance(err.config); } catch (refreshErr) { return Promise.reject(err); } } /** * 토큰을 갱신하고, 갱신된 토큰을 반환합니다. */ private async getRefreshedAccessToken(err: AxiosError) { // 토큰이 캐시되어 있다면, 캐시된 토큰을 반환합니다. if (this.cachedAccessToken) return this.cachedAccessToken; // 이미 토큰을 갱신하는 중이라면, 토큰이 갱신될 때까지 재요청하지 않고 대기합니다. if (this.isFetching) return new Promise<string>((resolve, reject) => this.queue.push([resolve, reject]) ); this.isFetching = true; try { this.cachedAccessToken = await this.options.refreshTokenFetcher(err); this.eventEmitter.emit('refreshed', this.cachedAccessToken); // 토큰이 갱신되었으므로, 대기중인 모든 promise를 resolve합니다. this.queue.forEach(([resolve]) => resolve(this.cachedAccessToken)); // CACHE_MAX_AGE 이후에 캐시된 토큰을 삭제합니다. setTimeout(() => { this.cachedAccessToken = null; }, this.CACHE_MAX_AGE); this.isFetching = false; } catch (err) { this.eventEmitter.emit('failed', err); this.queue.forEach(([, reject]) => reject()); this.isFetching = false; throw err; } return this.cachedAccessToken; } /** * accessToken 토큰이 갱신될 때 호출될 콜백을 등록합니다. */ on(key: 'refreshed', listener: (accessToken: string) => void): TUnsubscribe; /** * 토큰 갱신이 실패했을 때 호출될 콜백을 등록합니다. */ on(key: 'failed', listener: (err: AxiosError) => void): TUnsubscribe; on(key: string, listener: TListener) { return this.eventEmitter.on(key, listener); } off(key: 'refreshed', listener: (accessToken: string) => void): void; off(key: 'failed', listener: (err: AxiosError) => void): void; off(key: string, listener: TListener) { return this.eventEmitter.off(key, listener); } }
// ======================== // 사용 예제 // ======================== export const axiosRefreshTokenAdapter = new AxiosRefreshTokenAdapter( axiosClient, { async refreshTokenFetcher(err) { const refreshToken = getRefreshToken() const response = await axios({ url: API_REFRESH_ACCESS_TOKEN, method: 'POST', headers: { Authorization: refreshToken, }, }); return response.data.accessToken; }, tryRequestRefreshAccessTokenCondition(err) { return err.response?.status === 401; }, failedRequestRefreshAccessTokenCondition(err) { return err.config.url === API_REFRESH_ACCESS_TOKEN; }, } );
 

SimpleEventEmitter

  • 패키지 설치 없이 이벤트를 사용해야 할때 가볍게 사용 가능한 이벤트 처리 클래스입니다.
export type TListener = (value?: any) => void; export type TUnsubscribe = () => void; export class SimpleEventEmitter { private events = new Map<string, TListener[]>(); on(key: string, listener: TListener): TUnsubscribe { const listeners = this.events.get(key) ?? []; this.events.set(key, [...listeners, listener]); return () => this.off(key, listener); } off(key: string, listener: TListener) { const listeners = this.events.get(key); if (!listeners?.length) return; this.events.set( key, listeners.filter((l) => l !== listener) ); } emit(key: string, value?: any) { const listeners = this.events.get(key); if (!listeners?.length) return; listeners.forEach((listener) => listener(value)); } }
 

useStorage

  • 세션 또는 로컬 스토리지와 연동되는 상태가 필요할 때 사용합니다.
  • 다른 브라우저 탭 또는 창, 다른 컴포넌트에서 사용해도 키가 같다면 상태가 같이 변경됩니다.
import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { SimpleEventEmitter } from '@/util/simpleEventEmitter'; type TUseStorageOptions<T> = { storageType?: 'localStorage' | 'sessionStorage'; initialValue?: T; }; const emitter = new SimpleEventEmitter(); const useStorage = <T = null>( key: string, options: TUseStorageOptions<T> = {} ) => { const { storageType = 'localStorage', initialValue = null } = options; const storageKey = `useStorage.${key}`; const getStorageValue = () => { if (typeof window === 'undefined') return initialValue; const storageValue = window[storageType].getItem(storageKey); return storageValue ? JSON.parse(storageValue) : initialValue; }; const [value, setValue] = useState<T>(getStorageValue()); const setStorageValue: Dispatch<SetStateAction<T>> = (newValue) => { if (typeof newValue === 'function') { newValue = (newValue as (prevState: T) => T)(value); } emitter.emit(storageKey, newValue); window[storageType].setItem(storageKey, JSON.stringify(newValue)); setValue(newValue); }; useEffect(() => { const handler = (event: StorageEvent) => { if (event?.key === storageKey) setValue(JSON.parse(event.newValue)); }; window.addEventListener('storage', handler); const unsubscribe = emitter.on(storageKey, setValue); return () => { window.removeEventListener('storage', handler); unsubscribe(); }; }, [storageKey]); return [value, setStorageValue] as const; }; export default useStorage;
 

Observer

  • 값을 생성한 후 값 변경을 추적합니다.
  • react에서 외부 상태가 필요할 때 사용합니다
export class Observer<T> { listeners: Array<(value: T) => void> = [] value: T constructor(value: T) { this.value = value } subscribe(listener: (value: T) => void) { this.listeners.push(listener) return () => this.unsubscribe(listener) } unsubscribe(listener: (value: T) => void) { this.listeners = this.listeners.filter(l => l !== listener) } get() { return this.value } set(value: T) { this.value = value this.listeners.forEach(listener => listener(value)) } }
// ======================== // alertDialog 함수 구현 예제 (shadcn/ui) // ======================== interface GlobalAlertDialogStateOptions { id?: number | string _open?: boolean title: React.ReactNode description?: React.ReactNode reverseActions?: boolean onConfirm?: () => void | Promise<void> onCancel?: () => void | Promise<void> cancelText?: React.ReactNode confirmText?: React.ReactNode cancelButtonProps?: AsyncButtonProps confirmButtonProps?: AsyncButtonProps } export const globalAlertDialogState = new Observer<GlobalAlertDialogStateOptions[]>([]) let globalAlertDialogId = 1 const createGlobalAlertDialogItem = (options: GlobalAlertDialogStateOptions) => { const id = options.id ?? globalAlertDialogId++ globalAlertDialogState.set([...globalAlertDialogState.value, { id, _open: true, ...options }]) } // 직접 사용하는 함수 export const alertDialog = Object.assign(createGlobalAlertDialogItem, {}) // app/layout.tsx에 배치 export const GlobalAlertDialog = () => { const [items, setItems] = React.useState<GlobalAlertDialogStateOptions[]>([]) React.useEffect(() => { return globalAlertDialogState.subscribe(items => { setItems(items) }) }, []) const handleClickButton = (callback?: () => void | Promise<void>) => async () => { await callback?.() globalAlertDialogState.set( items.map(item => { if (item.id !== items[0].id) return item return { ...item, _open: false, } }), ) // 애니메이션 효과를 위해 잠시 뒤 삭제 setTimeout(() => { globalAlertDialogState.set(items.filter(item => item.id !== items[0].id)) }, 200) } return ( <> {items.map(item => { const actions = [ <AsyncButton key="cancel" variant={item.reverseActions ? 'default' : 'outline'} onClick={handleClickButton(item.onCancel)} shouldLoadingIconShow {...item.cancelButtonProps} > {item.cancelButtonProps?.children ?? item.cancelText ?? '취소'} </AsyncButton>, <AsyncButton key="confirm" variant={item.reverseActions ? 'outline' : 'default'} onClick={handleClickButton(item.onConfirm)} shouldLoadingIconShow {...item.confirmButtonProps} > {item.confirmButtonProps?.children ?? item.confirmText ?? '확인'} </AsyncButton>, ] return ( <AlertDialog open={item._open} key={item.id}> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>{item.title}</AlertDialogTitle> {item.description && ( <AlertDialogDescription asChild={typeof item.description !== 'string'}> {typeof item.description === 'string' ? ( item.description.split('\n').map((text, i) => ( <React.Fragment key={i}> {text} <br /> </React.Fragment> )) ) : ( <div>{item.description}</div> )} </AlertDialogDescription> )} </AlertDialogHeader> <AlertDialogFooter> {item.reverseActions ? actions.reverse() : actions} </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ) })} </> ) }

댓글

guest