티스토리 뷰
Environment-Agnostic Architecture: Frontend와 Backend의 환경 분리 패턴
pak2251 2025. 11. 3. 16:28작성일: 2025-11-03
카테고리: Architecture, API Design, Frontend
난이도: 중급
TL;DR
- 문제: dev/staging/prod 환경 prefix가 Frontend에 노출되어 코드 복잡도 증가 및 네트워크 낭비 (3배)
- 해결: Environment-Agnostic Pattern - Frontend는 환경을 몰라야 한다
- 핵심: 환경 정보는 인프라 레이어(Domain/Subdomain)에서 처리, API 응답은 baseName만 반환
- 결과: 네트워크 비용 66% 감소, Frontend 코드 간결화, 배포 유연성 향상
들어가며
imprun.dev는 "API 개발부터 AI 통합까지, 모든 것을 하나로 제공"하는 Kubernetes 기반 API Gateway 플랫폼입니다. CloudFunction을 개발하고 dev/staging/prod 3개 환경에 배포하는 워크플로우를 지원합니다.
초기 구현에서는 함수 이름을 dev/hello, staging/hello, prod/hello처럼 환경 prefix를 포함하여 저장하고, Frontend에서도 이 정보를 그대로 사용했습니다.
우리가 마주한 문제:
- ❓ Frontend가
dev/hello를 표시하고 있는데, 왜 환경 정보를 알아야 할까? - ❓ 함수 목록 조회 시
dev/hello,staging/hello,prod/hello3개를 모두 조회해야 할까? - ❓ URL을
https://gateway.dev.api.imprun.dev/dev/hello처럼 중복으로 표기해야 할까?
검증 과정:
현상 유지 (Environment 정보 Frontend 노출)
- ✅ 구현 간단
- ❌ Frontend 코드 복잡 (
getDisplayName()함수 9개 파일에 중복) - ❌ 네트워크 낭비 (3개 환경 모두 조회)
- ❌ URL 중복 (
/dev/hello)
Frontend에서 환경 필터링
- ✅ Backend 수정 불필요
- ❌ Frontend 책임 과중
- ❌ 네트워크 낭비 지속
Environment-Agnostic Architecture ← 최종 선택
- ✅ Frontend는 환경을 모름 (baseName만 사용)
- ✅ 환경 정보는 Domain/Subdomain으로 처리
- ✅ 네트워크 비용 66% 감소 (dev 환경만 조회)
- ✅ 코드 간결화 및 유지보수성 향상
결론:
- ✅ Frontend 9개 파일에서
getDisplayName()제거 - ✅ API 응답 크기 감소 (환경 prefix 제거)
- ✅ 인프라 레이어에서 환경 자동 처리
이 글은 imprun.dev 플랫폼 구축 경험을 바탕으로, Frontend와 Backend의 환경 분리 패턴을 상세히 공유합니다.
Environment-Agnostic Pattern이란?
핵심 원칙
"Frontend는 환경(dev/staging/prod)을 알 필요가 없다"
graph TB
subgraph "Before: Environment-Aware"
FE1[Frontend]
API1[API Server]
DB1["MongoDB
dev/hello
staging/hello
prod/hello"]
FE1 -->|"GET /functions"| API1
API1 -->|"3개 조회"| DB1
DB1 -->|"dev/hello
staging/hello
prod/hello"| API1
API1 -->|"3개 반환"| FE1
FE1 -->|"getDisplayName()로 변환"| FE1
end
style FE1 stroke:#dc2626,stroke-width:3px
style DB1 stroke:#dc2626,stroke-width:2px
graph TB
subgraph "After: Environment-Agnostic"
FE2[Frontend]
API2[API Server]
DB2["MongoDB
dev/hello
staging/hello
prod/hello"]
Infra["Infrastructure
Subdomain Routing"]
FE2 -->|"GET /functions"| API2
API2 -->|"dev만 조회"| DB2
DB2 -->|"dev/hello"| API2
API2 -->|"extractBaseName()
→ hello"| FE2
FE2 -->|"https://gateway.dev.api.imprun.dev/hello"| Infra
Infra -->|"dev/ prefix 자동 추가"| Runtime["Runtime
dev/hello 실행"]
end
style FE2 stroke:#16a34a,stroke-width:3px
style API2 stroke:#16a34a,stroke-width:3px
style Infra stroke:#2563eb,stroke-width:3px
아키텍처 계층 분리
| 계층 | 책임 | 환경 인식 |
|---|---|---|
| Frontend | UI/UX, 사용자 입력 처리 | ❌ 환경 몰라도 됨 |
| API Server | 비즈니스 로직, 환경 prefix 자동 처리 | ✅ 환경 알고 있음 |
| Infrastructure | Subdomain 기반 라우팅 (*.dev.api.imprun.dev) |
✅ 환경 처리 |
| Runtime | Function 실행 | ✅ 환경별 실행 |
MongoDB 구조: 오해하기 쉬운 핵심
⚠️ 중요: Environment별 독립 Function 존재
흔한 오해: "Environment-Agnostic이니까 MongoDB에 baseName만 저장하겠지?"
// ❌ 잘못된 이해
{
name: "hello" // baseName만 저장?
}
실제 구조: baseName "hello"에 대해 환경별로 독립 CloudFunction 문서가 존재합니다.
// ✅ 실제 MongoDB 구조
// baseName "hello" → 최소 1개, 최대 3개 Function
{
_id: ObjectId("..."),
name: "dev/hello", // dev 환경
source: { version: 1, files: {...} }
}
{
_id: ObjectId("..."),
name: "staging/hello", // staging 환경 (Promote 후 생성)
source: { version: 1, files: {...} }
}
{
_id: ObjectId("..."),
name: "prod/hello", // prod 환경 (Promote 후 생성)
source: { version: 1, files: {...} }
}
핵심 차이점
| 항목 | Before | After (Environment-Agnostic) |
|---|---|---|
| MongoDB 구조 | dev/hello, staging/hello, prod/hello |
동일 (환경별 독립 Function) |
| Function 목록 조회 | 3개 모두 조회 | dev만 조회 (66% 절감) |
| API 응답 | dev/hello, staging/hello, prod/hello |
baseName으로 변환 (hello) |
| Frontend 표시 | getDisplayName() 필요 |
baseName 그대로 표시 |
| URL | /dev/hello (중복) |
/hello (Subdomain으로 환경 구분) |
왜 dev만 조회?
- Frontend는 개발 중인 최신 버전만 보면 됨 (dev 환경)
- staging/prod는 이미 배포된 안정 버전 (Frontend 목록에 표시 불필요)
- 네트워크 비용 66% 감소 (3개 → 1개)
환경별 조회가 필요한 경우:
- History 조회:
GET /functions/hello/history?environment=staging - 각 환경은 독립 functionId를 가지므로 environment 지정 필수
구현 세부사항
1. Backend: baseName 추출 및 반환
유틸리티 함수 작성
// server/src/utils/getter.ts
/**
* Extract baseName from CloudFunction fullName (environment-agnostic)
* @param fullName - Function name with environment prefix
* @returns baseName without environment prefix
* @example
* extractBaseName("dev/hello") // "hello"
* extractBaseName("staging/user/me") // "user/me"
* extractBaseName("hello") // "hello" (no prefix)
*/
export function extractBaseName(fullName: string): string {
if (!fullName) return fullName
return fullName.replace(/^(dev|staging|prod)\//, '')
}
Function CRUD: dev 환경만 조회
// server/src/function/function.service.ts
async findAll(gatewayId: string) {
// Only query dev/* functions to avoid network waste
// Frontend is environment-agnostic and only needs baseName
const res = await this.db
.collection<CloudFunction>('CloudFunction')
.find({
gatewayId,
name: { $regex: /^dev\// } // ✅ dev 환경만 조회
})
.toArray()
return res
}
Before (3개 환경 조회):
// ❌ 네트워크 낭비
.find({ gatewayId }) // dev/hello, staging/hello, prod/hello 모두 조회
After (dev만 조회):
// ✅ 66% 네트워크 절감
.find({ gatewayId, name: { $regex: /^dev\// } }) // dev/hello만 조회
Controller: baseName 변환
// server/src/function/function.controller.ts
@Get()
async findAll(@Param('gatewayId') gatewayId: string) {
const data = await this.functionsService.findAll(gatewayId)
// Transform all function names to baseName (environment-agnostic)
const transformed = data.map(func => ({
...func,
name: extractBaseName(func.name) // dev/hello → hello
}))
return ResponseUtil.ok(transformed)
}
Function CRUD: baseName 입력 자동 처리
// server/src/function/function.controller.ts
@Post()
async create(
@Param('gatewayId') gatewayId: string,
@Body() dto: CreateFunctionDto,
) {
// Auto-add dev/ prefix if not already present (support baseName input)
const fullName = dto.name.includes('/') ? dto.name : `dev/${dto.name}`
const res = await this.functionsService.create(gatewayId, fullName, dto)
// Return baseName to Frontend
return ResponseUtil.ok({ ...res, name: extractBaseName(res.name) })
}
Frontend에서 "hello" 입력 → Backend가 "dev/hello"로 자동 변환
GET by baseName: 환경 자동 추가
// server/src/function/function.controller.ts
@Get(':baseName')
async findOne(
@Param('gatewayId') gatewayId: string,
@Param('baseName') baseName: string,
) {
// Auto-add dev/ prefix for baseName lookup
const fullName = baseName.includes('/') ? baseName : `dev/${baseName}`
const func = await this.functionsService.findOne(gatewayId, fullName)
if (!func) {
throw new NotFoundException('Function not found')
}
// Return baseName to Frontend
return ResponseUtil.ok({ ...func, name: extractBaseName(func.name) })
}
2. Frontend: 환경 정보 제거
Before: getDisplayName() 중복
// ❌ 9개 파일에 중복
function getDisplayName(fullName: string): string {
return fullName.replace(/^(dev|staging|prod)\//, '')
}
// FunctionCard.tsx
<CardTitle>{getDisplayName(func.name)}</CardTitle>
// FunctionEditor.tsx
<h1>{getDisplayName(func.name)}</h1>
// ... 7개 파일 더
After: baseName 직접 사용
// ✅ 변환 불필요
<CardTitle>{func.name}</CardTitle> // 서버가 이미 baseName 반환
Environment URL 생성 (Subdomain 기반)
// frontend/src/components/editor/hooks/useEnvironmentUrls.ts
export function useEnvironmentUrls({
gateway,
gatewayId,
functionName, // baseName (예: "hello")
}: UseEnvironmentUrlsOptions): EnvironmentUrl[] {
return useMemo(() => {
// Environment is determined by subdomain, NOT by URL path
const devDomain = gateway?.domain?.devDomain || `${gatewayId}.dev.api.imprun.dev`;
const stagingDomain = gateway?.domain?.stagingDomain || `${gatewayId}.staging.api.imprun.dev`;
const prodDomain = gateway?.domain?.prodDomain || `${gatewayId}.prod.api.imprun.dev`;
return [
{
name: "dev",
label: "개발",
url: `https://${devDomain}/${functionName}`, // /hello (NOT /dev/hello)
},
{
name: "staging",
label: "스테이징",
url: `https://${stagingDomain}/${functionName}`,
},
{
name: "production",
label: "운영",
url: `https://${prodDomain}/${functionName}`,
},
];
}, [gateway, gatewayId, functionName]);
}
환경은 Subdomain으로 구분, URL path에는 baseName만 사용
3. Infrastructure: Subdomain 기반 라우팅
APISIX Ingress 설정
# k8s/templates/apisix-routes/gateway-routes.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: "{{ $gatewayId }}-dev-route"
spec:
http:
- name: dev-environment
match:
hosts:
- "{{ $gatewayId }}.dev.api.imprun.dev" # Subdomain으로 환경 구분
paths:
- "/*" # baseName만 허용 (예: /hello)
backends:
- serviceName: "{{ $gatewayId }}-runtime"
servicePort: 8080
plugins:
- name: proxy-rewrite
enable: true
config:
regex_uri:
- "^/(.*)"
- "/dev/$1" # ✅ dev/ prefix 자동 추가
요청 흐름:
https://gateway.dev.api.imprun.dev/hello
↓ (APISIX Ingress)
Runtime: /dev/hello 실행API Directory: prod 필터링 최적화
문제
API Directory는 공개 API 카탈로그입니다. 모든 환경(dev/staging/prod)의 함수를 표시할 필요가 없습니다.
해결책
prod 환경에 배포된 안정적인 API만 표시
Backend 필터링
// server/src/function/function.service.ts
async getFunctionsDirectory(
search?: string,
page: number = 1,
pageSize: number = 20,
) {
const db = SystemDatabase.db
const matchStage: any = {}
// FILTER: Only show public functions in directory
matchStage.isPublic = true
// FILTER: Only show prod/* functions (stable, production-ready APIs)
matchStage.name = { $regex: /^prod\// }
if (search) {
matchStage.$text = { $search: search }
}
const result = await db
.collection<CloudFunction>('CloudFunction')
.aggregate([
{ $match: matchStage },
// ... pagination, lookup
])
.toArray()
// Transform to baseName
const list = functions.map((fn: any) => ({
_id: fn._id.toHexString(),
gatewayId: fn.gatewayId,
gatewayName: fn.gatewayName,
name: extractBaseName(fn.name), // prod/hello → hello
description: fn.desc,
tags: fn.tags,
docs,
createdAt: fn.createdAt,
updatedAt: fn.updatedAt,
}))
return { list, total, page, pageSize }
}
Frontend: Gateway 필터 제거
Before (Gateway 선택 드롭다운):
// ❌ 불필요한 필터
const [selectedGateway, setSelectedGateway] = useState<string>('all')
<Select value={selectedGateway} onValueChange={setSelectedGateway}>
<SelectItem value="all">전체 Gateway</SelectItem>
{gateways.map((gateway) => (
<SelectItem key={gateway.id} value={gateway.id}>
{gateway.name}
</SelectItem>
))}
</Select>
After (검색만 유지):
// ✅ 간결한 UI
const [searchQuery, setSearchQuery] = useState('')
<Input
placeholder="API 검색... (예: getTax, payment)"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
모든 Gateway의 공개 API를 한 번에 탐색
성능 최적화 결과
네트워크 비용 감소
| 항목 | Before | After | 개선율 |
|---|---|---|---|
| Function 목록 조회 | 3개 환경 (dev/staging/prod) | 1개 환경 (dev) | 66% 감소 |
| API Directory 조회 | 모든 환경 | prod만 | 66% 감소 |
| 응답 크기 | dev/hello (10 bytes) |
hello (5 bytes) |
50% 감소 |
코드 복잡도 감소
| 항목 | Before | After | 개선율 |
|---|---|---|---|
| getDisplayName() 함수 | 9개 파일에 중복 | 0개 (제거) | 100% 제거 |
| Frontend 환경 처리 로직 | 각 컴포넌트마다 처리 | 0개 (제거) | 100% 제거 |
| URL 생성 복잡도 | 환경 + baseName 결합 | baseName만 사용 | 간결화 |
마무리
핵심 요약
Environment-Agnostic Architecture는 Frontend가 환경을 몰라도 되도록 설계하는 패턴입니다.
핵심 원칙:
- Frontend는 baseName만 사용
- Backend는 환경 prefix 자동 처리
- Infrastructure는 Subdomain으로 환경 구분
- API 응답은 항상 baseName 반환
언제 사용하나?
Environment-Agnostic Pattern 권장:
- ✅ Multi-environment 배포 (dev/staging/prod)
- ✅ Frontend 코드 간결화 필요
- ✅ 네트워크 비용 최적화 필요
- ✅ 환경 변경이 잦은 프로젝트
Environment-Aware Pattern 권장:
- ✅ 환경별로 완전히 다른 로직 필요
- ✅ 단일 환경만 존재
- ✅ Frontend에서 환경별 UI 차이 필요
실제 적용 결과
imprun.dev 환경:
- ✅ 네트워크 비용 66% 감소 (3개 → 1개 환경 조회)
- ✅ Frontend 9개 파일에서
getDisplayName()제거 - ✅ API 응답 크기 50% 감소 (환경 prefix 제거)
- ✅ Subdomain 기반 라우팅으로 인프라 단순화
운영 경험:
- 적용 시간: 2시간 (Backend + Frontend + Infrastructure)
- 배포 영향: 없음 (Backward Compatible)
- 유지보수성: 매우 높음 😊
참고 자료
관련 글
- imprun의 진화: Serverless에서 API Gateway Platform으로
- APISIX Ingress Controller 2.0: CRD 선택 가이드
- State Machine 패턴으로 Kubernetes 리소스 생명주기 관리하기
태그: Architecture, EnvironmentAgnostic, APIDesign, Frontend, Backend, Kubernetes, imprun
"Frontend는 환경을 몰라야 한다. 환경은 인프라의 책임이다."
🤖 이 블로그는 실제 프로덕션 환경에서 Environment-Agnostic Architecture를 운영한 경험을 바탕으로 작성되었습니다.
질문이나 피드백은 블로그 댓글에 남겨주세요!
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| NestJS + React 표준 응답과 JWT 인증 완벽 가이드: ResponseUtil, Axios, Zustand (0) | 2025.11.06 |
|---|---|
| Environment-Agnostic Architecture 구현기: "baseName만 저장한다"는 착각에서 벗어나기 (0) | 2025.11.03 |
| CLAUDE.md 최적화 여정: AI가 패턴을 무시하는 이유와 해결책 (0) | 2025.11.03 |
| MongoDB Aggregation Pipeline로 N+1 문제 해결하기: $lookup과 $facet 활용 (0) | 2025.11.03 |
| MongoDB 인덱스 생성 베스트 프랙티스: 수동 vs 자동, 그리고 Hybrid 접근 (0) | 2025.11.02 |
- Total
- Today
- Yesterday
- Claude
- authentication
- frontend
- troubleshooting
- security
- LLM
- Ontology
- knowledge graph
- LangChain
- claude code
- authorization
- SHACL
- api gateway
- Tailwind CSS
- ai 개발 도구
- react
- Go
- AI agent
- Tax Analysis
- workflow
- Kubernetes
- AI Development
- Developer Tools
- Rag
- AI
- PYTHON
- backend
- Next.js
- 개발 도구
- architecture
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |