Files
Gnuboard7/docs/extension/module-assets.md
T
2026-04-20 20:37:49 +09:00

13 KiB
Raw Permalink Blame History

모듈 프론트엔드 에셋 시스템

이 문서는 G7의 모듈 프론트엔드 에셋 로딩 시스템을 다룹니다.


TL;DR (5초 요약)

1. module.json에 에셋 매니페스트 정의 (js, css, loading strategy)
2. Vite IIFE 빌드로 dist/에 번들 생성
3. TemplateApp 초기화 시 자동 로드 (global 전략)
4. 핸들러 네이밍: {module-identifier}.{handler-name}
5. 빌드 명령: php artisan module:build [identifier] (기본: _bundled, --active로 활성)

목차


개요

모듈/플러그인은 자체 프론트엔드 에셋(JS, CSS, 이미지, 폰트 등)을 포함할 수 있습니다. 이 시스템은 활성화된 모듈의 에셋을 동적으로 로드하여 ActionDispatcher에 핸들러를 등록합니다.

작동 원리

1. admin.blade.php 렌더링
   └─ window.G7Config.moduleAssets 주입

2. TemplateApp.init()
   └─ ModuleAssetLoader.loadActiveExtensionAssets()
       ├─ CSS 로드 (병렬)
       └─ JS 로드 (병렬 fetch + 순차 실행)
           ├─ priority 오름차순 정렬 후 DOM append
           ├─ script.async = false → 삽입 순서대로 실행 보장 (HTML 사양)
           └─ 각 모듈의 initModule() 실행
               └─ ActionDispatcher.registerHandler()

3. 레이아웃 렌더링
   └─ 모듈 핸들러 사용 가능

성능 참고: JS 번들은 Promise.all 로 병렬 fetch 되며, script.async = false 와 priority 정렬된 DOM append 순서로 실행 순서는 유지됩니다. N 개의 확장 IIFE 로딩이 N × (fetch 시간) 에서 max(fetch 시간) 으로 단축됩니다. 단, 확장 간 런타임 의존성 (다른 확장의 window 전역/핸들러 참조) 이 발생하면 priority 필드로 실행 순서를 명시해야 합니다.


에셋 매니페스트 스키마

module.json (통합 스키마)

모듈 루트에 module.json 파일을 생성합니다. 메타데이터와 에셋 설정이 하나의 파일에 통합되어 있습니다.

{
    "identifier": "sirsoft-ecommerce",
    "vendor": "sirsoft",
    "name": {
        "ko": "이커머스",
        "en": "Ecommerce"
    },
    "version": "1.0.0",
    "description": {
        "ko": "상품 및 주문 관리를 위한 이커머스 모듈",
        "en": "E-commerce module for product and order management"
    },
    "g7_version": ">=1.0.0",
    "dependencies": {
        "modules": {},
        "plugins": {}
    },
    "github_url": null,
    "github_changelog_url": null,
    "assets": {
        "js": {
            "entry": "resources/js/index.ts",
            "output": "dist/js/module.iife.js"
        },
        "css": {
            "entry": "resources/css/main.css",
            "output": "dist/css/module.css"
        },
        "handlers": true,
        "static": "resources/assets/"
    },
    "loading": {
        "strategy": "global",
        "priority": 100
    }
}

참고: identifier, vendor는 디렉토리명에서 자동 추론되므로 생략 가능합니다. name, version, description은 AbstractModule에서 자동 파싱됩니다.

스키마 설명

메타데이터 필드

필드 타입 필수 설명
identifier string 선택 모듈 식별자 (디렉토리명에서 자동 추론)
vendor string 선택 벤더명 (identifier에서 자동 추론)
name string|object 필수 모듈명 (다국어: {"ko": "...", "en": "..."})
version string 필수 시맨틱 버전 (예: 1.0.0)
description string|object 필수 모듈 설명 (다국어 지원)
g7_version string 선택 그누보드7 코어 버전 제약 (예: >=1.0.0)
dependencies object 선택 모듈/플러그인 의존성
github_url string|null 선택 GitHub 저장소 URL (업데이트 감지용)
github_changelog_url string|null 선택 GitHub 변경 이력 URL

에셋 필드

필드 설명
assets.js.entry JS 소스 엔트리 포인트
assets.js.output 빌드된 JS 출력 경로
assets.css.entry CSS 소스 엔트리 포인트
assets.css.output 빌드된 CSS 출력 경로
assets.handlers 핸들러 포함 여부
assets.static 정적 에셋 소스 디렉토리
loading.strategy 로딩 전략 (global, layout, lazy)
loading.priority 로드 우선순위 (낮을수록 먼저)

모듈 프론트엔드 구조

디렉토리 구조

modules/_bundled/sirsoft-ecommerce/
├── module.json              ← 에셋 매니페스트
├── package.json             ← npm 패키지 정의
├── vite.config.ts           ← Vite 빌드 설정
├── tsconfig.json            ← TypeScript 설정
├── dist/                    ← 빌드 출력 (git ignore)
│   ├── js/module.iife.js
│   ├── css/module.css
│   └── assets/
│       ├── fonts/
│       └── images/
├── resources/
│   ├── js/                  ← JS 소스
│   │   ├── index.ts
│   │   ├── types.ts
│   │   └── handlers/
│   │       ├── index.ts
│   │       └── updateProductField.ts
│   ├── css/main.css         ← CSS 소스
│   └── assets/              ← 정적 에셋 소스
│       ├── fonts/
│       └── images/
└── ...

package.json

{
    "name": "sirsoft-ecommerce",
    "version": "1.0.0",
    "private": true,
    "type": "module",
    "scripts": {
        "dev": "vite build --watch",
        "build": "vite build"
    },
    "devDependencies": {
        "typescript": "^5.0.0",
        "vite": "^6.0.0"
    }
}

vite.config.ts

import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
    build: {
        lib: {
            entry: path.resolve(__dirname, 'resources/js/index.ts'),
            name: 'SirsoftEcommerce',
            fileName: 'module',
            formats: ['iife'],
        },
        outDir: 'dist',
        rollupOptions: {
            output: {
                entryFileNames: 'js/[name].iife.js',
                assetFileNames: (assetInfo) => {
                    if (assetInfo.name?.endsWith('.css')) {
                        return 'css/[name][extname]';
                    }
                    return 'assets/[name][extname]';
                },
            },
        },
        emptyOutDir: true,
        minify: true,
        sourcemap: true,
    },
    resolve: {
        alias: {
            '@': path.resolve(__dirname, 'resources/js'),
        },
    },
});

핸들러 등록

모듈 엔트리 파일 (index.ts)

import '../css/main.css';
import { handlerMap } from './handlers';

const MODULE_IDENTIFIER = 'sirsoft-ecommerce';

export function initModule(): void {
    const registerHandlers = () => {
        const actionDispatcher = (window as any).G7Core?.getActionDispatcher?.();

        if (actionDispatcher) {
            Object.entries(handlerMap).forEach(([name, handler]) => {
                const fullName = `${MODULE_IDENTIFIER}.${name}`;
                actionDispatcher.registerHandler(fullName, handler);
            });
            console.log(`[Module:${MODULE_IDENTIFIER}] Handlers registered`);
        } else {
            // ActionDispatcher 초기화 대기
            setTimeout(registerHandlers, 100);
        }
    };

    if (document.readyState === 'complete') {
        registerHandlers();
    } else {
        window.addEventListener('load', registerHandlers);
    }
}

// IIFE 빌드 시 즉시 실행
initModule();

핸들러 정의

// handlers/updateProductField.ts
import type { ActionContext } from '@/types';

interface UpdateProductFieldParams {
    productId: string | number;
    field: string;
    value: string | number | boolean;
    stateKey?: string;
}

export function updateProductFieldHandler(
    params: UpdateProductFieldParams,
    context: ActionContext
): void {
    const { productId, field, value, stateKey = 'products' } = params;

    // setState를 통해 상태 업데이트
    if (context.setState) {
        context.setState({
            [stateKey]: {
                _modified: {
                    [productId]: { [field]: value }
                }
            }
        });
    }
}

// handlers/index.ts
import { updateProductFieldHandler } from './updateProductField';
import { updateOptionFieldHandler } from './updateOptionField';
import type { ActionHandler } from '@/types';

export const handlerMap: Record<string, ActionHandler> = {
    updateProductField: updateProductFieldHandler,
    updateOptionField: updateOptionFieldHandler,
};

레이아웃에서 핸들러 사용

{
    "component": "Input",
    "props": {
        "type": "number",
        "value": "{{row.stock_quantity}}"
    },
    "events": {
        "onBlur": {
            "handler": "sirsoft-ecommerce.updateProductField",
            "params": {
                "productId": "{{row.id}}",
                "field": "stock_quantity",
                "value": "{{$event.target.value}}"
            }
        }
    }
}

빌드 및 배포

Artisan 커맨드

# _bundled에서 빌드 (기본값)
php artisan module:build sirsoft-ecommerce

# 모든 _bundled 모듈 빌드
php artisan module:build --all

# 프로덕션 빌드 (_bundled)
php artisan module:build sirsoft-ecommerce --production

# 파일 감시 모드 (활성 디렉토리에서 자동 실행)
php artisan module:build sirsoft-ecommerce --watch

# 활성 디렉토리에서 빌드
php artisan module:build sirsoft-ecommerce --active

# 빌드 후 활성 디렉토리 반영
php artisan module:update sirsoft-ecommerce

npm 스크립트

# 모듈 디렉토리에서 직접 실행
cd modules/_bundled/sirsoft-ecommerce
npm install
npm run build

# 개발 모드 (파일 감시)
npm run dev

에셋 서빙 API

빌드된 에셋은 다음 API를 통해 서빙됩니다:

GET /api/modules/assets/{identifier}/{path}

예시:
/api/modules/assets/sirsoft-ecommerce/dist/js/module.iife.js
/api/modules/assets/sirsoft-ecommerce/dist/css/module.css

에셋 로딩 전략

global (기본값)

앱 초기화 시 자동으로 로드됩니다.

{
    "loading": {
        "strategy": "global",
        "priority": 100
    }
}
  • TemplateApp.init()에서 자동 로드
  • 모든 페이지에서 핸들러 사용 가능
  • 우선순위(priority)가 낮을수록 먼저 로드

layout (향후 지원)

특정 레이아웃에서만 로드됩니다.

{
    "loading": {
        "strategy": "layout"
    }
}
  • 레이아웃의 scripts 섹션에서 명시적 로드
  • 해당 레이아웃 진입 시 로드

lazy (향후 지원)

필요할 때 동적으로 로드됩니다.

{
    "loading": {
        "strategy": "lazy"
    }
}
  • 핸들러 호출 시점에 로드
  • 초기 로딩 시간 최적화

외부 라이브러리

외부 CDN 스크립트를 조건부로 로드할 수 있습니다.

module.json 설정

{
    "assets": {
        "external": [
            {
                "src": "https://cdn.example.com/chart.js",
                "id": "chartjs-cdn",
                "if": "{{_global.settings.useCharts}}"
            }
        ]
    }
}

레이아웃 scripts 섹션

{
    "scripts": [
        {
            "src": "https://cdn.example.com/lib.js",
            "id": "external-lib",
            "if": "{{_global.modules['sirsoft-ecommerce'].useExternalLib}}",
            "async": true
        }
    ]
}

AbstractModule 에셋 메서드

AbstractModule은 에셋 관련 헬퍼 메서드를 제공합니다:

메서드 설명
getAssets() module.json의 assets 섹션 반환
getAssetLoadingConfig() loading 설정 반환 (strategy, priority)
hasAssets() 에셋 정의 존재 여부
getBuiltAssetPaths() 빌드된 에셋 경로 반환 (js, css)

관련 문서