클라이언트 측 REST 서버로서의 프론트엔드: 상태 관리 혼돈 대신 아키텍처 규율
현대 프론트엔드 개발은 이미 DOM 조작과 간단한 레이아웃 단계를 훌쩍 넘어선 지 오래입니다. 아키텍처적으로 SPA와 SSR 애플리케이션은 전통적인 서버 시스템과 똑같은 작업을 수행합니다: 들어오는 신호를 받아들이고, 비즈니스 로직 규칙에 따라 데이터를 처리하며, 완성된 뷰를 반환하는 거죠. 차이점은 실행 환경과 상호작용 프로토콜에 불과합니다. 매 프레임워크 릴리스마다 새로운 추상화를 끝없이 발명하는 대신, 검증된 서버 패턴을 클라이언트 코드에 직접 적용하는 게 훨씬 효과적입니다.
역할의 진화: 하이퍼미디어에서 클라이언트 측 렌더링으로
원래 웹은 투명한 방식으로 작동했습니다: 브라우저가 URL을 요청하면 서버가 HTML을 생성하고, 클라이언트가 결과를 표시하는 거였죠. 놀랍게도 표준 HTML은 HATEOAS 원칙의 네이티브 구현입니다. 서버는 단순한 데이터가 아니라 인터페이스 상태와 가능한 전환(링크, 지정된 메서드를 가진 폼, 액션 버튼)을 함께 반환합니다. 클라이언트는 미리 라우트 맵을 저장하지 않고—현재 컨텍스트에서 허용된 것에 반응합니다.
JSON API를 사용하는 현대적 접근법은 상태 전송 형식을 바꿨을 뿐 본질은 같습니다. 동일한 원리는 HAL 형식으로 표현할 수 있습니다:
{
"id": 42,
"status": "pending",
"_links": {
"self": { "href": "/orders/42" },
"cancel": { "href": "/orders/42/cancel", "method": "POST" },
"detail": { "href": "/orders/42/detail" }
}
}
UI = f(state) 공식은 virtual DOM 등장 이전부터 존재했습니다. 이전에는 상태가 원격 서버에서 완전히 전송되었고, 지금은 로컬에서 계산되어 점진적으로 업데이트됩니다. 문제는 패러다임 전환에서 생긴 게 아니라, 로직을 클라이언트로 이식할 때 아키텍처 규율을 잃어버린 데 있습니다.
이벤트는 요청이다: 아키텍처 계층 매핑
백엔드 개발에서는 상호작용이 엄격하게 규정되어 있습니다. 컨트롤러가 HTTP 요청을 받아 입력 데이터를 검증하고, DTO로 변환한 후 서비스 계층에 처리를 위임합니다. 서비스는 도메인 모델을 다루고, 리포지토리는 영속성을 담당하죠. 책임 경계가 명확하며 특정 언어나 프레임워크에 독립적입니다.
프론트엔드에서는 사용자 액션(클릭, 입력, 스크롤, WebSocket 메시지)이 HTTP 요청의 완전한 아날로그 역할을 합니다. Event = Request. 하지만 통합된 컨트롤러 대신 생태계에 stores, composables, hooks, atoms, runes, view models 같은 용어의 동물원이 등장했습니다. 각 도구가 자체 상태 관리 철학을 강요하며 아키텍처 경계를 모호하게 만듭니다. 클라이언트 아키텍처를 서버 표준에 맞추는 게 더 논리적입니다:
Frontend
웃 → Event → Controller → Service → Repository → → → → → → Rest API
│ │ │ ↑ │ │
│ │ │ │ └─ Builds HTTP Request │
│ │ │ │ │
│ │ │ └─ Business logic │
│ │ │ │
│ │ └─ Extracts, validates & maps to DTO │
│ │ │
│ └─ Environment detail - Browser Event │
│ │
│ │
│ │
└─ ← Response ← Controller ← Service ← Repository ← ← ← ← ← ← Rest API
│ │ │
│ │ └─ Maps to Domain Model
│ │
│ └─ Maps to framework format: JSX/Vue template
│
└─ Automatically mapped to HTML chunk from JSX by framework
상태 유지 컨트롤러와 인프라 세부 사항
서버 패턴을 직접 이식하는 데 대한 주요 반론은 상태에 관한 것입니다. 전통적인 REST 컨트롤러는 수평 확장을 위해 stateless로 설계되었습니다. 프론트엔드는 본질적으로 상태 유지 컨텍스트에서 작동합니다: 사용자 세션, 로컬 캐시, 폼 및 UI 요소 상태가 브라우저 메모리에 살아 있죠. 이는 모순이 아니라 환경 특징입니다. 백엔드에서도 게임 개발, IoT 게이트웨이, gRPC나 WebSockets 기반 스트리밍 서비스에서 유사한 상태 유지 패턴을 사용합니다.
Hooks, signals, reactive atoms는 순전히 인프라 세부 사항으로 봐야 합니다. 이는 프레임워크가 효율적인 DOM 업데이트와 의존성 추적을 위해 사용하는 메커니즘입니다. 실수는 개발자들이 이 프리미티브 안에 비즈니스 규칙과 검증 로직을 직접 저장하기 시작한 겁니다. 상태 관리자는 종종 유출되는 추상화를 숨기며 UI 컴포넌트가 스토어 구조에 직접 의존하게 만듭니다. 컨트롤러와 서비스 계층을 도입하면 비즈니스 로직을 프레임워크의 리액티브 시스템에서 격리할 수 있습니다.
서버 규율의 실전 적용
백엔드 접근법을 채택한다고 React, Vue, Solid을 버릴 필요는 없습니다. 생태계 내에서 코드를 구조화하는 거죠. 예측 가능성을 높이고 신규 개발자 온보딩의 인지 부하를 줄이기 위해 몇 가지 원칙만 따르면 됩니다:
- 앱 경계에서 들어오는 이벤트와 API 데이터를 엄격히 검증 (Zod, Yup, 또는 TypeScript guards 사용).
- 원시 서버 응답과 사용자 입력을 비즈니스 계층으로 넘기기 전에 DTO로 변환.
- 프레임워크 훅을 import하지 않고 DOM에 대해 아무것도 모르는 순수 서비스에 도메인 로직 격리.
- 데이터 소스(LocalStorage, IndexedDB, REST, GraphQL)를 추상화하는 리포지토리 사용.
- 컨트롤러를 단일 연결 지점으로 활용: 이벤트를 서비스 호출로 매핑하고, 결과를 프레젠테이셔널 컴포넌트의 props로 변환.
이 접근법으로 코드는 프레임워크에 독립적이 됩니다. 비즈니스 로직은 렌더러 목 없이 단위 테스트로 쉽게 커버되고, UI 솔루션 간 마이그레이션은 얇은 어댑터 계층만 영향을 줍니다.
핵심 요약
- 프론트엔드 앱은 아키텍처적으로 브라우저에 배포된 REST 서버와 동등합니다: 이벤트가 HTTP 요청을 대체하고, 렌더링이 응답을 형성하죠.
- HTML은 역사적으로 HATEOAS 원칙을 구현해 상태와 가능한 액션을 클라이언트에 동시에 전달했습니다.
- stores, composables, atoms 같은 용어의 다양성은 신선함의 환상을 주지만 책임 경계를 모호하게 하고 유지보수를 복잡하게 만듭니다.
- Hooks와 signals는 프레임워크 인프라 프리미티브일 뿐, 비즈니스 규칙이나 복잡한 검증을 위한 장소가 아닙니다.
- 서버 규율(DTOs, controllers, services, repositories)을 적용하면 코드 결합도를 낮추고, 테스트를 단순화하며, 아키텍처를 특정 UI 프레임워크에 독립적으로 만듭니다.
— Editorial Team
아직 댓글이 없습니다.