React + NestJS(BFF) Frontend Platform 🎮⌨️
TypeScript | GraphQL BFF | Strapi Backend Integration | Monorepo | E2E Testing
CloneNOVA는 CodeNOVA 프로젝트를 계승한 React + NestJS(BFF) 기반 프론트엔드 프로젝트입니다.
Strapi Headless CMS를 백엔드 대용으로 활용하여 통합 E2E 테스트를 진행하였으며, TurboRepo 기반 Monorepo로 구성되었습니다.
- 프로젝트명: CloneNOVA
- 기간: 2025.10 - 2025.11
- 스택: React 19 + NestJS(BFF) + Strapi + TypeScript + GraphQL + Playwright
- 아키텍처: Monorepo (TurboRepo)
- React 19 + NestJS(BFF) 기반 프론트엔드 개발: GraphQL을 활용한 현대적인 프론트엔드 아키텍처
- TypeScript 마이그레이션: JavaScript에서 TypeScript로 전체 코드베이스 완전 전환
- GraphQL BFF 패턴: Client-BFF 간 GraphQL 통신, BFF-Backend 간 REST API 통신
- Strapi를 백엔드 대용으로 활용:
- 빠른 프로토타이핑과 테스트를 위한 Headless CMS 백엔드
- 향후 Spring Boot로 마이그레이션 예정
- Monorepo 구조 (TurboRepo): 효율적인 패키지 관리 및 빌드 최적화
apps/ web/ (React Frontend - GraphQL Client) bff/ (NestJS Backend for Frontend - GraphQL Server + REST Client) strapi/ (Strapi Headless CMS - Backend 대용) backend/ (Spring Boot - RESTful API Server - Future Migration) packages/ shared-types/ ui-components/ utils/ - 성능 최적화: React 최적화 패턴을 활용한 키보드 입력 무한 리렌더링 방지
- 통합 E2E 테스트: Playwright를 활용한 Frontend(React) + BFF(NestJS) + Backend(Strapi) 통합 테스트
- TypeScript를 통한 타입 안정성 확보
- Panda CSS를 활용한 제로 런타임 스타일링
- 데이터베이스 연동 및 CRUD 기능 구현
- OAuth 및 PortOne(결제 API) 연동 실습
- Redis 캐싱 및 실시간 랭킹 시스템
- Apollo Client를 활용한 상태 관리 및 캐싱
100% TypeScript 코드베이스 엄격한 타입 체크 적용
- 모든 프론트엔드 컴포넌트를
.tsx로 마이그레이션, 적절한 인터페이스 정의 - BFF 서비스 및 리졸버 완전 타입화
- 앱 간 타입 안정성을 위한 shared-types 패키지
- 타입 안전한 쿼리를 위한 GraphQL 스키마 자동 생성
최적의 데이터 페칭을 위한 현대적 3계층 아키텍처
- Frontend → BFF: GraphQL (Apollo Client) - 유연한 쿼리
- BFF → Backend: REST API (axios) - 안정적인 서버 간 통신
- 장점: 타입 안정성, 효율적인 데이터 로딩, 단일 엔드포인트
Frontend (React + Apollo Client)
↓ GraphQL Queries/Mutations/Subscriptions
BFF (NestJS + Apollo Server)
↓ REST API calls (axios)
Backend (Strapi/Spring Boot REST API)
빠른 프로토타이핑 및 통합 테스트를 위한 Headless CMS 백엔드
- Content Type에서 자동 생성되는 REST API로 백엔드 대용
- 로컬 개발용 SQLite, 프로덕션용 MySQL 지원
- 8명의 사용자, 12개의 게임 기록, 7개의 결제 데이터 포함 Mock 데이터 로더
BACKEND_TYPE환경 변수를 통한 Strapi ↔ Spring Boot 간 원활한 전환- 향후 Spring Boot 백엔드로 마이그레이션 예정
키보드 무한 리렌더링 해결
- 문제: 키 입력이후 무한정 발생하는 키보드 컴포넌트의 성능 저하
- 해결:
useCallback,useMemo,React.memo를 전략적으로 적용 - 결과: 모든 폼과 검색창에서 부드러운 타이핑 경험
- 예시: 최적화된 이벤트 핸들러를 적용한
SearchBar.tsx
Playwright를 활용한 Frontend + BFF + Backend(Strapi) 통합 테스트
- 범위: React(Frontend) + NestJS(BFF) + Strapi(Backend 대용) 통합 테스트
- Monorepo 구조: TurboRepo로 세 애플리케이션을 단일 저장소에서 관리
- 설정:
webServer설정을 통한 자동 서버 시작/종료 - 커버리지 (50+ 테스트 파일):
- 인증 플로우 (로그인, 회원가입, OAuth)
- 결제 통합 (PortOne 엔드투엔드)
- 게임 플레이 및 랭킹 모달
- 사용자 검색 및 팔로우 기능
- 지갑 및 거래 내역
- 마이페이지 대시보드 및 필터
- 실행:
pnpm test:e2e(세 애플리케이션 동시 실행)
| 카테고리 | 기술 | 버전 |
|---|---|---|
| 언어 | TypeScript (100% 타입 안전 코드베이스) | 5.x |
| 데이터베이스 | MySQL, Redis, SQLite (Strapi 개발용) | - |
| Backend (대용) | 현재: Strapi v5 (Headless CMS) - 백엔드 대용 향후: Spring Boot 마이그레이션 |
5.x / 미정 |
| BFF | NestJS, GraphQL (Code-First), TypeORM, TypeScript | 10.x |
| 프론트엔드 | React 19, Vite, Apollo Client, Panda CSS, Ark UI, Zustand | 19 / 5.x |
| Monorepo | TurboRepo, pnpm workspaces | latest |
| 테스트 | E2E: Playwright (통합) 통합: Jest + Supertest 단위: Jest |
1.x / 29.x |
| 인증 | JWT, OAuth 2.0 (Google, Kakao) | - |
| 결제 | PortOne (구 Iamport) | - |
| 통신 | Client ↔ BFF: GraphQL BFF ↔ Backend: REST API |
- |
| 기타 | framer-motion, react-markdown, highlight.js | - |
| No. | 요구사항명 | 요구사항 상세 | 우선순위 |
|---|---|---|---|
| AF01 | 회원가입 | 사용자 정보 등록 | Required |
| AF02 | ID/PW 로그인 | ID와 비밀번호를 이용한 로그인 | Required |
| AF03 | Google reCaptcha v3 | 인증 플로우 봇 방지 | Required |
| AF04 | ID 찾기 요청 | SMTP를 통한 ID 찾기 | Required |
| AF05 | ID 찾기 인증 | SMTP로 받은 코드 인증 | Required |
| AF06 | Google 로그인 | Google OAuth 로그인 | Required |
| AF07 | Kakao 로그인 | Kakao OAuth 로그인 | Optional |
| AF08 | Access Token 갱신 | 만료된 액세스 토큰 갱신 | Optional |
| AF09 | 비밀번호 재설정 요청 | 이메일을 통한 비밀번호 재설정 | Optional |
| AF10 | 비밀번호 재설정 인증 | 비밀번호 재설정 인증 | Optional |
| No. | 요구사항명 | 요구사항 상세 | 우선순위 |
|---|---|---|---|
| MF01 | 개인 프로필 조회 | 본인 프로필 정보 조회 | Required |
| MF02 | 개인 정보 수정 | 사용자 정보 수정 | Required |
| MF03 | 계정 삭제 | 사용자 계정 삭제 (삭제 대신 비활성화) | Required |
| No. | 요구사항명 | 요구사항 상세 | 우선순위 |
|---|---|---|---|
| SF01 | 상품 목록 조회 | 상점의 모든 상품 조회 | Required |
| SF02 | 상품 상세 정보 조회 | 특정 상품의 상세 정보 조회 | Required |
| No. | 요구사항명 | 요구사항 상세 | 우선순위 |
|---|---|---|---|
| PF01 | 결제 준비 | PortOne API를 사용한 결제 준비 | Required |
| PF02 | 결제 검증 및 화폐 지급 | 결제 검증 및 게임 내 화폐 지급 | Required |
| PF03 | PortOne Webhook 수신 | PortOne으로부터 재검증 webhook 수신 | Required |
| PF04 | 구매 내역 조회 | 사용자 본인의 구매 내역 조회 | Required |
| PF05 | 구매 상세 정보 조회 | 상세 구매 정보 조회 | Required |
| PF06 | 환불 요청 | 구매한 화폐에 대한 환불 요청 | Required |
| PF07 | 환불 내역 조회 | 사용자의 환불 내역 조회 | Required |
| PF08 | 환불 가능 여부 확인 | 구매가 환불 가능한지 확인 | Required |
| PF09 | 결제 실패 알림 | 결제 실패 시 사용자에게 알림 | Required |
| No. | 요구사항명 | 요구사항 상세 | 우선순위 |
|---|---|---|---|
| WF01 | 지갑 잔액 조회 | 사용자의 현재 화폐 잔액 조회 | Required |
| WF02 | 거래 내역 조회 | 화폐 획득/사용 내역 조회 | Required |
| WF03 | 거래 검색 및 필터 | 카테고리별 필터링 (결제/언어, 구매/환불), 날짜 | Optional |
| No. | 요구사항명 | 요구사항 상세 | 우선순위 |
|---|---|---|---|
| GF01 | 게임 결과 저장 | 게임 플레이 결과를 서버에 저장 | Required |
| GF02 | 게임 랭킹 조회 | 특정 게임의 랭킹 조회 | Required |
| GF03 | 개인 순위 조회 | 사용자 본인의 랭킹 위치 조회 | Required |
| GF04 | 개인 게임 통계 조회 | 사용자 본인의 게임 통계 조회 | Required |
| GF05 | 타 사용자 게임 통계 조회 | 특정 사용자의 게임 정보 조회 | Required |
핵심 기능:
- 게임 결과 저장 및 랭킹 업데이트
- Redis를 활용한 실시간 랭킹 시스템
- GraphQL Subscription을 통한 실시간 랭킹 업데이트
- 개인 랭킹 위치 조회
- 전체 리더보드 조회
- 특정 사용자 랭킹 및 게임 통계 조회
| No. | 요구사항명 | 요구사항 상세 | 우선순위 |
|---|---|---|---|
| FF01 | 팔로잉 목록 조회 | 사용자의 팔로잉 정보 조회 | Required |
| FF02 | 팔로워 목록 조회 | 사용자의 팔로워 정보 조회 | Required |
| FF03 | 사용자 검색 | 이름으로 사용자 검색 | Required |
| FF04 | 사용자 팔로우 | 다른 사용자를 팔로잉 목록에 추가 | Required |
| FF05 | 사용자 언팔로우 | 팔로잉 목록에서 사용자 제거 | Required |
┌───────────────────────────────────────────────────────────────┐
│ Frontend (React 19) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Apollo Client (GraphQL) │ │
│ │ - Queries/Mutations │ │
│ │ - Subscriptions (Real-time Ranking) │ │
│ │ - Normalized Cache │ │
│ │ - Optimistic UI │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Zustand (Local State) │ │
│ │ - UI State, Game State │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Panda CSS + Ark UI │ │
│ └──────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
↓ GraphQL over HTTP
POST /graphql
┌───────────────────────────────────────────────────────────────┐
│ BFF (NestJS + GraphQL + TypeORM) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Apollo Server (GraphQL) │ │
│ │ - Resolvers (Query, Mutation, Subscription) │ │
│ │ - Schema-first or Code-first │ │
│ │ - DataLoader (Solve N+1 Problem) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Services Layer │ │
│ │ - Data Aggregation (Multiple Backend API calls) │ │
│ │ - Business Logic │ │
│ │ - Authentication/Authorization │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Backend API Client (axios) │ │
│ │ - RESTful calls to Strapi/Spring Boot │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Redis Client │ │
│ │ - Real-time ranking query/update │ │
│ │ - PubSub for GraphQL Subscriptions │ │
│ └──────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
↓ REST API (JSON)
GET/POST/PATCH/DELETE /api/*
┌───────────────────────────────────────────────────────────────┐
│ Backend Server (Strapi v5 / Spring Boot) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ REST Controllers │ │
│ │ - User, Payment, Game, Product APIs │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Business Logic Layer │ │
│ │ - Service Layer │ │
│ │ - Transaction Management │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Data Access Layer │ │
│ │ - ORM (TypeORM/JPA) │ │
│ │ - Database Operations │ │
│ └──────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ Data Layer │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ MySQL Database │ │ Redis Cache │ │
│ │ (User, Game, │ │ (Ranking, Session)│ │
│ │ Payment Data) │ │ │ │
│ └────────────────────┘ └────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
clonenova/
├── apps/
│ ├── web/ # React Frontend (GraphQL Client)
│ │ ├── src/
│ │ │ ├── app/ # Routing, Providers
│ │ │ │ └── apollo-provider.tsx
│ │ │ ├── features/ # Feature Modules
│ │ │ │ ├── auth/
│ │ │ │ │ ├── graphql/
│ │ │ │ │ │ ├── queries.ts
│ │ │ │ │ │ └── mutations.ts
│ │ │ │ │ ├── components/
│ │ │ │ │ └── hooks/
│ │ │ │ ├── user/
│ │ │ │ ├── payment/
│ │ │ │ ├── game/
│ │ │ │ ├── ranking/
│ │ │ │ └── follow/
│ │ │ ├── shared/ # Shared Components
│ │ │ ├── stores/ # Zustand Stores
│ │ │ └── lib/ # Utilities
│ │ │ ├── apollo-client.ts
│ │ │ └── portone.ts
│ │ ├── tests/e2e/ # Playwright E2E Tests
│ │ ├── panda.config.ts
│ │ ├── playwright.config.ts
│ │ └── vite.config.ts
│ │
│ ├── bff/ # NestJS BFF (GraphQL Server)
│ │ ├── src/
│ │ │ ├── schema.gql # GraphQL Schema (Auto-generated)
│ │ │ ├── app.module.ts
│ │ │ ├── auth/ # Auth Module
│ │ │ ├── users/ # User Module
│ │ │ │ ├── users.resolver.ts
│ │ │ │ ├── users.service.ts
│ │ │ │ └── models/
│ │ │ │ └── user.model.ts
│ │ │ ├── products/ # Product Module
│ │ │ ├── payments/ # Payment Module
│ │ │ │ ├── payments.resolver.ts
│ │ │ │ ├── payments.service.ts
│ │ │ │ └── portone.service.ts
│ │ │ ├── wallets/ # Wallet Module
│ │ │ ├── games/ # Game Module
│ │ │ ├── rankings/ # Ranking Module
│ │ │ │ ├── rankings.resolver.ts
│ │ │ │ └── rankings.service.ts
│ │ │ ├── follows/ # Follow Module
│ │ │ │ ├── follows.resolver.ts
│ │ │ │ └── follows.service.ts
│ │ │ ├── common/ # Common Modules
│ │ │ │ ├── backend-api.service.ts
│ │ │ │ └── redis.service.ts
│ │ │ └── main.ts
│ │ ├── test/integration/ # Integration Tests (Jest + Supertest)
│ │ └── nest-cli.json
│ │
│ └── strapi/ # Strapi Headless CMS
│ ├── src/
│ │ ├── api/ # Content Types
│ │ │ ├── user/
│ │ │ ├── product/
│ │ │ ├── payment/
│ │ │ └── game/
│ │ └── index.ts
│ ├── data/mock/ # Mock Data Loaders
│ └── config/
│
├── backend/ # Spring Boot Backend (Future)
│ ├── src/main/java/com/clonenova/
│ │ ├── users/
│ │ ├── products/
│ │ ├── payments/
│ │ ├── wallets/
│ │ ├── games/
│ │ └── follows/
│ └── pom.xml
│
├── packages/
│ ├── shared-types/ # Shared TypeScript Types
│ │ ├── src/
│ │ │ ├── graphql/ # GraphQL Types
│ │ │ ├── rest/ # REST API Types
│ │ │ └── index.ts
│ │ └── package.json
│ ├── ui-components/ # Shared UI Components
│ └── utils/ # Shared Utilities
│
├── .husky/ # Git Hooks
│ ├── pre-commit # ESLint + lint-staged
│ └── pre-push # TypeCheck + Tests
│
├── turbo.json
├── pnpm-workspace.yaml
└── package.json
| 이름 | 역할 | GitHub |
|---|---|---|
| 민영재 | Frontend | @yeomin4242 |
MIT License