React 권한(인증·인가) 처리 모범 사례
React 애플리케이션에서 인증과 인가를 체계적으로 설계하는 전략. 토큰 저장 방식, 권한 기반 라우팅, protected route 구현과 관련된 실무 중심 전략
목차
개요
웹 애플리케이션에서 인증(Authentication)과 인가(Authorization)는 서로 다른 목적을 가진다. 인증은 사용자의 신원을 확인하는 것이고, 인가는 확인된 사용자에게 어떤 리소스에 접근 권한을 줄지 결정하는 것이다. React 환경에서는 클라이언트 특성상 토큰 관리와 라우팅 처리를 신중히 설계하는 것이 중요하다. 아래 내용은 실무에서 흔히 마주하는 상황을 중심으로 정리한 권장 패턴과 코드 예제이다.
핵심 원칙
- 토큰은 가능한 한 안전한 저장소에 보관. XSS 공격에 취약한 위치는 피함.
- 인증 상태는 중앙에서 관리하고, 컴포넌트는 최소 권한으로 접근.
- 네트워크 요청은 토큰 만료, 갱신 로직을 일관되게 처리.
- 라우팅은 역할 기반 접근 제어를 명확히 분리.
토큰 저장과 관리
쿠키 vs 로컬 스토리지
보안 측면에서 httpOnly 쿠키 사용이 권장된다. 쿠키에 refresh token을 두고 access token은 짧은 수명으로 발급해 서버에서 관리하는 패턴이 안전하다. 로컬스토리지는 XSS에 취약하므로 민감한 토큰 장기 저장은 피하는 편이 바람직하다.
토큰 갱신 전략
액세스 토큰 만료 시 자동 갱신 흐름이 필요하다. 클라이언트는 401 응답을 감지하면 refresh token으로 액세스 토큰을 재발급받고, 재요청을 수행하는 패턴이 일반적이다. 이 과정은 동시성 제어가 필요하다. 다수 요청이 동시에 401을 반환하면 한 번의 갱신만 수행하고 대기 중인 요청들이 갱신된 토큰을 재사용하도록 설계하는 것이 효율적이다.
React에서의 권한 라우팅
라우팅 라이브러리는 보통 역할 기반 접근 제어를 간단히 구현할 수 있는 구조를 제공한다. 'react 권한 라우팅'에서는 보호된 라우트(protected route)를 만들어 인증 상태와 권한을 검사한 뒤 렌더링 여부를 결정하는 방식이 일반적이다.
ProtectedRoute 기본 예제
다음은 React Router v6 환경에서 인증 상태를 확인하는 보호된 라우트 패턴 예시이다.
const ProtectedRoute = ({ element, isAuthenticated, fallback }) => {
return isAuthenticated ? element : fallback;
};
// 사용 예시
// <Route path="/dashboard" element={} isAuthenticated={auth} fallback={ } />} />
역할(Role) 기반 라우팅
역할 기반 접근 제어는 단순한 인증 플래그를 넘어서야 한다. 사용자 역할을 포함한 권한 체크 함수를 중앙에서 관리하고, 라우트 정의 시 필요한 권한을 명시하는 방식이 권장된다.
상태 관리와 훅
인증 상태는 Context 또는 전역 상태 관리 툴(Redux, Zustand 등)으로 관리하는 것이 편리하다. 전역에서 토큰, 유저 정보, 권한을 제공하면 컴포넌트는 훅을 통해 간단히 접근할 수 있다.
간단한 useAuth 훅
import { useContext } from 'react';
import AuthContext from './AuthContext';
const useAuth = () => {
const ctx = useContext(AuthContext);
return ctx; // { user, isAuthenticated, roles, login, logout }
};
export default useAuth;
API 요청 처리(인터셉터)
axios 같은 라이브러리를 사용할 때는 인터셉터에서 토큰을 주입하고, 401 응답 처리 및 재시도 로직을 담당하게 하는 것이 유지보수에 유리하다. 이때 refresh 토큰 흐름을 동기적으로 제어하는 유틸이 있으면 안전하다.
import axios from 'axios';
const api = axios.create({ baseURL: '/api' });
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) prom.reject(error);
else prom.resolve(token);
});
failedQueue = [];
};
api.interceptors.response.use(
res => res,
err => {
const originalRequest = err.config;
if (err.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise(function(resolve, reject) {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token;
return axios(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
return new Promise(function(resolve, reject) {
// refreshToken 요청 예시
axios.post('/auth/refresh').then(({data}) => {
processQueue(null, data.accessToken);
originalRequest.headers['Authorization'] = 'Bearer ' + data.accessToken;
resolve(axios(originalRequest));
}).catch(err => {
processQueue(err, null);
reject(err);
}).finally(() => { isRefreshing = false; });
});
}
return Promise.reject(err);
}
);
export default api;
검증 포인트 체크리스트
- 토큰 저장 위치와 만료 정책이 명확한가
- refresh 토큰 재발급 흐름에 동시성 제어 로직이 있는가
- 클라이언트에서 권한 검사와 서버 권한 검사가 일관된가
- 민감 정보 노출을 최소화했는가
- 로그아웃 시 토큰과 관련 세션을 확실히 정리하는가
마무리
React에서 권한 처리는 단순한 플래그 체크를 넘는 작업이다. 'react 인증 토큰 관리'와 'react 권한 라우팅'은 설계 단계부터 고려되어야 하며, 'protected route react' 패턴은 중앙화된 인증 흐름과 함께 적용될 때 효과가 크다. 위 원칙과 예시를 바탕으로 프로젝트 특성에 맞게 정책을 설계하면 보안과 유지보수성 모두 개선될 가능성이 높다.