공개 저장소 이슈 gnuboard/g7 (@bigmsg) 제보에서 출발한 작업이다. 브라우저가 화면을 그리려고 제3자 CDN 에 도달해야 하면, 그 도달 실패는 예외도 로그도 남기지 않고 화면 기능만 조용히 사라진다. 폐쇄망·방화벽·광고차단기에서 재현되는데 자체 서버 로그에는 흔적이 없어 운영자가 원인을 특정할 수 없다. 제보된 것은 편집기 하나였지만 같은 구조가 아이콘·글꼴·코드편집기·압축 라이브러리·설치 마법사·개발 대시보드에 똑같이 있었으므로, 번들 확장과 템플릿 전체를 자체 제공으로 옮겼다. 런타임에 외부로 나가는 것은 주소 검색 서비스 하나만 남았다. 자체 제공만으로는 부족하다 — 자기 서버에서 받는 파일도 실패할 수 있고, 종전에는 그 실패가 무음이었다. CSS 경로에 재시도 계층을 세우고(스크립트 경로와 동형), 서버가 HTML 에 직접 심는 externals 까지 실패를 붙잡아 안내 배너와 [다시 시도]로 표면화했다. 편집기·코드편집기는 확보 실패 시 평문 입력으로 내려앉되 저장 계약을 유지한다. 두 번째 축은 운영자가 CSS 를 덧붙일 자리가 없던 문제다(sir.kr 문의). 확장 디렉토리의 custom/ 을 운영자 소유로 정해, 확장 교체가 그 디렉토리만은 보존하게 했다. 출처에 의존하지 않는 서술자로 해석하므로 나중에 다른 출처가 붙어도 소비자는 바뀌지 않는다. 확장 자산과 같은 메커니즘으로 정적 게시되어 CSS 내부 상대 url 도 해석되고, 파일을 고치면 그 변경을 감지해 재게시까지 예약된다. FTP 접근이 없는 운영자에게는 그 자리도 없는 것과 같으므로 레이아웃 편집기에서 직접 넣고 고칠 수 있게 했다. 모듈·플러그인·템플릿이 한 엔드포인트를 공유한다 — 타입별로 나누면 같은 검증이 세 벌로 갈리고 그중 약한 하나가 우회로가 된다. 여기서 올린 스크립트는 그 레이아웃 한 장이 아니라 사이트 전 화면에서 실행되므로 레이아웃 편집과 분리된 전용 권한으로 연다. 운영자 CSS 가 화면을 조작 불능으로 만들면 그것을 고칠 화면에도 같은 CSS 가 실려 스스로 갇히므로, 서버가 목록을 비우는 탈출구(?custom=off)를 함께 뒀다. 동봉 자산은 재생성 경로에 버전 대조 가드를 붙였다. 선언과 다른 버전을 버전 디렉토리에 써 넣는 조용한 거짓말은 배포된 뒤에는 드러나지 않는다.
38 KiB
G7Core 전역 API 레퍼런스
버전: engine-v1.4.0+ 관련 문서: state-management.md | components.md | data-binding.md
TL;DR (5초 요약)
1. G7Core.state: get/set/subscribe 전역 상태 관리
2. G7Core.toast: success/error/info/warning 알림
3. G7Core.modal: open/close 모달 제어
4. G7Core.t(): 다국어 번역 함수
5. G7Core.dispatch(): 액션 프로그래밍 실행
분리된 문서
이 문서는 가독성을 위해 다음과 같이 분리되었습니다:
| 문서 | 내용 |
|---|---|
| g7core-api.md (현재) | 개요, 상태 관리, 토스트 알림, 모달 관리, 네비게이션, 스타일 헬퍼, 플러그인/모듈 설정, 확장 자산, 위지윅 편집기 |
| g7core-api-advanced.md | 다국어, 액션 실행, 컴포넌트 이벤트, 이벤트 생성 헬퍼, 렌더링 헬퍼, 인증/API, WebSocket, 반응형, React Hooks, 타입 정의 |
목차
- 개요
- 상태 관리 (G7Core.state)
- 토스트 알림 (G7Core.toast)
- 모달 관리 (G7Core.modal)
- 네비게이션 (G7Core.navigation)
- 스타일 헬퍼 (G7Core.style)
- 플러그인 설정 (G7Core.plugin)
- 모듈 설정 (G7Core.module)
- 확장 자산 (G7Core.asset, G7Core.assets)
- 위지윅 편집기 (G7Core.wysiwyg)
개요
G7Core는 템플릿 엔진이 전역으로 노출하는 API 네임스페이스입니다. 컴포넌트 코드(TypeScript/JavaScript)에서 템플릿 엔진의 기능에 접근할 때 사용합니다.
API 카테고리
| 카테고리 | API | 설명 | 문서 |
|---|---|---|---|
| 상태 관리 | G7Core.state |
전역 상태 조회/설정/구독 | 현재 문서 |
| UI 알림 | G7Core.toast |
토스트 알림 표시 | 현재 문서 |
| 모달 | G7Core.modal |
모달 열기/닫기 | 현재 문서 |
| 네비게이션 | G7Core.navigation |
페이지 전환 상태 | 현재 문서 |
| 스타일 | G7Core.style |
Tailwind 클래스 병합 | 현재 문서 |
| 플러그인 | G7Core.plugin |
플러그인 설정 조회 | 현재 문서 |
| 모듈 | G7Core.module |
모듈 설정 조회 | 현재 문서 |
| 다국어 | G7Core.locale, G7Core.t |
로케일 관리, 번역 | 고급 API |
| 액션 | G7Core.dispatch |
액션 실행 | 고급 API |
| 이벤트 | G7Core.componentEvent |
컴포넌트 간 이벤트 | 고급 API |
| 헬퍼 | G7Core.create*Event |
이벤트 객체 생성 | 고급 API |
| 렌더링 | G7Core.renderItemChildren |
반복 아이템 렌더링 | 고급 API |
| 인증 | G7Core.AuthManager |
인증 상태 관리 | 고급 API |
| API | G7Core.api |
API 클라이언트 | 고급 API |
| WebSocket | G7Core.websocket |
실시간 통신 | 고급 API |
| 확장 자산 URL | G7Core.asset |
템플릿/모듈/플러그인 자산 URL 생성, 재시도 로더 | 현재 문서 |
| 자산 실패 안내 | G7Core.assets |
자산 로드 실패 표면화 및 재시도 | 현재 문서 |
| 위지윅 | G7Core.wysiwyg |
레이아웃 편집기 | 현재 문서 |
| React Hooks | G7Core.useControllableState |
상태 패턴 훅 | 고급 API |
사용 컨텍스트
// G7Core는 window 객체에 노출됨
const G7Core = (window as any).G7Core;
// 또는 타입 안전하게 접근
const toast = (window as any).G7Core?.toast;
상태 관리 (G7Core.state)
전역 상태를 관리하는 API입니다.
상세 내용: state-management.md
API 목록
| 메서드 | 설명 | 반환값 |
|---|---|---|
get() |
현재 전역 상태 반환 | Record<string, any> |
set(updates, options?) |
상태 객체로 업데이트 (dot notation 지원, merge 옵션) | void |
getGlobal() |
get()의 별칭 (하위 호환성) |
Record<string, any> |
setGlobal(updates) |
set()의 별칭 (하위 호환성) |
void |
setLocal(updates, options?) |
컴포넌트 로컬 상태 업데이트 (dot notation, scope 옵션 지원) | void |
getLocal() |
컴포넌트 로컬 상태 조회 | Record<string, any> |
getIsolated(scopeId) |
격리된 스코프 상태 조회 (engine-v1.14.0+) | Record<string, any> | null |
setIsolated(scopeId, updates, options?) |
격리된 스코프 상태 업데이트 (engine-v1.14.0+, merge 옵션) | void |
update(updater) |
함수형 업데이트 | void |
subscribe(listener) |
상태 변경 구독 | 구독 해제 함수 |
getDataSource(id) |
데이터 소스 값 조회 | DataSourceValue | undefined |
setLocal / getLocal (engine-v1.12.0+)
커스텀 핸들러에서 컴포넌트의 로컬 상태(_local)를 직접 업데이트할 때 사용합니다.
시그니처:
setLocal(updates: Record<string, any>, options?: {
scope?: 'current' | 'parent' | 'root';
merge?: 'deep' | 'shallow' | 'replace'; // engine-v1.18.0+ (기본: 'deep')
debounce?: number; // engine-v1.41.0+ — 디바운스 지연 시간 (ms)
debounceKey?: string; // engine-v1.41.0+ — 디바운스 고유 키
render?: boolean; // engine-v1.42.0+ (기본: true) — false이면 React 리렌더 건너뜀
}): void
getLocal(): Record<string, any>
동작 원리:
- 액션 실행 중: 컴포넌트의
dynamicState를 직접 업데이트하여 즉시 UI에 반영 - 액션 외부: 전역
_local업데이트 (fallback)
render: false (engine-v1.42.0+): 값은 globalState._local에 저장하되 React 리렌더를 건너뜁니다.
외부 라이브러리(CKEditor 등)가 자체 DOM을 관리하는 경우, React 트리 리렌더가 불필요하므로 성능을 대폭 개선합니다.
flushPendingDebounceTimers 실행 시(저장 직전)에는 항상 render: true로 강제되어 데이터 정합성이 보장됩니다.
// 외부 라이브러리에서 자체 DOM을 관리하는 경우 — 리렌더 없이 값만 저장
G7Core.state.setLocal({
[`form.${name}.${locale}`]: html,
hasChanges: true,
}, {
debounce: 300,
debounceKey: `editor-sync-${name}`,
render: false,
});
dot notation 지원 (engine-v1.2.0+): 중첩된 객체 경로를 dot notation으로 표현할 수 있습니다.
// 커스텀 핸들러에서 사용
export function toggleOptionHandler(action: any, context: any): void {
const G7Core = (window as any).G7Core;
// 현재 로컬 상태 가져오기
const currentLocal = G7Core.state.getLocal();
// 로컬 상태 업데이트
G7Core.state.setLocal({
selectedItems: [...currentLocal.selectedItems, newItem],
isLoading: false,
});
// dot notation 지원 (중첩 객체)
G7Core.state.setLocal({
"filter.orderStatus": ["paid", "shipped"]
});
// 결과: { filter: { orderStatus: ["paid", "shipped"] } }
}
scope 옵션 (engine-v1.15.0+): 모달이나 중첩 레이아웃에서 부모/루트 컨텍스트의 _local에 접근할 때 사용합니다.
| scope 값 | 설명 | 사용 시점 |
|---|---|---|
'current' (기본값) |
현재 컨텍스트의 _local 업데이트 |
일반적인 상태 업데이트 |
'parent' |
부모 레이아웃의 _local 업데이트 |
모달에서 부모 상태 업데이트 시 |
'root' |
최상위 레이아웃의 _local 업데이트 |
중첩 모달에서 최상위 상태 업데이트 시 |
// 모달 확인 버튼 핸들러에서 부모 상태 업데이트
export function updateParentFormHandler(action: any, context: any): void {
const G7Core = (window as any).G7Core;
const form = action.params?.form;
// 부모 레이아웃의 _local.form.fields 업데이트
G7Core.state.setLocal({
'form.fields': updatedFields,
}, { scope: 'parent' });
G7Core.toast.success('변경되었습니다');
}
scope 사용 시 주의사항:
openModal시 부모 컨텍스트가__g7LayoutContextStack에 push됨closeModal시 스택에서 pop됨- sequence 순서가 중요: 핸들러가
closeModal보다 먼저 실행되어야 함- ❌
closeModal→handler: 핸들러 실행 시 스택이 비어있음 - ✅
handler→closeModal: 핸들러 실행 시 부모 컨텍스트 존재
- ❌
주의사항:
- ActionDispatcher의 액션 실행 중에만
dynamicState업데이트가 동작합니다 - 액션 외부에서 호출하면 전역
_local이 업데이트됩니다 expandChildren내부 액션에서도 정상 동작합니다 (componentContext 자동 전달)- dot notation 경로는 자동으로 중첩 객체로 변환되어 깊은 병합됩니다
scope: 'parent'/'root'사용 시 컨텍스트 스택이 비어있으면current로 폴백됩니다merge옵션 (engine-v1.18.0+): 병합 방식을 지정합니다 ('deep'|'shallow'|'replace', 기본:'deep')
merge 옵션 사용 예시:
// replace: 기존 _local을 완전히 교체
G7Core.state.setLocal({
formData: { name: '', price: 0 }
}, { merge: 'replace' });
// shallow: 최상위 키만 덮어쓰기
G7Core.state.setLocal({
filter: { status: 'all', keyword: '' }
}, { merge: 'shallow' });
getIsolated / setIsolated (engine-v1.14.0+)
격리된 스코프의 상태를 조회하거나 업데이트합니다. scopeId로 특정 격리 스코프를 식별합니다.
사용 시점:
- 커스텀 핸들러에서 특정 격리 스코프의 상태에 접근해야 할 때
- 컴포넌트 외부에서 격리된 상태를 프로그래밍 방식으로 조작해야 할 때
// 커스텀 핸들러에서 사용
export function categorySelectHandler(action: any, context: any): void {
const G7Core = (window as any).G7Core;
// 격리 상태 조회
const state = G7Core.state.getIsolated('category-selector');
console.log(state?.selectedCategories);
// 격리 상태 업데이트
G7Core.state.setIsolated('category-selector', {
currentStep: 2,
selectedCategories: [...state.selectedCategories, newCategory],
});
}
주의사항:
scopeId는 레이아웃 JSON의isolatedScopeId속성과 일치해야 합니다- 해당
scopeId의 격리 스코프가 마운트되지 않은 경우getIsolated는null을 반환합니다 setIsolated는 스코프가 없으면 경고 로그를 출력하고 무시합니다- 대부분의 경우 액션 context의
isolatedContext를 사용하는 것이 권장됩니다 options.merge(engine-v1.18.0+): 병합 방식 지정 ('deep'|'shallow'|'replace', 기본:'deep')
레이아웃에서 scopeId 정의:
{
"type": "Div",
"isolatedState": {
"selectedCategories": [],
"currentStep": 1
},
"isolatedScopeId": "category-selector",
"children": [...]
}
사용 예시
// 상태 조회
const state = G7Core.state.get();
console.log(state.sidebarOpen);
// 상태 설정 (기본: 깊은 병합)
G7Core.state.set({ theme: 'dark' });
// merge 옵션 (engine-v1.18.0+)
G7Core.state.set({ theme: 'dark', sidebar: { open: true } }, { merge: 'shallow' });
G7Core.state.set({ theme: 'light' }, { merge: 'replace' });
// 함수형 업데이트 (현재 상태 기반)
G7Core.state.update(prev => ({
count: (prev.count || 0) + 1
}));
// 상태 변경 구독
const unsubscribe = G7Core.state.subscribe((state) => {
console.log('상태 변경:', state);
});
// 데이터 소스 조회 (deprecated - G7Core.dataSource.get() 사용 권장)
const users = G7Core.state.getDataSource('users');
console.log(users?.data);
데이터소스 관리 (G7Core.dataSource)
데이터소스의 조회, 설정, refetch를 관리하는 API입니다.
engine-v1.5.0+ 추가
API 목록
| 메서드 | 설명 | 반환값 |
|---|---|---|
get(id) |
데이터소스 값 조회 | any | undefined |
set(id, data, options?) |
데이터소스 값 설정 + UI 리렌더링 | void |
updateItem(id, path, itemId, updates, options?) |
배열 내 특정 아이템 부분 업데이트 | boolean |
refetch(id, options?) |
서버에서 데이터 다시 가져오기 | Promise<any> |
set() 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
merge |
boolean |
false |
true면 기존 데이터와 shallow merge |
sync |
boolean |
false |
true면 동기 업데이트 |
updateItem() (engine-v1.13.0+)
배열 내 특정 아이템만 업데이트하여 성능을 최적화합니다.
시그니처:
updateItem(
dataSourceId: string,
itemPath: string,
itemId: string | number,
updates: Record<string, any>,
options?: { idField?: string; merge?: boolean; skipRender?: boolean }
): boolean
파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
dataSourceId |
string |
데이터소스 ID |
itemPath |
string |
배열 경로 (예: "data.data", "data.data[0].options") |
itemId |
string | number |
아이템 식별자 |
updates |
object |
업데이트할 필드 |
옵션:
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
idField |
string |
"id" |
ID 필드명 |
merge |
boolean |
true |
깊은 병합 여부 |
skipRender |
boolean |
false |
렌더링 스킵 여부 |
예시:
// 상품 옵션의 판매가 업데이트
G7Core.dataSource.updateItem(
'products',
'data.data[0].options',
123,
{ selling_price: 15000, _modified: true }
);
// uuid 필드 기준 사용자 업데이트
G7Core.dataSource.updateItem(
'users',
'data',
'user-456',
{ name: '홍길동' },
{ idField: 'uuid' }
);
사용 예시
// 데이터소스 조회
const products = G7Core.dataSource.get('products');
console.log(products?.data?.data); // API 응답 구조에 따라 다름
// 데이터소스 설정 (전체 교체)
G7Core.dataSource.set('products', {
...currentData,
data: {
...currentData.data,
data: updatedProductsArray,
},
});
// 데이터소스 설정 (병합)
G7Core.dataSource.set('products', { data: updatedData }, { merge: true });
// 서버에서 다시 가져오기
await G7Core.dataSource.refetch('products');
// 캐시 무시하고 다시 가져오기
await G7Core.dataSource.refetch('products', { skipCache: true });
데이터소스 vs 전역 상태
┌─────────────────────────────────────────────────────────────┐
│ 두 가지 데이터 저장소 │
├─────────────────────────────────────────────────────────────┤
│ 1. 데이터소스 (G7Core.dataSource) │
│ - API 응답 저장 │
│ - {{products?.data?.data}}로 바인딩 │
│ - 인라인 편집 등 런타임 수정 시 사용 │
│ │
│ 2. 전역 상태 (G7Core.state) │
│ - _global, _local 상태 저장 │
│ - UI 상태 (필터, 선택 등) 관리 │
│ - initGlobal로 데이터소스 복사 가능 (읽기 전용 용도) │
└─────────────────────────────────────────────────────────────┘
사용 시점
| 상황 | 사용 API |
|---|---|
| 인라인 편집, 실시간 데이터 수정 | G7Core.dataSource.set() |
| 서버 동기화 필요 | G7Core.dataSource.refetch() |
| UI 상태 (필터, 선택, 토글) | G7Core.state.set() |
| 초기 데이터 읽기 전용 복사 | initGlobal 옵션 |
토스트 알림 (G7Core.toast)
토스트 알림을 표시하는 편의 API입니다.
API 목록
| 메서드 | 설명 | 파라미터 |
|---|---|---|
show(message, options?) |
토스트 표시 | message, { type, duration } |
success(message, duration?) |
성공 토스트 | message, duration(ms) |
error(message, duration?) |
에러 토스트 | message, duration(ms) |
warning(message, duration?) |
경고 토스트 | message, duration(ms) |
info(message, duration?) |
정보 토스트 | message, duration(ms) |
토스트 타입
| 타입 | 설명 | 색상 |
|---|---|---|
success |
성공 메시지 | 녹색 |
error |
에러 메시지 | 빨간색 |
warning |
경고 메시지 | 노란색 |
info |
정보 메시지 | 파란색 |
사용 예시
// 편의 메서드 (권장)
G7Core.toast.success('저장되었습니다');
G7Core.toast.error('오류가 발생했습니다');
G7Core.toast.warning('주의가 필요합니다');
G7Core.toast.info('정보 메시지');
// duration 지정 (밀리초)
G7Core.toast.success('저장 완료', 3000); // 3초 후 자동 닫힘
// 상세 옵션
G7Core.toast.show('메시지', {
type: 'info',
duration: 5000
});
내부 동작
토스트 API는 내부적으로 G7Core.dispatch를 호출하여 toast 핸들러를 실행합니다:
// G7Core.toast.success('메시지')는 내부적으로:
G7Core.dispatch({
handler: 'toast',
params: {
message: '메시지',
type: 'success'
}
});
모달 관리 (G7Core.modal)
모달을 관리하는 편의 API입니다.
API 목록
| 메서드 | 설명 | 파라미터 |
|---|---|---|
open(modalId) |
모달 열기 | modalId: string |
close(modalId?) |
모달 닫기 | modalId?: string (생략 시 최상위) |
closeAll() |
모든 모달 닫기 | - |
isOpen(modalId) |
모달 열림 확인 | modalId: string |
getStack() |
모달 스택 반환 | - |
사용 예시
// 모달 열기
G7Core.modal.open('confirm_modal');
// 특정 모달 닫기
G7Core.modal.close('confirm_modal');
// 최상위 모달 닫기 (스택에서 pop)
G7Core.modal.close();
// 모든 모달 닫기
G7Core.modal.closeAll();
// 모달 열림 상태 확인
if (G7Core.modal.isOpen('confirm_modal')) {
console.log('모달이 열려 있습니다');
}
// 현재 모달 스택 조회
const stack = G7Core.modal.getStack();
// ['first_modal', 'second_modal'] - 순서대로 쌓인 모달 ID
멀티 모달 (중첩 모달) 지원
G7Core는 스택 기반 멀티 모달을 지원합니다:
// 1. 첫 번째 모달 열기
G7Core.modal.open('confirm_modal');
// stack: ['confirm_modal']
// 2. 두 번째 모달 열기 (첫 번째 위에 중첩)
G7Core.modal.open('error_modal');
// stack: ['confirm_modal', 'error_modal']
// 3. 최상위 모달 닫기
G7Core.modal.close();
// stack: ['confirm_modal'] - 첫 번째 모달 다시 표시
// 4. 남은 모달 닫기
G7Core.modal.close();
// stack: []
네비게이션 (G7Core.navigation)
페이지 전환 상태를 관리하는 API입니다.
API 목록
| 메서드 | 설명 | 반환값 |
|---|---|---|
isPending() |
전환 진행 중 여부 | boolean |
onComplete(callback) |
전환 완료 시 콜백 실행 | 구독 해제 함수 |
사용 예시
// 현재 페이지 전환 중인지 확인
if (G7Core.navigation.isPending()) {
console.log('페이지 전환 중...');
}
// 전환 완료 시 콜백 실행
const unsubscribe = G7Core.navigation.onComplete(() => {
console.log('페이지 전환 완료');
// DOM 조작, 포커스 설정 등
});
// 구독 해제
unsubscribe();
onComplete 동작 방식
onComplete는 전환이 시작(isPending=true)된 후 완료(isPending=false)될 때 콜백을 실행합니다:
// 페이지 이동 후 특정 요소에 포커스
G7Core.navigation.onComplete(() => {
document.getElementById('main-content')?.focus();
});
// 네비게이션 실행
G7Core.dispatch({
handler: 'navigate',
params: { path: '/users' }
});
스타일 헬퍼 (G7Core.style)
Tailwind CSS 클래스를 런타임에서 병합하는 헬퍼 API입니다.
컴포넌트에서 기본 클래스와 외부 className이 충돌할 때, 외부 클래스가 기본 클래스를 올바르게 오버라이드할 수 있도록 합니다.
API 목록
| 메서드 | 설명 | 반환값 |
|---|---|---|
mergeClasses(base, override) |
충돌하는 Tailwind 클래스 병합 | string |
conditionalClass(conditions) |
조건부 클래스 적용 | string |
joinClasses(...classes) |
여러 클래스 문자열 결합 | string |
mergeClasses
같은 CSS 속성을 제어하는 클래스가 충돌하면 override 클래스를 우선 적용합니다.
// 기본 사용: justify-center가 justify-between으로 대체됨
G7Core.style.mergeClasses('justify-center items-center', 'justify-between')
// 결과: 'items-center justify-between'
// variant가 다르면 충돌하지 않음
G7Core.style.mergeClasses('text-gray-900 dark:text-white', 'text-blue-500')
// 결과: 'dark:text-white text-blue-500'
// 같은 variant는 충돌
G7Core.style.mergeClasses('dark:text-white dark:bg-gray-800', 'dark:text-gray-100')
// 결과: 'dark:bg-gray-800 dark:text-gray-100'
지원하는 클래스 그룹
| 그룹 | 설명 | 예시 |
|---|---|---|
| justify | Flexbox justify-content | justify-center, justify-between |
| items | Flexbox align-items | items-center, items-start |
| textAlign | 텍스트 정렬 | text-left, text-center |
| display | 디스플레이 속성 | flex, block, hidden |
| bgColor | 배경색 | bg-white, bg-gray-100 |
| textColor | 텍스트색 | text-gray-900, text-blue-500 |
| width/height | 크기 | w-full, h-10 |
| padding/margin | 여백 | p-4, m-2 |
| ... | 대부분의 Tailwind 클래스 지원 | - |
conditionalClass
조건에 따라 클래스를 적용합니다.
G7Core.style.conditionalClass({
'bg-blue-500': isPrimary,
'bg-gray-500': !isPrimary,
'opacity-50': isDisabled,
})
// isPrimary=true, isDisabled=true: 'bg-blue-500 opacity-50'
joinClasses
여러 클래스 문자열을 결합합니다. falsy 값은 무시됩니다.
G7Core.style.joinClasses('flex', isActive && 'bg-blue-500', 'p-4')
// isActive=true: 'flex bg-blue-500 p-4'
// isActive=false: 'flex p-4'
컴포넌트에서 사용 패턴
// G7Core 접근 헬퍼
const G7Core = () => (window as any).G7Core;
export const Button: React.FC<Props> = ({ className = '', ...props }) => {
const baseClasses = 'inline-flex items-center justify-center';
// 외부 className이 baseClasses를 오버라이드할 수 있음
const mergedClassName = G7Core()?.style?.mergeClasses?.(baseClasses, className)
?? `${baseClasses} ${className}`;
return <button className={mergedClassName} {...props} />;
};
플러그인 설정 (G7Core.plugin)
플러그인 설정을 조회하는 API입니다. 백엔드에서 전달된 플러그인 설정값을 프론트엔드에서 사용할 수 있습니다.
API 목록
| 메서드 | 설명 | 반환값 |
|---|---|---|
getSettings(pluginId) |
플러그인 전체 설정 반환 | Record<string, any> | undefined |
get(pluginId, key, defaultValue?) |
특정 설정값 반환 | any |
getAll() |
모든 플러그인 설정 반환 | Record<string, Record<string, any>> |
사용 예시
// 플러그인 전체 설정 조회
const daumSettings = G7Core.plugin.getSettings('sirsoft-daum_postcode');
// { display_mode: 'layer', popup_width: 900, popup_height: 600 }
// 특정 설정값 조회
const displayMode = G7Core.plugin.get('sirsoft-daum_postcode', 'display_mode');
// 'layer'
// 기본값 지정
const timeout = G7Core.plugin.get('sirsoft-daum_postcode', 'timeout', 3000);
// 3000 (설정이 없을 경우)
// 모든 플러그인 설정 조회
const allPlugins = G7Core.plugin.getAll();
// { 'sirsoft-daum_postcode': {...}, 'sirsoft-analytics': {...} }
컴포넌트에서 사용 패턴
// 주소 검색 컴포넌트
const AddressInput: React.FC = () => {
const displayMode = G7Core.plugin.get('sirsoft-daum_postcode', 'display_mode', 'popup');
const handleSearch = () => {
if (displayMode === 'layer') {
// 레이어 모드로 표시
} else {
// 팝업 모드로 표시
}
};
return <Button onClick={handleSearch}>주소 검색</Button>;
};
모듈 설정 (G7Core.module)
모듈 설정을 조회하는 API입니다. 백엔드에서 전달된 모듈 설정값을 프론트엔드에서 사용할 수 있습니다.
API 목록
| 메서드 | 설명 | 반환값 |
|---|---|---|
getSettings(moduleId) |
모듈 전체 설정 반환 | Record<string, any> | undefined |
get(moduleId, key, defaultValue?) |
특정 설정값 반환 | any |
getAll() |
모든 모듈 설정 반환 | Record<string, Record<string, any>> |
사용 예시
// 모듈 전체 설정 조회
const ecommerceSettings = G7Core.module.getSettings('sirsoft-ecommerce');
// { default_currency: 'KRW', tax_rate: 10, shipping_methods: ['standard', 'express'] }
// 특정 설정값 조회
const currency = G7Core.module.get('sirsoft-ecommerce', 'default_currency');
// 'KRW'
// 기본값 지정
const taxRate = G7Core.module.get('sirsoft-ecommerce', 'tax_rate', 0);
// 10
// 모든 모듈 설정 조회
const allModules = G7Core.module.getAll();
// { 'sirsoft-ecommerce': {...}, 'sirsoft-board': {...} }
컴포넌트에서 사용 패턴
// 가격 표시 컴포넌트
const PriceDisplay: React.FC<{ price: number }> = ({ price }) => {
const currency = G7Core.module.get('sirsoft-ecommerce', 'default_currency', 'KRW');
const taxRate = G7Core.module.get('sirsoft-ecommerce', 'tax_rate', 0);
const priceWithTax = price * (1 + taxRate / 100);
return (
<Span>
{priceWithTax.toLocaleString()} {currency}
</Span>
);
};
확장 자산 (G7Core.asset, G7Core.assets)
버전: engine-v1.62.0+
확장(템플릿·모듈·플러그인)이 자기 자산을 런타임에 불러올 때 쓰는 API입니다. 확장 번들은 코어 모듈을 import 할 수 없으므로, 자산 URL 생성과 실패 표면화는 이 전역을 통해 제공됩니다.
구동에 필요한 자산(js·css·웹폰트)은 제3자 CDN 에서 실시간으로 받지 않고 확장이 함께 담아
dist/vendor/{라이브러리}/{버전}/ 에서 자체 제공합니다. 자세한 규약은
module-assets.md를 참조하세요.
G7Core.asset — 자산 URL 생성
| 메서드 | 시그니처 | 설명 |
|---|---|---|
template |
(identifier, path, version?) => string |
템플릿 자산 URL |
templateDir |
(identifier, path) => string |
템플릿 자산 디렉토리 URL |
module |
(identifier, path, version?) => string |
모듈 자산 URL |
plugin |
(identifier, path, version?) => string |
플러그인 자산 URL |
convertToCurrentMode |
(url) => string |
서버가 확장자 형태로 굳혀 내려준 URL 을 현재 모드로 보정 |
loadScript |
(url, attrs?, options?) => Promise<void> |
재시도 계층을 갖춘 스크립트 로더 |
loadStylesheet |
(url, attrs?, options?) => Promise<void> |
재시도 계층을 갖춘 스타일시트 로더 |
path 기준이 확장 타입마다 다릅니다. 템플릿은 서버가 dist/ 를 자동으로 붙이므로 path 에
dist/ 를 포함하지 않고, 모듈·플러그인은 확장 루트 기준이라 dist/ 를 직접 포함합니다.
서버측 App\Support\AssetUrl 과 같은 비대칭입니다.
// 템플릿 — dist/ 를 붙이지 않는다
G7Core.asset.template('sirsoft-admin_basic', 'vendor/monaco-editor/0.54.0/vs/loader.js');
// 플러그인 — 확장 루트 기준이라 dist/ 를 포함한다
G7Core.asset.plugin('sirsoft-ckeditor5', 'dist/vendor/ckeditor5/43.3.1/ckeditor5.umd.js');
// 모듈
G7Core.asset.module('sirsoft-board', 'dist/js/board.js');
자산 URL 을 문자열로 조립하지 않습니다
// ❌ 금지 — 확장자를 정적 location 이 가로채는 서버에서 404 가 된다
const url = '/api/plugins/assets/sirsoft-ckeditor5/dist/vendor/ckeditor5/43.3.1/ckeditor5.umd.js';
// ✅ 올바른 사용
const url = G7Core.asset.plugin('sirsoft-ckeditor5', 'dist/vendor/ckeditor5/43.3.1/ckeditor5.umd.js');
자산 URL 은 서버 설정(general.asset_url_mode)과 정적 게시 상태에 따라 확장자 형태
(.../ckeditor5.umd.js)와 쿼리 형태(...?file=...), 정적 게시본 경로
(/build/ext/{버전}/...) 중 하나로 해석됩니다. 문자열로 조립하면 그 판정을 건너뛰어, 정규식
location 이 확장자를 먼저 가로채는 서버에서 그 자산만 조용히 404 가 됩니다.
templateDir — 디렉토리 접두가 필요한 소비자
AMD 로더나 워커처럼 디렉토리 접두 뒤에 파일명을 이어 붙이는 소비자는 template() 대신
templateDir() 을 씁니다. 쿼리 형태(?file=)는 뒤에 파일명을 이어 붙일 수 없기 때문입니다.
확장자 없는 모드에서 404 일 수 있으므로 소비자가 폴백을 갖춰야 합니다.
loader.config({
paths: { vs: G7Core.asset.templateDir('sirsoft-admin_basic', 'vendor/monaco-editor/0.54.0/vs') },
});
G7Core.assets — 자산 실패 표면화
로드에 끝내 실패한 자산을 화면 상단 배너로 알리고 [다시 시도] 를 제공합니다. 실패를
console.error 한 줄로 끝내면 사용자에게는 "빈 자리" 로만 나타나고 자체 서버 로그에도 흔적이
남지 않아 운영자가 원인을 특정할 수 없습니다.
| 메서드 | 시그니처 | 설명 |
|---|---|---|
notifyFailure |
(failure: AssetFailure) => void |
실패 등록 (같은 id 는 갱신) |
clearFailure |
(id: string) => void |
해당 실패 해제 |
clearAll |
() => void |
전체 해제 |
getFailures |
() => AssetFailure[] |
현재 등록된 실패 목록 |
retryAll |
() => Promise<void> |
등록된 실패 전부 재시도 — 각 retry 를 순차 await 하므로 완료를 기다리려면 반환값을 await 한다 |
AssetFailure 필드:
| 필드 | 타입 | 설명 |
|---|---|---|
id |
string |
중복 누적을 막는 식별자 (필수) |
label |
string |
짧은 항목명 — 여러 건 합산 표시에 쓰인다 |
message |
string? |
사용자에게 보일 안내 문장. 생략하면 label 기반 기본 문구 |
retry |
() => Promise<void> | void |
생략하면 [다시 시도] 버튼을 렌더하지 않는다 |
try {
await G7Core.asset.loadStylesheet(url, { id: 'my-plugin-css' });
G7Core.assets.clearFailure('my-plugin-css');
} catch (error) {
G7Core.assets.notifyFailure({
id: 'my-plugin-css',
label: '편집기 스타일',
retry: () => G7Core.asset.loadStylesheet(url, { id: 'my-plugin-css' }),
});
}
배너는 독립 레이아웃(extends 없음)에서도 떠야 하므로 호스트 컴포넌트에 의존하지 않고
DOM 에 직접 주입됩니다. Toast 와 달리 사용자가 닫을 때까지 유지되며 role="alert" 를
갖습니다. retry 가 resolve 되면 그 실패는 자동으로 해제되고, reject 되면 배너를 유지한 채
재시도 실패를 알립니다.
위지윅 편집기 (G7Core.wysiwyg)
engine-v1.11.0+ 추가
레이아웃 위지윅 편집기 관련 API입니다.
API 목록
| 메서드 | 설명 | 반환값 |
|---|---|---|
isEditMode() |
편집 모드 여부 반환 | boolean |
setEditMode(layoutName, templateId) |
편집 모드 활성화 | void |
clearEditMode() |
편집 모드 비활성화 | void |
getCurrentLayoutName() |
현재 편집 중인 레이아웃명 | string | null |
getCurrentTemplateId() |
현재 편집 중인 템플릿 ID | string | null |
isEditModeFromUrl() |
URL에서 편집 모드 여부 확인 | boolean |
getEditModeUrl(route, templateId) |
편집 모드 URL 생성 | string |
enterEditMode(route, templateId) |
편집 모드로 페이지 이동 | void |
exitEditMode() |
편집 모드 종료 (일반 페이지로) | void |
getVersion() |
위지윅 모듈 버전 | string |
getPhase() |
현재 구현 Phase | number |
사용 예시
// 편집 모드 확인
if (G7Core.wysiwyg.isEditMode()) {
console.log('현재 편집 모드입니다');
console.log('레이아웃:', G7Core.wysiwyg.getCurrentLayoutName());
console.log('템플릿:', G7Core.wysiwyg.getCurrentTemplateId());
}
// URL에서 편집 모드 여부 확인 (쿼리 파라미터 기반)
if (G7Core.wysiwyg.isEditModeFromUrl()) {
// ?mode=edit 파라미터가 있는 경우
}
// 편집 모드 URL 생성
const editUrl = G7Core.wysiwyg.getEditModeUrl('/shop', 'sirsoft-basic');
// 결과: 'https://example.com/shop?mode=edit&template=sirsoft-basic'
// 편집 모드로 진입
G7Core.wysiwyg.enterEditMode('/shop', 'sirsoft-basic');
// 편집 모드 종료
G7Core.wysiwyg.exitEditMode();
편집 모드 상태 관리
// 프로그래밍 방식으로 편집 모드 설정
G7Core.wysiwyg.setEditMode('home', 'sirsoft-basic');
// 편집 모드 해제
G7Core.wysiwyg.clearEditMode();
커스텀 핸들러 Stale State 방지 규칙
engine-v1.17.0+ 추가
커스텀 핸들러에서 비동기 작업 후 상태를 사용할 때 발생하는 stale state 문제를 방지하는 규칙입니다.
await 후 상태 재조회 필수
비동기 작업(API 호출, setTimeout 등) 후에는 캡처된 상태가 오래된 값(stale)일 수 있습니다. 반드시 await 후 최신 상태를 재조회하세요.
// ❌ 금지: await 전에 캡처한 상태를 await 후에 사용
const state = G7Core.state.getLocal();
const currentUi = state.ui || {};
await someAsyncOperation(); // 이 동안 다른 곳에서 상태가 변경될 수 있음
G7Core.state.setLocal({ ui: { ...currentUi, loading: false } }); // STALE!
// ✅ 필수: await 후 최신 상태 재조회
await someAsyncOperation();
const latestState = G7Core.state.getLocal() || {}; // 재조회
const latestUi = latestState.ui || {};
G7Core.state.setLocal({ ui: { ...latestUi, loading: false } });
실전 패턴
API 호출 후 상태 업데이트
// 복사 중 상태 설정 (await 전)
const initialState = G7Core.state.getLocal() || {};
G7Core.state.setLocal({ ui: { ...initialState.ui, isCopying: true } });
try {
// API 호출
const response = await G7Core.api.post('/api/copy', data);
// await 후 최신 상태 재조회
const latestState = G7Core.state.getLocal() || {};
G7Core.state.setLocal({
ui: { ...latestState.ui, isCopying: false },
form: { ...latestState.form, ...response.data },
});
} catch (error) {
// 에러 시에도 최신 상태 재조회
const errorState = G7Core.state.getLocal() || {};
G7Core.state.setLocal({
ui: { ...errorState.ui, isCopying: false, error: true },
});
}
루프 내 await (순차 처리)
// 각 반복에서 최신 상태 조회 (권장)
for (const file of files) {
// 매번 최신 상태 조회
const currentState = G7Core.state.getLocal() || {};
G7Core.state.setLocal({
ui: { ...currentState.ui, uploadProgress: { [file.name]: 0 } },
});
const response = await G7Core.api.upload('/api/upload', file);
// 업로드 후 최신 상태 재조회
const successState = G7Core.state.getLocal() || {};
const existingImages = successState.form?.images || [];
G7Core.state.setLocal({
form: { ...successState.form, images: [...existingImages, response.data] },
});
}
왜 이 문제가 발생하는가?
- JavaScript 클로저: 함수가 생성될 때 외부 변수를 캡처합니다.
- React setState 비동기성:
setLocal호출 후 즉시 상태가 반영되지 않습니다. - 비동기 작업 중 상태 변경: await 중에 사용자가 다른 UI 조작을 할 수 있습니다.
관련 엔진 보호 메커니즘
그누보드7 템플릿 엔진은 다음 메커니즘으로 stale state 문제를 완화합니다:
| 메커니즘 | 설명 | 파일 |
|---|---|---|
stateRef.current |
useCallback 캐싱 우회 | DynamicRenderer.tsx |
__g7PendingLocalState |
setLocal 후 dispatch 동기화 | G7CoreGlobals.ts |
G7Core.state.getLocal() |
최신 로컬 상태 조회 | G7CoreGlobals.ts |
하지만 커스텀 핸들러에서는 개발자가 직접 최신 상태 재조회를 구현해야 합니다.
체크리스트
커스텀 핸들러 작성 시 확인:
□ await 후 상태를 사용하는 모든 곳에서 G7Core.state.getLocal() 재조회
□ try-catch 블록의 catch에서도 최신 상태 재조회
□ 루프 내 await 시 각 반복에서 상태 재조회
□ 모달 컨텍스트인 경우 getParent()도 재조회 고려
관련 문서
- 고급 API - 다국어, 액션 실행, 컴포넌트 이벤트, 렌더링 헬퍼, WebSocket, React Hooks
- state-management.md - 전역 상태 관리 상세
- components.md - 컴포넌트 개발 규칙
- data-binding.md - 데이터 바인딩 문법
- auth-system.md - 인증 시스템
- responsive-layout.md - 반응형 레이아웃