# Component API

`Component`와 `LiveComponent`의 공개 멤버 전부다. 여기 없는 멤버는 밑줄이 없어도 **내부**이고,
마이너 릴리스에서 바뀔 수 있다([호환성 정책](/wireview/upgrade/compatibility/)). 사용법은 표의 링크에 있다.

`tests/test_public_api.py`가 이 표와 클래스를 대조한다. 밑줄 없는 멤버를 새로 만들면 여기 적거나
아래 "내부" 목록에 넣어야 테스트가 통과한다.

## 이름

클래스 선언의 키워드 두 개가 컴포넌트의 이름을 정한다. `class XCounter(Component)`는 클래스명 `XCounter`로
등록되고 템플릿이 `{% component "XCounter" %}`, `"myapp:XCounter"`, FQN으로 찾는다. `name="..."`을 주면
클래스명 대신 그 이름으로 등록된다.

`public=False`면 등록하지 않는다. 템플릿과 join이 이 클래스를 찾지 못한다. 테스트의 컴포넌트나 공통 베이스가
흔히 이렇게 선언한다. 그래도 로그·계측·서명 상태가 쓸 이름은 있어야 하고, 그 이름은 아래 순서로 정한다.

1. `name="..."`을 줬으면 그 이름
2. 등록된 클래스를 상속했으면 그 클래스의 이름
3. 둘 다 아니면 자기 클래스명. 등록되지 않은 베이스(`LiveComponent` 포함)의 이름은 물려받지 않는다

## 필드

| 이름 | 뜻 |
|------|----|
| `id` | 페이지 안에서 고유한 컴포넌트 id |
| `user` | 연결의 사용자 (`AnonymousUser` 포함) |
| `session` | Django 세션의 읽기 전용 뷰 ([session](/wireview/reference/session/)) |
| `wire` | 내비게이션과 쿼리. 공개는 `params`, `redirect_to`, `replace_to`, `push_to`뿐이다 ([navigation](/wireview/reference/navigation/)) |
| `uploads` | 업로드 항목. 템플릿의 `this.uploads.<name>` ([external-uploads](/wireview/reference/external-uploads/)) |

선언한 필드는 상태다. JSON으로 직렬화되어야 하고 모델 인스턴스는 pk로 서명된다. 설정은 `class Meta:`에
둔다(README의 Meta 표).

## 오버라이드하는 것

wireview가 부르는 콜백이다. `get_subscriptions()`와 `new()` 말고는 모두 `async def`로 쓴다 —
sync로 쓰면 실행되지 않고 `manage.py check`가 `wireview.W002`로 알린다.

이 문서의 모든 멤버는 프레임워크의 것이라 오버라이드해도 클라이언트가 이벤트로 부를 수 없다. 클라이언트가 부를 수
있는 것은 사용자 코드가 정의한, `_`로 시작하지 않는 메서드뿐이다. `{% on "click" "joined" %}`처럼 그 밖의 이름에
바인딩하면 렌더가 `AssertionError`로 멈춘다.

| 메서드 | 언제 |
|--------|------|
| `joined()` | 소켓에 연결되어 첫 렌더를 보내기 전. 이미 연결된 페이지에서 부모의 렌더가 새로 그린 `{% component %}`는 부모 템플릿 안에서 인라인으로 그려져 나가고, 페이지가 그 요소를 받아 join할 때 돈다 — 그 join에 답하는 렌더가 `joined()`가 바꾼 상태를 그린다. LiveComponent는 부모의 렌더가 자기 렌더 전에 부른다 ([live-component](/wireview/reference/live-component/)) |
| `leaving()` | 컴포넌트가 떠날 때 (소켓이 닫힘, 페이지 이동, 부모가 뺌). `joined()`와 짝이다 — `joined()`가 돈 인스턴스만 받는다(예외로 끝났어도). 페이지가 join하기 전에 떠난 중첩 `{% component %}`, 부모의 렌더가 `joined()`를 부르기 전에 사라진 LiveComponent는 받지 않는다. 그 인스턴스의 비동기 작업은 그래도 취소된다. 그래서 `joined()`에서 등록하고 여기서 해제하면 짝이 맞는다 |
| `params_changed(params, uri)` | URL 쿼리가 바뀌었을 때 |
| `mutation(channel, action, instance)` | 구독한 모델이 바뀌었을 때 (`AUTO_BROADCAST`). `instance`는 알림에 실려 온 값에서 복원한 것이다 — DB에서 다시 읽지 않고, 관계는 id만 있다. 같은 알림을 받는 컴포넌트마다 따로 복원하므로 고쳐도 다른 컴포넌트가 받는 값은 그대로다. 페이로드에 없는 필드(다중 테이블 상속의 부모 모델 필드, `senders` 매핑에 적지 않은 필드 등)는 deferred라 읽으면 DB를 조회한다 — async에서는 `SynchronousOnlyOperation`이므로 `await instance.arefresh_from_db(fields=[...])`로 불러온다. 저장하면 보통의 저장이다(모델의 `save()`, `raw=False` 시그널). 무엇을 쓰는지는 [설정의 모델 알림](/wireview/reference/settings/#모델-알림) |
| `notification(channel, **kwargs)` | 구독한 채널로 브로드캐스트가 왔을 때 |
| `handle_async(name, result)` | `start_async()`가 끝났을 때. `result`는 끝난 `AsyncResult`(`ok`/`failed`) ([async-operations](/wireview/reference/async-operations/)) |
| `handle_hook_event(hook_id, event, payload)` | 클라이언트 훅이 `pushEvent`로 보냈을 때 ([hooks](/wireview/reference/hooks/)) |
| `on_upload_complete(name, entry)` | 업로드 항목 하나가 끝났을 때, 다시 렌더하기 전. 청크 업로드는 파일이 서버에 다 있을 때만 불린다 ([chunked-uploads](/wireview/reference/chunked-uploads/), [external-uploads](/wireview/reference/external-uploads/)) |
| `get_subscriptions()` | 구독 채널을 상태에 따라 정할 때. 기본은 `Meta.subscriptions` |
| `new(**kwargs)` (classmethod) | 인스턴스를 만들 때. 페이지 렌더, join, 테스트의 `mount()` 모두 이것을 거친다. `wire`·`user`·`session`과 상태 필드를 키워드로 받고 `cls(**kwargs)`를 돌려줘야 한다 |
| `LiveComponent.update(**assigns)` | 부모가 새 값을 줄 때 ([live-component](/wireview/reference/live-component/)) |
| `LiveComponent.update_many(updates)` (classmethod) | 부모 렌더 한 번에 값이 바뀐 같은 클래스 자식 전부를 `[(component, assigns), ...]`로. 기본은 각자의 `update()` |

## 부르는 것

| 메서드 | 하는 일 |
|--------|---------|
| `skip_render()` | 이번 이벤트의 렌더를 건너뛴다 |
| `force_render()` | 바뀐 것이 없어도 렌더한다 |
| `await send_render()` | 지금 렌더를 보낸다 (긴 작업의 중간 진행률) |
| `freeze()` | 이후 렌더를 보내지 않는다 |
| `await destroy()` | 컴포넌트를 페이지에서 뺀다 |
| `await broadcast(channel, **kwargs)` | 채널로 보낸다. `joined()` 전이면 모았다가 보낸다 |
| `await push_event(event, payload=None, hook_id=None)` | 클라이언트 훅으로 보낸다 |
| `await push_js(js)` | `JS()` 명령을 실행한다. 같은 핸들러의 렌더가 아직 패치되지 않았으면 그 패치 뒤에 돈다 — 렌더로 드러낸 요소를 `to=`로 겨냥해도 된다 |
| `await push_title(title)` | 문서 제목을 바꾼다 |
| `await put_flash(flash_type, message, *, timeout=5000, dismissible=True)` / `await clear_flash(flash_id=None)` | 플래시 ([flash](/wireview/reference/flash/)). `clear_flash()`는 모두 닫는다. id는 브라우저가 만들어 서버는 모른다 |
| `await focus_on(selector)` | 요소에 포커스 |
| `await scroll_into_view(element_id, *, behavior="auto", block="start", inline="nearest")` | 요소를 보이게 스크롤 |
| `await defer(f, *args, **kwargs)` | 지금 이벤트가 끝난 뒤 `f`를 부른다. 호출은 클라이언트 이벤트처럼 연결을 한 바퀴 돌아 이름으로 다시 들어오므로, `f`는 **클라이언트가 부를 수 있는 이 컴포넌트의 핸들러**여야 한다. `_` 헬퍼나 프레임워크 메서드를 넘기면 오류 없이 경고 로그 한 줄만 남기고 버려진다 |
| `await start_async(name, coro)` / `await cancel_async(name)` | 백그라운드 작업 ([async-operations](/wireview/reference/async-operations/)) |
| `await assign_async(coro, *, on_error=None)` | 결과를 `AsyncResult` 필드로 받는다 |
| `await stream(name, items, *, template=None, dom_id=None, limit=0)` | 스트림을 채우거나 다시 채운다 ([Streams API 심화 튜토리얼](/wireview/tutorial/streams-api/)) |
| `await stream_insert(name, item, *, at=-1, template=None, dom_id=None, limit=0)` / `await stream_delete(name, dom_id)` | 스트림 항목 |
| `allow_upload(name, *, accept=None, max_entries=1, max_file_size=None, chunk_size=None, auto_upload=True, external=None)` | 업로드를 받는다 ([chunked-uploads](/wireview/reference/chunked-uploads/)) |
| `consume_uploads(name)` / `await cancel_upload(name, ref)` | 완료된 업로드를 꺼낸다 / 취소한다 |
| `attach_hook(name, stage, callback)` / `detach_hook(name, stage=None)` | 이 인스턴스에 수명주기 훅을 단다 ([lifecycle-hooks](/wireview/reference/lifecycle-hooks/)) |
| `await send_update(live_component_id, **assigns)` | 자식 LiveComponent에 값을 준다 |
| `await LiveComponent.send_to_parent(event, **kwargs)` | 부모의 핸들러를 부른다 |

## 내부

밑줄이 없지만 공개가 아닌 것: `LiveComponent.myself`. Pydantic `BaseModel`에서 온 멤버
(`model_dump` 등)는 Pydantic의 것이고 wireview가 약속하지 않는다.
