레거시 플랫폼 프론트엔드 전환 설계
Server: Spring Boot · Client: React + Next.js (BFF)
작성 목적: 리뷰용 설계 문서
1. 목적
- 현재 레거시 플랫폼은 front-end와 back-end가 긴밀하게 종속(tightly coupled)되어 있어, 책임 경계가 모호하고 독립적인 변경·배포가 어렵다.
- SSR(Server-Side Rendering)을 유지하여 SEO·초기 로딩 이점을 지키면서, 클라이언트 인터랙션(상태/DOM 동기화)을 강화해 사용자 UX를 개선한다.
- 주의: "전부 SPA로 전환"이 아니다. 순수 SPA(CSR)는 SEO에 불리하므로, Next.js로 부분 SSR/RSC를 유지한 채 인터랙션만 더한다.
- 중복 기능을 제거하고, 일부 기능의 버전 관리·현행화를 수행한다.
--
전환 이유 (4축)
- 책임 분리 (Client / Server)
- front/back의 책임 소재를 명확히 분리, 독립 배포 가능.
- Trade-off: 운영 대상이 2배(Spring + Node 런타임), API 계약·빌드 파이프라인 추가. → 운영 복잡도 증가를 감수.
- 보안 (공격 표면 축소)
- Spring API를 내부망(private network)에 격리 → 외부에서 직접 노출되지 않음.
- 서버 컴포넌트(RSC, React Server Components)에서 민감 로직·데이터 접근을 처리하면 해당 코드가 브라우저로 전송되지 않음.
- 개발 생산성 (상태/DOM 동기화)
- React의 선언적 렌더링으로 상태(state)와 DOM 동기화를 자동화 → 인터랙션 개발 생산성 향상.
- SEO (부분 SSR)
- Next.js로 SEO가 필요한 영역에 SSR/RSC를 적용.
2. 현재 구조 (As-Is)
- MPA(Multi-Page Application) + SSR 방식.
- 페이지 이동마다 서버가 완성된 HTML을 렌더링해 전달.
- 장점: 초기 로딩이 빠르고 SEO에 유리 → 이벤트 페이지에서 가장 중요한 요구사항.
- 단점: front/back 종속, 인터랙션 한계.
3. 전환 결론 (To-Be)
| 구분 | 기술 |
|---|
| Server | Spring Boot |
| Client | React + Next.js (BFF 역할) |
4. 구조: BFF (Backend For Frontend)
Next.js가 브라우저와 Spring API 사이의 단일 진입점(BFF) 역할을 한다.
[브라우저]
│ httpOnly 쿠키 (User JWT) 자동 첨부 + CSRF 토큰
▼ 외부 (HTTPS)
[Next.js 서버 = BFF] ← 외부에 노출되는 유일한 진입점
│ 쿠키에서 토큰 추출 → Authorization 헤더로 부착
▼ 내부망 (server-to-server)
[Spring API] ← 외부에서 직접 접근 불가
▼
[DB]
4.1 핵심 원칙
- User JWT:
httpOnly 쿠키에 저장 (브라우저 JS 접근 차단).
- 브라우저 호출 방식: 모든 데이터 요청은 BFF의
/api/proxy를 경유한다. 브라우저가 Spring API를 직접 호출하지 않는다(same-origin 강제). → 서버 컴포넌트든 클라이언트 컴포넌트든 동일하게 적용.
- 토큰 헤더 부착: 토큰을 요청 헤더에 싣는 작업은 서버 프록시(BFF)에서 수행. 브라우저는 토큰 값을 직접 다루지 않는다.
- 쿠키 기반 인증 교환:
POST /api/auth/login (BFF 라우트, fetch) → 내부에서 백엔드 /auth/cookie-login 호출.
5. 인증 / 세션 처리
5.1 토큰 발급·검증의 책임 분담
| 단계 | 주체 | 역할 |
|---|
| 토큰 발급 | Spring | 자격 검증 후 accessToken / refreshToken 발급 (발급 권한 독점) |
| 1차 검증·중계 | Next.js (BFF) | 쿠키에 토큰 심기, 요청마다 서명·만료 검증, 내부망으로 중계. 로그인 여부·경로 보호 목적 |
| 최종 검증·권한 판단 | Spring | 토큰을 독립적으로 재검증 + 리소스 접근 권한(authorization) 판단 |
심층 방어(defense in depth): BFF가 검증했더라도 Spring은 자기에게 도착한 토큰을 독립적으로 다시 검증한다. BFF 우회 호출 시에도 데이터가 보호되도록, 데이터를 내주는 Spring이 최종 방어선을 갖는다.
5.2 토큰 저장 위치
- 토큰은 브라우저 JS가 읽을 수 있는 곳(localStorage)이 아니라 JS 접근이 차단된
httpOnly 쿠키에 보관한다.
- 쿠키는 브라우저에 저장되지만 JS 접근이 차단되며, 읽고 검증하는 주체는 Next.js 서버(BFF)다.
- 대비 축은 "브라우저 vs 서버"가 아니라 "JS가 읽을 수 있는가(localStorage) vs 없는가(httpOnly)"이다.
5.3 토큰 클레임(claim) 원칙
- JWT payload는 암호화가 아니라 base64 인코딩이므로, 누구나 디코딩하면 내용을 읽을 수 있다(서명은 위조만 막을 뿐 열람은 못 막는다).
- 따라서 토큰에는 개인정보를 넣지 않는다.
- 담는 것: 내부 식별자(
sub = USER_ID 등), 권한(role/scope), 메타데이터(iat, exp, iss).
- 절대 넣지 않는 것: 비밀번호, 주민번호, 카드번호, 이름·이메일·전화번호 등 직접 식별 가능한 개인정보.
- 실제 개인정보(USERNAME 등)는 토큰에서 꺼내지 않고, 토큰의 ID로 Spring이 DB를 조회하여 제공한다.
5.4 키 관리 전략 (다중 서버 대응)
- stateless 토큰은 키만 있으면 어느 서버든 독립 검증 가능 → 스케일아웃에 강함.
- 단, 서버마다 키가 다르면 검증이 실패하므로 키 일관성이 필수.
- 비대칭키(RS256) 권장: Spring(인증 서버)이 private key로 발급, BFF·여러 Spring 인스턴스는 public key로 검증만. 공개키는 노출돼도 위조 불가.
- 키 교체(rotation) 시 무중단을 위해 옛 키·새 키를 일정 기간 동시 인정(또는 JWKS 엔드포인트로 공개키 동적 배포).
5.5 로그인 흐름
신규 플랫폼의 accessToken 유효성에 따라 분기한다. (단순 "유/무"가 아니라 서명·만료 검증 통과 여부로 판단)
- 유효한 accessToken이 없는 경우 (없거나 검증 실패)
POST /api/auth/login → 내부 /auth/cookie-login 호출하여 accessToken 발급·수신.
- 유효한 accessToken이 있는 경우
/api/proxy/auth/profile 호출 → 로그인 정보(USER_ID, USERNAME 등)를 서버 조회로 획득.
5.6 토큰 만료·갱신
- accessToken 만료 시간: 15분.
- API 호출 시마다 토큰의 서명·만료를 검증.
- 토큰이 만료/부재면 refreshToken으로 재발급 요청.
- refreshToken으로도 재발급에 실패하면 최종 실패 → 세션 종료 후 로그인 페이지로 리다이렉트.
6. CSRF 방어
httpOnly 쿠키는 XSS(토큰 탈취)는 막지만, 쿠키 자동 첨부 성질 때문에 CSRF(cross-site request forgery)에 노출된다. XSS와 CSRF는 별개의 공격이므로 둘 다 방어해야 한다.
| 공격 | 노리는 것 | 방어 |
|---|
| XSS | 토큰 탈취 | httpOnly 쿠키 (JS 접근 차단) |
| CSRF | 토큰은 그대로, 자동 첨부 악용 | SameSite 속성 + CSRF 토큰 |
- SameSite: 쿠키에
SameSite=Lax(현대 브라우저 기본·권장) 설정 → cross-site 요청의 쿠키 자동 첨부 차단.
- CSRF 토큰: 상태 변경 요청(POST/PUT/DELETE)에 예측 불가능한 토큰을 요구. 외부 사이트는 same-origin 정책상 이 값을 읽을 수 없어 위조 요청이 걸러짐.
- BFF 구조에서는 브라우저 ↔ Next.js 구간에 방어를 건다(Next.js ↔ Spring은 내부망 통신이라 무관).
- 쿠키 설정 시
httpOnly + secure(HTTPS 전용) + sameSite를 한 세트로 적용.
cookies().set('token', jwt, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
});
7. Data Fetching
- 경로: 브라우저 → Next.js 서버(BFF) → Spring API.
- 클라이언트 컴포넌트의 모든 데이터 요청은 Next.js Route Handler(
/api/proxy)를 경유하며, 브라우저에서 Spring API를 직접 호출하지 않는다(same-origin 강제). → CORS 문제 회피, 토큰을 브라우저가 만지지 않음.
- 서버 컴포넌트(RSC)는 Next.js 서버에서 직접 Spring을 호출(내부망). 민감 데이터는 RSC에서 처리해 클라이언트 번들로 나가지 않게 한다.
8. 하이드레이션 불일치 (Hydration Mismatch) 대응
- 하이드레이션: SSR이 보낸 정적 HTML에 JS로 이벤트·상태를 부착해 인터랙티브하게 만드는 과정.
- 불일치: 서버가 그린 HTML과 브라우저가 재계산한 HTML이 다를 때 발생. SSR을 쓰는 이상 반드시 고려해야 함.
- 원인: 시간(
new Date()), 랜덤 값(Math.random()), 타임존·로케일 차이, 브라우저 전용 API(window, localStorage).
- 대응: 환경에 따라 달라지는 값은 첫 렌더에서 제외하고,
useEffect로 하이드레이션 후 채우거나 클라이언트 전용 렌더로 격리한다.
- 서버에서는
useEffect가 실행되지 않으므로, 첫 렌더 결과가 서버·클라이언트 동일해져 불일치가 방지됨.
- 이벤트 페이지 대표 사례: 카운트다운 타이머, "마감까지 D-3" 같은 시간 의존 표시.
function Countdown() {
const [remaining, setRemaining] = useState(null);
useEffect(() => {
setRemaining(calcRemaining());
}, []);
return <span>{remaining ?? "로딩 중"}</span>;
}
9. 마이그레이션 전략
- 점진 전환(strangler fig 패턴): 기존 MPA를 한 번에 갈아엎지 않는다.
- 신규 이벤트 도메인부터 적용 → 새로 생성되는 이벤트부터 신규 구조(React + Next.js)로 점차 확대.
- 기존 안정 페이지는 그대로 두어 마이그레이션 위험을 최소화.
10. 운영 고려사항
- Node 런타임 운영: Spring 외에 Next.js(Node) 프로세스의 모니터링·패치·스케일링·배포 주체를 정의해야 함.
- 세션 전략: stateless(서명된 토큰) 방식 채택 → 서버 메모리 세션 불필요, 수평 확장 용이. (refresh 토큰 관리에서 일부 상태가 필요할 수 있음.)
- BFF 자체 보안: 외부 노출 진입점이므로 Next.js 서버의 의존성 취약점·SSRF 등을 관리.
- HTTPS 적용: 개인정보가 지나가는 브라우저 ↔ BFF 구간 암호화 필수. 로그에 개인정보·토큰을 남기지 않도록 주의.
11. 책임 분담 요약
| 항목 | Next.js (BFF) | Spring API |
|---|
| 토큰 발급 | ✕ | ○ (private key 서명) |
| 토큰 검증 | ○ (1차 관문: 서명·만료) | ○ (최종 재검증) |
| 권한 판단 (authorization) | △ (경로 보호 수준) | ○ (리소스 소유권·권한) |
| 개인정보 조회 | ✕ (통로 역할) | ○ (ID로 DB 조회) |
| 쿠키 설정·CSRF 방어 | ○ | ✕ |
| 외부 노출 | ○ (유일한 진입점) | ✕ (내부망) |
부록: 용어
- BFF (Backend For Frontend): 프론트엔드 전용 백엔드. 여기서는 Next.js 서버가 브라우저와 Spring 사이의 중계·보안 계층 역할.
- RSC (React Server Components): 서버에서만 실행되어 클라이언트로 JS를 전송하지 않는 컴포넌트.
- stateless 토큰: 서버가 세션을 저장하지 않고, 토큰 자체에 정보를 담아 검증하는 방식(JWT).
- 하이드레이션(hydration): SSR HTML에 클라이언트 JS로 동작을 부착하는 과정.