클라이언트 사이드 동작 확장하기

많은 종류의 일반적인 사용자 정의는 자바스크립트에 손대지 않고도 수행할 수 있지만, 활용하거나 사용자 정의하려는 클라이언트 측 상호 작용의 부분에 따라 React, Stimulus 또는 일반(vanilla) JS를 사용해야 할 수도 있습니다.

React는 사이드바, 댓글 시스템, Draftail 리치 텍스트 편집기와 같이 Wagtail의 더 복잡한 부분에 사용됩니다. 기본적인 자바스크립트 기반 상호 작용을 위해 Wagtail은 Stimulus로 마이그레이션하고 있습니다.

요소에 사용자 정의 동작을 추가하기 위해 이러한 라이브러리를 알거나 사용할 필요는 없으며, 많은 경우 간단한 자바스크립트로도 충분하지만, 더 복잡한 사용 사례에는 Stimulus가 권장되는 접근 방식입니다.

이러한 라이브러리를 기반으로 구축된 많은 사용자 정의를 위해 사용자 정의 Wagtail 설치에 Node.js 도구를 실행할 필요는 없지만, 패키지 빌드와 같은 일부 경우에는 더 복잡한 개발을 더 쉽게 만들 수 있습니다.

참고

jQuery 및 문서화되지 않은 jQuery 플러그인은 향후 Wagtail 버전에서 제거될 예정이므로 사용을 피하세요.

사용자 정의 자바스크립트 추가하기

Wagtail의 관리자 인터페이스 내에서 자바스크립트를 추가하는 몇 가지 방법이 있습니다.

가장 간단한 방법은 훅을 통해 전역 자바스크립트 파일을 추가하는 것입니다. insert_editor_jsinsert_global_admin_js를 참조하세요.

특정 위젯이 사용될 때 추가되는 자바스크립트의 경우, 내부 Media 클래스를 추가하여 위젯이 사용될 때 파일이 로드되도록 할 수 있습니다. 양식 Media 클래스에 대한 Django의 문서를 참조하세요.

비슷한 방식으로 Wagtail의 템플릿 컴포넌트는 렌더링될 때 스크립트를 추가하기 위해 media 속성 또는 Media 클래스를 제공합니다.

이렇게 하면 핵심 자바스크립트 관리자 파일이 이미 로드된 후 추가된 파일이 관리자에서 사용되도록 할 수 있습니다.

DOM 이벤트로 확장하기

클라이언트 측 사용자 정의나 새 구성 요소 채택에 접근할 때, 먼저 구현을 간단하게 유지하려고 노력하세요. 목표를 달성하기 위해 Stimulus, React, 자바스크립트 모듈 또는 빌드 시스템에 대한 지식이 필요하지 않을 수 있습니다.

브라우저에 동작을 연결하는 가장 간단한 방법은 DOM 이벤트와 일반(vanilla) 자바스크립트를 통하는 것입니다.

Wagtail의 사용자 정의 DOM 이벤트

Wagtail은 사용자 정의 DOM 이벤트를 수신하거나 전달하여 일부 사용자 정의 동작을 지원합니다.

Stimulus로 확장하기

Wagtail은 관리자 인터페이스 내에서 가벼운 클라이언트 측 상호 작용이나 사용자 정의 자바스크립트 위젯을 제공하는 방법으로 Stimulus를 사용합니다.

Stimulus 사용의 주요 이점은 모달, InlinePanel 또는 StreamField 패널과 같이 위젯이 동적으로 나타날 때 코드가 수동 초기화의 필요성을 피할 수 있다는 것입니다.

Stimulus 핸드북은 Stimulus를 사용하고 이해하는 방법에 대한 최고의 자료입니다.

사용자 정의 Stimulus 컨트롤러 추가하기

Wagtail은 Stimulus를 사용하기 위해 두 개의 클라이언트 측 전역 변수를 노출합니다.

  1. window.wagtail.app 핵심 관리자 Stimulus 애플리케이션 인스턴스.

  2. window.StimulusModule @hotwired/stimulus 에서 내보낸 Stimulus 모듈.

먼저, 자바스크립트 클래스 상속을 사용하여 기본 window.StimulusModule.Controller 를 확장하는 사용자 정의 Stimulus 컨트롤러를 만드세요. 빌드 도구를 사용하는 경우 import { Controller } from '@hotwired/stimulus'; 를 통해 기본 컨트롤러를 가져올 수 있습니다.

사용자 정의 컨트롤러를 만든 후에는 window.wagtail.app.register 메서드를 통해 Stimulus 컨트롤러를 수동으로 등록해야 합니다.

간단한 컨트롤러 예제

먼저, Wagtail 관리자 내 어딘가에 나타나도록 HTML을 만듭니다.

<!-- 콘솔에 'My controller has connected: hi'를 기록합니다. -->
<div data-controller="my-controller">Hi</div>
<!-- 콘솔에 'My controller has connected: hello'를 span 요소와 함께 기록합니다. -->
<div data-controller="my-controller">
    Hello <span data-my-controller-target="label"></span>
</div>

둘째, 컨트롤러 코드를 포함할 자바스크립트 파일을 만듭니다. 이 컨트롤러는 connect 시 간단한 메시지를 기록하는데, 이는 컨트롤러가 생성되고 일치하는 data-controller 속성을 가진 HTML 요소에 연결되었을 때 한 번 발생합니다.

// 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 관리자에 로드합니다.

# 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'<script src="{static("js/example.js")}"></script>',
    )

이제 HTML을 표시하던 관리자를 새로고침하면 콘솔에 두 개의 로그가 표시되는 것을 볼 수 있습니다.

더 복잡한 컨트롤러 예제

이제 입력된 단어 수를 보여주는 작은 output 요소를 제어되는 input 요소 옆에 추가하는 WordCountController 를 만들겠습니다.

// 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 = `<output name='word-count' for='${this.element.id}' class='output-label'></output>`;
        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 가 이 컨트롤러의 ‘구성’을 결정하고, 데이터 속성 작업이 출력 요소 업데이트의 ‘트리거’를 결정하게 됩니다.

# 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 훅 사용법의 더 고급 버전을 보여줍니다.

# 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', '<script src="{0}"></script>',
        ((static(filename),) for filename in js_files)
    )

이제 블로그 페이지에서 소개 필드에 사용된 단어 수와 최대 단어 수를 보여주는 작은 output 요소가 표시되는 것을 볼 수 있습니다.

더 복잡한 위젯 예제

더 복잡한 위젯의 경우, 이제 위젯이 렌더링된 HTML에 나타날 때마다, 초기 로드 시 또는 동적으로 인라인 script 요소 없이 추가 라이브러리를 통합할 수 있습니다.

이 예에서는 사용자 정의 위젯 옵션을 지원하는 Coloris 자바스크립트 라이브러리를 사용하여 색상 선택기 위젯을 빌드합니다.

먼저, Wagtail이 FieldPanelFieldBlock 에 대해 지원하는 Django 위젯 시스템을 기반으로 HTML부터 시작하겠습니다. build_attrs 메서드를 사용하여 컨트롤러에 전달되는 일반적인 데이터 구조를 지원하기 위해 적절한 Stimulus 데이터 속성을 구성합니다.

복잡한 값(이 경우 문자열 목록)에 대해 json.dumps 를 사용하고 있음을 확인하세요. Django는 렌더링될 때 이러한 값을 자동으로 이스케이프하여 안전하지 않은 클라이언트 측 코드의 일반적인 원인을 피합니다.

# 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 를 통해 제어되는 요소에 대한 참조를 포함하여 값을 자바스크립트 라이브러리로 전달합니다.

// 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 에서 사용할 수 있으며, 필드의 요소에 자바스크립트를 자동으로 인스턴스화합니다.

# 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)
# 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 를 사용하여 직접 제공할 수 있습니다.

// 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 컴포넌트를 사용자 정의하거나 확장하려면 React뿐만 아니라 다른 관련 라이브러리도 사용해야 할 수 있습니다.

이를 더 쉽게 하기 위해 Wagtail은 React 관련 종속성을 관리자 내에서 전역 변수로 노출합니다. 사용 가능한 패키지는 다음과 같습니다.

// 'focus-trap-react'
window.FocusTrapReact;
// 'react'
window.React;
// 'react-dom'
window.ReactDOM;
// 'react-transition-group/CSSTransitionGroup'
window.CSSTransitionGroup;

Wagtail은 또한 자체 React 컴포넌트 중 일부를 노출합니다. 다음을 재사용할 수 있습니다.

window.wagtail.components.Icon;
window.wagtail.components.Portal;

리치 텍스트 편집기를 포함하는 페이지는 다음에도 액세스할 수 있습니다.

// '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 확장하기

StreamField 확장하기