forked from CodeitPart3/thejulge
-
Notifications
You must be signed in to change notification settings - Fork 0
ProtectedRoute
이토 edited this page May 29, 2025
·
1 revision
서비스 내 각 페이지마다 로그인 여부, 사용자 권한에 따라 접근을 제한하는 요구사항이 있었습니다.
하지만 초기 구현은 각 페이지 컴포넌트마다 useEffect로 직접 조건을 검사하고, 조건 불일치 시 모달을 띄우거나 리다이렉트하는 방식이었습니다.
이 방식은 다음과 같은 문제를 낳았습니다:
- 같은 조건 분기 로직이 페이지마다 반복적으로 중복되고,
- 어떤 페이지가 어떤 권한 조건을 갖는지 한눈에 파악하기 어려우며,
- 조건 변경 시 모든 페이지를 직접 수정해야 하는 비효율이 발생했습니다.
더 나아가, 신규 조건이 추가될수록 각 페이지의 useEffect 로직은 점점 더 길어지고 불안정해질 수 있는 구조였습니다.
export default function Page() {
...
useEffect(() => {
if (!user) {
openModal({
type: "alert",
iconType: "warning",
message: "로그인 후에 이용 가능한 기능입니다.",
onClose: () => navigate(ROUTES.AUTH.SIGNIN),
});
return;
}
if (user.type === "employer") {
openModal({
type: "alert",
iconType: "warning",
message: "알바생 계정으로만 이용 가능한 기능입니다.",
onClose: () => navigate(ROUTES.SHOP.ROOT),
});
return;
}
}, []);
useEffect(() => {
const fetchUser = async () => {
if (!user?.id) return;
const res = await getUser(user.id);
if (!res.data.item?.name) {
navigate(ROUTES.PROFILE.REGISTER);
return;
}
const { name, phone, address, bio } = res.data.item;
setForm({
name: name ?? "",
phone: phone ?? "",
address,
bio: bio ?? "",
});
};
fetchUser();
}, [user?.id, navigate]);
return ( ... );
}저는 이 문제를 근본적으로 해결하기 위해, 조건 기반의 보호 라우트 시스템(ProtectedRoute)을 설계했습니다.
핵심 전략은 다음과 같습니다:
- 조건 배열 기반 구조화 - 각 페이지에 필요한 접근 조건을 ProtectedRoute의 conditions props로 선언하도록 만들고, 조건은
{ isPass, redirectPath, message }형태의 객체 배열로 전달됩니다. - 권한 검사 로직을 컴포넌트 외부로 이동 - 기존처럼 페이지 내부에서 검사하지 않고, 라우터 선언부에서 모든 권한 조건을 선언형으로 작성할 수 있도록 했습니다.
- 모달 표시 및 리다이렉트도 추상화 - 조건 불충족 시 메시지와 경로가 있으면 자동으로 모달 → 리다이렉트를 트리거하도록 구성하여, 반복되는 UI 처리도 제거했습니다.
- 향후 확장 문제까지 고려한 설계 방향 마련 - 프로젝트가 커짐에 따라 라우터 파일이 방대해질 수 있는 점을 우려했고, 실제로 별도의 파일로 분리하지는 않았지만, 도메인 단위로 라우터 파일을 분리하는 전략을 병렬적으로 고려했습니다. 이는 향후 확장성과 유지보수 편의를 높이기 위한 설계적 대비였습니다.
// Router.tsx 중 일부
<ProtectedRoute
conditions={({ isLoggedIn, user }) => [
loginProtectCondition(isLoggedIn),
{
isPass: user?.type === "employer",
redirectPath: ROUTES.PROFILE.ROOT,
message: "사장님 계정으로만 이용 가능한 기능입니다.",
},
]}
>
<NoticeEmployerPage />
</ProtectedRoute>// ProtectedRoute.tsx
interface ProtectedRouteProps {
children: ReactNode;
conditions: (
params: ProtectedRouteParamType,
) => ProtectedRouteConditionType[];
}
function ProtectedRoute({ children, conditions }: ProtectedRouteProps) {
const navigate = useNavigate();
const user = useUserStore((state) => state.user);
const isLoggedIn = useUserStore((state) => state.isLoggedIn);
const openModal = useModalStore((state) => state.openModal);
useLayoutEffect(() => {
if (user === undefined) return;
const receivedConditions = conditions({ user, isLoggedIn });
for (const { isPass, redirectPath, message } of receivedConditions) {
if (!isPass) {
if (message) {
openModal({
type: "alert",
iconType: "warning",
message,
onClose: () => navigate(redirectPath),
});
} else {
navigate(redirectPath);
}
break;
}
}
}, [user, isLoggedIn, conditions, openModal, navigate]);
if (user === undefined) {
return null;
}
return children;
}
export default ProtectedRoute;- 조건 로직의 중복 제거: 각 페이지마다 중복되던 useEffect 기반 조건 검사 코드가 완전히 제거되었습니다.
- 권한 구조의 가시성 확보: 모든 조건이 라우터 파일 상에서 한눈에 드러나, 유지보수와 리뷰 시에도 명확히 파악 가능해졌습니다.
- 확장성과 재사용성 강화: 새로운 조건 추가 또는 수정 시, 선언만으로 적용 가능하여 빠르게 대응할 수 있는 구조가 되었습니다.
- 모달과 리다이렉트 로직 일관화: 메시지 출력과 이동 처리까지 구조 내에서 자동화되어 사용자 경험도 안정적으로 유지되었습니다.