๐Ÿฌ ๊ธดํ˜ธํก/๊ณ ์Šค๋ฝ ํ‹ฐ์ผ“

[2.0] React Query ๊ณตํ†ต ์—๋Ÿฌ ํ•ธ๋“ค๋ง

ํ•œ๊ทœ์ง„ 2022. 8. 12. 04:13

ํ”„๋กœ์ ํŠธ์— ๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ๋ฅผ ๋„์ž…ํ•˜๋ฉด์„œ ์ด๊ฒƒ์ €๊ฒƒ ํ•ด๋ณด๊ณ  ์‹ถ์€๊ฒŒ ๋งŽ์•˜๋‹ค. ์•„๋ž˜๋Š” ๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ์˜ ์žฅ์ ์ด๋ผ๊ณ  ํ•˜๋ฉด ์ž์ฃผ ๋‚˜์˜ค๋Š” ๊ฒƒ๋“ค์ด๋‹ค.

  • ํŒจ์น˜ํ•ด์˜จ ๋ฐ์ดํ„ฐ๋“ค์„ ์บ์‹ฑ
  • staleํ•œ ๋ฐ์ดํ„ฐ์ธ์ง€ ์•„๋‹Œ์ง€๋ฅผ ๊ตฌ๋ถ„ํ•ด์„œ ํ•ญ์ƒ ์‹ ์„ ํ•œ ๋ฐ์ดํ„ฐ๋กœ ์œ ์ง€
  • ์„œ๋ฒ„ ์ƒํƒœ์™€ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ์˜ ๋ถ„๋ฆฌ
  • success์™€ error ๋“ฑ์˜ ํŒจ์นญ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌ

ํŠนํžˆ ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ์žˆ์–ด์„œ ๊ณ ๋ฏผ์„ ๋” ํ•ด๋ณด๊ณ  ์‹ถ์—ˆ๋‹ค. axios๋กœ ๋งค๋ฒˆ ํŒจ์นญํ•ด์˜ฌ๋•Œ ํŠธ๋ผ์ด ์บ์น˜๋กœ ๊ฐ์‹ธ์„œ ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž์ฃผ ์‚ฌ์šฉํ•œ๋‹ค. ๊ทธ๋Ÿด ๋•Œ๋งˆ๋‹ค ๋น„์Šทํ•œ ๋กœ์ง(๋ณดํ†ต์€ ๊ทธ๋ƒฅ ์ฝ˜์†”๋กœ๊ทธ..)์„ ์—ฌ๊ธฐ์ €๊ธฐ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜, ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜์–ด์•ผ ํ•˜๋Š” ๋กœ์ง๋„ ์ค‘๋ณต์œผ๋กœ ์“ฐ์ด๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•˜๋‹ค. ์ „๋ถ€ํ„ฐ ๊ณ ๋ฏผ์„ ๋ช‡๋ฒˆ ํ–ˆ์—ˆ๋˜ ๋ถ€๋ถ„์ด์—ˆ๋Š”๋ฐ ๋งˆ์นจ ์ฐธ๊ณ ํ• ๋งŒํ•œ ์ข‹์€ ๋ ˆํผ๋Ÿฐ์Šค๋“ค์ด ์žˆ์–ด์„œ ํ”„๋กœ์ ํŠธ์— ๋„ฃ์–ด๋ณด๊ณ ์ž ํ–ˆ๋‹ค. ์ฐธ๊ณ 

 

๋ฐฑ์—”๋“œ ์นœ๊ตฌ์—๊ฒŒ ์ด๋ฒˆ์—” ์—๋Ÿฌ์ฒ˜๋ฆฌ๋ฅผ ์ด๋Ÿฌ์ฟต์ €๋Ÿฌ์ฟต ํ•ด๋ณด๊ณ  ์‹ถ๋‹ค๊ณ  ๋งํ–ˆ๋”๋‹ˆ ์ด๋ ‡๊ฒŒ ๋‚ด๋ ค์ฃผ์—ˆ๋‹ค. ๋ชจ๋“  ์—๋Ÿฌ์— ๋Œ€ํ•ด์„œ ๊ณตํ†ต์ ์œผ๋กœ dto์„ ์ •์˜ํ•ด์ฃผ๊ณ , ์Šค์›จ๊ฑฐ๊นŒ์ง€ ์นœ์ ˆํ•˜๊ฒŒ. ์—๋Ÿฌ ์ฝ”๋“œ๋งˆ๋‹ค ์˜ค๋Š” ํ˜•์‹์„ ํ•œ๋ˆˆ์— ๋ณผ ์ˆ˜ ์žˆ๋‹ค. http ์ƒํƒœ์ฝ”๋“œ๋ฅผ ๋”ฐ๋ฅด๊ณ  ๊ทธ์— ๋งž๊ฒŒ ์—๋Ÿฌ๋ฉ”์‹œ์ง€๊ฐ€ ๋”ฐ๋ผ์˜จ๋‹ค.

 

 

400 ์—๋Ÿฌ์—๋Š” ๋‹ค์–‘ํ•œ ์ด์œ ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค. BadRequest ์— ๋Œ€ํ•ด์„  "Auth-0002"์™€ ๊ฐ™์ด ์ปค์Šคํ…€ํ•œ code๋ฅผ ๊ฐ™์ด ๋‚ด๋ ค์ค€๋‹ค. ValidationError๋Š” ๊ฒ€์ฆ์˜ค๋ฅ˜๊ฐ€ ๋‚œ ํ•„๋“œ๋ฅผ ๊ฐ™์ด ๋‚ด๋ ค์ค€๋‹ค. ๋˜‘๊ฐ™์€ 400์ด๊ธฐ ๋•Œ๋ฌธ์— ์—๋Ÿฌ ์ฝ”๋“œ๋งŒ ๋ณด๊ณ  ์–ด๋–ค ์—๋Ÿฌ์ธ์ง€ ํŒ๋‹จํ•˜๊ธฐ ์–ด๋ ต๋‹ค.

 

export interface ICustomError {
  error: string;
  statusCode: number;
  message: string;
  code: string;
}

export interface IValidationError extends ICustomError {
  validationErrorInfo: { [key: string]: string };
}

export type TCustomErrorResponse = {
  method: string;
  timestamp: number;
  statusCode: string;
  path: string;
  error: ICustomError;
};

์—๋Ÿฌ ๊ฐ์ฒด์˜ ํƒ€์ž…์€ ์ด๋ ‡๊ฒŒ ์ •์˜ํ–ˆ๋‹ค. ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์šฐ์„  React Query์˜ onError๋ฅผ ํ†ตํ•ด ์—๋Ÿฌ ๊ฐ์ฒด๋ฅผ ๋ฐ›๋Š”๋‹ค. ๊ทธ ์ดํ›„์— ์—๋Ÿฌ ๋ฆฌ์Šคํฐ์Šค๋ฅผ ๋œฏ์–ด๋ณด๊ณ  ๊ฐ๊ฐ ์—๋Ÿฌ๋งˆ๋‹ค ์ง€์ •๋œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

 

์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ๋ฆ„์„ ์ผ๊ด€์ ์œผ๋กœ ์ž˜ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ ๊ด€๋ จ ์ฝ”๋“œ๋“ค์„ ๋ถ„์‚ฐ์‹œํ‚ค์ง€ ์•Š๊ณ  ๊ฐ€๊ธ‰์  ๋ชจ์•„์„œ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ React๋ฅผ ์ด์šฉํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ Hook์œผ๋กœ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ๋ฆ„์˜ ์ฃผ์š” ๋กœ์ง์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

 

 

์—๋Ÿฌ ์ฒ˜๋ฆฌ Hook

Query Client์— ๊ณตํ†ต์ ์œผ๋กœ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋ฆฌ์•กํŠธ ํ›…์„ ์‚ฌ์šฉํ•œ๋‹ค.

 

const useApiError = () => {
  const { openErrorModal } = useErrorModal();

  const handleError = useCallback((axiosError: AxiosError) => {
    const errorResponse = axiosError.response?.data as TCustomErrorResponse;
    const error = errorResponse.error;
    const status = error.statusCode;

    switch (status) {
      // BadRequestException | ValidationError
      case 400:
        if (isValidationError(error)) {
          console.log(error.validationErrorInfo);
        } else {
          openErrorModal(error);
        }
        break;
      // ๊ณผ๋„ํ•œ ์š”์ฒญ์„ ๋ณด๋‚ผ ์‹œ
      case 429:
        openErrorModal(error);
        break;
      // ๋ฌธ์ž๋ฉ”์‹œ์ง€ ๋ฐœ์†ก ์‹คํŒจ
      case 500:
        defaultHandler(error);
        break;
      default:
        defaultHandler(error);
        break;
    }
  }, []);
  return { handleError } as const;
};

export default useApiError;

handleError ํ•จ์ˆ˜์—์„  ์—๋Ÿฌ ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ์ฝ”๋“œ๋ณ„๋กœ ์ง€์ •๋œ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ํ”„๋กœ์ ํŠธ์—์„  ์‚ฌ์šฉ์ž๊ฐ€ ๋ด์•ผํ•  ์—๋Ÿฌ๋Š” ์ „๋ถ€ ๋ชจ๋‹ฌ์„ ํ†ตํ•ด ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ๋‹ค. 400๊ณผ 429์—๋Ÿฌ๊ฐ€ ์ด์— ํ•ด๋‹น๋จ. (ํ”„๋ก ํŠธ์•ค๋“œ ๋‹จ์—์„œ ํ•œ๋ฒˆ ๋ง‰๊ณ  ์š”์ฒญ์„ ๋ณด๋‚ด๊ธฐ ๋•Œ๋ฌธ์—) ์ •์ƒ์ ์ธ ๊ฒฝ์šฐ๋ผ๋ฉด ๋œฐ ์ผ๋„ ์—†๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ ธ์„œ๋„ ์•ˆ๋˜๋Š” ๋ฐธ๋ฆฌ๋ฐ์ด์…˜ ์—๋Ÿฌ๋Š” ๊ทธ๋ƒฅ ์ฝ˜์†”์— ๋„์šฐ๋Š”๊ฑฐ๋กœ ์ฒ˜๋ฆฌํ–ˆ๋‹ค. ์‚ฌ์‹ค ํƒ€์ž…๊ฐ€๋“œ๋ฅผ ์—ฌ๊ธฐ์„œ ํ•œ๋ฒˆ ์จ๋ณด๊ณ  ์‹ถ์—ˆ์Œ.

 

function isValidationError(error: any): error is IValidationError {
  return (error as IValidationError).validationErrorInfo !== undefined; // T of F
}

 

๊ฐ์ฒด๋ฅผ ๊นŒ์„œ ๋‚ด๋ถ€ ๊ฐ’์„ ๋ณด๊ธฐ ์ „์— ํƒ€์ž… ๊ฐ€๋“œ๋ฅผ ํ†ตํ•ด error๊ฐ€ validationError ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค. if๋ฌธ์œผ๋กœ ์ด๋ฆฌ์ €๋ฆฌ ํ™•์ธํ•ด๊ฐ€๋ฉด์„œ ๋Œ๋ฆฌ๋Š”๊ฒƒ๋ณด๋‹ค ํ›จ์‹  ๊ฐ€๋…์„ฑ์žˆ๋‹ค.

 

export type TErrorCode =
  | 'Auth-0000'
  | 'Auth-0001'
  | 'Auth-9000'
  | 'Auth-5000'
  | 'Auth-0002';

const errorMessage = {
  'Auth-0000': '์ธ์ฆ๋ฒˆํ˜ธ์˜ ๊ธฐํ•œ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์–ด์š”',
  'Auth-0001': '์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค',
  'Auth-9000': '์ž ์‹œ ๋’ค์— ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”',
  'Auth-5000': '๋ฌธ์ž ๋ฐœ์†ก์— ์‹คํŒจํ–ˆ์–ด์š”\n์นด์นด์˜ค ์ฑ„๋„๋กœ ๋ฌธ์˜์ฃผ์„ธ์š”',
  'Auth-0002': '์ด๋ฏธ ๊ฐ€์ž…๋œ ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค',
};

const useErrorModal = () => {
  const { openModal, closeModal } = useModal();

  const openErrorModal = (error: ICustomError) => {
    const code = error.code as TErrorCode;
    console.log(code);
    openModal({
      modalType: 'Notice',
      modalProps: {
        onClick: () => {
          closeModal();
        },
        type: '์—๋Ÿฌ์ฒ˜๋ฆฌ',
        errorMessage: errorMessage[code],
      },
    });
  };

  return { openErrorModal } as const;
};

export default useErrorModal;

useErrorModal์—์„œ๋Š” ์ปค์Šคํ…€ ์—๋Ÿฌ ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ์—๋Ÿฌ ์ฝ”๋“œ์— ๋งž๋Š” ๋ชจ๋‹ฌ์„ ๋„์šฐ๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค. ์—๋Ÿฌ์šฉ ๋ชจ๋‹ฌ์€ ๊ฐ™์€ ๋””์ž์ธ์— ๋ฌธ๊ตฌ๋งŒ ๋‹ค๋ฅด๊ฒŒ ๋“ค์–ด๊ฐ„๋‹ค.

 

 

 

๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ Default onError

QueryClient๋ฅผ ์ดˆ๊ธฐํ™” ํ• ๋•Œ ๋ชจ๋“  ์ฟผ๋ฆฌ๋ฌธ์— ๋””ํดํŠธ๋กœ ์ ์šฉ๋˜๋Š” ์„ค์ •์„ ๋„ฃ์–ด ์ค„ ์ˆ˜ ์žˆ๋‹ค.

const queryClient = new QueryClient({
  defaultOptions: {
    onError: handleError,
  },
})

๊ฐ„๋‹จํžˆ ์ด๋Ÿฐ ์‹์œผ๋กœ ํ•˜๋ฉด ๋จ.

 

ํ•˜์ง€๋งŒ ๋‚ด ๊ฒฝ์šฐ์—” onError ์ด๋ฒคํŠธ์— ์ปค์Šคํ…€ํ›…์—์„œ ๋ฐ›์•„์˜จ handleError ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ index.tsx์—์„œ App์„ ๊ฐ์‹ธ๊ณ  ์žˆ๋˜ <QueryClientProvider/>๋ฅผ App.tsx ๋‚ด๋ถ€๋กœ ๊ฐ€์ ธ์™€ ๊ฐ์‹ธ๋Š” ๊ฑธ๋กœ ์ˆ˜์ •ํ–ˆ๋‹ค. ๊ทธ๋žฌ๋”๋‹ˆ ์›ฌ๊ฑธ, ๋ชจ๋‹ฌ์„ ๋„์›Œ์•ผํ•˜๋Š” ์ƒํ™ฉ๋งˆ๋‹ค ์ฟผ๋ฆฌ์— ์žˆ๋˜ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋“ค์ด ๋ชจ์กฐ๋ฆฌ ์‚ฌ๋ผ์ง€๋Š” ์ผ์ด ์žˆ์—ˆ๋‹ค. ์‘์›ํ†ก ํŽ˜์ด์ง€์—์„œ ๋ฉ”์‹œ์ง€๋“ค์€ ์ œ๋Œ€๋กœ ํŒจ์นญ์ด ๋˜๋Š”๋ฐ ์ฟผ๋ฆฌ ๋ฐ๋ธŒํˆด์—๋Š” ์•ˆ๋ณด์ด๋Š” ์ƒํ™ฉ์ด์—ˆ์Œ. ์ ์ž–์ด ๋‹นํ™ฉํ–ˆ๋‹ค. ๋‹คํ–‰ํžˆ ์Šคํƒ์— ๋‚˜๋ž‘ ๋น„์Šทํ•œ ๊ฒฝํ—˜์ด ์žˆ์—ˆ๋˜ ์‚ฌ๋žŒ์ด ์ž๋ฌธ์ž๋‹ตํ–ˆ๋˜ ๊ธ€์ด ์žˆ์—ˆ๋‹ค. queryClient๊ฐ€ App๋‚ด์— ์žˆ์–ด์„œ ์žฌ๋ Œ๋”๋ง์ด ๋˜๋Š”๊ฒŒ ๋ฌธ์ œ์˜€๋‹ค.

 

function App() {
  const { handleError } = useApiError();
  const queryClient = useQueryClient();

  queryClient.setDefaultOptions({
    queries: { onError: (error: any) => handleError(error) },
    mutations: {
      onError: (error: any) => {
        handleError(error);
      },
    },
  });

  // ...

์šฐ์„  index.tsx์—์„œ๋Š” ์•„๋ฌด๋Ÿฐ ์˜ต์…˜ ์—†์ด new QueryClient();๋กœ ์ฟผ๋ฆฌํด๋ผ์ด์–ธํŠธ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ๊ทธ ํ›„ app.tsx์—์„œ useQueryClient()๋กœ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ฐ€์ ธ์™€ ๋””ํดํŠธ ์˜ต์…˜์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. onError ์ด๋ฒคํŠธ์— handleError๋ฅผ ์ „๋‹ฌํ•ด์ค€๋‹ค. ๋œปํ•˜์ง€ ์•Š์€ ์‚ฝ์งˆ์ด์—ˆ์ง€๋งŒ ๋‚˜๋ฆ„ ์–ป์–ด๊ฐ€๋Š”๊ฒŒ ๋งŽ์•˜๋‹ค.