React + SPA 아키텍처 가이드
React SPA는 가장 익숙한 조합이지만, 익숙하다는 이유로 쉽게 과소설계된다. 화면을 빠르게 띄우고 컴포넌트를 조합하는 데는 강하지만, 애플리케이션 규모가 커질수록 데이터 흐름과 상태 경계, 라우팅, 성능 최적화, 번들 전략이 얽히며 구조적 품질 차이가 크게 난다.
좋은 React SPA 아키텍처는 컴포넌트를 함수로 잘 쪼개는 것에서 끝나지 않는다. 어떤 상태를 어디에 두고, 어떤 화면을 독립된 기능으로 운영하고, 어떤 데이터는 서버 상태로 보고 어떤 데이터는 UI 상태로 분리할지를 명확히 해야 한다.
아키텍처 그림 설명
[Browser]
|
v
[React Router]
|
v
[Page / Feature Shell]
|
+---------------------------+
| |
v v
[Server State Layer] [Global / Local UI State]
| |
v v
[API / BFF] [Form / Modal / Filters]
|
v
[Backend Services]
SPA 구조에서는 첫 진입 이후 대부분의 책임이 브라우저 안에서 이어집니다. 그래서 라우팅, 서버 상태, 로컬 UI 상태를 섞지 않는 것이 특히 중요합니다. 이 그림처럼 URL과 화면 조립은 라우터가, 원격 데이터는 서버 상태 계층이, 상호작용 상태는 UI 계층이 맡아야 규모가 커져도 안정적입니다.
React SPA가 잘 맞는 문제
React SPA는 다음과 같은 환경에서 매우 강하다.
- 로그인 이후 상호작용이 많은 제품형 서비스
- 빠른 화면 전환과 풍부한 클라이언트 인터랙션이 중요한 경우
- SEO보다 작업 효율, 사용자 생산성, 복잡한 편집 경험이 중요한 경우
- 프론트엔드가 독립적으로 제품 경험을 빠르게 실험해야 하는 경우
반대로 검색 유입이 매우 중요하고, 초기 응답 속도와 미리보기 메타 정보가 핵심이라면 순수 SPA만으로는 한계가 있다. 이때는 SSR이나 정적 렌더링 전략을 함께 검토해야 한다.
구조는 라우트가 아니라 기능 중심으로 나눠라
React SPA가 커질수록 흔히 발생하는 문제는 pages, components, hooks, utils 같은 기술 분류 디렉터리만 남고 실제 기능 경계가 사라지는 것이다. 이렇게 되면 제품 화면은 존재하지만 제품 구조는 보이지 않게 된다.
실무에서는 기능 중심 구조가 훨씬 낫다. 예를 들면 features/orders, features/billing, features/editor 같은 단위로 API, 훅, 컴포넌트, 테스트를 함께 둔다. 공통 UI와 인프라는 별도 계층으로 두되, 제품 고유 로직은 기능 단위에서 닫히게 하는 것이 유지보수에 유리하다.
상태는 네 종류로 나눠 생각하라
React 프로젝트가 복잡해지는 가장 큰 이유는 “상태”를 하나의 문제로 다루기 때문이다. 실제로는 상태의 성격이 다르며, 같은 도구로 모두 처리하려고 할수록 아키텍처가 무너진다.
주요 상태는 보통 다음 네 가지로 구분된다.
- 서버 상태: API에서 가져오고 동기화와 재요청이 필요한 데이터
- 전역 UI 상태: 토스트, 모달, 테마, 사이드바 열림 여부
- 도메인 상태: 장바구니, 편집 세션, 다단계 폼 진행 상황
- 로컬 상태: 컴포넌트 내부 입력값, hover, 열린 드롭다운
React SPA에서 흔한 실패는 Context 하나나 전역 스토어 하나에 모든 것을 넣는 것이다. 서버 상태를 전역 상태처럼 다루면 동기화와 캐싱이 무너지고, 로컬 상태까지 중앙화하면 결합도가 급격히 올라간다.
라우팅은 URL 설계이자 화면 계약이다
React Router 같은 도구는 단순 이동 기능이 아니라 화면 계약을 표현한다. URL은 사용자에게 보이는 경로이면서, 브라우저 히스토리, 접근 제어, 데이터 로딩 타이밍, deep link, 복구 가능성과 직접 연결된다.
따라서 라우트 설계 시 다음을 고려해야 한다.
- 화면 상태 중 무엇을 URL에 노출할 것인가
- 탭, 필터, 정렬, 페이지네이션은 새로고침 후 복원돼야 하는가
- 중첩 라우트는 레이아웃 재사용과 권한 경계에 어떤 도움을 주는가
- 상세 화면 진입 경로가 여러 개인 경우도 동일한 상태를 재현할 수 있는가
잘 설계된 SPA는 URL만 보고도 사용자의 작업 맥락을 상당 부분 복구할 수 있다.
데이터 계층은 컴포넌트에서 떼어내라
React 프로젝트 초반에는 useEffect 안에서 직접 fetch를 호출해도 큰 문제가 없어 보인다. 하지만 화면 수가 늘어나면 로딩, 에러, 재시도, 캐시, 동시 요청, 중복 요청 방지가 얽히며 곧 유지보수가 어려워진다.
실무에서는 서버 상태를 전담하는 계층을 두는 것이 좋다. TanStack Query 같은 도구를 사용하든 자체 패턴을 만들든, 중요한 것은 데이터 접근 규칙을 표준화하는 것이다.
- 어떤 키로 캐시할 것인가
- 어떤 시점에 refetch할 것인가
- 쓰기 작업 후 어떤 범위를 무효화할 것인가
- 낙관적 업데이트는 어디까지 허용할 것인가
이 규칙이 없으면 화면마다 데이터 처리 방식이 달라지고, 제품 전체 경험이 불안정해진다.
컴포넌트는 표현과 조합을 분리하라
React의 장점은 조합이지만, 조합이 곧 좋은 구조를 보장하지는 않는다. 재사용을 과도하게 의식해 범용 컴포넌트를 만들다 보면 실제 비즈니스 문맥이 사라지고, 반대로 특정 화면 전용 로직이 UI에 그대로 박히면 재사용성과 테스트 가능성이 떨어진다.
현실적인 기준은 다음과 같다.
- 디자인 시스템 컴포넌트는 표현 책임만 가진다.
- 기능 컴포넌트는 특정 유스케이스를 위해 화면을 조합한다.
- 페이지는 데이터와 기능 블록을 오케스트레이션한다.
이 세 층을 구분하면 “컴포넌트는 많은데 구조는 없는 상태”를 피하기 쉽다.
성능은 메모이제이션보다 분할 전략이 중요하다
React SPA에서 성능 이슈가 생기면 memo, useMemo, useCallback부터 붙이는 경우가 많다. 하지만 대부분의 병목은 렌더링 세부 최적화보다 더 상위의 구조 문제에서 발생한다.
실무에서 더 중요한 것은 다음이다.
- 초기 번들을 얼마나 줄일 수 있는가
- 라우트와 기능 단위로 코드 분할이 되는가
- 리스트 렌더링과 가상화가 필요한가
- 재요청과 불필요한 캐시 무효화가 과도하지 않은가
- 폼과 편집 경험에서 입력 지연이 생기지 않는가
즉 SPA 성능은 훅 미세 최적화보다, 어떤 코드를 언제 로드하고 어떤 상태를 어디서 바꾸는지가 더 큰 영향을 준다.
오류 처리와 관측 가능성도 아키텍처다
React SPA는 브라우저에서 동작하므로 서버 애플리케이션보다 오류가 덜 중요해 보일 수 있다. 그러나 실제 제품 운영에서는 사용자 환경, 네트워크 편차, 서드파티 스크립트, 브라우저 확장 프로그램 등 변수가 많아 예외 상황이 더 다양하다.
따라서 다음은 초기에 구조에 포함하는 것이 좋다.
- Error Boundary 기준
- API 오류 표시 정책
- 사용자 액션 로깅과 핵심 이벤트 추적
- 프론트 오류 수집 도구와 릴리즈 연결
- 느린 화면과 실패한 요청을 관찰할 수 있는 계측
React SPA에서 자주 생기는 구조적 문제
- 전역 상태가 데이터베이스처럼 비대해짐
- 페이지 컴포넌트가 API, 권한, 렌더링을 모두 떠안음
- URL 상태와 UI 상태가 분리되어 deep link가 깨짐
- 서버 상태를
useEffect로 직접 관리하며 로딩 규칙이 일관되지 않음 - 공통 컴포넌트 추상화가 과도해 실제 기능 개발 속도가 느려짐
마무리
React SPA 아키텍처의 핵심은 “컴포넌트를 어떻게 나눌까”보다, 상태와 기능, URL, 데이터 흐름을 어떻게 분리하고 연결할까에 있다. 규모가 커질수록 React의 유연성은 장점이기도 하지만, 팀 규칙이 없으면 빠르게 구조적 부채로 변한다.
결국 좋은 React SPA는 라이브러리를 많이 쓰는 앱이 아니라, 상호작용이 많은 제품 경험을 일관된 규칙으로 운영할 수 있는 앱이다.
운영 환경에서 어려워지는 지점
- React SPA에서는 SPA에서는 navigation, 데이터 패칭, 권한, 레이아웃 유지가 각자 따로 진화하면 아키텍처가 급격히 어려워진다.
- 브라우저 세션이 길어질수록 메모리 누수, stale cache, 암묵적 전역 상태가 생각보다 빨리 드러난다.
- SPA 모델은 여전히 유효하지만 페이지 생애주기를 화면 모음이 아니라 하나의 시스템으로 다뤄야 한다.
중요한 아키텍처 결정
- React Router와 명시적인 화면 경계를 중심으로 route 소유권을 설계한다.
- route 전환 사이에서 서버 데이터가 어떻게 캐시되고 무효화되고 새로고침되는지 정의한다.
- 애플리케이션 shell, feature 모듈, 공용 도메인 로직을 분리한다.
실무 예시
신뢰할 수 있는 SPA는 대개 shell, feature route, 데이터 접근 책임을 분리한다.
app shell
-> route modules
-> screen containers
-> presentational components
-> 공용 query 계층
-> 공용 auth/session 계층
피해야 할 안티패턴
- route 컴포넌트가 공통 정책 없이 모든 것을 제각각 fetch하는 것.
- navigation 사이에서 너무 많은 일시 상태를 계속 살려두는 것.
- feature 소유권 없는 거대한
src/pages디렉터리를 키우는 것.
운영 체크리스트
- route별 번들 크기와 전환 지연을 검토한다.
- stale cache와 중복 요청 패턴을 추적한다.
- 핵심 워크플로우에서 브라우저 navigation과 reload 동작을 테스트한다.
- 긴 세션 동안 메모리 증가를 측정한다.
최종 판단
React SPA에서는 SPA는 상호작용이 많은 제품에 여전히 강한 선택이지만, route 전환, 상태 소유권, 캐시 정책을 의도적으로 설계할 때만 그렇다.
Continue Reading
다음으로 읽기 좋은 글
React 아키텍처 설계 가이드
React 애플리케이션을 유지보수 가능한 시스템으로 설계하는 방법을 정리합니다. 컴포넌트 경계, 상태 계층, 서버 상태, 렌더링 모델, 팀 협업 구조까지 실무 중심으로 다룹니다.
🖥️ FrontendReact 디자인 시스템 아키텍처 가이드
React 기반 디자인 시스템을 설계할 때 필요한 토큰, 컴포넌트 계층, 접근성, 배포 전략을 실무 관점에서 정리합니다.
📈 최신 동향React Foundation 이 엔지니어링 팀에 의미하는 것
React Foundation 소식이 단순 거버넌스 뉴스가 아니라 프레임워크 생태계의 장기 예측 가능성에 어떤 의미를 갖는지 정리한 글입니다.
⚙️ BackendCQRS + Event Sourcing 실전 구현 가이드
CQRS와 Event Sourcing을 도메인 경계와 운영 복잡성의 관점에서 설명하고, Aggregate, Event Store, Projection, Snapshot, 정합성 모델의 선택 기준을 정리합니다.
다음 탐색