docs.membloc.com

membloc docs

제품, 플랫폼, 운영 문서를 한곳에서 읽는 Membloc 공식 문서 포털입니다.

Phase 2: WebView Runtime — 외부 모듈 실행 환경

외부 사업체가 웹앱으로 개발한 모듈을 homb 앱 내 WebView에서 안전하게 실행할 수 있는 런타임 환경을 구축한다.


1. 목표

  • WebView 모듈 실행 컨테이너 (Flutter InAppWebView)
  • JS Bridge SDK (homb.js) — 모듈 ↔ homb 앱 간 통신
  • Module Runtime API (membloc-app-engine) — 모듈 데이터 CRUD
  • 권한 동의 플로우 UI
  • 데이터 샌드박싱 (모듈별 격리)
  • 샘플 WebView 모듈 1개 개발 (dogfooding)

2. 아키텍처

┌──────────────────────────────────────────────────┐
│ membloc-app (Flutter)                                │
│                                                   │
│  ┌─────────────────────────────────────────────┐  │
│  │ ModuleWebViewScreen                          │  │
│  │  ┌───────────────────────────────────────┐   │  │
│  │  │ InAppWebView                          │   │  │
│  │  │                                       │   │  │
│  │  │  3rd-party Web App                    │   │  │
│  │  │  ┌─────────────────────────────────┐  │   │  │
│  │  │  │ <script src="membloc-sdk.js">      │  │   │  │
│  │  │  │                                 │  │   │  │
│  │  │  │ const user = await homb.auth    │  │   │  │
│  │  │  │   .getUserInfo();               │  │   │  │
│  │  │  │ const items = await homb.data   │  │   │  │
│  │  │  │   .list("expenses");            │  │   │  │
│  │  │  └─────────────────────────────────┘  │   │  │
│  │  └──────────────┬────────────────────────┘   │  │
│  │                 │ postMessage / handler       │  │
│  │  ┌──────────────▼────────────────────────┐   │  │
│  │  │ JsBridgeController                    │   │  │
│  │  │  - Permission checker                 │   │  │
│  │  │  - Request router                     │   │  │
│  │  │  - Response serializer                │   │  │
│  │  └──────────────┬────────────────────────┘   │  │
│  └─────────────────┼───────────────────────────┘  │
│                    │                              │
│  ┌─────────────────▼───────────────────────────┐  │
│  │ ModuleRuntimeService                         │  │
│  │  - auth: scoped token 발급                   │  │
│  │  - family: 가족 정보 (읽기 전용)              │  │
│  │  - data: module_data CRUD (REST)             │  │
│  │  - storage: module storage upload/download   │  │
│  │  - activity: 피드 이벤트 발행                │  │
│  │  - notifications: 푸시 알림 전송             │  │
│  └──────────────────┬──────────────────────────┘  │
│                     │ HTTP                        │
└─────────────────────┼────────────────────────────┘
                      ▼
         ┌────────────────────────┐
         │ membloc-app-engine        │
         │ /api/runtime/*         │
         │                        │
         │ - module_data CRUD     │
         │ - activity write       │
         │ - notification relay   │
         │ - permission enforce   │
         └────────────────────────┘

3. JS Bridge SDK (membloc-sdk.js)

외부 모듈 개발자가 <script> 로 포함하는 클라이언트 SDK.

3.1 통신 프로토콜

모듈 (JS) → postMessage → Flutter (Dart handler)
                          → 권한 체크
                          → API 호출 or 로컬 데이터
                          → 결과
Flutter (Dart) → evaluateJavascript → 모듈 (JS callback)

메시지 포맷:

// 요청
interface BridgeRequest {
  id: string;           // UUID, 응답 매칭용
  method: string;       // "auth.getUserInfo", "data.list" 등
  params: Record<string, any>;
}

// 응답
interface BridgeResponse {
  id: string;           // 요청 id와 매칭
  success: boolean;
  data?: any;
  error?: { code: string; message: string };
}

3.2 SDK API 상세

// membloc-sdk.js

class HombSDK {
  // === 인증 ===
  auth: {
    // 현재 사용자 정보 (permission: 없음 — 항상 허용)
    getUserInfo(): Promise<{
      uid: string;
      displayName: string;
      photoUrl: string | null;
    }>;

    // 모듈용 scoped access token (백엔드 API 호출용)
    getAccessToken(): Promise<string>;
  };

  // === 가족 정보 ===
  // permission: family.read
  family: {
    getCurrent(): Promise<{
      id: string;
      name: string;
      memberCount: number;
    }>;

    // permission: members.read
    getMembers(): Promise<Array<{
      uid: string;
      displayName: string;
      photoUrl: string | null;
      role: string;
    }>>;
  };

  // === 모듈 데이터 (샌드박스) ===
  // permission: 없음 — 자기 모듈 데이터는 항상 접근 가능
  data: {
    get(key: string): Promise<any>;
    set(key: string, value: any): Promise<void>;
    delete(key: string): Promise<void>;
    list(prefix?: string, options?: {
      limit?: number;
      offset?: number;
      orderBy?: string;
    }): Promise<{ items: DataEntry[]; total: number }>;
  };

  // === 파일 스토리지 ===
  // permission: storage.write
  storage: {
    upload(file: File | Blob, path: string): Promise<{
      url: string;
      path: string;
      size: number;
    }>;
    getUrl(path: string): Promise<string>;
    delete(path: string): Promise<void>;
    list(prefix?: string): Promise<StorageItem[]>;
  };

  // === 알림 ===
  // permission: notifications.send
  notifications: {
    send(options: {
      title: string;
      body: string;
      targetUids?: string[];  // 비면 전체 가족
      data?: Record<string, string>;
    }): Promise<void>;
  };

  // === 액티비티 피드 ===
  // permission: activity.write
  activity: {
    post(event: {
      eventType: string;
      title: string;
      summary: string;
      entityRef?: { entityType: string; entityId: string };
      payload?: Record<string, any>;
    }): Promise<void>;
  };

  // === UI 연동 ===
  // permission: 없음
  ui: {
    close(): void;
    setTitle(title: string): void;
    showToast(message: string, type?: 'info' | 'success' | 'error'): void;
    showConfirm(options: {
      title: string;
      message: string;
      confirmText?: string;
      cancelText?: string;
    }): Promise<boolean>;
    // permission: channel.share
    shareToChannel(data: {
      text?: string;
      imageUrl?: string;
      linkUrl?: string;
      linkTitle?: string;
    }): Promise<void>;
  };

  // === 설정 ===
  // permission: 없음 — 자기 모듈 설정만
  settings: {
    get(): Promise<Record<string, any>>;
    update(partial: Record<string, any>): Promise<void>;
  };

  // === 이벤트 리스너 ===
  on(event: 'resume' | 'pause' | 'themeChange', callback: Function): void;
  off(event: string, callback: Function): void;
}

// 글로벌에 자동 노출
declare const membloc: MemblocSDK;
declare const homb: MemblocSDK; // legacy alias

3.3 SDK 배포

Primary artifact: `membloc-sdk.js`
Legacy alias: `membloc-sdk.js`

npm primary: `@membloc/module-sdk`
npm legacy alias: `@homb/module-sdk`

4. Flutter 구현

4.1 ModuleWebViewScreen

// features/modules/module_webview_screen.dart

class ModuleWebViewScreen extends StatefulWidget {
  final String moduleKey;
  final String entryUrl;
  final String familyId;
  final List<String> grantedPermissions;
}

핵심 동작:

  1. InAppWebView로 entryUrl 로드
  2. 페이지 로드 완료 시 bridge 스크립트 주입 (네이티브 측 핸들러)
  3. JS → Dart postMessage 수신 → JsBridgeController 라우팅
  4. 결과를 evaluateJavascript로 콜백

4.2 JsBridgeController

// core/modules/js_bridge_controller.dart

class JsBridgeController {
  final String moduleKey;
  final String familyId;
  final List<String> grantedPermissions;
  final ModuleRuntimeService runtimeService;

  // postMessage 수신 시 호출
  Future<BridgeResponse> handleRequest(BridgeRequest request) async {
    // 1. permission 체크
    final requiredPermission = _permissionFor(request.method);
    if (requiredPermission != null &&
        !grantedPermissions.contains(requiredPermission)) {
      return BridgeResponse.error('PERMISSION_DENIED', '...');
    }

    // 2. 메서드 라우팅
    return switch (request.method) {
      'auth.getUserInfo'   => _handleGetUserInfo(),
      'auth.getAccessToken'=> _handleGetAccessToken(),
      'family.getCurrent'  => _handleGetFamily(),
      'family.getMembers'  => _handleGetMembers(),
      'data.get'           => _handleDataGet(request.params),
      'data.set'           => _handleDataSet(request.params),
      'data.delete'        => _handleDataDelete(request.params),
      'data.list'          => _handleDataList(request.params),
      'ui.close'           => _handleClose(),
      'ui.setTitle'        => _handleSetTitle(request.params),
      'ui.showToast'       => _handleShowToast(request.params),
      'ui.shareToChannel'  => _handleShareToChannel(request.params),
      'activity.post'      => _handleActivityPost(request.params),
      'notifications.send' => _handleNotificationSend(request.params),
      _                    => BridgeResponse.error('UNKNOWN_METHOD', '...'),
    };
  }

  String? _permissionFor(String method) {
    return switch (method) {
      'family.getCurrent' => 'family.read',
      'family.getMembers' => 'members.read',
      'storage.upload'    => 'storage.write',
      'storage.delete'    => 'storage.write',
      'notifications.send'=> 'notifications.send',
      'activity.post'     => 'activity.write',
      'ui.shareToChannel' => 'channel.share',
      _ => null,  // auth, data, settings, ui.basic — 항상 허용
    };
  }
}

4.3 권한 동의 다이얼로그

// features/modules/permission_consent_dialog.dart

class PermissionConsentDialog extends StatelessWidget {
  final RemoteModuleDescriptor module;
  final List<String> requestedPermissions;
  final VoidCallback onAccept;
  final VoidCallback onDecline;
}

표시 형태:

┌─────────────────────────────────────┐
│  [아이콘] 가계부                     │
│  Example Corp                       │
│                                     │
│  이 모듈은 다음 권한을 요청합니다:    │
│                                     │
│  ✅ 가족 정보 읽기                   │
│  ✅ 가족 멤버 목록 읽기              │
│  ✅ 알림 전송                       │
│  ✅ 채널에 공유                      │
│                                     │
│  데이터는 이 모듈 내에서만           │
│  사용되며, 다른 모듈과 공유되지      │
│  않습니다.                          │
│                                     │
│  [거절]              [동의 및 설치]  │
└─────────────────────────────────────┘

권한 설명 매핑:

const permissionLabels = {
  'family.read':        '가족 정보 읽기',
  'members.read':       '가족 멤버 목록 읽기',
  'members.profile':    '멤버 프로필 상세 보기',
  'storage.read':       '파일 읽기',
  'storage.write':      '파일 업로드 및 삭제',
  'notifications.send': '알림 전송',
  'activity.write':     '활동 피드에 게시',
  'calendar.read':      '캘린더 일정 읽기',
  'calendar.write':     '캘린더 일정 추가/수정',
  'channel.share':      '채널에 콘텐츠 공유',
};

5. 백엔드 — Module Runtime API

5.1 새로운 파일 구조

internal/
├── handler/
│   └── runtime.go              # [신규] 모듈 런타임 API
├── model/
│   └── module_data.go          # [신규] 모듈 데이터 모델
├── repository/
│   └── module_data.go          # [신규] module_data CRUD
├── service/
│   └── module_runtime.go       # [신규] 런타임 비즈니스 로직
├── auth/
│   └── module_token.go         # [신규] 모듈용 scoped token 발급/검증

5.2 데이터베이스 마이그레이션

migrations/004_module_data.up.sql

-- 모듈 데이터 (sandboxed key-value store)
CREATE TABLE module_data (
    id          TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
    module_key  TEXT NOT NULL,
    family_id   TEXT NOT NULL,
    data_key    TEXT NOT NULL,
    data_value  JSONB NOT NULL DEFAULT '{}',
    created_by  TEXT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE(module_key, family_id, data_key)
);

CREATE INDEX idx_module_data_lookup ON module_data(module_key, family_id);
CREATE INDEX idx_module_data_key_prefix ON module_data(module_key, family_id, data_key text_pattern_ops);

-- 모듈 API 키 (Server Module용, Phase 3에서 본격 사용)
CREATE TABLE module_api_keys (
    id          TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
    module_key  TEXT NOT NULL,
    key_hash    TEXT NOT NULL,
    name        TEXT NOT NULL DEFAULT 'default',
    permissions TEXT[] NOT NULL DEFAULT '{}',
    last_used_at TIMESTAMPTZ,
    expires_at  TIMESTAMPTZ,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_api_keys_module ON module_api_keys(module_key);
CREATE INDEX idx_api_keys_hash ON module_api_keys(key_hash);

5.3 Runtime API 엔드포인트

# 인증: Bearer <module-scoped-token>
# module-scoped-token: Firebase UID + moduleKey + familyId + permissions 포함

# 모듈 데이터
GET    /api/runtime/:moduleKey/families/:fid/data
       ?prefix=expenses&limit=50&offset=0&orderBy=created_at
POST   /api/runtime/:moduleKey/families/:fid/data
       Body: { "key": "expense_001", "value": { ... } }
GET    /api/runtime/:moduleKey/families/:fid/data/:key
PUT    /api/runtime/:moduleKey/families/:fid/data/:key
       Body: { "value": { ... } }
DELETE /api/runtime/:moduleKey/families/:fid/data/:key

# 액티비티
POST   /api/runtime/:moduleKey/families/:fid/activity
       Body: { "eventType": "...", "title": "...", ... }

# 알림
POST   /api/runtime/:moduleKey/families/:fid/notifications
       Body: { "title": "...", "body": "...", "targetUids": [...] }

5.4 Module Scoped Token

// internal/auth/module_token.go

// Flutter 앱이 WebView 모듈에 발급하는 단기 토큰
// 모듈은 이 토큰으로 Runtime API를 호출

type ModuleTokenClaims struct {
    UID         string   `json:"uid"`
    ModuleKey   string   `json:"moduleKey"`
    FamilyID    string   `json:"familyId"`
    Permissions []string `json:"permissions"`
    jwt.RegisteredClaims
}

// 발급: Flutter → membloc-app-engine /api/auth/module-token
// 유효기간: 1시간
// 갱신: homb.auth.getAccessToken() 호출 시 Flutter가 재발급

5.5 Runtime 미들웨어

// 모든 /api/runtime/* 요청에 적용

func ModuleRuntimeMiddleware() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // 1. module-scoped-token 검증
            // 2. URL의 :moduleKey와 토큰의 moduleKey 일치 확인
            // 3. URL의 :fid와 토큰의 familyId 일치 확인
            // 4. 요청에 필요한 permission이 토큰에 포함되어 있는지 확인
            // → 통과하면 context에 claims 저장
        }
    }
}

6. 보안

6.1 WebView 격리

항목대책
URL 제한manifest의 entry_url 도메인만 로드 허용
JS Bridgewindow.membloc 객체를 우선 사용하고, window.homb는 legacy alias로 유지
쿠키/스토리지모듈별 독립 (WebView 인스턴스 분리)
CSP 헤더모듈 HTML에 Content-Security-Policy 권장
외부 요청모듈 자체 서버 + homb Runtime API만 허용

6.2 데이터 격리

module_data 테이블:
  - WHERE module_key = :moduleKey AND family_id = :familyId
  - 다른 모듈의 데이터 접근 불가 (토큰에 moduleKey 바인딩)

Storage:
  - /modules/{moduleKey}/families/{familyId}/ 경로로 격리
  - 다른 모듈 경로 접근 시 403

6.3 Rate Limiting

// 모듈별 API 호출 한도
var rateLimits = map[string]int{
    "free":     1000,   // 1000 calls/day
    "paid":     10000,  // 10000 calls/day
    "premium":  100000, // 100000 calls/day
}

// Redis 또는 인메모리 카운터로 관리
// 한도 초과 시 429 Too Many Requests

7. 샘플 WebView 모듈 — 가계부

dogfooding 목적으로 직접 개발. 실제 외부 모듈과 동일한 방식으로 구현.

7.1 스펙

{
  "module_key": "com.homb.budget-sample",
  "display_name": "가계부 (샘플)",
  "type": "webview",
  "entry_url": "https://modules.homb.app/budget-sample/",
  "permissions": ["family.read", "members.read", "notifications.send"],
  "supported_entity_types": ["transaction"]
}

7.2 기능

  • 지출/수입 기록 (homb.data API로 저장)
  • 가족 멤버별 지출 현황
  • 월별 요약 → 액티비티 피드에 자동 게시
  • 예산 초과 시 → 알림 전송

7.3 기술 스택

React (or Vue) + TypeScript
@membloc/module-sdk
Vite 빌드 → 정적 호스팅 (Cloudflare Pages 또는 homb CDN)

7.4 프로젝트 구조

homb-module-budget-sample/
├── src/
│   ├── App.tsx
│   ├── api/homb.ts          # SDK 래퍼
│   ├── pages/
│   │   ├── Dashboard.tsx     # 월별 요약
│   │   ├── AddTransaction.tsx
│   │   └── History.tsx
│   └── components/
├── public/
│   └── index.html
├── homb-module.json          # manifest
├── package.json
└── vite.config.ts

8. 작업 목록

8.1 JS Bridge SDK

#작업예상
S1membloc-sdk.js 코어 구현 (postMessage 프로토콜)1d
S2auth, family API 구현0.5d
S3data API 구현1d
S4storage API 구현1d
S5ui API 구현 (close, toast, confirm, share)0.5d
S6activity, notifications API 구현0.5d
S7TypeScript 타입 정의 + npm 패키지화0.5d
S8SDK 문서 (README + 예제 코드)0.5d

8.2 Flutter

#작업의존성예상
F1flutter_inappwebview 패키지 추가 + 기본 설정-0.5d
F2ModuleWebViewScreen — WebView 컨테이너F11d
F3JsBridgeController — postMessage 핸들러F2, S12d
F4Bridge 네이티브 주입 (homb-bridge.js inject)F30.5d
F5PermissionConsentDialog-0.5d
F6ModuleLauncher 확장 — webview 타입 분기F2, F50.5d
F7Module scoped token 요청 로직-0.5d
F8WebView ↔ 네이티브 UI 연동 (setTitle, close, toast)F30.5d
F9shareToChannel 브릿지 연동F30.5d

8.3 백엔드

#작업의존성예상
B1004_module_data.up.sql 마이그레이션-0.5d
B2model/module_data.go-0.5d
B3repository/module_data.go — CRUD + prefix 검색B11d
B4auth/module_token.go — scoped token 발급/검증-1d
B5service/module_runtime.go — 런타임 비즈니스 로직B3, B41d
B6handler/runtime.go — REST 엔드포인트B51d
B7Runtime 미들웨어 (토큰 검증 + 권한 체크)B40.5d
B8Rate limiting (인메모리 → 향후 Redis)-0.5d
B9액티비티 발행 → Firestore 동기화B50.5d
B10알림 발행 → Firebase Cloud Messaging 연동B51d
B11/api/auth/module-token 엔드포인트B40.5d

8.4 샘플 모듈

#작업의존성예상
M1프로젝트 scaffolding (React + Vite + TypeScript)S70.5d
M2Dashboard (월별 요약)M11d
M3AddTransaction (지출/수입 기록)M11d
M4History (내역 조회)M10.5d
M5알림 연동 (예산 초과)M10.5d
M6빌드 + 정적 호스팅 배포M10.5d
M7manifest 등록 + 마켓플레이스에 publishM60.5d

9. 완료 기준

  • WebView 모듈이 homb 앱 내에서 정상 로드/실행
  • JS Bridge를 통해 getUserInfo, getCurrent, data.* 동작
  • 모듈 데이터가 module_data 테이블에 격리 저장
  • 권한 동의 다이얼로그 표시 후 설치
  • 허가되지 않은 API 호출 시 PERMISSION_DENIED 반환
  • 샘플 가계부 모듈이 지출 기록 → 조회 → 알림 전체 플로우 동작
  • Rate limit 초과 시 429 응답
  • 다른 모듈의 데이터 접근 시도 시 차단 확인