(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)=