728x90
반응형
목차
- 리액트 버전 19에서 meta 태그 최적화 하기
- 라우트 분리하기
- feature 폴더를 통한 폴더 구조 개선하기
- 라우터 보호하기
- api 요청 타입 지정
- api 로깅하기
- 공통 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("이메일 업데이트 성공!");
},
});
};
반응형
'코딩 정보 > React' 카테고리의 다른 글
[React] 소켓 통신 전 개념에 대해 정리해보자 (0) | 2025.04.11 |
---|---|
[React] 에서의 에러 핸들링 전략에 대해 살펴보자 (0) | 2025.04.10 |
[vite][vitest] 프로젝트 중 테스트 코드를 작성하며.. (0) | 2025.02.26 |
[React][Zustand] SessionStorage를 사용해 보자 (0) | 2024.12.08 |
[React][Vite] vite-plugin-svgr을 사용해 보자 (0) | 2024.12.06 |