Loading...
본문 바로가기
👥
총 방문자
📖
0개 이상
총 포스팅
🧑
오늘 방문자 수
📅
0일째
블로그 운영

여러분의 방문을 환영해요! 🎉

다양한 개발 지식을 쉽고 재미있게 알려드리는 블로그가 될게요. 함께 성장해요! 😊

코딩 정보/React

리액트 리팩토링 정리글

by 꽁이꽁설꽁돌 2025. 3. 28.
728x90
반응형
     

목차

     

     

    프로젝트를 진행하면서 코드를 개선한 점을 정리하면 좋을 것 같아 적고자 한다.

     

    리액트 버전 19에서 meta 태그 최적화 하기

    리액트 19에서는 react-helmet을 쓰지 않아도 된다. 
    쓰게되면 충돌이 나니 유의하자

    interface SEOProps {
      title: string;
      description: string;
      keywords?: string;
    }
    
    function SEO({ title, description, keywords }: SEOProps) {
      return (
        <>
          <title>{title}</title>
          <meta name="title" content={title} />
          <meta name="description" content={description} />
          <meta name="keywords" content={keywords || ""} />
          <meta name="robots" content="index, follow" />
        </>
      );
    }
    
    export default SEO;

     

    import SEO from "@/components/SEO";
    import { CategorySection, HomeSection } from "@/features/home";
    
    function Home() {
      return (
        <>
          <SEO
            title="모꼬지 | 세종대학교의 모든 동아리"
            description="모꼬지 메인 페이지입니다."
            keywords="세종대학교, 세종대, 동아리"
          />
          <HomeSection />
          <CategorySection />
        </>
      );
    }
    
    export default Home;

     

     

    라우트 분리하기

    import { RouteObject } from "react-router-dom";
    import { ClubDetail, ClubList } from "./lazyLoad";
    
    const clubRoutes: RouteObject = {
      path: "clubs",
      children: [
        {
          path: "",
          element: <ClubList />,
        },
        {
          path: "group/:affiliation",
          element: <ClubList />,
        },
        {
          path: ":id",
          element: <ClubDetail />,
        },
      ],
    };
    
    export default clubRoutes;

     

    const router = createBrowserRouter([
      {
        path: "/",
        element: <CommonLayout />,
        errorElement: <NotFound />,
        children: [
          {
            index: true,
            element: <Home />,
          },
          clubRoutes,
          {
            path: "recruit",
            element: <Recruitment />,
          },
          {
            path: "favorites",
            loader: CheckAuthLoader,
            element: <Favorite />,
          },
          {
            path: "maintenance",
            element: <SystemMaintenance />,
          },
          {
            path: "NoResults",
            element: <NoResults />,
          },
          {
            path: "/privacy-policy",
            element: <PrivacyPolicyPage />,
          },
        ],
      },
    ]);

     

     

    feature 폴더를 통한 폴더 구조 개선하기

    각 라우터별로 폴더를 생성하고 그 안에서 나누어 준다.

     

    index.ts에서 한번에 기능별로 export를 해준다.

    import ClubBox from "./components/ClubBox";
    import ClubDetailInfo from "./components/ClubDetailInfo";
    
    import { ClubType } from "./types/clubType";
    
    import { convertLinks } from "./utils/covertLinks";
    
    import { useGetClubs, useGetClubsDetail } from "./query/clubs.query";
    
    export {
      ClubBox,
      useGetClubs,
      useGetClubsDetail,
      convertLinks,
      ClubDetailInfo,
    };
    export type { ClubType };

     

    아래와 같이 import문이 깔끔해진다.

    import { ClubBox, ClubType, useGetClubs } from "@/features/clubs";

     

     

    라우터 보호하기

    기존 방식의 문제점

    즐겨찾기를 들어갔을 때 사용자가 아무 경고 없이 home으로 이동되고 모달이 열리기 때문에 영문을 알 수 없다.

    interface ProtectedProps {
      children: ReactNode;
    }
    
    export const ProtectedRoute = ({ children }: ProtectedProps) => {
      const { openModal } = useModalStore();
      const loginChecking = isLoginChecking();
    
      useEffect(() => {
        if (loginChecking) {
          openModal();
        }
      }, [loginChecking, openModal]);
    
      if (loginChecking) {
        return <Navigate to="/" replace />;
      }
    
      return children;
    };

     

     

    loader를 통한 방식

    경고창을 보여준 뒤 이동하므로 사용자가 그 이유를 알 수 있다.

     

    import { isLoginChecking } from "@/features/login/store/useAuthStore";
    import { redirect } from "react-router-dom";
    
    function CheckAuthLoader() {
      const loginChecking = isLoginChecking();
    
      if (loginChecking) {
        alert("로그인을 해야 이용하실 수 있습니다");
        return redirect("/");
      }
    
      return null;
    }
    
    export default CheckAuthLoader;

     

      {
            path: "favorites",
            loader: CheckAuthLoader,
            element: <Favorite />,
          },

     

    api 요청 타입 지정

    export interface ApiResponse<T> {
      status: number;
      message: string | undefined;
      data: T;
      error: string | undefined;
    }

     

    아래와 같이 공용 사용 가능하다.

    export type ClubResponseType = ApiResponse<{
      clubs: ClubType[];
      pagination: PaginationType;
    }>;

     

     

    api 로깅하기

    네트워크 탭을 이용해서 볼 수 있지만 내가 필요한 정보만 빠르게 골라 보면 좋을 것 같아 추가했다.

    import { AxiosResponse } from "axios";
    
    function apiLogging(response: AxiosResponse) {
      if (import.meta.env.VITE_NODE_ENV === "development")
        console.log(
          `응답 코드:${response.status}, 요청url:${response.config.url}, params:${
            JSON.stringify(response.config.params) || "없음"
          }`,
          response
        );
    }
    
    export default apiLogging;

     

    api.interceptors.response.use(
      (response) => {
        apiLogging(response);
        return response;
      },

     

    내가 필요한 정보만 추려 볼 수 있다.

     

    공통 api로직 추가하기

    기존 폴더 구조에 api를 폴더를 만든 뒤 각 파일마다 api 로직을 추가하고 쿼리로 또 작성하는게

    불필요하다고 느껴  공용 api로 개선해 보았다.

     

    getData.ts

    import { ApiResponse } from "@/types/ApiResponse";
    import { AxiosError, AxiosRequestConfig } from "axios";
    import api from ".";
    
    /**
     *
     * @param url
     * @param config
     * @returns
     */
    
    const getData = async <T>(
      url: string,
      config?: AxiosRequestConfig // config를 추가하여 params 등 설정 가능
    ): Promise<ApiResponse<T>> => {
      try {
        const { data } = await api.get<ApiResponse<T>>(url, config);
        return data;
      } catch (error) {
        console.error(error);
    
        if (error instanceof AxiosError) {
          throw new Error(error.response?.data?.message || "API 요청 실패");
        }
    
        throw new Error("Unknown error occurred");
      }
    };
    export default getData;

     

    updateData.ts

    import { ApiResponse } from "@/types/ApiResponse";
    import { AxiosError, AxiosRequestConfig } from "axios";
    import api from ".";
    
    
    /**
     * 
     * @param method 
     * @param url 
     * @param data 
     * @param config 
     * @returns 
     */
    export const updateData = async <T>(
      method: "post" | "put" | "patch" | "delete",
      url: string,
      data?: unknown,
      config?: AxiosRequestConfig
    ): Promise<ApiResponse<T>> => {
      try {
        let response;
        switch (method) {
          case "post":
            response = await api.post<ApiResponse<T>>(url, data, config);
            break;
          case "put":
            response = await api.put<ApiResponse<T>>(url, data, config);
            break;
          case "patch":
            response = await api.patch<ApiResponse<T>>(url, data, config);
            break;
          case "delete":
            response = await api.delete<ApiResponse<T>>(url, { ...config, data });
            break;
          default:
            throw new Error(`Unsupported method: ${method}`);
        }
        return response.data;
      } catch (error) {
        console.error(method, error);
    
        if (error instanceof AxiosError) {
          throw new Error(error.response?.data?.message || `${method} 요청 실패`);
        }
    
        throw new Error(`${method}: Unknown error occurred`);
      }
    };

     

    활용 예시

    import getData from "@/api/getData";
    import { UserInfoType, UserResponseType } from "@/types/userInfoType";
    import {
      useMutation,
      useQueryClient,
      useSuspenseQuery,
    } from "@tanstack/react-query";
    import { updateData } from "@/api/updateData";
    
    export const useGetUser = () => {
      return useSuspenseQuery<UserResponseType>({
        queryKey: ["users"],
        queryFn: () => getData("/users"),
      });
    };
    
    export const useUserInfoEdit = () => {
      const queryClient = useQueryClient();
    
      return useMutation({
        mutationFn: (email: string) => updateData("put", "/users", { email }),
    
        // ✅ 낙관적 업데이트
        onMutate: async (email) => {
          await queryClient.cancelQueries({ queryKey: ["users"] }); // 기존 요청 중지
    
          const oldData = queryClient.getQueryData<UserInfoType>(["users"]); // 안전한 타입 적용
    
          // 캐시 업데이트 (oldData 존재 시만)
          if (oldData) {
            queryClient.setQueryData<UserInfoType>(["users"], {
              ...oldData,
              email,
            });
          }
    
          return { oldData }; // context로 전달
        },
    
        // ❌ 요청 실패 시 이전 데이터 복원
        onError: (err: Error, _, context) => {
          console.error("Error Email put:", err.message);
          alert(`이메일 업데이트 실패: ${err.message}`);
          if (context?.oldData) {
            queryClient.setQueryData(["users"], context.oldData); // 안전하게 복원
          }
        },
    
        // ✅ 성공/실패 후 쿼리 무효화
        onSettled: () => {
          queryClient.invalidateQueries({ queryKey: ["users"] });
        },
        onSuccess: () => {
          alert("이메일 업데이트 성공!");
        },
      });
    };

     

    반응형