플래시와 토스트

화면 한쪽에 잠깐 떴다 사라지는 메시지다. 그 화면의 사용자가 일으킨 것은 핸들러가 put_flash()로 띄우는 플래시이고, 다른 사용자·뷰·백그라운드 작업이 보낸 것은 toast()·atoast()로 보내는 토스트다.

동작하는 예제: examples/notifications/ — 토스트를 다른 사용자에게 보낸다.

개요#

화면 한쪽에 잠깐 떴다 사라지는 메시지는 누가 일으켰느냐로 둘로 나뉜다.

플래시 토스트
일으키는 쪽 그 화면의 사용자 자신 ("저장했습니다") 다른 곳 — 다른 사용자, 뷰, 백그라운드 작업 ("bob이 리뷰를 요청했습니다")
경로 이벤트를 처리한 연결로 바로 받는 사람의 채널 → 레이아웃의 {% wireview_toasts %} → 그 연결
코드 핸들러에서 await self.put_flash(...) 보내는 쪽 toast(user, ...)·await atoast(user, ...), 받는 쪽은 레이아웃에 {% wireview_toasts %} 한 줄

토스트는 다른 곳에서 보낸 플래시다. 화면에 그리는 일은 put_flash() 하나가 하고, 토스트가 더하는 것은 "누구의 어느 연결로 가는가"뿐이다. 그래서 스타일·자동 닫힘·닫기 버튼이 둘 사이에 어긋나지 않는다.

둘 다 저장되지 않는다. 지금 열려 있는 페이지에만 뜨고, 나중에 연 페이지는 보지 못한다. 사용자가 나중에 확인해야 하는 것이면 모델에 저장하는 알림이 맞다 — 예제의 Notification이 그 모양이다.

작동 방식#

put_flash()는 그 컴포넌트의 연결로 flash 메시지를 보낸다. 클라이언트는 페이지에서 [wire-flash] 요소를 찾아 그 안에 메시지 요소를 붙인다.

<div wire-flash>
  <!-- put_flash()가 붙이는 요소 -->
  <div id="flash-…" class="wireview-flash wireview-flash-success wireview-flash-enter"
       role="alert" data-flash-type="success">
    <span class="wireview-flash-message">저장했습니다</span>
    <button type="button" class="wireview-flash-dismiss" aria-label="Dismiss">×</button>
  </div>
</div>
클래스 붙는 때
wireview-flash 항상
wireview-flash-<flash_type> flash_type 값 그대로 (success, error, info, warning 등)
wireview-flash-enter 붙은 다음 프레임. 등장 애니메이션을 여기에 건다
wireview-flash-exit 사라지기 시작할 때. animationend에 지워지고, 애니메이션이 없어도 500ms 뒤 지워진다

wireview는 스타일을 제공하지 않는다. 위 클래스에 CSS를 건다.

사용법#

플래시#

class ProductForm(Component):
    async def save(self):
        await Product.objects.acreate(name=self.name)
        await self.put_flash("success", "저장했습니다")
인자 기본값 뜻
flash_type — 클래스 이름에 들어가는 종류
message — 텍스트. HTML로 해석하지 않는다
timeout 5000 자동으로 닫히는 시간(ms). 0이면 닫지 않는다
dismissible True 닫기 버튼을 붙인다

await self.clear_flash()는 떠 있는 것을 모두 닫는다. 플래시 하나하나의 id는 브라우저가 띄울 때 만들므로 서버는 그 id를 알지 못한다 — clear_flash(flash_id)는 브라우저 쪽에서 얻은 id를 받았을 때만 쓸 수 있다.

컨테이너#

<body>
  {% component 'Header' %}
  <div class="toasts" wire-flash></div>
  ...
</body>

컨테이너는 컴포넌트 밖에 둔다. 컴포넌트 템플릿 안에 두면 그 컴포넌트가 다시 렌더될 때 morph가 서버 HTML에 없는 메시지 요소를 지운다.

토스트#

받는 쪽: 모든 페이지의 레이아웃에 {% wireview_toasts %}를 한 번 둔다. 보이지 않는 컴포넌트 하나가 그 연결의 사용자와 세션의 토스트 채널을 구독하고, 받은 것을 플래시로 띄운다.

<body>
  <div class="toasts" wire-flash></div>
  {% wireview_toasts %}
  ...
</body>

보내는 쪽: 어디서든 받을 사람을 이름으로 부른다.

from wireview import atoast, toast

# 뷰·시그널·관리 명령 같은 동기 코드. 트랜잭션이 커밋된 뒤에 나간다
toast(user, "다시 오신 것을 환영합니다")

# 컴포넌트 핸들러 같은 비동기 코드. 바로 나간다
await atoast(user, "지금 회의 들어와요", flash_type="warning", timeout=0)

# 로그인하지 않은 방문자: 세션 키로
toast(request.session.session_key, "장바구니에 담았습니다")
인자 기본값 뜻
첫째 (받는 사람) — 저장된 사용자, 또는 세션 키 문자열. 세션이 아직 없으면(None, "") ValueError
message — 텍스트
flash_type, timeout, dismissible "info", 5000, True put_flash()와 같다

구독이 곧 접근 제어다. 받는 컴포넌트는 자기 연결의 user와 세션의 채널만 구독하므로, 한 사람에게 보낸 토스트는 다른 사람의 페이지에 닿지 않는다. 채널 이름은 toast_channel(user_or_session_key)가 돌려준다 — 토스트를 자기 컴포넌트에서 직접 받고 싶으면 이 채널을 구독하고 notification()에서 put_flash()한다. 세션 키는 이름에 그대로 들어가지 않고 다이제스트로 들어간다. 세션 키는 그 세션의 자격 증명이고, signed_cookies 백엔드의 키는 :가 든 서명 쿠키 문자열이라 채널 레이어가 그룹 이름으로 받지 않는다. 사용자의 pk도 레이어가 받지 않는 문자(@ 등)가 있으면 다이제스트가 된다. 이름의 모양은 공개가 아니니 늘 toast_channel()로 얻는다.

주의사항#

  • [wire-flash]가 없으면 메시지는 버려진다. 브라우저 콘솔에 경고 한 줄이 남을 뿐 서버는 모른다.
  • 토스트는 {% wireview_toasts %}가 있는 열린 페이지에만 간다. 그 태그가 없는 페이지, 닫힌 탭에는 가지 않고 다시 오지도 않는다. 세션 키로 보낸 것은 그 세션이 연결될 때 이미 있던 키여야 한다 — 로그인하면 Django가 세션 키를 바꾼다. signed_cookies 백엔드에서는 세션에 무엇이든 쓸 때마다 키가 바뀐다(키가 세션 내용의 서명이다). 장바구니를 세션에 담고 toast(request.session.session_key, …)를 보내면 이미 열린 탭에는 닿지 않는다. 세션 키 토스트가 필요하면 서버 저장형 백엔드를 쓴다. 같은 사용자의 탭이 여럿이면 모든 탭에 뜬다.
  • 동기 broadcast()는 트랜잭션이 커밋된 뒤에 나간다. 롤백되면 나가지 않는다.
  • 리다이렉트 뒤에 보여줄 메시지는 플래시가 아니다. 플래시는 연결에 가므로 전체 페이지 이동이 끝나면 사라진다. 그 경우는 Django의 messages 프레임워크로 다음 페이지에 그린다.

관련 기능#