뱁새 다리찢기

들어가며

권한 관리를 구현하게 되었습니다. 확장성, 단일 책임, 시점 이동 최소화 3가지 원칙에 집중해서 설계했습니다.

핵심 설계 원칙

1. 컴포지션 패턴으로 확장성 확보

/**
 * 접근 권한이 없는 경우 메인 페이지로 리다이렉트하는 기본 보호 라우트
 * @param isAccessible 접근 권한 여부
 * @param children 자식 컴포넌트
 */

// 기본 protected route - 단일 책임
export const ProtectedRoute = ({
  isAccessible,
  children,
}: ProtectedRouteProps) => {
  if (!isAccessible) {
    return <Navigate to={ROUTE_PATHS.root} replace />;
  }
  return children;
};

// 역할 기반 - protected route 재사용
export const RoleProtectedRoute = ({
  requiredRole,
  children,
}: RoleProtectedRouteProps) => {
  const { user } = useUser();

  return (
    <ProtectedRoute isAccessible={user?.role === requiredRole}>
      {children}
    </ProtectedRoute>
  );
};

// 권한 기반 - protected route 재사용
export const PermissionProtectedRoute = ({
  requiredPermission,
  children,
}: PermissionProtectedRouteProps) => {
  const { user } = useUser();

  return (
    <ProtectedRoute isAccessible={getHasPermission(user, requiredPermission)}>
      {children}
    </ProtectedRoute>
  );
};

// 컴포넌트 단위 권한 제한
export const RequirePermission = ({
  permission,
  children,
}: RequirePermissionProps) => {
  const { user } = useUser();

  return getHasPermission(user, permission) ? children : null;
};

새로운 권한 타입이 추가되어도 기본 ProtectedRoute만 재사용하면 됩니다. 리다이렉트 로직 변경도 한 곳에서만 수정 가능합니다.

useUser를 외부에서 주입하도록 해서 완벽한 단일 책임을 구현하고 싶었으나, 그러면 바깥에서 훅을 선언해 주입해줘야 하므로 사용성이 떨어집니다. 어쩔 수 없는 트레이드오프인 것 같습니다.

2. 선언적 라우터 설정으로 가독성 챙기기

export const router = createBrowserRouter([
  {
    path: ROUTE_PATHS.root,
    Component: AdminLayout,
    children: [
      {
        path: ROUTE_PATHS.pageA,
        element: (
          <PermissionProtectedRoute requiredPermission="page_a_access">
            <PageA />
          </PermissionProtectedRoute>
        ),
      },
      {
        path: ROUTE_PATHS.pageB,
        element: (
          <RoleProtectedRoute requiredRole="admin">
            <PageB />
          </RoleProtectedRoute>
        ),
      },
    ],
  },
]);

라우터 설정만 봐도 각 페이지의 권한 요구사항을 즉시 파악할 수 있습니다. 권한 정책이 코드에서 바로 드러납니다.

JSX단에서 사용 가능

react router, tanstack table의 accessor처럼 컴포넌트 단위로 조건 제한이 필요한 상황에서 용이합니다

React Router v7

// 권한 체크가 라우터 레벨에서 선언적으로 처리됨
element: (
  <PermissionProtectedRoute requiredPermission="finance_access">
    <Finance />
  </PermissionProtectedRoute>
)

TanStack Table

// 테이블 컬럼별 권한 제어
{
  id: 'actions',
  header: '작업',
  cell: ({ row }) => (
    <RequirePermission permission="user_management">
      <UserActionButtons user={row.original} />
    </RequirePermission>
  ),
}

덕분에 권한 체크를 컴포넌트 외부에서 처리할 수 있습니다. 관심사 분리가 자연스럽게 이루어집니다.

결과

  • 확장성: 새로운 권한 타입 추가 시 기본 패턴 재사용
  • 가독성: 권한 요구사항이 코드에서 바로 드러남
  • 시점 이동 최소화: 관련 정보가 한 곳에 모여있음
profile

뱁새 다리찢기

@donghyk2-eric

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!