(extending_client_side)=
# 클라이언트 사이드 동작 확장하기
많은 종류의 일반적인 사용자 정의는 자바스크립트에 손대지 않고도 수행할 수 있지만, 활용하거나 사용자 정의하려는 클라이언트 측 상호 작용의 부분에 따라 React, Stimulus 또는 일반(vanilla) JS를 사용해야 할 수도 있습니다.
[React](https://reactjs.org/)는 사이드바, 댓글 시스템, Draftail 리치 텍스트 편집기와 같이 Wagtail의 더 복잡한 부분에 사용됩니다.
기본적인 자바스크립트 기반 상호 작용을 위해 Wagtail은 [Stimulus](https://stimulus.hotwired.dev/)로 마이그레이션하고 있습니다.
요소에 사용자 정의 동작을 추가하기 위해 이러한 라이브러리를 알거나 사용할 필요는 없으며, 많은 경우 간단한 자바스크립트로도 충분하지만, 더 복잡한 사용 사례에는 Stimulus가 권장되는 접근 방식입니다.
이러한 라이브러리를 기반으로 구축된 많은 사용자 정의를 위해 사용자 정의 Wagtail 설치에 Node.js 도구를 실행할 필요는 없지만, 패키지 빌드와 같은 일부 경우에는 더 복잡한 개발을 더 쉽게 만들 수 있습니다.
```{note}
jQuery 및 문서화되지 않은 jQuery 플러그인은 향후 Wagtail 버전에서 제거될 예정이므로 사용을 피하세요.
```
(extending_client_side_injecting_javascript)=
## 사용자 정의 자바스크립트 추가하기
Wagtail의 관리자 인터페이스 내에서 자바스크립트를 추가하는 몇 가지 방법이 있습니다.
가장 간단한 방법은 훅을 통해 전역 자바스크립트 파일을 추가하는 것입니다. [](insert_editor_js) 및 [](insert_global_admin_js)를 참조하세요.
특정 위젯이 사용될 때 추가되는 자바스크립트의 경우, 내부 `Media` 클래스를 추가하여 위젯이 사용될 때 파일이 로드되도록 할 수 있습니다. 양식 `Media` 클래스에 대한 [Django의 문서](inv:django#assets-as-a-static-definition)를 참조하세요.
비슷한 방식으로 Wagtail의 [](./template_components)는 렌더링될 때 스크립트를 추가하기 위해 `media` 속성 또는 `Media` 클래스를 제공합니다.
이렇게 하면 핵심 자바스크립트 관리자 파일이 이미 로드된 후 추가된 파일이 관리자에서 사용되도록 할 수 있습니다.
(extending_client_side_using_events)=
## DOM 이벤트로 확장하기
클라이언트 측 사용자 정의나 새 구성 요소 채택에 접근할 때, 먼저 구현을 간단하게 유지하려고 노력하세요. 목표를 달성하기 위해 Stimulus, React, 자바스크립트 모듈 또는 빌드 시스템에 대한 지식이 필요하지 않을 수 있습니다.
브라우저에 동작을 연결하는 가장 간단한 방법은 [DOM 이벤트](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events)와 일반(vanilla) 자바스크립트를 통하는 것입니다.
### Wagtail의 사용자 정의 DOM 이벤트
Wagtail은 사용자 정의 DOM 이벤트를 수신하거나 전달하여 일부 사용자 정의 동작을 지원합니다.
- [업로드 시 이미지 제목 생성](images_title_generation_on_upload) 참조.
- [업로드 시 문서 제목 생성](docs_title_generation_on_upload) 참조.
- [`InlinePanel` DOM 이벤트](inline_panel_events) 참조.
(extending_client_side_stimulus)=
## Stimulus로 확장하기
Wagtail은 관리자 인터페이스 내에서 가벼운 클라이언트 측 상호 작용이나 사용자 정의 자바스크립트 위젯을 제공하는 방법으로 [Stimulus](https://stimulus.hotwired.dev/)를 사용합니다.
Stimulus 사용의 주요 이점은 모달, `InlinePanel` 또는 `StreamField` 패널과 같이 위젯이 동적으로 나타날 때 코드가 수동 초기화의 필요성을 피할 수 있다는 것입니다.
[Stimulus 핸드북](https://stimulus.hotwired.dev/handbook/introduction)은 Stimulus를 사용하고 이해하는 방법에 대한 최고의 자료입니다.
### 사용자 정의 Stimulus 컨트롤러 추가하기
Wagtail은 Stimulus를 사용하기 위해 두 개의 클라이언트 측 전역 변수를 노출합니다.
1. `window.wagtail.app` 핵심 관리자 Stimulus 애플리케이션 인스턴스.
2. `window.StimulusModule` `@hotwired/stimulus` 에서 내보낸 Stimulus 모듈.
먼저, [자바스크립트 클래스 상속](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)을 사용하여 기본 `window.StimulusModule.Controller` 를 확장하는 사용자 정의 [Stimulus 컨트롤러](https://stimulus.hotwired.dev/reference/controllers)를 만드세요. 빌드 도구를 사용하는 경우 `import { Controller } from '@hotwired/stimulus';` 를 통해 기본 컨트롤러를 가져올 수 있습니다.
사용자 정의 컨트롤러를 만든 후에는 `window.wagtail.app.register` 메서드를 통해 [Stimulus 컨트롤러를 수동으로 등록](https://stimulus.hotwired.dev/reference/controllers#registering-controllers-manually)해야 합니다.
#### 간단한 컨트롤러 예제
먼저, Wagtail 관리자 내 어딘가에 나타나도록 HTML을 만듭니다.
```html
Hi
Hello
```
둘째, 컨트롤러 코드를 포함할 자바스크립트 파일을 만듭니다. 이 컨트롤러는 `connect` 시 간단한 메시지를 기록하는데, 이는 컨트롤러가 생성되고 일치하는 `data-controller` 속성을 가진 HTML 요소에 연결되었을 때 한 번 발생합니다.
```javascript
// myapp/static/js/example.js
class MyController extends window.StimulusModule.Controller {
static targets = ['label'];
connect() {
console.log(
'My controller has connected:',
this.element.innerText,
this.labelTargets,
);
}
}
window.wagtail.app.register('my-controller', MyController);
```
마지막으로, 훅을 사용하여 자바스크립트 파일을 Wagtail 관리자에 로드합니다.
```python
# myapp/wagtail_hooks.py
from django.templatetags.static import static
from django.utils.html import format_html
from wagtail import hooks
@hooks.register('insert_global_admin_js')
def global_admin_js():
return format_html(
f'',
)
```
이제 HTML을 표시하던 관리자를 새로고침하면 콘솔에 두 개의 로그가 표시되는 것을 볼 수 있습니다.
#### 더 복잡한 컨트롤러 예제
이제 입력된 단어 수를 보여주는 작은 `output` 요소를 제어되는 `input` 요소 옆에 추가하는 `WordCountController` 를 만들겠습니다.
```javascript
// myapp/static/js/word-count-controller.js
class WordCountController extends window.StimulusModule.Controller {
static values = { max: { default: 10, type: Number } };
connect() {
this.setupOutput();
this.updateCount();
}
setupOutput() {
if (this.output) return;
const template = document.createElement('template');
template.innerHTML = ``;
const output = template.content.firstChild;
this.element.insertAdjacentElement('beforebegin', output);
this.output = output;
}
updateCount(event) {
const value = event ? event.target.value : this.element.value;
const words = (value || '').split(' ');
this.output.textContent = `${words.length} / ${this.maxValue} words`;
}
disconnect() {
this.output && this.output.remove();
}
}
window.wagtail.app.register('word-count', WordCountController);
```
이렇게 하면 데이터 속성 `data-word-count-max-value` 가 이 컨트롤러의 '구성'을 결정하고, 데이터 속성 작업이 출력 요소 업데이트의 '트리거'를 결정하게 됩니다.
```python
# models.py
from django import forms
from wagtail.admin.panels import FieldPanel
from wagtail.models import Page
class BlogPage(Page):
# ...
content_panels = Page.content_panels + [
FieldPanel('subtitle', classname="full"),
FieldPanel(
'introduction',
classname="full",
widget=forms.TextInput(
attrs={
'data-controller': 'word-count',
# 속성을 사용하여 최대 수를 결정할 수 있도록 허용
# 여기서 파이썬 값을 사용할 수 있으며, 장고가 문자열 변환을 처리합니다 (해당되는 경우 이스케이프 포함)
'data-word-count-max-value': 5,
# 데이터 액션으로 카운트를 업데이트할 시점 결정
# (예: 'blur->word-count#updateCount'는 필드가 포커스를 잃을 때만 업데이트됨)
'data-action': 'word-count#updateCount paste->word-count#updateCount',
}
)
),
#...
```
다음 코드 스니펫은 향후 컨트롤러를 위해 추가 스크립트를 추가하도록 설정된 `insert_editor_js` 훅 사용법의 더 고급 버전을 보여줍니다.
```python
# wagtail_hooks.py
from django.utils.html import format_html_join
from django.templatetags.static import static
from wagtail import hooks
@hooks.register('insert_editor_js')
def editor_js():
# 필요에 따라 더 많은 컨트롤러 코드 추가
js_files = ['js/word-count-controller.js',]
return format_html_join('\n', '',
((static(filename),) for filename in js_files)
)
```
이제 블로그 페이지에서 소개 필드에 사용된 단어 수와 최대 단어 수를 보여주는 작은 `output` 요소가 표시되는 것을 볼 수 있습니다.
(extending_client_side_stimulus_widget)=
#### 더 복잡한 위젯 예제
더 복잡한 위젯의 경우, 이제 위젯이 렌더링된 HTML에 나타날 때마다, 초기 로드 시 또는 동적으로 인라인 `script` 요소 없이 추가 라이브러리를 통합할 수 있습니다.
이 예에서는 사용자 정의 위젯 옵션을 지원하는 [Coloris](https://coloris.js.org/) 자바스크립트 라이브러리를 사용하여 색상 선택기 위젯을 빌드합니다.
먼저, Wagtail이 `FieldPanel` 및 `FieldBlock` 에 대해 지원하는 [Django 위젯](inv:django#ref/forms/widgets) 시스템을 기반으로 HTML부터 시작하겠습니다. `build_attrs` 메서드를 사용하여 컨트롤러에 전달되는 일반적인 데이터 구조를 지원하기 위해 적절한 Stimulus 데이터 속성을 구성합니다.
복잡한 값(이 경우 문자열 목록)에 대해 `json.dumps` 를 사용하고 있음을 확인하세요. Django는 렌더링될 때 이러한 값을 자동으로 이스케이프하여 안전하지 않은 클라이언트 측 코드의 일반적인 원인을 피합니다.
```py
# myapp/widgets.py
import json
from django.forms import Media, TextInput
from django.utils.translation import gettext as _
class ColorWidget(TextInput):
"""
https://coloris.js.org/ 참조
"""
def __init__(self, attrs=None, swatches=[], theme='large'):
self.swatches = swatches
self.theme = theme
super().__init__(attrs=attrs);
def build_attrs(self, *args, **kwargs):
attrs = super().build_attrs(*args, **kwargs)
attrs['data-controller'] = 'color'
attrs['data-color-theme-value'] = self.theme
attrs['data-color-swatches-value'] = json.dumps(swatches)
return attrs
@property
def media(self):
return Media(
js=[
# UI 라이브러리 로드
"https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.js",
# 컨트롤러 JS 로드
"js/color-controller.js",
],
css={"all": ["https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.css"]},
)
```
Stimulus 컨트롤러의 경우, `this.element.id` 를 통해 제어되는 요소에 대한 참조를 포함하여 값을 자바스크립트 라이브러리로 전달합니다.
```javascript
// myapp/static/js/color-controller.js
class ColorController extends window.StimulusModule.Controller {
static values = { swatches: Array, theme: String };
connect() {
// 생성
Coloris({ el: `#${this.element.id}` });
// 초기 생성 후 옵션 설정
setTimeout(() => {
Coloris({ swatches: this.swatchesValue, theme: this.themeValue });
});
}
}
window.wagtail.app.register('color', ColorController);
```
이제 이 위젯을 모든 `FieldPanel` 또는 StreamFields의 모든 `FieldBlock` 에서 사용할 수 있으며, 필드의 요소에 자바스크립트를 자동으로 인스턴스화합니다.
```py
# blocks.py
# ... 기타 가져오기
from django import forms
from wagtail.blocks import FieldBlock
from .widgets import ColorWidget
class ColorBlock(FieldBlock):
def __init__(self, *args, **kwargs):
swatches = kwargs.pop('swatches', [])
theme = kwargs.pop('theme', 'large')
self.field = forms.CharField(widget=ColorWidget(swatches=swatches, theme=theme))
super().__init__(*args, **kwargs)
```
```py
# models.py
# ... 기타 가져오기
from django import forms
from wagtail.admin.panels import FieldPanel
from .blocks import ColorBlock
from .widgets import ColorWidget
BREAD_COLOR_PALETTE = ["#CFAC89", "#C68C5F", "#C47647", "#98644F", "#42332E"]
class BreadPage(Page):
body = StreamField([
# ...
('color', ColorBlock(swatches=BREAD_COLOR_PALETTE)),
# ...
], use_json_field=True)
color = models.CharField(blank=True, max_length=50)
# ... 기타 필드
content_panels = Page.content_panels + [
# ... 기타 패널
FieldPanel("body"),
FieldPanel("color", widget=ColorWidget(swatches=BREAD_COLOR_PALETTE)),
]
```
#### 빌드 시스템 사용하기
빌드 출력이 ES6/ES2015 이상인지 확인해야 합니다. `window.StimulusModule` 에서 노출된 전역 모듈을 사용하거나 npm 모듈 `@hotwired/stimulus` 를 사용하여 직접 제공할 수 있습니다.
```javascript
// myapp/static/js/word-count-controller.js
import { Controller } from '@hotwired/stimulus';
class WordCountController extends Controller {
// ... 위와 동일
}
window.wagtail.app.register('word-count', WordCountController);
```
자바스크립트 출력에 Stimulus를 번들로 포함하는 것을 피하고 전역을 외부/별칭 모듈로 처리하고 싶을 수 있습니다. 이 작업을 수행하는 방법에 대한 지침은 빌드 시스템 설명서를 참조하세요.
## React로 확장하기
[React](https://reactjs.org/) 컴포넌트를 사용자 정의하거나 확장하려면 React뿐만 아니라 다른 관련 라이브러리도 사용해야 할 수 있습니다.
이를 더 쉽게 하기 위해 Wagtail은 React 관련 종속성을 관리자 내에서 전역 변수로 노출합니다. 사용 가능한 패키지는 다음과 같습니다.
```javascript
// 'focus-trap-react'
window.FocusTrapReact;
// 'react'
window.React;
// 'react-dom'
window.ReactDOM;
// 'react-transition-group/CSSTransitionGroup'
window.CSSTransitionGroup;
```
Wagtail은 또한 자체 React 컴포넌트 중 일부를 노출합니다. 다음을 재사용할 수 있습니다.
```javascript
window.wagtail.components.Icon;
window.wagtail.components.Portal;
```
리치 텍스트 편집기를 포함하는 페이지는 다음에도 액세스할 수 있습니다.
```javascript
// 'draft-js'
window.DraftJS;
// 'draftail'
window.Draftail;
// Wagtail의 Draftail 관련 API 및 컴포넌트.
window.draftail;
window.draftail.DraftUtils;
window.draftail.ModalWorkflowSource;
window.draftail.ImageModalWorkflowSource;
window.draftail.EmbedModalWorkflowSource;
window.draftail.LinkModalWorkflowSource;
window.draftail.DocumentModalWorkflowSource;
window.draftail.Tooltip;
window.draftail.TooltipEntity;
```
## Draftail 확장하기
- [](extending_the_draftail_editor)
## StreamField 확장하기
- [](streamfield_widget_api)
- [](custom_streamfield_blocks_media)
(extending_client_side_react)=