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

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

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

코딩 정보/React

리액트 리팩토링 정리글

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

목차

  1. 리액트 버전 19에서 meta 태그 최적화 하기
  2. 라우트 분리하기
  3. feature 폴더를 통한 폴더 구조 개선하기
  4. 라우터 보호하기
  5. api 요청 타입 지정
  6. api 로깅하기
  7. 공통 api로직 추가하기

 

 

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

 

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

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

javascript
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;

 

javascript
import SEO from "@/components/SEO";
import { CategorySection, HomeSection } from "@/features/home";

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

export default Home;

 

 

라우트 분리하기

javascript
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;

 

javascript
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를 해준다.

javascript
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문이 깔끔해진다.

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

 

 

라우터 보호하기

기존 방식의 문제점

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

javascript
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를 통한 방식

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

 

javascript
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;

 

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

 

api 요청 타입 지정

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

 

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

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

 

 

api 로깅하기

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

javascript
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;

 

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

 

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

 

공통 api로직 추가하기

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

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

 

getData.ts

javascript
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

javascript
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`);
  }
};

 

활용 예시

javascript
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("이메일 업데이트 성공!");
    },
  });
};

 

반응형