12 KiB
12 KiB
모듈 프론트엔드 에셋 시스템
이 문서는 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 로드 (순차)
└─ 각 모듈의 initModule() 실행
└─ ActionDispatcher.registerHandler()
3. 레이아웃 렌더링
└─ 모듈 핸들러 사용 가능
에셋 매니페스트 스키마
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) |
관련 문서
- module-basics.md - 모듈 개발 기초
- hooks.md - 훅 시스템 (핸들러와 연계)
- module-commands.md - 모듈 Artisan 커맨드
- ../frontend/components.md - 컴포넌트 핸들러 호출