RBAC는 복잡한 프론트엔드에서 페이지 접근, 버튼 노출, API 호출을 안전하게 통제하는 핵심 패턴입니다. 실무에서 바로 쓸 수 있는 권한 설계, 훅/컴포넌트, 라우팅 가드, 서버 연동까지 단계별로 정리합니다.
1. 왜 RBAC인가
역할(roles)과 권한(permissions)을 분리하면 기능 추가/변경 시 코드 수정 범위를 최소화할 수 있습니다. 또한 UI와 라우팅, API 레벨에서 일관된 통제가 가능합니다.
2. 권한 모델 설계
역할-권한 매핑을 한 곳에 선언합니다. 최소 권한 원칙을 지키고, 비즈니스 용어로 이름을 짓습니다.
// permissions.js
export const Roles = {
ADMIN: 'admin',
EDITOR: 'editor',
VIEWER: 'viewer',
}
export const Permissions = {
READ_POST: 'read:post',
EDIT_POST: 'edit:post',
DELETE_POST: 'delete:post',
MANAGE_USER: 'manage:user',
}
export const RolePermissions = {
[Roles.ADMIN]: Object.values(Permissions),
[Roles.EDITOR]: [Permissions.READ_POST, Permissions.EDIT_POST],
[Roles.VIEWER]: [Permissions.READ_POST],
}
export const hasPermission = (user, perm) => {
const roles = user?.roles || []
return roles.some(r => (RolePermissions[r] || []).includes(perm))
}
3. AuthContext와 useAuth 훅
사용자와 역할을 전역 상태로 관리합니다. 예시는 JWT에서 roles 클레임을 읽어오는 방식입니다.
// auth-context.jsx
import React, { createContext, useContext, useEffect, useState } from 'react'
const AuthContext = createContext(null)
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const token = localStorage.getItem('access_token')
if (!token) { setLoading(false); return }
try {
// 실무에서는 'jwt-decode' 라이브러리 사용 권장
const payload = JSON.parse(atob(token.split('.')[1]))
setUser({ id: payload.sub, roles: payload.roles || [], name: payload.name })
} catch (e) {
console.error('Invalid token', e)
localStorage.removeItem('access_token')
} finally {
setLoading(false)
}
}, [])
const login = async (email, password) => {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (!res.ok) throw new Error('Login failed')
const { accessToken } = await res.json()
localStorage.setItem('access_token', accessToken)
const payload = JSON.parse(atob(accessToken.split('.')[1]))
setUser({ id: payload.sub, roles: payload.roles || [], name: payload.name })
}
const logout = () => {
localStorage.removeItem('access_token')
setUser(null)
}
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => useContext(AuthContext)
4. 권한 체크 훅
권한 판단 로직을 훅으로 캡슐화해 재사용성을 높입니다.
// use-permission.js
import { useAuth } from './auth-context'
import { hasPermission } from './permissions'
export function usePermission(required) {
const { user, loading } = useAuth()
const allowed = !loading && hasPermission(user, required)
return { allowed, isLoading: loading }
}
5. 라우트 가드: 인증과 권한
React Router v6 기준으로 인증 가드와 권한 가드를 분리합니다.
// route-guards.jsx
import React from 'react'
import { Navigate, Outlet } from 'react-router-dom'
import { useAuth } from './auth-context'
import { usePermission } from './use-permission'
export function ProtectedRoute() {
const { user, loading } = useAuth()
if (loading) return <div>Loading...</div>
return user ? <Outlet /> : <Navigate to='/login' replace />
}
export function RequirePermission({ permission }) {
const { allowed, isLoading } = usePermission(permission)
if (isLoading) return <div />
return allowed ? <Outlet /> : <Navigate to='/403' replace />
}
// App.jsx (라우팅 연결)
import React from 'react'
import { Routes, Route } from 'react-router-dom'
import { ProtectedRoute, RequirePermission } from './route-guards'
import { Permissions } from './permissions'
import LoginPage from './pages/LoginPage'
import Home from './pages/Home'
import Posts from './pages/Posts'
import EditPost from './pages/EditPost'
import AdminPage from './pages/AdminPage'
import Forbidden from './pages/Forbidden'
export default function App() {
return (
<Routes>
<Route path='/login' element={<LoginPage />} />
<Route element={<ProtectedRoute />}>
<Route path='/' element={<Home />} />
<Route path='/posts' element={<Posts />} />
<Route element={<RequirePermission permission={Permissions.EDIT_POST} />}>
<Route path='/posts/:id/edit' element={<EditPost />} />
</Route>
<Route element={<RequirePermission permission={Permissions.MANAGE_USER} />}>
<Route path='/admin' element={<AdminPage />} />
</Route>
</Route>
<Route path='/403' element={<Forbidden />} />
</Routes>
)
}
6. UI 단위 권한 제어
버튼, 메뉴 등 컴포넌트 레벨에서도 안전하게 숨기거나 비활성화합니다.
// HasPermission.jsx
import React from 'react'
import { usePermission } from './use-permission'
export default function HasPermission({ permission, fallback = null, children }) {
const { allowed, isLoading } = usePermission(permission)
if (isLoading) return null
return allowed ? children : fallback
}
// 사용 예시
// <HasPermission permission={Permissions.DELETE_POST} fallback={<span>권한 없음</span>}>
// <button onClick={handleDelete}>삭제</button>
// </HasPermission>
7. 서버와 토큰 연동
프론트 권한 가드는 UX 용도입니다. 실제 보안은 서버에서 반드시 재검증해야 합니다.
// api.js: 토큰을 자동으로 포함하는 fetch 래퍼
export const api = (path, options = {}) => {
const token = localStorage.getItem('access_token')
return fetch(path, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.headers || {}),
},
}).then(async res => {
if (res.status === 403) {
// 전역 알림 또는 403 페이지로 유도
// window.dispatchEvent(new CustomEvent('forbidden'))
}
return res
})
}
서버는 JWT 서명 검증 후 roles/permissions을 확인하고 403을 반환해야 합니다. 민감한 리소스 보호는 프론트에서 숨기는 것으로 충분하지 않습니다.
8. 로딩과 UX 다듬기
권한/인증 로딩 동안 깜빡임을 줄입니다. 전체 페이지 스피너 또는 스켈레톤을 사용하고, 403 전용 페이지를 제공합니다.
// Forbidden.jsx
export default function Forbidden() {
return <div style={{ padding: 32 }}>접근 권한이 없습니다.</div>
}
9. 테스트 전략
권한 로직은 회귀가 잦습니다. 단위/통합 테스트를 추가하세요.
- 단위: hasPermission, usePermission의 다양한 조합 테스트
- 컴포넌트: HasPermission이 올바르게 노출/비노출 되는지
- E2E: 보호 라우트 접근 시 로그인/403 동작 확인
10. 흔한 함정과 체크리스트
- 클라이언트만 믿지 말 것: 서버에서 최종 권한 검증
- 권한 상수 하드코딩 금지: permissions.js 한 곳에서 관리
- 비동기 깜빡임: loading 상태 처리
- 권한 변경 반영: 재로그인 또는 /me 재조회로 동기화
- 감사 로그: 민감 액션은 서버에서 로깅
11. 확장 아이디어
RBAC에서 한 걸음 더 나아가 리소스 소유권, 속성 기반 정책(ABAC), 피처 플래그와 조합하면 대규모 제품에서도 유연한 접근 제어가 가능합니다.
요약: 역할/권한을 명확히 정의하고, 컨텍스트-훅-가드-UI 컴포넌트 레이어로 일관되게 적용하세요. 프론트는 UX 가드, 서버는 최종 보안이라는 원칙을 지키면 유지보수와 보안성을 모두 확보할 수 있습니다.
'React' 카테고리의 다른 글
| React에서 컴포넌트 성능 측정 및 분석하기 (1) | 2026.05.13 |
|---|---|
| React 앱 CI/CD 파이프라인 구축하기 (0) | 2026.05.12 |
| React에서 Progressive Image Loading 구현하기 (0) | 2026.05.11 |
| React 앱에서 애니메이션을 위해 Framer Motion 활용하기 (0) | 2026.05.11 |
| React에서 Highcharts로 데이터 시각화하기 (1) | 2026.05.08 |