mimizae 님의 블로그
인증/인가 전략 본문
37기 DIVE SOPT에서 진행한 프로젝트 CareNA에서 보안에 대해 공부하며 인증/인가 관련 전략을 구성했던 전반적인 과정을 담은 글입니다! 🙌🏻

CareNA는 사용자의 건강검진 데이터를 입력·분석·시각화하는 서비스로, 일반적인 서비스와 달리 개인에게 매우 민감한 의료·건강 정보를 다룬다는 특징이 있었다.
건강검진 수치, 분석 결과, 생활 습관 추천 데이터는 단순한 콘텐츠가 아니라 유출되었을 경우 사용자에게 직접적인 피해를 줄 수 있는 정보이기 때문에, 기능 구현 이전에 프론트엔드에서 어디까지 보안을 책임져야 하는가라는 질문부터 고민하게 되었다.
이러한 문제의식 속에서 프로젝트를 살펴보니, 프론트엔드 단에서도 보안과 직결되는 요소들이 곳곳에 존재했다.
- OAuth 기반 로그인 및 리다이렉트 흐름
- accessToken / refreshToken 분리 관리와 저장 전략
- OCR을 통한 건강검진 문서 텍스트 입력
- 인증 상태에 따라 민감 페이지 접근을 제어해야 하는 라우팅 구조
이처럼 보안과 밀접한 요소들이 프론트엔드 전반에 걸쳐 얽혀 있었기 때문에, 단순히 API를 연결하는 수준이 아니라 인증·인가 구조 전체를 어떻게 설계할 것인가를 고민하는 과정 자체가 이번 프로젝트에서 가장 도전적인 요소라고 생각했다…!!
💡 어떤 이유로 도전하게 되었는지
보안은 흔히 서버의 영역으로 인식되지만, CareNA 프로젝트를 진행하며 프론트엔드 역시 보안의 첫 번째 방어선이 될 수 있다는 점을 직접 고민해보고자 했다.
💭 특히 다음과 같은 질문들이 문제의 출발점이었다.
- accessToken과 refreshToken은 어떤 역할 차이로 분리되어 있는가?
- 토큰은 프론트엔드에서 어디에 저장하는 것이 가장 안전한가?
- localStorage, sessionStorage, cookie, memory는 각각 어떤 보안적 특성을 가지는가?
- React 환경에서 XSS, CSRF는 실제로 어떤 경로로 발생할 수 있는가?
- 새로고침, 리다이렉트, OAuth 인증 흐름 속에서 인증 상태는 어떻게 유지되어야 하는가?
기존에는 라이브러리나 관례에 따라 인증 방식을 적용해왔지만, CareNA에서는 인증 구조의 선택 이유를 스스로 설명할 수 있는 상태를 만드는 것을 목표로 했다.
즉, 단순히 기능을 구현하는 데 그치지 않고, 프론트엔드 관점에서 발생 가능한 보안 위협을 이해한 뒤 책임질 수 있는 영역을 명확히 정의하고 그에 맞는 인증/인가 전략을 직접 설계하는 경험을 하고자 했다!!!
💡 접근 방법
1️⃣ 프론트엔드 관점에서 발생 가능한 위협을 먼저 정리
가장 먼저 한 일은 프론트엔드에서 무엇을 지켜야 하는가?를 정리하는 것이었다.
CareNA는 사용자가 건강검진 데이터를 입력하고 그 데이터를 기반으로 분석 결과를 렌더링하는 구조를 가진다.
즉, 민감 정보가 프론트엔드로 유입되는 지점과 민감 정보가 다시 사용자에게 노출되는 지점이 모두가 공격 표면이 된다.
이 흐름을 기준으로, 프론트엔드에서 반드시 고려해야 할 보안 위협을 다음처럼 정리했다!
- 사용자 입력 및 OCR 결과를 통한 XSS 공격 가능성
- 쿠키 기반 인증 구조에서 발생할 수 있는 CSRF 공격
- React 환경에서의 DOM 조작, state 관리로 인한 의도치 않은 민감 정보 노출
- 새로고침, 리다이렉트, OAuth 흐름 속에서 인증 상태가 흔들리는 구간
2️⃣ 프론트엔드 단독으로 책임질 수 있는 것과, 서버의 도움이 필요한 것을 분리
프론트엔드 보안만으로 완벽한 방어선을 구축할 수는 없지만, 그렇다고 서버에 모든 책임을 넘길 수도 없다고 판단했기에! 책임 범위를 명확히 나누는 게 맞다고 생각했다.
그래서 보안 항목을 다음과 같이 분리했다.
- 프론트엔드에서 기본적으로 반드시 지켜야 하는 것
- XSS 방어 (입력·출력 처리, DOM 조작 방식)
- CSRF 기본 방어 인식
- 민감 데이터 렌더링 및 state 관리 전략
- 서버의 도움이 반드시 필요한 영역
- refreshToken을 HttpOnly 쿠키로 관리
- 토큰 탈취 방지를 위한 쿠키 옵션(Secure, SameSite)
- 토큰 재발급 흐름의 서버 검증
- 프레임워크 특성과 연결되는 보안 고려 사항
- React의 기본 escape 동작
- dangerouslySetInnerHTML 사용 시의 위험성
- 리렌더링으로 인한 민감 정보 재노출 가능성
이렇게 정리함으로써, 프론트엔드에서 무엇까지 책임지고 설계해야 하는지를 스스로 명확히 했다.
이렇게 위협 정리와 책임 범위가 정리된 뒤, 이를 바탕으로 1차 인증/인가 전략을 구성했다!
인증/인가 전략을 구성하기 위해 서버와 논의하며 정한 사용자 인증 흐름, 토큰 저장 방식과 구현까지의 과정을 자세히 담은 아티클
위의 아티클을 요약하자면,
인증과 관련된 주요 위험은 토큰 탈취(XSS) 와 쿠키 기반 인증에서의 위조 요청(CSRF), 그리고 리다이렉트 및 새로고침 등 다양한 진입 시점에서 인증 상태가 어긋날 수 있다는 점이라고 판단했다.
이러한 위험을 최소화하기 위해, accessToken은 JavaScript로 접근 가능한 저장소를 배제하고 메모리에서만 관리하고, refreshToken은 서버의 책임 영역으로 두어 HttpOnly 쿠키로 관리하는 전략을 선택했다.
또한 메모리에 저장된 accessToken이 새로고침 시 사라지는 특성을 고려해, 앱 시작 시 refreshToken을 기준으로 인증 상태를 다시 판단하도록 하고, 이 과정이 끝나기 전까지는 인증 여부를 단정하지 않도록 앱 최상위에서 인증 초기화 로직을 수행하도록 설계했다.
그 결과 인증 판단은 앱 진입 시 한 번만 일관되게 수행되고, 각 페이지와 라우트는 그 결과를 안전하게 소비하는 구조를 만들 수 있었다!!
💡 결과
1차로 구성한 인증/인가 전략을 팀원과 공유한 뒤 논의의 시간을 가졌다.
(이 논의의 시간이 없었다면 아마 지금까지 못했을지도 모르겠다… ☠️)
이 과정에서 드러난 문제는, 인증 로직과 라우팅 로직이 마운트 타이밍에서 서로 충돌할 수 있다는 점이었다.
즉, 인증 전략 자체는 논리적으로 맞더라도, React 앱이 다시 마운트되는 순간과 ProtectedRoute가 인증 여부를 판단하는 시점을 함께 고려하지 않으면 의도한 로그인 흐름이 성립하지 않을 수 있었다…!!!!
이러한 논의를 바탕으로 1차 리팩토링을 진행했다.
1️⃣ 회원가입 페이지 라우팅 구조 변경
기존 구조에서는 회원가입 페이지가 ProtectedRoute 하위에 위치해 있었다.
이 경우 회원가입 페이지로 진입하더라도 앱이 최초 마운트되면서 AuthInitializerProvider가 실행되고, 아직 토큰이 발급되지 않은 상태이기 때문에 즉시 로그인 페이지로 강제 라우팅되는 문제가 발생했다.
이는 로그인 이전 단계인 회원가입 플로우와 맞지 않는 동작이라고 판단했고, 회원가입 페이지를 PublicRoute 하위로 이동했다.
이를 통해 토큰이 없는 상태에서도 회원가입 페이지에 정상적으로 접근할 수 있도록 개선할 수 있었다.
2️⃣ 토큰 교환 로직을 HomePage → OAuth Callback 페이지로 이동
초기에는 회원가입 성공 후 HomePage로 리다이렉트하고, 그 시점에 카카오 인가 코드 기반 토큰 교환 API를 호출하여 accessToken과 refreshToken을 발급받는 흐름을 구상했다.
하지만 이 구조에서는 중요한 문제가 있었는데,
- 앱이 HomePage에 도착하는 순간 앱이 새로 마운트
- 마운트 시점에 AuthInitializerProvider가 실행
- AuthInitializerProvider는 accessToken 재발급 요청을 시도
- 이 시점에는 아직 토큰 교환이 이루어지지 않았으므로 accessToken, refreshToken 모두 없는 상태
- 그 결과 ProtectedRoute 하위에 있는 HomePage는 토큰이 없다고 판단되어 즉시 로그인 페이지로 리다이렉트
즉, HomePage에 진입하자마자 토큰 교환 로직이 실행되기 전에 라우팅이 끊겨 의도했던 로그인 흐름 자체가 성립할 수 없는 구조였다... ☠️
이를 해결하기 위해 auth/callback 페이지를 새로 생성했다.
- 회원가입 성공 이후의 리다이렉트 경로를 auth/callback으로 변경
- 해당 페이지는 PublicRoute 하위에 위치
- 토큰이 없는 상태에서도 접근 가능
- 이 페이지에서 인가 코드 기반 토큰 교환을 먼저 수행
- 토큰 발급이 완료된 이후, 클라이언트에서 HomePage로 라우팅
이로써 인증 플로우가 마운트 타이밍과 충돌하지 않도록 재구성되어 바로 테스트를 진행했다!!!
그러나… ㅠㅠ 실제 플로우를 따라 테스트를 진행하면서 또 하나의 구조적인 문제가 드러났다.
당시 회원가입 페이지에는
- 기본 정보 입력 (이름, 생년월일, 성별)
- 건강검진 정보 입력
이 두 단계가 하나의 페이지에 함께 존재하고 있었고, 회원가입 시 모든 정보를 한 번에 서버로 전송하는 구조였다.
하지만 실제로는 회원으로 등록되어 accessToken이 발급된 이후에만 건강검진 정보를 저장할 수 있었다…!!!
즉, 회원가입 이전에는 토큰이 없고 토큰이 없으면 건강검진 저장 API를 호출할 수 없는 구조였기 때문에 회원가입과 건강검진 입력을 하나의 단계로 처리하는 것은 인증 흐름과 맞지 않는 설계였다.
🔁 회원가입 플로우 재설계의 필요성
이 문제로 인해 회원가입 페이지를 다음과 같이 분리해야 한다는 결론에 도달했다.
- 기본 정보 입력 → 회원가입 완료 (회원 생성)
- accessToken 발급 이후 → 건강검진 정보 입력
이 지점까지의 문제 정의와 방향 설정은 웹 리드와 함께 논의하며 정리했으며, 이후의 세부 구현과 플로우 정리는 웹 리드가 이어서 진행해 주셔서 무사히 끝마칠 수 있었다. 🙏🏻
💡 회고
이번 경험을 통해 단순히 새로고침 시 토큰이 유지되는가? 같은 표면적인 흐름만 보는 것이 아니라, React 앱이 언제 마운트되고, 그 시점에 어떤 인증 로직과 라우팅 로직이 실행되는지를 함께 고려하는 것이 인증/인가 설계에서 얼마나 중요한지 깨닫게 되었다.
다시 같은 문제를 마주한다면, 앱의 전체 생명주기와 실행 순서를 먼저 그려보고, 각 단계에서 어떤 상태와 토큰이 존재하는지를 기준으로 플로우를 설계했을 것 같다.
또한 이번 작업은 혼자였다면 끝까지 밀어붙이기 어려웠을 것이라는 생각이 든다.
1차로 구성한 전략을 팀원들과 공유하고, 의도하지 않은 충돌 가능성을 함께 논의하는 과정에서 문제를 더 빠르게 인지할 수 있었고, 결과적으로 인증 흐름을 한 단계 더 안정적으로 다듬을 수 있었다!!
인증/인가처럼 복잡도가 높은 영역일수록, 혼자 완벽하게 설계하려 하기보다 설계를 공유하고 검증받는 과정 자체가 구현의 일부라는 점을 배운 경험이었다.
'FE' 카테고리의 다른 글
| 프론트엔드의 보안 (0) | 2026.03.01 |
|---|---|
| drop-down 공통 컴포넌트 구현 (0) | 2026.02.18 |
| button 공통 컴포넌트 구현 (0) | 2026.02.16 |
| 마이페이지 min-height 문제 (0) | 2026.02.10 |
| ticker 구현기 (0) | 2026.02.09 |