
웹 플랫폼 프로젝트를 진행하면서 겪었던 인증 이슈와, 이를 해결하기 위해 인증 흐름을 보강한 과정을 정리합니다. 사용자 인증은 언제나 민감한 영역이고, 브라우저의 동작 방식 때문에 예상치 못한 경로로 취약점이 생기기도 합니다.
저희가 주목한 시나리오는 "로그인한 상태에서 브라우저를 닫았다가 다시 열었을 때"였습니다. 이때 이전 세션 정보(특히 쿠키)가 애매하게 남아 있으면, 애플리케이션이 사용자를 로그인 상태로 오인할 수 있었습니다. 단순히 세션에 유저 정보가 남아있다고 유저 정보 API를 호출하는 것만으로는 이 케이스를 깔끔하게 막기 어렵다고 판단했습니다.
그래서 서버/클라이언트 양쪽에서 인증 정보를 더 확실하게 검증하도록 미들웨어와 전역 상태 로직을 손봤습니다. 작업을 진행하면서 SSR/CSR 환경의 차이와, 쿠키 기반 인증에서 "쿠키의 존재 여부를 언제/어디서 확인할 것인가"가 꽤 중요하다는 것도 다시 확인했습니다.
SSR/CSR 환경에서 인증 흐름 다듬기
저희 애플리케이션은 Nuxt.js 기반으로 SSR과 CSR을 모두 사용하고 있습니다. 이번 개선의 핵심은 "어느 시점에 어떤 근거로 로그인 상태를 판단할지"를 SSR/CSR 각각에 맞게 정리하는 것이었습니다. 먼저 배경이 되는 개념을 간단히 짚고 넘어갑니다.
Nuxt.js Route Middleware의 역할
Nuxt.js의 Route Middleware는 특정 라우트로 이동하기 전(또는 라우트 변경 중)에 실행되는 기능입니다. auth.global.ts처럼 .global이 붙은 미들웨어는 모든 라우트 변경마다 자동으로 실행되기 때문에, 로그인 상태 확인/권한 체크 같은 전역 인증 로직을 한 곳에서 관리하기 좋습니다. 프로젝트에서는 이 지점을 "중앙 관문"처럼 두고, 라우트 전환 때마다 인증 상태를 점검하도록 했습니다.
HTTP Cookies와 sessionId의 중요성
웹에서 HTTP 쿠키는 세션 관리 등에 쓰이는 작은 데이터입니다. 브라우저는 요청을 보낼 때 관련 쿠키를 자동으로 전송하고, 서버는 이를 바탕으로 사용자의 상태를 판단합니다. 저희 프로젝트에서는 sessionId 쿠키를 세션 식별자로 사용했고, 결국 이 쿠키가 "지금 이 순간 로그인으로 볼 근거가 있는지"를 가르는 기준이 됐습니다.
특히 "브라우저 세션 복원" 상황에서는 클라이언트에서 sessionId를 한 번 더 확인하는 게 결정적이었습니다. 브라우저/OS 설정에 따라 닫혔던 탭이 복원되기도 하는데, 이때 클라이언트 상태만 보고 로그인으로 판단하면 위험할 수 있습니다. 그래서 클라이언트 환경에서는 sessionId 쿠키가 실제로 존재하지 않으면 즉시 로그아웃 처리하도록 했습니다.
이러한 개념들을 바탕으로, auth.global.ts 미들웨어는 다음과 같은 흐름으로 사용자의 인증 상태를 관리하게 됩니다.
- 라우트 변경 감지: 사용자가 페이지를 이동하거나 새로고침할 때마다 미들웨어 함수가 실행됩니다.
- 환경 확인: 현재 코드가 서버에서 실행 중인지 (
process.server) 클라이언트에서 실행 중인지 확인합니다. - 서버 환경 (SSR):
authStore에 사용자 정보가 아직 없으면서 (!authStore.userId),sessionId쿠키가 존재한다면 (useCookie('sessionId').value),fetchUserInfo()를 호출하여 사용자 정보를 서버에서 가져옵니다. 이는 불필요한 API 호출을 줄이면서도 유효한 세션이 있다면 초기 사용자 정보를 설정합니다.
- 클라이언트 환경 (CSR):
sessionId쿠키가 브라우저에 존재하지 않는다면 (!getClientCookieByName("sessionId")),authStore.logout()를 호출하여 강제로 로그아웃 처리합니다. 이는 브라우저 재시작 등으로 인한 이전 세션 정보 잔존 문제를 해결합니다.
이러한 분리된 접근 방식은 각 환경의 특성을 고려하여 사용자 인증 흐름을 더욱 견고하고 효율적으로 만듭니다.
구현 포인트: 미들웨어와 스토어 로직 개선
이제 실제 코드에서 변경점을 정리합니다. 핵심 변경은 src/middleware/auth.global.ts(미들웨어)와 src/store/index.js(스토어) 쪽에서 이뤄졌습니다.
1. 변경 전 (Before) - 미들웨어의 한계
이전에는 미들웨어에서 userId가 없을 때 서버 환경에서만 fetchUserInfo()를 호출하는 비교적 간단한 로직을 가지고 있었습니다.
// src/middleware/auth.global.ts
import { useAuthStore } from "~/store";
export default defineNuxtRouteMiddleware(async (to, from) => {
const authStore = useAuthStore();
// page vue 파일 내에서 options api는 created hook부터, composition api는 어디서나 로그인 권한 체크가 가능하다.
if (!authStore.userId && process.server) {
await authStore.fetchUserInfo();
}
});위 코드는 authStore에 사용자 ID가 없으면, SSR에서만 fetchUserInfo()를 호출하도록 되어 있었습니다. SSR 초기 상태를 잡는 데는 도움이 됐지만, 브라우저 복원 이후처럼 클라이언트에서 세션 상태가 애매해지는 케이스를 다루기엔 부족했습니다. 특히 클라이언트에서 sessionId 쿠키를 기준으로 "세션이 성립하는지"를 확인하는 로직이 없어서, 쿠키가 사라졌거나 만료된 뒤에도 화면 상태가 엇갈릴 여지가 있었습니다.
2. 쿠키 파싱 유틸리티 함수 추가
클라이언트 사이드에서 document.cookie를 직접 파싱하여 특정 쿠키 값을 가져와야 할 필요가 생겼습니다. 이를 위해 다음과 같은 유틸리티 함수를 추가했습니다.
// src/middleware/auth.global.ts 에 추가된 함수
function getClientCookieByName(name: string) {
const cookieString = document.cookie;
const cookies = cookieString.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith(name + "=")) {
return cookie.substring(name.length + 1);
}
}
return null;
}getClientCookieByName는 document.cookie에서 지정한 이름의 쿠키 값을 찾아 반환합니다. document.cookie가 문자열 형태로 여러 쿠키를 담고 있어서, 필요한 쿠키를 직접 파싱해 꺼내는 과정이 필요했습니다. 이 함수 덕분에 클라이언트에서 sessionId의 "존재 여부"를 명시적으로 확인할 수 있게 됐습니다.
3. 변경 후 (After) - 미들웨어 인증 로직 강화
이제 auth.global.ts 미들웨어는 SSR과 CSR 환경에 맞춰 더욱 견고한 인증 로직을 가지게 되었습니다.
// src/middleware/auth.global.ts
import { useAuthStore } from "~/store";
// getClientCookieByName 함수는 위에 정의된 코드와 동일하게 추가됨
export default defineNuxtRouteMiddleware(async (to, from) => {
const authStore = useAuthStore();
if (process.server) {
// 서버 사이드 렌더링 (SSR) 환경
// 사용자 정보가 없고 sessionId 쿠키가 존재할 때만 fetchUserInfo 호출하여 API 호출 최적화
if (!authStore.userId) {
const sessionIdCookie = useCookie("sessionId"); // Nuxt 3의 useCookie 헬퍼 사용
if (sessionIdCookie.value) {
await authStore.fetchUserInfo();
}
}
} else {
// 클라이언트 사이드 렌더링 (CSR) 환경
// sessionId 쿠키가 없으면 강제로 로그아웃 처리하여 세션 복원 취약점 방지
if (!getClientCookieByName("sessionId")) {
authStore.logout();
}
}
});이 개선된 미들웨어는 크게 두 가지 환경(process.server와 else 즉 process.client)으로 분리되어 동작합니다.
-
SSR 환경 (
if (process.server)):- 기존에는
userId가 없으면 무조건fetchUserInfo()를 호출했지만, 이제는sessionId쿠키가useCookie를 통해 존재할 때만await authStore.fetchUserInfo()를 호출합니다. 이 변경은 불필요한 사용자 정보 페칭을 방지하고, 실제 유효한 세션이 있을 때만 서버에서 사용자 정보를 가져오도록 하여 API 호출을 최적화하는 데 기여합니다. - 이는 서버 측에서 이미
sessionId쿠키를 가지고 있다면, 클라이언트가 Hydration 되기 전에 사용자 정보를 미리 로드하여 로그인 상태를 유지하려는 전략입니다.
- 기존에는
-
CSR 환경 (
else):- 이 부분이 특히 브라우저 세션 복원 시 발생하는 로그인 취약점을 해결하는 핵심입니다. 클라이언트 측에서
getClientCookieByName('sessionId')를 사용하여sessionId쿠키가 현재 브라우저에 존재하는지 명시적으로 확인합니다. - 만약 이 쿠키가 없다면,
authStore.logout()를 호출하여 사용자를 강제로 로그아웃 처리합니다. 이는 브라우저 재시작 등으로 인해 애플리케이션 상태는 로그인된 것으로 보일 수 있으나, 실제 인증 쿠키가 없는 경우 발생할 수 있는 문제를 사전에 방지합니다. 사용자가 유효하지 않은 세션 정보로 인해 잘못된 페이지에 접근하는 것을 막고, 보안성을 강화합니다.
- 이 부분이 특히 브라우저 세션 복원 시 발생하는 로그인 취약점을 해결하는 핵심입니다. 클라이언트 측에서
미들웨어 성능 임팩트
전역 미들웨어는 모든 라우트 전환마다 실행되기 때문에, 성능에 미치는 영향을 확인해볼 필요가 있었습니다. 가장 눈에 띄는 변화는 SSR 환경에서 세션이 없는 경우였습니다. 이전에는 세션 유무와 관계없이 fetchUserInfo를 호출했기 때문에 매번 120~180ms가 소요됐는데, 개선 후에는 sessionId 쿠키가 없으면 API 호출 자체를 건너뛰면서 1~2ms로 끝나게 됐습니다. 반대로 CSR 쪽에서는 document.cookie 파싱이 추가되긴 했지만, 문자열 파싱 자체가 0.5~1ms 수준이라 사용자가 체감할 수 있는 지연은 없었습니다.
검증 시나리오
배포 전 다음 시나리오들을 직접 재현하여 정상 동작을 확인했습니다.
- 세션 쿠키 만료 후 접근 차단: 쿠키 만료 시간이 지난 뒤 페이지를 이동하면 로그아웃 처리가 정상적으로 이루어졌습니다.
- 브라우저 세션 복원: 브라우저를 닫았다가 "이전 탭 복원"으로 열었을 때, 만료된 세션으로 인증 상태가 유지되지 않고 로그아웃 처리되는 것을 확인했습니다.
- 다중 탭 동기화: 한 탭에서 로그아웃한 뒤 다른 탭에서 라우트를 전환하면 로그아웃 상태가 반영되었습니다.
- SSR 초기 로드 시 쿠키 검증: 유효하지 않은
sessionId쿠키가 있을 때fetchUserInfo가 실패 처리되고, 사용자에게 적절한 피드백이 제공되는 것을 확인했습니다. - 쿠키 플래그:
HttpOnly,Secure,SameSite속성이 올바르게 설정되어 있는 것을 확인했습니다. - CSRF 토큰 연동: 세션 갱신 시 CSRF 토큰도 함께 갱신되는 것을 확인했습니다.
- 네트워크 오류 시 폴백:
fetchUserInfoAPI 호출이 타임아웃되거나 실패했을 때, 사용자가 빈 화면에 갇히지 않고 로그인 페이지로 리다이렉트되는 것을 확인했습니다.
결과 및 시사점
이번 작업을 통해 저희는 브라우저 세션 복원 시 발생할 수 있는 로그인 취약점을 성공적으로 개선하고, 전반적인 인증 흐름을 강화할 수 있었습니다.
- 보안 강화: 클라이언트 측에서
sessionId쿠키의 존재 여부를 직접 검증하고, 유효하지 않을 경우 즉시 로그아웃 처리함으로써, 이전 세션 정보로 인한 잠재적 보안 취약점을 효과적으로 방지했습니다. - 일관된 사용자 경험: SSR과 CSR 환경에서의 인증 로직을 명확히 분리하고 강화하여, 사용자가 어떤 방식으로 애플리케이션에 접근하더라도 일관되고 올바른 로그인/로그아웃 상태를 유지할 수 있게 되었습니다.
이번 개선은 기능을 추가하는 수준을 넘어, 클라이언트/서버 상호작용 속에서 인증이 어떻게 깨질 수 있는지 점검하는 과정이었습니다. 브라우저의 복원 기능과 쿠키 수명 주기가 보안에 어떤 영향을 주는지 직접 경험하면서, "예외 시나리오까지 포함한" 인증 로직이 필요하다는 걸 다시 확인했습니다.
다음 단계로는 세션 쿠키에 HttpOnly/Secure 플래그를 적용해 XSS나 평문 HTTP 통신에서의 노출 위험을 낮추는 방안을 검토 중입니다. 또 만료 시간 정책을 명확히 하고, 서버 측에서 세션 유효성을 재검증하는 메커니즘을 더해 인증을 한층 단단하게 만들 계획입니다.
참고 자료
Related Posts

Next.js 15 App Router 마이그레이션 여정
Express 서버와 Page Router로 구성된 레거시 프로젝트를 Next.js 15 App Router 기반으로 전환한 대규모 마이그레이션 과정을 다룹니다.

HOC와 미들웨어로 구축하는 중앙집중식 오류 처리
Next.js SSR 환경에서 HOC 패턴과 미들웨어 헤더 전파를 활용하여 분산된 오류 처리를 중앙집중화하고 일관된 사용자 경험을 구현한 과정을 다룹니다.

통합 로그인 시스템 구축기: 소셜 로그인과 이메일 로그인을 하나로
네이버, 구글 소셜 로그인과 이메일 계정 시스템을 통합하여 일관된 사용자 경험을 제공하고 신규 회원 가입 플로우를 단일화한 과정을 다룹니다.