Skip to main content

Wireview - Django를 위한 Phoenix LiveView

Wireview는 Django Channels를 사용하여 실시간 서버 렌더링 인터랙티브 UI를 구축할 수 있게 해주는 라이브러리입니다. Phoenix Framework의 LiveView와 유사합니다.

Wireview 아키텍처 개요

무엇이 포함되어 있나요?

VueJS나 ReactJS를 대체하는 것은 아니지만, Django의 모든 잠재력을 활용하여 인터랙티브한 프론트엔드를 만들 수 있습니다. 모든 것이 서버 사이드에서 렌더링되므로, 첫 번째 요청에서 의미 있는 정보가 포함된 인터페이스가 제공됩니다. Django 템플릿과 ORM의 모든 기능을 컴포넌트에서 직접 사용하고, 이벤트 구독을 통해 실시간으로 인터페이스를 업데이트할 수 있습니다.

주요 기능:

  • 실시간 업데이트가 가능한 서버 사이드 렌더링 컴포넌트
  • 자동 검증이 포함된 Pydantic 기반 상태 관리
  • Django Channels를 통한 WebSocket 통신
  • 효율적인 대역폭 사용을 위한 HTML diff
  • 자동 UI 업데이트를 위한 모델 구독
  • 대규모 리스트를 효율적으로 처리하는 Streams API
  • 온라인 사용자 및 타이핑 표시를 위한 Presence 추적
  • 진행률 추적이 가능한 파일 업로드
  • Chart.js, Mapbox 등 서드파티 라이브러리 통합을 위한 JavaScript Hooks

django-reactor 대비 개선 사항

Wireview는 django-reactor의 현대적인 진화 버전으로, 다음과 같은 중요한 개선 사항이 있습니다:

새로운 기능

기능 reactor wireview 설명
Streams API - ✅ stream(), stream_insert(), stream_delete()로 메모리 효율적인 대규모 리스트 처리
Presence API - ✅ PresenceMixin, PresenceTrackerMixin으로 실시간 사용자 추적 및 타이핑 표시
파일 업로드 - ✅ 진행률 추적, 매직 바이트 검증이 포함된 청크 업로드
AsyncResult - ✅ 비동기 작업을 위한 로딩/성공/에러 상태 관리
JS 명령어 - ✅ JS() 빌더로 Phoenix LiveView.JS 스타일의 클라이언트 사이드 명령어
테스트 유틸리티 - ✅ WebSocket 없이 쉽게 컴포넌트 테스트를 위한 mount() 유틸리티
디버그 도구 - ✅ wireview.debug로 브라우저 콘솔 디버깅
JavaScript Hooks - ✅ Chart.js, Mapbox 등 서드파티 JavaScript 라이브러리 통합
live_session - ✅ 페이지 단위 인증 경계. 같은 술어가 뷰와 join 양쪽에서 돌고, 경계를 넘는 이동은 전체 페이지 로드가 된다 (문서)

아키텍처 개선

항목 reactor wireview
Pydantic v1 (레거시) v2 (최신)
DOM Morphing morphdom idiomorph (더 나은 속성 보존)
Python ≥3.9 ≥3.12
Django 3.2+ 5.0, 5.1, 5.2, 6.0, 6.1
모듈 구조 플랫 체계적 (core/, features/)

새로운 컴포넌트 메서드

# 라이프사이클
async def leaving(self):
    """컴포넌트 연결 해제 시 호출 - 정리 훅"""

# UI 제어
await self.scroll_into_view(element_id, behavior="smooth")
await self.push_js(JS().set_value("input", ""))

# Streams
await self.stream("items", items)
await self.stream_insert("items", item, at=0)
await self.stream_delete("items", item_id)

# Presence
await self.presence_join()
await self.presence_set_typing(True)

# 비동기 로딩
self.data = await self.assign_async(fetch_data())

# JavaScript Hooks
await self.push_event("update_chart", {"data": [1, 2, 3]})

reactor에서 마이그레이션

대부분의 reactor 컴포넌트는 최소한의 변경으로 작동합니다:

# reactor
from reactor.component import Component

class XCounter(Component):
    _subscriptions = {"counter"}

# wireview (동일한 API)
from wireview.component import Component

class XCounter(Component):
    _subscriptions = {"counter"}

주요 차이점:

  • 패키지 이름: reactor → wireview
  • 설정 접두사: REACTOR_* → WIREVIEW dict
  • 템플릿 태그: {% load reactor %} → {% load wireview %}

목차

설치 및 설정

Wireview는 Python ≥3.12과 Django ≥5.0이 필요합니다 (Django 5.0, 5.1, 5.2 LTS, 6.0, 6.1 지원).

pip install django-wireview daphne

daphne는 개발 서버용입니다. Django의 runserver는 WSGI 서버라 WebSocket을 받지 못하고, daphne 앱이 INSTALLED_APPS 맨 위에 있을 때에만 ASGI로 바뀝니다. 빠뜨려도 오류는 나지 않고 페이지가 반응 없이 남습니다(runserver 기동 로그의 wireview.W013 경고가 유일한 신호입니다). daphne 대신 uvicorn project_name.asgi:application --reload로 띄워도 됩니다(Windows에서는 이쪽입니다 — docs/DEPLOYMENT.md).

Wireview는 django-channels를 사용하고, 채널 레이어가 반드시 있어야 합니다. Channels에는 기본 레이어가 없어서 CHANNEL_LAYERS를 비워 두면 WebSocket 연결이 전부 거절됩니다(manage.py check의 wireview.W012). 개발과 단일 프로세스에는 아래 설정의 InMemory 레이어면 충분합니다. 다만 InMemory는 프로세스 하나 안에서만 통하므로, 프로세스를 여러 개 띄우면 브로드캐스트가 오류 없이 같은 프로세스의 연결에만 닿습니다. 프로덕션에서는 프로세스를 잇는 레이어를 씁니다.

  • channels-nats — 이 프로젝트가 목표로 하는 레이어입니다. NATS 서버는 Go 바이너리 하나이고 Linux·macOS·Windows 네이티브 빌드가 있어, Redis 없이 SQLite 단일 서버와 Windows까지 같은 구성으로 갑니다.
  • channels_redis — Redis가 이미 있다면 이쪽입니다. 실측상 성능은 대등합니다.

배포 구성은 docs/DEPLOYMENT.md, 두 레이어의 실측 비교는 docs/design/transport-abstraction.md §5-3에 있습니다.

Django 애플리케이션보다 먼저 wireview와 channels를 INSTALLED_APPS에 추가하세요:

INSTALLED_APPS = [
    'daphne',      # 맨 위. runserver가 WebSocket을 받게 합니다
    'wireview',
    'channels',
    ...
]

ASGI_APPLICATION = 'project_name.asgi.application'

# 개발·단일 프로세스용. 프로세스를 늘릴 때는 위의 channels-nats나 channels_redis로 바꿉니다.
CHANNEL_LAYERS = {
    'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'},
}

project_name/asgi.py를 수정하세요:

import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings')

import django
django.setup()

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from wireview.urls import websocket_urlpatterns

application = ProtocolTypeRouter({
    'http': get_asgi_application(),
    'websocket': AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
})

템플릿에 wireview JavaScript를 포함하세요:

{% load wireview %}
<!doctype html>
<html>
    <head>
        {% wireview_header %}
    </head>
    ...
</html>

빠른 시작

x-counter.html 템플릿을 생성하세요:

{% load wireview %}
<div {% tag_header %}>
  {{ amount }}
  <button {% on 'click' 'inc' %}>+</button>
  <button {% on 'click' 'dec' %}>-</button>
  <button {% on 'click' 'set_to' amount=0 %}>reset</button>
</div>

live.py에 컴포넌트를 생성하세요:

from wireview.component import Component


class XCounter(Component):
    _template_name = 'x-counter.html'

    amount: int = 0

    async def inc(self):
        self.amount += 1

    async def dec(self):
        self.amount -= 1

    async def set_to(self, amount: int):
        self.amount = amount

뷰 템플릿에서 컴포넌트를 렌더링하세요:

{% load wireview %}
<!doctype html>
<html>
    <head>
        {% wireview_header %}
    </head>
    <body>
        {% component 'XCounter' %}
        {% component 'XCounter' amount=100 %}
    </body>
</html>

예제

동작하는 예제 앱 11개가 examples/에 있습니다. 각 디렉터리가 개념 하나이고, 테스트와 README를 함께 가지고 있으며, CI가 매번 실행합니다.

예제 개념
todo 모델 구독으로 여러 탭이 같은 목록을 함께 본다
poll 쓰기는 핸들러가, 다시 그리기는 브로드캐스트가
rating 잠깐 쓰는 상태와 남는 상태를 갈라 둔다
search 디바운스한 입력과 키보드로 고르는 결과
quiz 컴포넌트 상태로 굴리는 상태 머신
chat Streams와 Presence
dashboard AsyncResult로 느린 조회를 미룬다
notifications 이름 붙인 채널로 컴포넌트끼리 알린다
livecomp 연결을 공유하는 중첩 컴포넌트
slots 내용을 호출자가 채우는 레이아웃 컴포넌트
hooks 브라우저만 할 수 있는 일을 컴포넌트에 붙인다
make build-js && make run-daphne     # http://localhost:8000

컴포넌트 라이프사이클

초기화 및 렌더링

컴포넌트는 템플릿에 포함될 때 초기화됩니다:

{% component 'Component' param1=1 param2=2 %}

파라미터는 컴포넌트 인스턴스를 반환하는 Component.new()에 전달됩니다.

조인 (Joins)

컴포넌트가 프론트엔드에 도달하면 WebSocket을 통해 백엔드에 "조인"합니다. 직렬화된 상태가 백엔드로 전송되고, 백엔드는 컴포넌트를 재구성하고 Component.joined()를 호출합니다.

class ChatRoom(Component):
    async def joined(self):
        # 컴포넌트가 WebSocket으로 연결될 때 호출됨
        await self.broadcast(f"room.{self.room_id}", action="joined", user=self.username)

퇴장 (Leaving)

컴포넌트가 파괴되거나 WebSocket 연결이 닫히면 Component.leaving()이 호출됩니다. 정리 작업에 사용하세요:

class ChatRoom(Component):
    async def leaving(self):
        # 컴포넌트 연결이 해제될 때 호출됨
        await self.broadcast(f"room.{self.room_id}", action="left", user=self.username)

사용자 이벤트

조인 후 컴포넌트는 {% on %} 템플릿 태그를 통해 사용자 이벤트를 받을 수 있습니다. 이벤트는 백엔드로 전송되고, 핸들러가 실행되며, 컴포넌트가 다시 렌더링됩니다.

모델 구독

컴포넌트는 모델 변경을 구독할 수 있습니다. 변경이 발생하면 Component.mutation()이 호출됩니다:

class TodoList(Component):
    _subscriptions = {"todo.item"}  # todo 앱의 Item 모델 변경 구독

    async def mutation(self, channel: str, action: ModelAction, instance):
        # 구독한 모델이 변경될 때 호출됨
        self.items = await self.load_items()

알림

임의의 메시지에는 broadcast()와 notification()을 사용하세요:

# 발신자
await self.broadcast("chat.room.1", message="Hello!", sender=self.username)

# 수신자 ("chat.room.1" 구독 중)
async def notification(self, channel: str, **kwargs):
    message = kwargs.get("message")
    sender = kwargs.get("sender")

이벤트 바인딩

기본 문법

{% on <event.modifiers> <handler> [kwargs] %}

예제:

<button {% on "click" "increment" %}>+1</button>
<button {% on "click" "increment" amount=5 %}>+5</button>
<button {% on "click.prevent" "submit" %}>제출</button>
<input {% on "keypress.enter" "search" %}>
<input {% on "input.debounce.300" "filter" %}>

사용 가능한 수정자

수정자 설명
prevent event.preventDefault() 호출
stop event.stopPropagation() 호출
ctrl, alt, shift, meta 수정 키 필요
debounce.<ms> 이벤트 디바운스 (예: debounce.300)
throttle.<ms> 이벤트 쓰로틀 (예: throttle.100)
enter, tab, delete, backspace, space 키 별칭
up, down, left, right 화살표 키 별칭
key.<keycode> 특정 키 (예: key.escape)
inlinejs 핸들러를 리터럴 JavaScript로 처리

암시적 인자

컴포넌트 내의 폼 입력은 자동으로 인자로 전송됩니다:

<div {% tag_header %}>
  <input name="query">
  <button {% on "click" "search" %}>검색</button>
</div>
async def search(self, query: str):
    self.results = await self.do_search(query)

URL 상태 관리

URL 쿼리 문자열에 컴포넌트 상태를 저장하세요:

class SearchList(Component):
    query: str = ""

    @classmethod
    def new(cls, wire, **kwargs):
        kwargs.setdefault("query", wire.params.get("query", ""))
        return cls(wire=wire, **kwargs)

    async def filter_results(self, query: str):
        self.query = query
        self.wire.params["query"] = query  # URL 업데이트

복잡한 값에는 .json 접미사를 사용하세요:

class TreeView(Component):
    @classmethod
    def new(cls, wire, id: str, **kwargs):
        kwargs["expanded"] = id in wire.params.get("expanded.json", [])
        return cls(wire=wire, id=id, **kwargs)

    async def toggle_expanded(self):
        self.expanded = not self.expanded
        expanded = self.wire.params.setdefault("expanded.json", [])
        if self.expanded:
            expanded.append(self.id)
        elif self.id in expanded:
            expanded.remove(self.id)

모델 구독

자동 UI 업데이트를 위해 Django 모델 변경을 구독하세요:

class TodoList(Component):
    _subscriptions = {"todo.item"}  # {app_label}.{model_name} 형식

    async def mutation(self, channel: str, action: ModelAction, instance):
        if action == ModelAction.CREATED:
            self.items.append(instance)
        elif action == ModelAction.DELETED:
            self.items = [i for i in self.items if i.id != instance.id]

설정에서 자동 브로드캐스트를 활성화하세요:

WIREVIEW = {
    "AUTO_BROADCAST": AutoBroadcast(
        model=True,      # 모델 변경 시 브로드캐스트
        model_pk=True,   # 채널 이름에 PK 포함
    ),
}

Streams API

Streams는 아이템을 개별적으로 렌더링하고 증분 업데이트를 전송하여 대규모 리스트를 메모리 효율적으로 처리합니다.

기본 사용법

스트림 컨테이너가 있는 템플릿:

{% load wireview %}
<div {% tag_header %}>
  <ul wire-stream="messages">
    {% for message in messages %}
      {% include "chat/message_item.html" %}
    {% endfor %}
  </ul>
</div>

아이템 템플릿 (chat/message_item.html):

<li id="messages-{{ message.pk }}">
  <strong>{{ message.sender }}:</strong> {{ message.text }}
</li>

컴포넌트:

class MessageList(Component):
    _template_name = "chat/message_list.html"
    messages: list = []

    async def joined(self):
        # 스트림으로 초기 로드
        messages = await Message.objects.order_by('-created')[:50]
        await self.stream("messages", reversed(messages))

    async def add_message(self, text: str):
        message = await Message.objects.acreate(sender=self.user, text=text)
        await self.stream_insert("messages", message, at=-1)  # 끝에 추가
        await self.scroll_into_view(f"messages-{message.pk}")

    async def delete_message(self, message_id: int):
        await Message.objects.filter(id=message_id).adelete()
        await self.stream_delete("messages", message_id)

Stream 메서드

메서드 설명
stream(name, items) 스트림 초기화/리셋
stream_insert(name, item, at=-1) 아이템 삽입 (-1=끝, 0=처음, n=인덱스)
stream_delete(name, dom_id) DOM ID 또는 PK로 아이템 삭제

DOM ID 규칙

기본적으로 DOM ID는 {stream_name}-{item.pk} 패턴을 따릅니다. 커스텀 ID 함수:

await self.stream("items", items, dom_id=lambda item: f"item-{item.uuid}")

커스텀 아이템 템플릿

await self.stream_insert("messages", message, template="chat/special_message.html")

Presence API

온라인 사용자와 타이핑 표시를 실시간으로 추적합니다.

PresenceMixin (프로듀서)

자신의 프레즌스를 브로드캐스트하는 컴포넌트용:

from wireview.component import Component
from wireview.features.presence import PresenceMixin


class ChatInput(PresenceMixin, Component):
    _template_name = "chat/input.html"
    room_id: int
    username: str

    def _presence_topic(self) -> str:
        return f"room.{self.room_id}"

    def _presence_user_id(self) -> str:
        return str(self.user_id)

    def _presence_username(self) -> str:
        return self.username

    async def joined(self):
        await self.presence_join()

    async def leaving(self):
        await self.presence_leave()

    async def on_typing(self):
        await self.presence_set_typing(True)  # 3초 후 자동 해제

PresenceTrackerMixin (컨슈머)

다른 사용자의 프레즌스를 표시하는 컴포넌트용:

from wireview.features.presence import PresenceTrackerMixin


class OnlineUsers(PresenceTrackerMixin, Component):
    _template_name = "chat/online_users.html"
    room_id: int
    username: str

    def _presence_topic(self) -> str:
        return f"room.{self.room_id}"

    def _presence_my_user_id(self) -> str:
        return str(self.user_id)

    @property
    def _subscriptions(self):
        return {self._presence_channel()}

    async def joined(self):
        await self.presence_track_self(username=self.username)

템플릿:

{% load wireview %}
<div {% tag_header %}>
  <h3>온라인 ({{ this.presence_online_count }})</h3>
  <ul>
    {% for user in this.presence_users %}
      <li>
        {{ user.username }}
        {% if user.is_typing %}<span class="typing">입력 중...</span>{% endif %}
      </li>
    {% endfor %}
  </ul>
</div>

Presence 속성

속성 설명
presence_users 모든 추적된 사용자 목록
presence_online_count 온라인 사용자 수
presence_typing_users 현재 타이핑 중인 사용자 목록

설정

from wireview.features.presence import PresenceConfig

class MyComponent(PresenceMixin, Component):
    _presence_config = PresenceConfig(
        typing_timeout=3.0,     # 타이핑 자동 해제까지 초
        sync_on_join=True,      # 조인 시 다른 사용자에게 동기화 요청
        channel_prefix="presence",
    )

파일 업로드

진행률 추적과 검증이 포함된 파일 업로드를 처리합니다.

기본 설정

from wireview.component import Component
from wireview.features.uploads import UploadConfig


class FileUploader(Component):
    _template_name = "uploader.html"

    async def joined(self):
        self.allow_upload(UploadConfig(
            name="avatar",
            accept=[".jpg", ".png", ".gif"],
            max_file_size=5 * 1024 * 1024,  # 5MB
            max_entries=1,
        ))

    async def save_avatar(self):
        for upload in self.consume_uploads("avatar"):
            path = await upload.save_to("avatars/", filename=f"{self.user_id}.jpg")
            self.avatar_url = path

템플릿:

{% load wireview %}
<div {% tag_header %}>
  <input type="file" wire-upload="avatar" accept=".jpg,.png,.gif">

  {% for entry in this.uploads.avatar %}
    <div class="upload-entry">
      {{ entry.client_name }} - {{ entry.progress }}%
      {% if entry.errors %}
        <span class="error">{{ entry.errors|join:", " }}</span>
      {% endif %}
    </div>
  {% endfor %}

  <button {% on "click" "save_avatar" %}>저장</button>
</div>

UploadConfig 옵션

옵션 기본값 설명
name 필수 업로드 필드 식별자
accept [] 허용된 확장자 (예: [".jpg", ".png"])
max_entries 1 최대 동시 업로드 수
max_file_size 10MB 최대 파일 크기 (바이트)
chunk_size 64KB 업로드 청크 크기
auto_upload True 선택 시 즉시 업로드 시작

ConsumedUpload 메서드

메서드 설명
read() 전체 파일을 메모리로 읽기
open(mode="rb") 파일 핸들 열기
save_to(directory, filename=None) Django 스토리지에 저장
name 원본 파일명
size 파일 크기 (바이트)
content_type MIME 타입

보안

Wireview는 확장자 위조를 방지하기 위해 저장 전에 파일 시그니처(매직 바이트)를 검증합니다.

AsyncResult와 비동기 작업

로딩/에러 상태와 함께 비동기 데이터 로딩을 처리합니다:

from wireview import Component, AsyncResult


class Dashboard(Component):
    _template_name = "dashboard.html"
    stats: AsyncResult = None

    async def joined(self):
        self.stats = await self.assign_async(self.load_stats())

    async def load_stats(self):
        return await Stats.objects.aget()

템플릿:

{% if stats.loading %}
  <div class="spinner">로딩 중...</div>
{% elif stats.ok %}
  <div>총계: {{ stats.result.total }}</div>
{% elif stats.failed %}
  <div class="error">{{ stats.error_message }}</div>
{% endif %}

AsyncResult 속성

속성 설명
loading 작업 진행 중이면 True
ok 작업 성공이면 True
failed 작업 실패면 True
done 완료되면 True (성공 또는 실패)
result 결과 값 (성공 시)
error 예외 (실패 시)
error_message 에러의 문자열 표현

AsyncResult 메서드

메서드 설명
map(func) 결과 값 변환
get_or(default) 결과 또는 기본값 가져오기
get_or_raise() 결과 가져오기 또는 에러 발생

JS 명령어 빌더

서버 왕복 없이 실행되는 클라이언트 사이드 명령어를 빌드합니다:

from wireview import JS

# 템플릿에서
<button {% on "click" JS().toggle("#modal") %}>모달 토글</button>

# 명령어 체이닝
<button {% on "click" JS().add_class("#btn", "loading").push("save") %}>
  저장
</button>

# 트랜지션과 함께
<div {% on "click" JS().hide(transition=("fade-out", 300)) %}></div>

서버에서 JS 푸시

이벤트 핸들러에서 JS 명령어 전송:

async def clear_input(self):
    await self.push_js(JS().set_value("input[name=search]", ""))

사용 가능한 명령어

표시:

  • show(selector, transition=None, display=None)
  • hide(selector, transition=None)
  • toggle(selector, show=None, hide=None)

CSS 클래스:

  • add_class(selector, classes, transition=None)
  • remove_class(selector, classes, transition=None)
  • toggle_class(selector, classes, transition=None)

속성:

  • set_attr(selector, attr, value)
  • remove_attr(selector, attr)
  • set_value(selector, value) - 입력 값 설정

포커스:

  • focus(selector)
  • focus_first(selector, input_only=False)

트랜지션:

  • transition(selector, classes, time=None)

서버 통신:

  • push(event, value=None, target=None) - 서버로 이벤트 전송

네비게이션:

  • navigate(url, replace=False)
  • dispatch(event, to=None, detail=None, bubbles=True)

로딩 클래스

서버 요청 중 다음 클래스가 자동으로 추가됩니다:

클래스 설명
wireview-loading 모든 요청 중에 추가
wireview-click-loading 클릭 이벤트에 추가
wireview-submit-loading 제출 이벤트에 추가
.wireview-loading {
  opacity: 0.5;
  pointer-events: none;
}

JavaScript Hooks

JavaScript Hooks를 사용하면 Chart.js, Mapbox, CodeMirror 등 서드파티 JavaScript 라이브러리를 wireview 컴포넌트와 통합할 수 있습니다. Phoenix LiveView의 Hooks API를 따릅니다.

Hook 정의

window.wireview.hooks.ChartHook = {
  mounted() {
    // 엘리먼트가 페이지에 추가되면 호출
    const config = JSON.parse(this.el.dataset.config);
    this.chart = new Chart(this.el, config);
  },

  updated() {
    // DOM 업데이트 후 호출
    this.chart.update();
  },

  destroyed() {
    // 엘리먼트가 제거되면 호출
    this.chart.destroy();
  },

  disconnected() {
    // WebSocket 연결이 끊기면 호출
    this.el.classList.add('offline');
  },

  reconnected() {
    // WebSocket이 재연결되면 호출
    this.el.classList.remove('offline');
  }
};

템플릿에서 사용

<div wire-hook="ChartHook" data-config='{"type": "line", "data": {...}}'>
</div>

서버로 이벤트 전송 (pushEvent)

window.wireview.hooks.InfiniteScroll = {
  mounted() {
    this.page = 1;
    this.observer = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting) {
        this.loadMore();
      }
    });
    this.observer.observe(this.el.querySelector('.sentinel'));
  },

  loadMore() {
    this.pushEvent("load_more", { page: this.page }, (response) => {
      if (response.hasMore) {
        this.page++;
      } else {
        this.observer.disconnect();
      }
    });
  },

  destroyed() {
    this.observer.disconnect();
  }
};

서버에서 이벤트 받기 (handleEvent)

window.wireview.hooks.Notification = {
  mounted() {
    this.handleEvent("show_toast", ({ message, type }) => {
      this.showToast(message, type);
    });
  },

  showToast(message, type) {
    // 토스트 표시 구현
  }
};

서버 사이드 핸들러

class Dashboard(Component):
    _template_name = "dashboard.html"

    async def handle_hook_event(self, hook_id: str, event: str, payload: dict):
        """JavaScript Hook에서 보낸 이벤트 처리"""
        if event == "load_more":
            items = await self.fetch_items(payload.get("page", 1))
            return {"hasMore": len(items) == 20}
        return None

    async def update_chart(self, data: list):
        """모든 Hook에 이벤트 전송"""
        await self.push_event("update_data", {"values": data})

Hook 라이프사이클

콜백 호출 시점
mounted() 엘리먼트가 조인되고 첫 렌더링 후
beforeUpdate() DOM morph 전 (동기)
updated() DOM morph 완료 후
destroyed() 엘리먼트가 DOM에서 제거될 때
disconnected() WebSocket 연결이 닫힐 때
reconnected() WebSocket이 재연결될 때

Hook 컨텍스트

속성/메서드 설명
this.el Hook이 연결된 DOM 엘리먼트
this.pushEvent(event, payload, callback) 서버로 이벤트 전송
this.handleEvent(event, callback) 서버 이벤트 핸들러 등록

자세한 내용은 JavaScript Hooks 문서를 참조하세요.

컴포넌트 API 레퍼런스

클래스 속성

속성 기본값 설명
_template_name 필수 템플릿 경로
_exclude_fields {"user", "wire"} 직렬화에서 제외할 필드
_subscriptions set() 구독할 채널

라이프사이클 메서드

메서드 설명
new(cls, wire, **kwargs) 인스턴스 생성 클래스 메서드
joined() 컴포넌트가 WebSocket으로 연결될 때 호출
leaving() 컴포넌트 연결이 해제될 때 호출
mutation(channel, action, instance) 모델 변경 시 호출
notification(channel, **kwargs) 브로드캐스트 메시지 시 호출
handle_hook_event(hook_id, event, payload) Hook 이벤트 수신 시 호출

렌더 제어

메서드 설명
skip_render() 다음 렌더 사이클 건너뛰기
send_render() 즉시 렌더 강제
force_render() 다시 렌더링 표시
freeze() 모든 향후 렌더 방지

액션

메서드 설명
destroy() 인터페이스에서 컴포넌트 제거
focus_on(selector) 요소에 포커스
scroll_into_view(element_id, behavior="auto", block="start", inline="nearest") 요소를 뷰로 스크롤
push_js(js) 클라이언트에서 JS 명령어 실행
dom(action, id, component_or_template, **kwargs) DOM 조작
deffer(func, *args, **kwargs) 함수 실행 지연
push_event(event, payload, hook_id=None) Hook에 이벤트 전송

브로드캐스팅

메서드 설명
broadcast(channel, **kwargs) 채널로 메시지 전송 (joined()에서 큐잉)
abroadcast(channel, **kwargs) 즉시 메시지 전송 (비동기)

네비게이션

메서드 설명
wire.redirect_to(url, **kwargs) 네비게이트하고 새 페이지 가져오기
wire.replace_to(url, **kwargs) 현재 URL 교체
wire.push_to(url, **kwargs) 가져오기 없이 URL 푸시

Streams

메서드 설명
stream(name, items, template=None, dom_id=None) 스트림 초기화/리셋
stream_insert(name, item, at=-1, template=None, dom_id=None) 아이템 삽입
stream_delete(name, dom_id) 아이템 삭제

Uploads

메서드 설명
allow_upload(config) 업로드 설정 등록
consume_uploads(name) 완료된 업로드 가져오기
cancel_upload(name, ref) 업로드 취소

템플릿 태그 레퍼런스

{% load wireview %}
태그 설명
{% wireview_header %} 필요한 JavaScript 포함 (~10KB 압축)
{% component 'Name' kwarg=value %} 컴포넌트 렌더링
{% on 'event.modifiers' 'handler' kwargs %} 이벤트 핸들러 바인딩
{% tag_header %} 루트 요소에 컴포넌트 속성 추가
{% cond {'hidden': is_hidden} %} 조건부 속성
{% class {'active': is_active} %} 조건부 CSS 클래스

프론트엔드 API

// 컴포넌트에 이벤트 전송
wireview.send(element, 'handler_name', {arg1: value1})

// 디바운스/쓰로틀
wireview.debounce(300)(fn)
wireview.throttle(100)(fn)

// JS 명령어 실행
wireview.exec(element, commands)

// Hook 정의
wireview.hooks.MyHook = {
  mounted() { /* ... */ },
  updated() { /* ... */ },
  destroyed() { /* ... */ }
}

// 디버그 유틸리티
wireview.debug.enable()
wireview.debug.disable()
wireview.debug.status()

컴포넌트 테스트

WebSocket 없이 컴포넌트 테스트:

import pytest
from wireview.testing import mount


@pytest.mark.asyncio
async def test_counter_increment():
    view = await mount(Counter, count=0)
    await view.call("increment", amount=5)
    assert view.component.count == 5
    assert len(view.sent_messages) > 0


@pytest.mark.asyncio
async def test_redirect():
    view = await mount(MyComponent)
    await view.call("do_redirect", url="/dashboard")
    assert view.redirected_to == "/dashboard"
    assert view.is_frozen

테스트 API

메서드/속성 설명
mount(ComponentClass, **kwargs) 테스트용 컴포넌트 마운트
view.component 컴포넌트 인스턴스 접근
view.call(handler, **kwargs) 이벤트 핸들러 호출
view.sent_messages 전송될 메시지들
view.redirected_to 리다이렉트 URL (있는 경우)
view.is_frozen 컴포넌트 동결 여부
view.clear_messages() 전송 메시지 초기화

디버그 도구

// 디버그 로깅 활성화
wireview.debug.enable()

// 디버그 로깅 비활성화
wireview.debug.disable()

// 네트워크 지연 시뮬레이션
wireview.debug.latency(500)  // 500ms 지연

// 연결 상태 표시
wireview.debug.status()

// 모든 컴포넌트 나열
wireview.debug.components()

// 특정 컴포넌트 가져오기
wireview.debug.component("rx-123")

설정

from wireview.schemas import AutoBroadcast

WIREVIEW = {
    "TRANSPILER_CACHE_SIZE": 1024,    # 이벤트 핸들러 캐시 크기
    "USE_HTML_DIFF": True,            # HTML diff 활성화
    "USE_HMIN": False,                # django-hmin 압축 사용
    "BOOST_PAGES": False,             # 클라이언트 사이드 네비게이션 활성화
    # 서명 상태(data-state) — docs/features/html-diff.md
    "STATE_MAX_AGE": 14 * 24 * 3600,  # 서명 상태 유효 기간(초). 기본 14일
    "STATE_REFRESH_AFTER": None,      # 상태가 같아도 이 시간이 지나면 토큰 재발급. None이면 STATE_MAX_AGE // 2
    "STATE_ACCEPT_LEGACY": False,     # v1 봉투 이전 형식 허용(혼재 배포 구간에만)
    # 서명 키 — docs/features/chunked-uploads.md
    "SIGNING_KEY": None,              # None이면 Django의 SECRET_KEY. 업로드 토큰과 data-state의 수명을 분리한다
    "SIGNING_KEY_FALLBACKS": None,    # None이면 SECRET_KEY_FALLBACKS. 자체 키를 두면 이것도 같이 둔다
    # 청크 업로드 — docs/features/chunked-uploads.md
    "UPLOAD_TEMP_DIR": None,          # 청크 저장소. None이면 시스템 temp. 워커들이 공유해야 한다
    "UPLOAD_MAX_FILE_SIZE": 10 * 1024 * 1024,
    "UPLOAD_CHUNK_SIZE": 64 * 1024,
    "UPLOAD_TOKEN_MAX_AGE": 3600,     # 업로드 토큰 유효 기간이자 청소 기준 나이
    "AUTO_BROADCAST": AutoBroadcast(
        model=False,       # 모델 변경 시 브로드캐스트
        model_pk=False,    # 채널에 PK 포함
        related=False,     # 관련 모델 변경 브로드캐스트
        m2m=False,         # M2M 변경 브로드캐스트
        senders=set(),     # 자동 브로드캐스트할 모델
    ),
}

성능 최적화

최적의 성능을 위해:

  • uvloop 사용: Uvicorn에서 --loop uvloop 옵션으로 더 나은 비동기 성능 달성
  • 개발 중 전환 추적: DEBUG_SYNC_TRANSITIONS=True로 중첩 async/sync 전환 감지
  • 컴포넌트에서 asend_to() 선호: async 컨텍스트에서는 send_to() 대신 asend_to() 사용
WIREVIEW = {
    "DEBUG_SYNC_TRANSITIONS": True,  # 개발 환경에서만
    "USE_HTML_DIFF": True,
    "USE_HMIN": True,  # django-hmin 설치 필요
}

자세한 내용은 성능 가이드를 참조하세요.

문서

개발 및 기여

git clone git@github.com:itda-work/django-wireview.git
cd django-wireview
make install
make test

테스트 서버 실행:

cd tests
python manage.py runserver

라이선스

MIT 라이선스 - 자세한 내용은 LICENSE를 참조하세요.

Release files for django-wireview 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-wireview 0.4.0
File Size Uploaded
django_wireview-0.4.0.tar.gz 275.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-wireview 0.4.0
File Interpreter ABI Platform
django_wireview-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 524.5 kB

Release files / django_wireview-0.4.0.tar.gz

Download URL django_wireview-0.4.0.tar.gz
Size 275.6 kB
Tags Source
SHA-256 checksum
How to use checksums
75899b059ac50a0756850f36883c1a8d32fde131d5a3d69a09ae4fc10c44bdcc
BLAKE2b-256 checksum
How to use checksums
5295060493d8bc4eae43092a9c0fa479bc663158a34f4799fb7f946fa2d14bcc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release files / django_wireview-0.4.0-py3-none-any.whl

Download URL django_wireview-0.4.0-py3-none-any.whl
Size 249.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
79e705d6111ff825e57473bd82f4493911f96998c48348517a2ba91ff764e793
BLAKE2b-256 checksum
How to use checksums
3e3faab5ed52c35d0a28e5d62c1c149dec74c6a61c000eefab3f18e510a0476b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page