문제: 사이드바가 권한을 안다
-
사용자 포털 사이드 메뉴: 상수 배열로 하드코딩
- 두 그룹의 메뉴가 파일 안에 박혀 있음
- 각 항목에
permission문자열 부착
// 권한 코드와 경로는 가명. 구조만 그대로다.
const GROUP_A_MENU_ITEMS = {
ALPHA: { label: "알파 관리", icon: AlphaIcon, href: "/group-a/alpha", permission: "ALPHA_VIEW" },
BETA: { label: "베타", icon: BetaIcon, href: "/group-a/beta", permission: "BETA_VIEW" },
// ...
} as const;
const hasPermission = (permissions: string[] | undefined, required: string): boolean => {
return permissions?.includes(required) ?? false;
};
const shouldShowGroupBMenu = (permissions: string[] | undefined): boolean => {
return hasPermission(permissions, "GAMMA_VIEW") || hasPermission(permissions, "DELTA_VIEW");
};
-
useMemo안에서 사용자 정보 API의 권한 배열을 돌려 필터링 -
추가로 그룹 이름은 API 메뉴 데이터에서 찾아 덮어쓰는 코드까지 부착
const blockchainGroupName =
frontMenuGroups.find((g) => g.menus?.some((m) => m.url?.startsWith("/blockchain")))
?.groupName ?? "블록체인 서비스";
메뉴 정보가 이미 서버에서 오고 있는데, 프론트는 그중 이름만 가져다 쓰고 항목 목록과 표시 여부는 자기 상수로 다시 만들고 있었다.
- 같은 정보를 두 곳에서 관리하는 전형적 형태
증상 목록:
- 권한 하나 추가 → 백엔드·프론트 양쪽 수정 필요
-
한쪽만 배포 → 메뉴가 안 보이거나, 눌렀더니 403
- 직전 커밋도 역할 기반 표시 조건을 하나 더 추가하는 내용
- 조건이 계속 붙는 구조
어떻게 고쳤나: 다 지웠다
-
useFrontMenu()가 이미 그룹과 메뉴 목록을 통째로 제공 - 조치: 그걸 그대로 렌더링하도록 변경
type MenuInfo = { id: string; name: string; url: string };
type GroupInfo = { groupId: string; groupName: string; menus: MenuInfo[] };
const NavSideMenu = () => {
const pathname = usePathname();
const { refetch: refetchFrontMenu, data: frontMenuData } = useFrontMenu();
const groups: GroupInfo[] = (frontMenuData as any)?.data ?? [];
// ...
- 프론트에 남은 것: URL → 아이콘 매핑 상수 하나
const ICON_MAP: Record<string, { icon: any; iconColor?: string }> = {
"/blockchain": { icon: BlockchainIcon },
"/blockchain/statistics": { icon: BarChartIcon, iconColor: "white" },
// ...
};
const getMenuIcon = (url: string) => {
return ICON_MAP[url] ?? { icon: FeedOutlinedIcon, iconColor: "white" };
};
-
핵심:
??폴백 - 백엔드가 새 메뉴 추가 + 프론트에 아이콘 매핑 없음 → 기본 아이콘으로 렌더
- 메뉴 추가가 프론트 배포를 기다리지 않음
-
아이콘이 예쁘지 않을 뿐 기능은 즉시 나감
- 삭제 156줄 / 추가 52줄
-
사라진 의존:
useMemo,useAuthStore,useUserInfo - 결과: 컴포넌트는 순수하게 API 응답을 그리는 일만 담당
왜 권한 필터링을 프론트에서 하면 안 되는가
- 이유 두 가지, 둘 다 독립적으로 충분
보안
- 프론트 권한 필터링 = 메뉴를 안 보이게 하는 것뿐
- URL 직접 입력 → 페이지 열림
-
결국 서버가 API 레벨에서 막아야 함
- 프론트 필터링의 성격: UX 편의, 보안 장치 아님
그런데 코드가 permission: "ALPHA_VIEW" 같은 문자열을 들고 있으면, 이게 보안 로직인 것처럼 읽힌다.
- 가장 위험한 코드: 실제 방어선은 다른 곳인데 여기 있는 것처럼 보이는 코드
이중 관리 비용
-
권한 판정 규칙이 서버·클라이언트에 각각 존재 → 언젠가 반드시 어긋남
- 백엔드에서 권한 코드를 나누거나 합치면 프론트 상수도 수정 필요
- 그걸 강제하는 장치 없음
- 어긋난 결과: “메뉴는 보이는데 403” 또는 “권한은 있는데 메뉴가 없다”
-
두 경우 다 사용자 신고 전에는 아무도 모름
- 권한 판정을 백엔드 한 곳으로 집중 → 진실 공급원 단일화
- 프론트의 역할: “받은 걸 그린다”뿐, 어긋날 여지 없음
반대편: 관리자 포털에서 이 메뉴를 편집한다
-
같은 시기 관리자 포털에 메뉴 관리 화면 추가
- 메뉴·메뉴 그룹의 순서를 드래그로 변경하는 모달
- 사용 라이브러리:
@dnd-kit
import { DndContext, useSortable, arrayMove, SortableContext,
verticalListSortingStrategy } from "@dnd-kit/...";
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<SortableContext items={orderedItems.map((i) => i.id)} strategy={verticalListSortingStrategy}>
{/* useSortable({ id: item.id }) 을 쓰는 행 컴포넌트들 */}
</SortableContext>
</DndContext>
-
useSortable→ 각 행에 드래그 핸들과 transform 부여 -
onDragEnd→arrayMove로 배열 순서 변경 후 로컬 상태 반영 - 저장 시 서버로 전송
-
arrayMove를 쓰는 이유: 순서 계산 로직을 직접 짤 필요가 없음 - 여기서 구조 완성
| 관리자 포털 | 사용자 포털 | |
|---|---|---|
| 하는 일 | 메뉴 생성·수정·순서 편집 | 받은 메뉴 렌더링 |
| 권한 판정 | 없음 (백엔드) | 없음 (백엔드) |
| 프론트가 가진 상수 | 없음 | ICON_MAP만 |
-
관리자가 메뉴 순서 변경 → 사용자 사이드바 순서도 프론트 배포 없이 반영
-
이전 구조: 상수 배열의 원소 순서를 고쳐서 배포해야 했음
남는 교훈
“서버에서 이미 내려주는 데이터를 프론트 상수로 다시 정의하고 있다”는 신호가 보이면, 그 상수는 대체로 지워질 수 있다.
- 이 경우의 신호: 그룹 이름을 API에서 찾아 덮어쓰는 코드
- 그 의미: 서버 데이터를 부분적으로만 신뢰
그리고 삭제 156줄 / 추가 52줄인 커밋에서 실제로 중요한 건 지워진 쪽이다. hasPermission, shouldShowBlockchainMenu 같은 헬퍼는 잘 짜여 있었지만, 애초에 프론트에 있으면 안 되는 판단을 잘 수행하고 있었을 뿐이다.