1. 들어가며: 어떤 문제를 해결해야 했나?
최근 진행한 프로젝트에서는 서비스 자체적인 로그인 화면을 구현하는 대신, 사내(또는 외부) 공통 인증 시스템을 거쳐 진입해야 하는 요구사항이 있었습니다. 즉, 일종의 SSO(Single Sign-On) 스타일의 로그인 흐름을 구축해야 했습니다.
이 과정에서 프론트엔드 레벨에서 풀어야 할 핵심 과제는 다음과 같았습니다.
자연스러운 UX 제공: 사용자가 외부 인증을 마친 후, 원래 보려던 페이지로 흐름이 끊기지 않고 자연스럽게 복귀해야 합니다.
실패 복구 프로세스: 인증이 실패하거나 만료되었을 때, 사용자가 빈 화면에 갇히지 않고 안전하게 재인증 경로로 유도되어야 합니다.
관심사 분리: 토큰 발급, 상태 저장, API 헤더 주입 등의 로직이 파편화되지 않고 하나의 명확한 파이프라인으로 동작해야 합니다.
이번 글에서는 외부 시스템의 콜백을 처리하는 전용 페이지를 중심으로, 프론트엔드 인증 흐름을 어떻게 단순화하고 안정적으로 설계했는지 공유하고자 합니다.
2. 전체 인증 흐름 및 아키텍처 구조
복잡한 세부 구현을 걷어내고, 시스템 간의 역할과 데이터 흐름을 추상화하면 다음과 같은 구조가 나옵니다.
1. 외부 인증 시스템
⬇️ 로그인 완료 후 리다이렉트 (식별자 전달)
2. 프론트엔드 콜백 페이지 (/auth)
⬇️ 백엔드에 토큰 교환 요청
실패 시
다시 외부 인증으로 이동
(무한 루프 방지)
성공 시
토큰 및 유저 정보 반환
⬇️ 전역 상태 저장 및 원래 페이지 복귀
3. 클라이언트 상태 관리 (Store)
⬇️ API 호출 시 헤더 자동 주입
4. 공통 API 클라이언트 (Axios 등)
이 아키텍처에서 프론트엔드는 크게 4가지 책임을 가집니다.
콜백 파라미터 정규화: 외부에서 전달받은 식별자(Identifier) 데이터의 인코딩을 해제하고 검증합니다.
토큰 교환 요청 트리거: 정규화된 식별자를 백엔드로 보내, 실제 서비스에서 사용할 수 있는 액세스 토큰으로 교환합니다.
상태 저장 및 라우팅: 발급받은 토큰과 유저 정보를 전역 상태로 초기화하고, 원래 목적지(Return Path)로 이동합니다.
실패 시 재인증 유도 (루프 방지): 통신 오류나 유효하지 않은 식별자로 인해 교환에 실패하면 다시 외부 인증 시스템으로 돌려보냅니다.
3. 구현에서 중요했던 설계 포인트
① 인증 전용 콜백 페이지를 통한 '책임의 분리'
모든 페이지 컴포넌트에서 인증 상태를 체크하고 토큰을 발급받는 로직을 넣는 것은 유지보수에 치명적입니다. 따라서 /auth 와 같은 인증 전용 콜백 페이지를 두어, 인증 처리(토큰 교환, 전역 상태 세팅)의 책임을 오직 이 페이지에만 위임했습니다.
// AuthCallbackPage.tsx
async function handleAuthCallback() {
// 1. 외부 시스템에서 넘어온 파라미터 정규화
const normalizedValue = normalizeIncomingValue(rawValue);
// 2. 백엔드에 토큰 교환 요청
const authResult = await requestAccessToken(normalizedValue);
// 3. 실패 시: 무한 루프 방지 로직을 거쳐 재인증 유도
if (!authResult.ok) {
handleAuthFailureAndRedirect();
return;
}
// 4. 성공 시: 인증 상태 초기화 및 복귀 경로로 이동
initializeAuthState(authResult);
redirectToReturnPath();
}② API 공통 클라이언트 계층화
인증 이후 발생하는 수많은 비즈니스 API 호출마다 인증 헤더를 수동으로 넣는 것은 비효율적입니다. 콜백 페이지에서 성공적으로 토큰을 받아 전역 상태에 저장해 두면, 공통 API 클라이언트(Axios instance 등)의 인터셉터가 이를 가로채어 모든 요청의 헤더에 토큰을 자동 주입하도록 구성했습니다.
이를 통해 비즈니스 로직을 개발할 때는 '인증'이라는 컨텍스트를 전혀 신경 쓰지 않아도 되도록 결합도를 낮췄습니다.
③ 무한 리다이렉트 방지와 실패 복구 UX
가장 신경 썼던 부분 중 하나입니다. 인증 실패 시 단순히 외부 시스템으로 다시 리다이렉트만 시키면, 외부 시스템과 우리 서비스 사이에서 핑퐁을 치는 무한 리다이렉트 루프에 빠질 위험이 있습니다.
이를 막기 위해 브라우저의 임시 스토리지(Session Storage 등)를 활용하여 '재시도 횟수'를 카운팅하는 보호 장치를 두었습니다. 일정 횟수 이상 인증에 실패하면 더 이상 리다이렉트하지 않고 사용자에게 명확한 에러 화면("인증에 실패했습니다. 관리자에게 문의해주세요.")을 노출하여 흐름을 안전하게 끊어주었습니다.
4. 마무리 및 배운 점
이번 구조를 설계하며 얻은 가장 큰 레슨은 "인증 흐름에서는 '성공(Happy Path)'보다 '실패했을 때 어떻게 복구되는가'가 사용자 경험(UX)에 훨씬 더 큰 영향을 준다"는 것입니다.
또한, 인증 상태를 여러 곳(쿠키, 로컬 스토리지, 인메모리 등)에 분산 저장해야 할 때, 각각의 저장소가 가지는 역할(예: 토큰 갱신용, API 헤더 주입용, UI 렌더링용)을 명확히 정의하고 경계를 나누어야 코드의 복잡도를 낮출 수 있음을 체감했습니다.
외부 인증 시스템과의 연동은 파라미터 이름이나 헤더의 키값 같은 세부적인 '데이터'보다, 시스템 간의 신뢰 모델을 바탕으로 한 '흐름과 경계(Flow & Boundary)' 설계가 핵심이라는 점을 다시 한번 배울 수 있는 프로젝트였습니다.