인증 토큰을 잘 붙이고(Authorization), 만료되면 refresh로 재발급하는 흐름까지 만들었다면 그 다음 실무 과제는 보통 이 세 가지입니다.
- 앱을 새로고침/재실행해도 로그인 상태를 유지하려면 토큰을 어디에 두고 어떻게 복원할 것인가
- 보안 관점에서 localStorage vs cookie 중 무엇을 선택할 것인가
- SSR(Next.js 등) 환경에서 토큰과 쿠키를 어떻게 다룰 것인가
RTK Query + TypeScript를 기준으로, 프론트와 서버가 함께 맞춰야 하는 “운영 가능한 인증 정책”을 정리합니다.
로그인 세션 유지의 핵심은 “앱 시작 시점”
브라우저에서 새로고침이 일어나면 메모리(Redux state)는 초기화됩니다. 따라서 Access Token을 메모리에만 저장해두면, 새로고침 순간 로그아웃처럼 보이기 쉽습니다.
실무에서 “끊김 없는 로그인”을 만들려면 앱 시작 시 아래 중 하나를 해야 합니다.
- Access Token을 영속 저장소(localStorage 등)에 저장하고 앱 시작 시 복원
- Refresh Token(쿠키)로 앱 시작 시 Access Token을 재발급받아 복원
보안까지 함께 고려하면, 보통은 두 번째(쿠키 기반 refresh로 복원)가 더 안정적입니다.
권장 아키텍처(실무에서 가장 흔한 조합)
다음 조합은 현재 실무에서 가장 널리 쓰이는 형태입니다.
- Refresh Token : HttpOnly Cookie (JS에서 접근 불가)
- Access Token : 메모리(Redux) 저장
- 앱 시작 시 : /auth/refresh 호출로 Access Token 재발급 & Redux에 주입
이 구조의 장점은 명확합니다.
- XSS로 JS가 localStorage를 읽어도 Refresh Token은 안전 (HttpOnly)
- Access Token이 유출되어도 만료가 짧아 피해 범위가 제한됨
- 앱 시작 시 refresh로 복원하면 UX가 부드러움
앱 시작 시 토큰 복원: “부트스트랩(bootstrap)” 패턴
앱이 처음 렌더링될 때 refresh를 한 번 호출해서 세션을 복원합니다. 이 작업은 UI를 그리기 전에 “인증 상태를 확정”하는 용도로 쓰입니다.
1. authSlice 예시
type AuthState = {
accessToken: string | null;
bootstrapped: boolean; // 앱 시작 시 복원 시도 완료 여부
};
const initialState: AuthState = {
accessToken: null,
bootstrapped: false,
};
// setAccessToken, logout, setBootstrapped 같은 액션을 둔다고 가정
2. 앱 시작 시 refresh 호출
RTK Query endpoint로 refresh를 만들어두고, 앱 시작 시 1회 실행합니다.
const Boot = ({ children }: { children: React.ReactNode }) => {
const dispatch = useAppDispatch();
const { data, isLoading } = useRefreshQuery(undefined, {
// 앱 시작 1회 실행 용도라면, 컴포넌트 마운트 시 자동 실행 형태로 둡니다.
});
useEffect(() => {
if (!isLoading) {
// refresh 성공/실패 여부와 상관없이 bootstrapped는 true
dispatch(authActions.setBootstrapped(true));
if (data?.ok) {
dispatch(authActions.setAccessToken(data.data.accessToken));
} else {
dispatch(authActions.setAccessToken(null));
}
}
}, [isLoading, data, dispatch]);
// 아직 인증 상태가 확정되지 않았다면 로딩 UI
if (!useAppSelector(s => s.auth.bootstrapped)) {
return <div>Loading...</div>;
}
return <>{children}</>;
};
이 방식의 핵심은 “로그인 여부”가 아니라 앱이 인증 상태를 확정했는지(bootstrapped)를 기준으로 화면을 그리는 것입니다. 이 기준이 없으면 새로고침 직후 잠깐 로그아웃 UI가 보였다가 로그인으로 바뀌는 플리커가 발생합니다.
4. localStorage vs cookie — 보안 관점에서의 선택
실무에서 가장 많이 논쟁되는 부분입니다. 결론부터 정리하면 다음과 같습니다.
1. localStorage에 토큰 저장
- 장점: 구현이 단순, SSR과 무관하게 클라이언트에서 쉽게 사용
- 단점: XSS에 취약 (스크립트가 토큰을 읽어갈 수 있음)
Access Token만 localStorage에 두는 경우도 있지만, Access가 유출되면 그 만료 시간 동안은 피해가 발생할 수 있습니다. 따라서 localStorage 전략을 쓸 때는 짧은 만료 + 강한 CSP + 입력 검증이 사실상 필수입니다.
2. Cookie(HttpOnly) 기반
- 장점: HttpOnly면 JS로 접근 불가 → XSS로부터 강함
- 단점: CSRF 대응이 필요(특히 SameSite가 약한 설정이면)
쿠키 기반을 쓸 때의 실무 기본 세팅:
- HttpOnly: true
- Secure: true(HTTPS 필수)
- SameSite: Lax 또는 Strict(가능하면)
- 크로스 도메인이 필요하면 SameSite=None + Secure + CSRF 토큰 전략
정리하면:
- XSS 리스크를 줄이고 싶다면 → Refresh는 HttpOnly cookie
- CSRF까지 함께 고려하면 → SameSite + 필요 시 CSRF 토큰
- 운영에서 가장 흔한 절충안 → Refresh 쿠키 + Access 메모리
SSR 환경(Next.js 등)에서의 처리
SSR에서는 “서버에서 먼저 렌더링”이 일어나기 때문에 클라이언트 전용 저장소(localStorage) 접근이 불가능합니다. 따라서 인증 처리 전략이 달라집니다.
1. SSR에서 localStorage 토큰 전략이 불리한 이유
- 서버 렌더링 시 localStorage 접근 불가
- 초기 HTML은 비로그인 상태로 렌더링될 수 있음
- 클라이언트 hydration 이후에만 로그인 복원이 가능
즉, SSR을 제대로 활용하려면 localStorage 중심 전략은 맞지 않는 경우가 많습니다.
2. SSR에서 쿠키 기반 전략이 자연스러운 이유
쿠키는 서버 요청에도 함께 전달되므로, SSR에서도 “서버가 로그인 여부를 판단”할 수 있습니다.
- 서버에서 쿠키로 세션을 확인하고 초기 렌더에 반영
- 클라이언트와 SSR 상태 불일치(플리커) 감소
3. 실무에서 흔한 SSR 처리 방식 2가지
- 서버 세션(쿠키) 기반: SSR 시점에 사용자 정보를 조회해 초기 props로 내려줌
- BFF(Backend For Frontend): Next.js 서버가 API 프록시 역할을 하며 쿠키/토큰을 서버에서만 다룸
특히 BFF 패턴을 쓰면, 브라우저는 토큰을 “직접” 다루지 않고 Next 서버가 인증을 대행하므로 보안과 운영 난이도가 크게 내려갑니다.
SSR + RTK Query를 쓸 때의 현실적인 가이드
SSR에서 RTK Query까지 완벽하게 동기화하려면 추가 설계가 필요합니다. 실무적으로는 다음 중 하나를 선택하는 경우가 많습니다.
- CSR 중심: 인증은 클라이언트 부트스트랩(refresh)로 확정하고 SSR은 최소화
- SSR 중심: 서버에서 유저 정보를 먼저 확정하고, 클라이언트는 그 상태를 이어받음
개인적으로는 “인증이 중요한 서비스”일수록 SSR 중심 또는 BFF를 권장합니다. 반대로 내부 어드민이나 SEO 영향이 적은 서비스는 CSR 중심이 운영이 편한 경우가 많습니다.
실무 권장 체크리스트
- 앱 시작 시 refresh로 Access Token 복원(부트스트랩) + 플리커 방지
- Refresh는 HttpOnly 쿠키로 관리하고 credentials include 적용
- localStorage 저장은 XSS 리스크를 전제로 하고 CSP/검증을 강화
- SSR이라면 쿠키 기반 또는 BFF 패턴을 고려
- SameSite / Secure / HttpOnly 정책을 서버에서 명확히
로그인 세션 유지와 보안, SSR 처리는 서로 따로 떨어진 주제가 아닙니다. 결국 하나의 질문으로 귀결됩니다.
“토큰을 누가(브라우저/서버) 어디에 저장하고, 언제 복원할 것인가?”
- 끊김 없는 UX: 앱 시작 시 refresh로 인증 상태 확정
- 보안: Refresh는 HttpOnly 쿠키, Access는 짧게
- SSR: 쿠키 기반 또는 BFF로 서버가 인증을 소유
