페이지 이동 boost (BOOST_PAGES)
같은 사이트 안의 링크 이동을 전체 페이지 로드 없이 처리한다. WebSocket 연결은 끊기지 않고, 새 페이지의 컴포넌트가 그 연결로 join한다.
설정:
WIREVIEW = {"BOOST_PAGES": True}. 기본은 꺼져 있다.
개요#
켜면 같은 사이트 안의 링크 이동이 전체 로드가 아니다. 다음 페이지를 fetch로 받아 <body>를 morph하고,
history에 항목을 남긴다. htmx의 hx-boost, Turbo Drive와 같은 생각이다. WebSocket 연결은 이어지고,
새 페이지의 컴포넌트가 그 연결로 join한다.
무엇이 boost되나#
| 무엇 | boost | 비고 |
|---|---|---|
같은 사이트의 링크(왼쪽 클릭, 수정키 없음, target 없음) |
✅ | |
{% on "click.prevent" %}가 처리한 링크 |
— | 컴포넌트 이벤트다 |
wire-boost를 단 폼 |
✅ | 아래 |
wire-boost가 없는 폼 |
— | 평소의 제출 |
wireview.visit(url) |
✅ | 코드에서 부르는 이동 |
live_session 경계를 넘는 이동은 boost하지 않고 전체 로드로 바꾼다. 응답을 받은 뒤에 판단하므로
리다이렉트가 경계 밖에서 끝나도 잡는다(live_session).
폼은 스스로 청한다 (wire-boost)#
<form method="post" action="{% url 'comments:add' %}" wire-boost>
{% csrf_token %}
...
</form>
폼은 기본으로 boost하지 않는다. boost한 이동은 WebSocket을 그대로 쓰는데, 로그인·로그아웃처럼 누가
로그인했는지를 바꾸는 폼이 boost되면 그 연결은 옛 신분으로 남는다. 안전한 폼만 wire-boost로 청한다.
- GET 폼: 필드를 쿼리로 붙인 주소로 이동한다. 브라우저가 가는 곳과 같다.
- 그 밖의 폼: fetch로 보낸다. 응답이 리다이렉트로 끝나면(post/redirect/get) 그 주소로 history 항목을 만든다. 리다이렉트 없이 페이지로 답하면(오류가 있는 폼) 주소는 그대로다. 새로고침해도 폼을 다시 보내지 않는다.
- 누른 버튼의
formaction·formmethod와name/value를 따른다.target이 다른 창이면 boost하지 않는다. - 컴포넌트가
{% on "submit.prevent" %}로 처리한 폼은 건드리지 않는다.
코드에서 이동하기 (wireview.visit)#
wireview.visit("/rooms/3/"); // 새 history 항목
wireview.visit("/rooms/3/", { replace: true }); // 지금 항목을 바꾼다
boost가 켜져 있고 같은 사이트면 링크처럼 이동하고, 아니면 평소처럼 페이지를 연다. 전체 로드로 넘어갔으면
false로 끝나는 Promise를 돌려준다. 숨긴 <a>를 만들어 click()하던 우회가 필요 없다.
이동을 건너 살아남기 (sticky)#
boost 이동은 body를 새 페이지로 morph하고, 두 페이지에 다 있는 컴포넌트도 새 페이지의 상태로 다시 join한다.
음악 플레이어, 채팅 창, 진행 중인 업로드처럼 페이지를 가로질러 이어져야 하는 컴포넌트는 sticky로 선언한다.
class Player(Component):
class Meta:
template_name = "player.html"
sticky = True
{# 두 페이지의 레이아웃에. id를 주지 않으면 클래스에서 만든 id가 붙는다 #}
{% component 'Player' %}
- 짝은 id로 맞춘다.
id=를 주지 않은 sticky 컴포넌트는 클래스의 전체 경로에서 만든 id를 받는다 (myapp.live.Player→sticky-myapp-live-Player). 어느 페이지에서 그려도 같은 id라 그대로 짝이 맞는다. sticky가 아닌 컴포넌트는 전처럼 렌더마다 새 id(rx-<uuid>)다. - 같은 sticky 클래스를 한 페이지에 둘 이상 두면 각자 id를 준다. id 없이 두 번 그리면 둘째부터는 새 id를
받아 이동을 건너지 못하고,
wireview로거에 경고가 남는다(#128). 두 요소가 id 하나를 나누면 페이지에는 컴포넌트 하나로 보이기 때문에 같은 id를 줄 수는 없다. 이 판단은 페이지의 첫 렌더(HTTP)에서 한다 — 라이브 렌더에서는 같은 id가 "같은 인스턴스를 다시 그림"이라 둘째 인스턴스와 구별되지 않는다. - 다음 페이지에도 같은 id로 있으면 요소를 건드리지 않는다. 서버 인스턴스와 상태, 구독, 요소의 DOM, 그 안의 훅이 그대로 이어진다. 새 페이지가 그 컴포넌트에 준 HTML과 값은 쓰이지 않는다.
- 다음 페이지에 없으면 보통 컴포넌트처럼 떠난다(
leaving()). 그 뒤 다시 나오면 새로 시작한다. live_session경계를 넘는 이동은 전체 로드라서 살아남지 않는다. 경계는 인증 가정이 바뀌는 곳이고, 그곳에서는 모든 것이 새 핸드셰이크를 탄다(live-session).BOOST_PAGES가 꺼져 있어도 모든 이동이 전체 로드라 살아남지 않는다.- 이어지는 것은 sticky 요소 안뿐이다. 그 밖은
<body>의 속성과 class까지 새 페이지의 것으로 바뀐다. sticky 안의 훅은 이동 중에mounted·updated·destroyed어느 것도 받지 않는다. 훅이 페이지 전체에 건 효과(body class, 스크롤 잠금 같은 것)는 이동 뒤 풀리므로, 훅의navigated()에서 다시 건다(아래 「이동을 알기」). 페이지마다 서버 템플릿이 그리게 둘 수 있으면 그쪽이 더 단순하다. - 이동한 뒤 쿼리가 바뀌었으면 sticky 컴포넌트도
params_changed()를 받는다. Component에만 쓴다.LiveComponent는 부모가 소유하므로 부모와 함께 간다.
이동을 알기 (wireview:navigated, navigated())#
boost 이동이 끝나면 — 새 페이지가 morph되고 그 컴포넌트들이 join을 보낸 뒤 — 두 가지가 이동마다 한 번 온다(#128).
- 이동 전부터 페이지에 있었고 이동 뒤에도 남은 훅의
navigated(). sticky 컴포넌트의 훅이 이동을 아는 유일한 길이다. 새 페이지가 가져온 훅은mounted()를, 빠진 훅은destroyed()를 받고navigated()는 받지 않는다. document에wireview:navigated이벤트.detail은{ url, previousUrl }이다.url은 리다이렉트를 따라 도착한 주소다.
window.wireview.hooks.Player = {
mounted() { document.body.classList.add("with-player"); },
navigated() { document.body.classList.add("with-player"); }, // 새 <body>에 다시
};
document.addEventListener("wireview:navigated", (e) => {
analytics.page(e.detail.url);
});
- 뒤로 가기는 캐시된 페이지를 먼저 그리고 서버의 페이지로 다시 맞추지만, 알림은 서버의 페이지가 그려진 뒤 한 번이다.
live_session경계를 넘는 이동과BOOST_PAGES가 꺼진 이동은 전체 로드라서 오지 않는다. 새 문서가 처음부터 시작한다.- 리다이렉트·
push_to처럼 서버가 보낸 이동도 boost로 가면 똑같이 온다.
리다이렉트#
이동이 리다이렉트를 따라가면 주소창은 도착한 주소다. 요청한 주소가 남아 있으면 새로고침이 리다이렉트하는 뷰를 다시 돌린다(#104).
관련 기능#
- live_session — 경계를 넘는 이동은 전체 로드
- JavaScript Hooks — 이동으로 빠지는 컴포넌트의 훅은
destroyed()를 받는다