파편화된 권한 로직 하나로 묶기

파편화된 권한 로직 하나로 묶기

지난 프로젝트의 프론트엔드 코드 전반을 리팩토링하는 과정에서, 권한 관리 구조도 함께 수정하였습니다. 권한 관리는 라우터 설정, 메뉴 렌더링 로직, 개별 화면 컴포넌트 등 서비스 전반에 걸쳐 사용되는 횡단 관심사입니다. 리팩토링을 통해 이 로직을 보다 선언적이고 일관되게 관리할 수 있는 구조로 발전시켰습니다. 개선한 부분은 크게 세 가지입니다.

  1. 페이지 접근 로직 통합: 라우터와 메뉴를 그리는 코드에서 권한을 각각 관리하고 있어, 이를 한 파일에서 관리하도록 변경했습니다.

  2. 화면 내부 코드의 통일: 화면마다 권한을 체크하는 방식이 달라, 이를 하나의 커스텀 훅으로 통일하여 관리했습니다.

  3. 성능 최적화: 렌더링마다 권한 계산이 반복되던 부분을 useMemo로 개선해, 권한 정보가 변경될 때만 다시 계산하도록 최적화했습니다.

라우터 및 메뉴 권한 기준 통합: PAGE_PERMISSIONS

기존 구조에서는 특정 페이지에 접근할 수 있는 권한 정의가 라우터 보안 가드와 사이드바 메뉴 렌더링 로직에 각각 파편화되어 존재했습니다. 이로 인해 기획상의 권한 변경이 발생할 때마다 관련 파일을 모두 찾아 수정해야 했고, 한 곳이라도 누락될 경우 메뉴는 보이지만 접근은 막히거나, 반대로 접근은 가능한데 메뉴가 노출되지 않는 일관성 문제가 빈번했습니다.

이를 해결하기 위해 프로젝트 내의 모든 경로를 정의한 PATHS 상수를 키(key)로 활용하여, 각 경로에 허용된 역할을 배열 형태로 관리하는 PAGE_PERMISSIONS라는 단일 권한 지도를 구축했습니다.

export const PAGE_PERMISSIONS: Record = {

[PATHS.NOTICE_LIST]: ['USER', 'MANAGER', 'ADMIN'],

[PATHS.NOTICE_DETAIL]: ['MANAGER', 'ADMIN'],

[PATHS.ORG_MANAGEMENT]: ['ADMIN'],

;

이렇게 권한 지도를 만들어두면, 라우터 가드나 메뉴 컴포넌트에서는 이제 별도의 로직을 작성할 필요 없이, 현재 경로(Path)를 넘겨 해당 객체에서 허용된 역할을 조회하기만 하면 됩니다.

<RoleCheckLayout allowedRoles={PAGE_PERMISSIONS[PATHS.NOTICE_LIST]}>

  <NoticeListPage />

</RoleCheckLayout>

이러한 방식은 권한 설정을 비즈니스 로직에서 완전히 분리하여 선언적으로 관리할 수 있게 해줍니다. 향후 새로운 역할(Role)이 추가되거나 특정 페이지의 보안 정책이 강화되더라도 PAGE_PERMISSIONS 객체 하나만 수정하면 전체 서비스에 즉시 일괄 반영되는 확장성을 갖추게 되었습니다.

흩어져 있던 내부 로직을 하나로: useAuthCheck

페이지 단위의 접근 제어 외에도, 화면 내부에서 특정 버튼을 노출하거나 폼을 비활성화하는 등의 세밀한 UI 제어 로직 역시 중요한 과제였습니다. 이전에는 각 컴포넌트에서 직접 사용자 상태를 가져와 includes 연산을 수행하거나, 복잡한 조건문을 중복해서 작성하고 있었습니다.

이러한 파편화된 로직을 useAuthCheck라는 단일 커스텀으로 추상화했습니다. 특히 이 훅은 단순히 역할을 확인하는 것을 넘어, 전역 상태에서 관리되는 roleNames와 특정 테넌트나 조직에 종속된 tenantRoleNames를 하나로 병합하여 판별하는 로직을 내포하고 있어, 복잡한 권한 구조를 가진 프로젝트에서도 개발자가 직관적으로 사용할 수 있도록 설계했습니다.

export const useAuthCheck = () => {

const { roleNames = [], tenantRoleNames = [] } = useAuth();

const hasRole = useCallback(

(role: RoleName) => [...roleNames, ...tenantRoleNames].includes(role),

[roleNames, tenantRoleNames]

);

const { isUser, isManager, isAdmin } = useMemo(() => ({

isUser: hasRole(RoleName.USER),

isManager: hasRole(RoleName.MANAGER) || hasRole(RoleName.ADMIN),

isAdmin: hasRole(RoleName.ADMIN),

}), [hasRole]);

return { hasRole, isUser, isManager, isAdmin };

};

실제 화면 구현 단계에서는 다음과 같이 매우 간결한 코드로 대응이 가능합니다.

const { isAdmin, isManager } = useAuthCheck();

return (

<>

{isManager && 수정}

{isAdmin && 삭제}

);

이 훅의 도입으로 얻은 가장 큰 이점은 변경에 유연한 구조입니다. 만약 '매니저'의 정의가 '시니어 유저'까지 포함하도록 변경된다면, 수많은 화면의 코드를 고치는 대신 useAuthCheck 내부의 isManager 계산 로직만 수정하면 됩니다. 이는 휴먼 에러를 획기적으로 줄이고 코드의 가독성을 높이는 결과를 가져왔습니다.

성능 최적화와 안정성: useMemo와 useCallback의 활용

권한 확인 로직은 컴포넌트가 리렌더링될 때마다 반복적으로 실행되는 특성이 있습니다. 특히 사용자 역할이 많거나, [...roleNames, ...tenantRoleNames]와 같이 배열을 병합하고 순회하는 연산이 포함될 경우 불필요한 연산 비용이 발생합니다. 또한, 함수가 매번 새로 생성되면 이를 props로 받는 하위 컴포넌트의 불필요한 리렌더링까지 유발할 수 있습니다.

리팩토링 과정에서는 이러한 불필요한 연산 오버헤드를 최소화하기 위해 React의 최적화 API를 적극적으로 활용했습니다.

  • 함수 재생성 방지: hasRole 함수를 useCallback으로 감싸 의존성 배열인 roleNames 등이 바뀌지 않는 한 동일한 참조를 유지하게 했습니다. 이는 해당 함수를 사용하는 하위 컴포넌트의 최적화(React.memo 등)를 가능하게 합니다.

  • 파생 상태의 캐싱: isAdmin, isManager와 같은 역할 판별 결과값은 순수 함수적 성격을 띠므로 useMemo를 통해 캐싱했습니다. 이를 통해 복잡한 권한 조합 계산이 렌더링 시마다 반복되는 것을 방지하고, 연산 결과가 동일할 경우 이전 값을 재사용하도록 했습니다.

이러한 최적화는 단순히 실행 속도를 높이는 것뿐만 아니라, 데이터의 일관성을 보장하는 측면에서도 유효합니다. 렌더링 도중 권한 정보가 일시적으로 불안정해지더라도 메모이즈된 값을 통해 안정적인 UI를 유지할 수 있기 때문입니다.

이번 리팩토링의 핵심은 권한 관리의 일원화입니다. 파편화되어 있던 로직을 PAGE_PERMISSIONS, useAuthCheck라는 명확한 단일 인터페이스로 통합함으로써 다음과 같은 실질적인 이득을 얻었습니다.

  • 유지보수 효율 극대화: 권한 정책 변경 시 수정 범위가 단일 파일로 제한되어 대응 속도가 빨라졌습니다.

  • 코드의 선언성 향상: "어떻게 권한을 체크할 것인가"가 아닌 "누가 이 기능에 접근할 수 있는가"에 집중할 수 있는 코드가 되었습니다.

  • 시스템 안정성 확보: 중앙 집중식 관리를 통해 권한 누락이나 불일치로 인한 보안 취약점을 원천 차단했습니다.

파편화된 로직을 하나로 묶는 단순해 보이는 작업이었으나, 이를 통해 가독성과 유지 보수성이라는 두 마리 토끼를 잡을 수 있었으며, 향후 서비스가 더 복잡해지더라도 안정적으로 확장할 수 있는 탄탄한 기반을 마련했습니다.

Lynn

Site footer