네트워킹 서비스 Next.js & 로그인 유지 구조 구축

2024.07 ~ 2024.10

2024.07 ~ 2024.10

SSR & CSRRefrash Token

Next.jsTypeScriptZustandTanStack QueryTailwind CSS

목차

  • 프로젝트 개요
  • 트러블 슈팅
  • A. SSR/CSR 혼합 환경 인증 상태 일관성 설계
  • 문제
  • 원인 분석
  • 1. 브라우저 전용 저장소 의존 구조
  • 2. 인증 저장소 이원화 없이 단일 경로 설계
  • 3. API 계층의 실행 환경 미분리
  • 4. 서버 요청 기준 세션 복원 경로 부재
  • 해결 방법
  • 1. 인증 저장 구조 및 API 계층 분리
  • 2. 토큰 이중 저장을 통한 상태 동기화
  • 3. 서버 요청 기준 인증 흐름 재구성
  • 결과
  • 핵심 포인트
  • B. Error Boundary + Interceptor 기반 로그인 유지 API 예외 처리
  • 문제
  • 원인 분석
  • 1. 환경별 에러 처리 구조 불일치
  • 2. 토큰 재발급과 요청 재시도의 연결 부재
  • 3. 에러 코드 기반 중앙 처리 체계 부재
  • 4. 인증 복구 흐름 미구현
  • 해결 방법
  • 1. SSR/CSR 통합 에러 처리 구조 설계
  • 2. 토큰 재발급 및 요청 재시도 흐름 구현
  • 3. 토큰 상태별 분기 처리 및 세션 정리
  • 결과
  • 핵심 포인트

SSR & CSRRefrash Token

Next.jsTypeScriptZustandTanStack QueryTailwind CSS

목차

  • 프로젝트 개요
  • 트러블 슈팅
  • A. SSR/CSR 혼합 환경 인증 상태 일관성 설계
  • 문제
  • 원인 분석
  • 1. 브라우저 전용 저장소 의존 구조
  • 2. 인증 저장소 이원화 없이 단일 경로 설계
  • 3. API 계층의 실행 환경 미분리
  • 4. 서버 요청 기준 세션 복원 경로 부재
  • 해결 방법
  • 1. 인증 저장 구조 및 API 계층 분리
  • 2. 토큰 이중 저장을 통한 상태 동기화
  • 3. 서버 요청 기준 인증 흐름 재구성
  • 결과
  • 핵심 포인트
  • B. Error Boundary + Interceptor 기반 로그인 유지 API 예외 처리
  • 문제
  • 원인 분석
  • 1. 환경별 에러 처리 구조 불일치
  • 2. 토큰 재발급과 요청 재시도의 연결 부재
  • 3. 에러 코드 기반 중앙 처리 체계 부재
  • 4. 인증 복구 흐름 미구현
  • 해결 방법
  • 1. SSR/CSR 통합 에러 처리 구조 설계
  • 2. 토큰 재발급 및 요청 재시도 흐름 구현
  • 3. 토큰 상태별 분기 처리 및 세션 정리
  • 결과
  • 핵심 포인트

프로젝트 개요


Next.js 기반 중앙대학교 동문회 네트워킹 웹 서비스 Causw 의 Front-end 개발 프로젝트 설계를 수행하였다.

SSR과 CSR 환경을 동시에 고려한 인증 구조를 설계하고, 서비스 전반에 공통 적용되는 레이아웃 및 상태 관리 구조를 구축하였다.

1. App Structure 및 Layout 설계

인증 상태를 기준으로 /auth(로그인 이전)와 /causw(로그인 이후) 영역을 분리하고, Navigation Bar와 Side Bar를 layout 단에서 통합 구성하여 페이지 개발 시 UI 간섭을 제거하였다.

  • 인증 여부 기반 라우팅 및 폴더 구조 분리
  • layout.tsx 기반 공통 UI(NavigationBar, SideBar) 구성
  • 반응형 레이아웃 및 사용자 정보 UI 구성
  • Zustand 기반 전역 UI 상태 및 에러 메시지 관리
  • 2. SSR,CSR 통합 인증/인가 시스템 구축

    SSR과 CSR 환경에서 모두 동작하도록 토큰 저장 및 사용 구조를 분리 설계하였다.

  • SSR: cookies 기반 access/refresh token 관리
  • CSR: axios + storage 기반 토큰 관리
  • Service / RscService 분리로 실행 환경별 API 계층 분리
  • 로그인 시 토큰을 SSR/CSR 양쪽에 동시 저장하는 구조 구현
  • 3. API 에러 처리 구조 구축

    API 에러를 error code 기준으로 분류하고, SSR과 CSR 환경에서 일관된 방식으로 처리할 수 있도록 구조를 구성하였다.

  • error code 기반 에러 처리 기준 정의
  • SSR: error.tsx 기반 처리
  • CSR: axios interceptor 기반 처리
  • 권한, 토큰 상태 등에 따른 분기 처리 구조 적용
  • 트러블 슈팅


    A. SSR/CSR 혼합 환경 인증 상태 일관성 설계


    문제

  • CSR에서는 access token을 메모리 및 axios header에 유지하고 refresh token을 localStorage 또는 sessionStorage에 저장하여 인증 상태가 정상적으로 유지되었다.
  • SSR에서는 fetch 실행 시 window 객체 접근이 불가능하여 localStorage 기반 refresh token을 사용할 수 없었고 인증 헤더를 구성하지 못해 API 요청에서 인증 정보가 누락되었다.
  • 동일 사용자 요청임에도 CSR에서는 인증 상태가 유지되지만 SSR에서는 인증이 없는 요청으로 처리되어 초기 렌더링 시 데이터 조회 실패 및 인증 페이지 리다이렉트가 발생했다.
  • CSR 전용 axios 기반 API 계층을 SSR에서도 재사용하려 했으나 RSC 환경에서는 hook 사용이 불가능하고 실행 컨텍스트가 달라 동일한 방식으로 호출할 수 없었다.
  • 렌더링 환경에 따라 인증 상태가 달라지고 동일 세션이 유지되지 않는 문제가 발생했다.

    원인 분석

    1. 브라우저 전용 저장소 의존 구조

    인증 토큰 저장 방식이 localStorage 및 sessionStorage에 의존하도록 설계되어 SSR 환경에서는 토큰 접근 자체가 불가능한 구조였다.

    2. 인증 저장소 이원화 없이 단일 경로 설계

    access token은 메모리, refresh token은 브라우저 저장소에 분산되어 있었고 SSR에서는 해당 저장소에 접근할 수 없어 동일한 토큰 세트를 공유하지 못했다.

    결과적으로 CSR과 SSR 간 토큰 소스가 달라지면서 인증 상태 동기화가 불가능한 구조였다.

    3. API 계층의 실행 환경 미분리

    API 호출 구조가 axios 기반 CSR Service 계층 하나로만 구성되어 있었고 SSR 환경을 위한 fetch 기반 계층이 존재하지 않았다. 이로 인해 인증 헤더 주입 및 에러 처리 흐름이 환경 별로 분리되지 못했다.

    4. 서버 요청 기준 세션 복원 경로 부재

    SSR 요청 시 사용할 수 있는 공통 저장소(cookies)가 없었기 때문에 최초 렌더링 시 인증 정보를 복원할 수 없었고 매 요청이 비인증 상태로 처리되었다.

    이 구조에서는 새로고침 또는 서버 렌더링 시 세션 지속성이 유지될 수 없다.

    해결 방법

    1. 인증 저장 구조 및 API 계층 분리

    CSR과 SSR 각각의 실행 환경에서 사용할 수 있는 저장소와 API 계층을 분리하고 동일한 인증 데이터를 두 환경에서 모두 사용할 수 있도록 구조를 재설계했다.

    1.1. 기존 구조

  • CSR: access token은 axios header, refresh token은 localStorage 또는 sessionStorage 저장
  • SSR: 인증 저장소 없음, API 호출 시 인증 헤더 구성 불가
  • 단일 Service 계층만 존재하여 실행 환경 구분 없음
  • 1.2. 변경된 구조

  • CSR: 기존 방식 유지 (axios + localStorage/sessionStorage)
  • SSR: cookies를 활용하여 access token과 refresh token 저장
  • Service / RscService로 API 계층 분리하여 실행 환경별 API 호출 구조 구성
  • SSR에서는 cookies를 사용하여 토큰을 저장하고 fetch 요청 시 해당 값을 header로 구성하도록 구현했다.

    또한 인증 헤더를 구성하는 공통 함수로 SSR API 요청 시 동일한 방식으로 토큰을 주입하도록 처리했다.

    1.3. 구조 변화 요약

  • 인증 저장소를 CSR(localStorage)와 SSR(cookies)로 분리
  • 동일 토큰을 양쪽 저장소에 동시에 저장
  • API 계층을 Service / RscService로 분리하여 실행 환경별 처리 구조 확립
  • 2. 토큰 이중 저장을 통한 상태 동기화

    로그인 시 access token과 refresh token을 CSR 저장소와 SSR cookies에 동시에 저장하도록 변경했다.

    이 구조를 통해 CSR과 SSR 모두 동일한 인증 데이터를 기반으로 요청을 수행할 수 있도록 만들었다.

    CSR에서는 기존 방식대로 axios header와 localStorage를 사용하고 SSR에서는 cookies 기반으로 동일 토큰을 참조하도록 구성했다.

    3. 서버 요청 기준 인증 흐름 재구성

    SSR 환경에서는 fetch 요청 시마다 cookies에서 access token을 읽어 header에 주입하는 방식으로 인증 흐름을 구성했다.

    이 구조를 통해 서버 렌더링 시에도 인증이 필요한 API 요청이 정상적으로 수행되도록 보장했다.

    또한 refresh token 기반 세션 유지 로직을 SSR에서도 동작할 수 있도록 구조를 통합했다.

    결과

    CSR과 SSR 환경 모두에서 동일한 인증 상태를 유지할 수 있는 구조가 확보되었고 서버 렌더링 시에도 인증 기반 데이터 요청이 정상적으로 수행되도록 개선되었다.

    새로고침이나 재접속 시에도 cookies에 저장된 refresh token을 기반으로 인증 상태를 유지할 수 있게 되었고 사용자 세션 단절 문제가 제거되었다.

    API 호출 계층이 실행 환경 기준으로 분리되면서 인증 처리 로직의 중복이 제거되고 유지보수성이 개선되었다.

    핵심 포인트

    이 개선의 핵심은 실행 환경에 따라 인증 저장소와 API 계층을 분리하고 동일한 토큰을 양쪽 환경에서 동시에 사용할 수 있도록 구조를 재설계한 데 있다.

  • CSR(localStorage)와 SSR(cookies) 기반 이중 저장 구조 적용
  • Service / RscService 분리를 통한 실행 환경별 API 계층 구성
  • SSR에서도 인증 헤더를 구성할 수 있도록 fetch 기반 토큰 주입 구조 설계
  • CSR 중심 인증 구조에서 SSR까지 확장 가능한 이중 저장 및 이원화된 API 계층 구조로 변경하였다

    B. Error Boundary + Interceptor 기반 로그인 유지 API 예외 처리


    문제

  • Access token이 만료된 상태에서 API 요청이 발생하면 요청이 실패하고 기능이 중단되거나 에러 화면이 노출되었다.
  • SSR에서는 API 호출 중 에러가 발생하면 전체 렌더링이 중단되고 error.tsx로 전환되어 기존 요청 흐름이 복구되지 않았다.
  • CSR에서는 API 요청 실패 시 axios 요청이 reject되면서 이후 로직이 실행되지 않고 사용자 인터랙션이 중단되었다.
  • 결과적으로 토큰 만료 상황에서 요청이 자동으로 복구되지 않고 사용자 흐름이 끊기는 문제가 발생했다.

    원인 분석

    1. 환경별 에러 처리 구조 불일치

    SSR은 API 에러 발생 시 throw를 통해 error boundary로 전달되어 화면 단위로 처리되고, CSR은 axios 요청이 reject되면서 요청 단위에서 종료되는 구조였다.

    동일한 에러 코드라도 처리 진입 지점이 달라 공통된 복구 로직을 실행할 수 없는 구조였다.

    2. 토큰 재발급과 요청 재시도의 연결 부재

    Access token 만료 시 refresh token으로 재발급 후 기존 요청을 재실행하는 흐름이 정의되지 않았다.

    CSR에서는 interceptor가 없어 요청 실패 후 재시도 경로가 없었고, SSR에서는 error.tsx에서 렌더링만 수행되고 기존 API 호출을 다시 실행하지 않는 구조였다.

    3. 에러 코드 기반 중앙 처리 체계 부재

    토큰 만료, refresh token 만료, 권한 없음 등 인증 관련 에러가 존재했지만 이를 하나의 기준으로 분기하는 구조가 없었다.

    각 API 또는 화면에서 개별적으로 처리되면서 동일한 에러 코드에 대해 일관된 대응이 이루어지지 않았다.

    4. 인증 복구 흐름 미구현

    에러 발생 시 “복구”가 아니라 “중단”을 기준으로 처리되도록 설계되어 토큰 만료 상황을 정상 흐름으로 다루지 못했다.

    이로 인해 인증 문제 발생 시 재발급 → 재요청으로 이어지는 연속 흐름이 아니라 단절된 상태로 처리되었다.

    해결 방법

    1. SSR/CSR 통합 에러 처리 구조 설계

    에러 처리 기준을 error code로 통일하고 SSR과 CSR 각각에서 동일한 기준으로 동작하도록 구조를 재설계했다.

    1.1. 기존 구조

  • SSR: 에러 발생 시 error.tsx로 진입 후 화면 렌더링
  • CSR: axios 요청 실패 시 개별 처리 혹은 reject
  • 토큰 상태에 따른 분기 없음
  • 요청 재시도 없음
  • 1.2. 변경된 구조

  • 모든 API 에러를 error code 기반으로 분류
  • SSR: error.tsx에서 error.message를 기반으로 분기 처리
  • CSR: axios interceptor에서 error.response.data.errorCode 기반으로 분기 처리
  • 동일 error code 배열을 shared/configs에 정의하여 공통 사용
  • 1.3. 구조 변화 요약

    에러 처리 기준을 실행 환경이 아닌 error code로 통일하고 SSR과 CSR에서 동일한 분기 로직을 수행하도록 변경했다.

    2. 토큰 재발급 및 요청 재시도 흐름 구현

    Access token 만료 시 refresh token을 활용하여 재발급 후 기존 요청을 다시 실행하도록 흐름을 구성했다.

    CSR에서는 interceptor 내부에서 요청을 가로채고 토큰 갱신 후 기존 config로 재요청을 수행하도록 구현했다.

    SSR에서는 error.tsx 내부에서 updateAccess 실행 후 reset을 호출하여 동일 요청을 다시 수행하도록 구성했다.

    이 구조를 통해 토큰 갱신과 요청 재시도를 하나의 흐름으로 연결했다.

    3. 토큰 상태별 분기 처리 및 세션 정리

    Refresh token이 만료된 경우에는 복구가 불가능하므로 모든 토큰을 제거하고 로그인 페이지로 이동하도록 처리했다.

    권한이 없는 경우에는 no-permission 페이지로 이동하도록 분기 처리했다.

    SSR과 CSR 모두 동일한 조건으로 분기하도록 구성하여 처리 결과를 통일했다.

    또한 처리 가능한 에러 코드에 대해서는 LoadingComponent를 반환하여 사용자에게 중간 상태를 노출하고 비동기 복구 흐름을 유지했다.

    결과

  • Access token 만료 상황에서도 요청이 즉시 실패하지 않고 refresh token을 통해 재발급 후 자동으로 재요청되면서 기능 단절이 발생하지 않게 되었다.
  • SSR 환경에서도 에러 화면으로 전환되지 않고 내부에서 토큰을 갱신한 뒤 reset을 통해 동일 요청이 다시 실행되면서 페이지 단위 중단이 제거되었다.
  • CSR 환경에서는 interceptor가 모든 API 요청을 일관되게 처리하면서 개별 API에서 에러 처리를 구현할 필요가 사라졌다.
  • Refresh token 만료 시에는 모든 토큰을 제거하고 로그인 페이지로 이동하도록 통일되어 잘못된 인증 상태가 유지되는 문제가 제거되었다.
  • 에러 코드 기반 분기 구조를 통해 동일한 인증 상태에서 항상 동일한 처리 결과가 보장되면서 사용자 경험이 안정화되었다.
  • 핵심 포인트

    이 개선의 핵심은 토큰 에러를 단순 실패가 아닌 복구 가능한 상태로 정의하고 요청 흐름 안에서 처리하도록 구조를 변경한 것에 있다.

  • SSR과 CSR에서 분리되어 있던 에러 처리 기준을 error code 기반으로 통합
  • 토큰 재발급과 요청 재시도를 하나의 흐름으로 연결
  • interceptor와 error boundary를 역할에 맞게 분리하여 계층형 처리 구조 구성
  • 요청 실패 후 종료되는 구조에서 토큰 갱신 후 동일 요청을 복구하는 구조로 변경
    eexport const setRscToken = async (access: string, refresh: string | false) => {
      cookies().set(storageAccessKey, access);
      if (refresh) cookies().set(storageRefreshKey, refresh);
    };
    export const setRscHeader = async (): Promise<{ Authorization: string }> => {
      const token = await getRscAccess();
      if (token) {
        return { Authorization: `Bearer ${token}` };
      }
      return { Authorization: "" };
    };
    const headers = await setRscHeader();
    const response = await fetch(URI, { headers });
    export const noAccessTokenCode = ["4105", "4110"];
    export const noRefreshTokenCode = ["4000", "4102", "4103"];
    export const noPermissionCode = ["4107"];
    if (noAccessTokenCode.includes(errorCode)) {
      const refresh = getRccRefresh();
      const accessToken = await updateAccess(refresh);
      config.headers["Authorization"] = `Bearer ${accessToken}`;
      return API.request(config);
    }
    await updateAccess(refresh);
    setTimeout(() => {
      reset();
    }, 1000);
    const handleNoRefresh = async () => {
      await signout();
      location.href = "auth/signin";
    };