# 비동기 로딩과 여러 채팅방

> uchat.js가 불러와지기 전에 이벤트를 거는 방법, 한 페이지의 여러 채팅방에 이벤트를 나눠 거는 방법, SPA에서 채팅방을 붙이고 정리하는 방법, 오른쪽 아래에 떠 있는 위젯을 붙이는 방법을 설명해요.

`uchat.js`는 `async`로 불러오므로 페이지 스크립트와 실행 순서가 정해져 있지 않아요. 이 문서는 로딩 순서와 관계없이 이벤트를 거는 방법, 채팅방 여러 개를 다루는 방법, SPA에서 채팅방을 붙였다 떼는 방법을 다뤄요.

## 미리 등록 코드 {#preload}

아래 코드를 **`uchat.js`보다 먼저** 넣어요. `uchat.js`가 준비되기 전에 건 이벤트를 `U.events`에 보관해 두었다가, 준비되면 그대로 써요.

```html
<script>U=window.U=window.U||{},U.events=U.events||[],U.chat=function(n){return{on:function(e,t){U.events.push([n,e,t])},off:function(e){for(var t=U.events.length;t>0;)U.events[--t][0]==n&&U.events[t][1]==e&&U.events.splice(t,1)}}};</script>
<script async src="https://client.uchat.io/uchat.js"></script>
```

`uchat.js`가 준비되면 `U.chat`은 진짜 함수로 바뀌어요. 그 전과 후의 차이는 하나예요.

| | 준비 전(미리 등록 코드) | 준비 후 |
| --- | --- | --- |
| `on(이름, 콜백)` | 보관 | 등록(이미 준비된 방의 `after.create`는 바로 호출) |
| `off(이름)` | 이름이 **글자 그대로 같은** 것만 지움 | 이름표·이벤트 이름으로 골라 지움(아래 참고) |

준비 전에 `off('#이름표')`처럼 이름표만 주면 아무것도 지워지지 않아요. 언제 불릴지 모르는 정리 코드에서는 `on`에 쓴 이름 전체(`'after.message#alarm'`)를 그대로 `off`에 넘기세요. 이 방식은 준비 전과 후 모두에서 동작해요.

## 채팅방 고르기 {#select-room}

```js
U.chat('*').on('after.join', handler);      // 페이지의 모든 채팅방
U.chat('room-a').on('after.join', handler); // room-a만
U.chat('room-b').on('after.join', handler); // room-b만
```

- `U.chat('채팅방아이디')`는 그 채팅방에만, `U.chat('*')`는 페이지의 모든 채팅방(나중에 추가되는 채팅방 포함)에 걸어요.
- 아이디 여러 개를 한 번에 넣을 수는 없어요. 각각 걸거나 `'*'`로 걸고 콜백 안에서 `room.id`로 나누세요.
- 채팅방이 이미 열려 있으면 `U.chat('채팅방아이디')`는 그 채팅방의 `room` 객체를 돌려줘요. 아직 없으면 `on`·`off`만 있는 등록기를 돌려줘요. 어느 쪽이든 `on`·`off`는 같게 써요. → [U.chat()과 room 객체](/doc/3.2)

## 이름표와 해제 {#labels}

이벤트 이름 뒤에 `#이름표`를 붙이면 나중에 이름표로 골라 해제할 수 있어요.

```js
U.chat('*').on('after.message#alarm', onMessage);
U.chat('*').on('after.user.join#alarm', onJoin);

U.chat('*').off('#alarm');              // 이름표가 alarm인 것 모두
U.chat('*').off('after.message');       // after.message 모두(이름표 상관없이)
U.chat('*').off('after.message#alarm'); // 둘 다 맞는 것만
```

`U.chat('*').off(...)`는 `'*'`로 건 것뿐 아니라 **채팅방별로 건 것까지 모두** 지워요. 다른 스크립트(플러그인, 다른 위젯)가 건 이벤트까지 지우지 않도록, 내 코드의 이벤트에는 겹치지 않는 이름표(`#my-widget`처럼)를 붙이고 이름표로 해제하세요.

## after.create와 준비 시점 {#after-create}

`after.create`는 채팅방 화면(스킨)이 준비되면 와요. 접속보다 먼저 오므로 [스킨 UI API](/doc/3.4)와 채널 등록은 이 안에서 해요.

이미 준비된 채팅방에 `after.create`를 걸면 기다리지 않고 바로 한 번 불려요. 그래서 스크립트가 늦게 실행되어도 놓치지 않아요.

```js
U.chat('*').on('after.create#my-widget', function (room) {
	if (room.id !== 'my-room') return;
	// room.skin, room.plugin 사용
});
```

## 태그 추가·삭제와 속성 변경 {#tag-lifecycle}

`uchat.js`는 페이지의 `<u-chat>` 태그를 지켜봐요.

- `<u-chat>`을 페이지에 넣으면 채팅방이 열리고, 지우면 채팅방이 닫혀요.
- 채팅방이 닫히면 **그 채팅방 아이디로 건 이벤트는 함께 지워져요.** `'*'`로 건 이벤트는 남아요. 태그를 다시 넣었을 때도 이벤트가 필요하면 다시 걸거나 `'*'`로 거세요.
- 같은 `room` 아이디의 태그는 페이지에 하나만 열려요. 두 번째 태그는 무시되고, 첫 태그를 지워도 두 번째 태그가 대신 열리지 않아요.
- `room`, `user_data` 같은 **속성을 바꾸는 것은 감지하지 않아요.** 값을 바꾸려면 태그를 지우고 새 속성으로 다시 넣으세요.

```js
function replaceRoom(container, roomId) {
	container.replaceChildren(); // 기존 <u-chat> 제거 → 채팅방 닫힘
	var tag = document.createElement('u-chat');
	tag.setAttribute('room', roomId);
	tag.style.cssText = 'display:block; width:100%; height:500px;';
	container.appendChild(tag); // 새 채팅방 열림
}
```

## SPA에서 쓰기 {#spa}

SPA에서는 화면에 채팅방이 들어올 때 `uchat.js`를 한 번만 불러오고, 화면을 떠날 때 내가 건 이벤트를 정리해요. `<u-chat>` 태그는 프레임워크가 지우면 채팅방도 함께 닫혀요.

아래 `uchat()` 함수는 미리 등록 코드와 같은 일을 하므로 `uchat.js`보다 먼저 실행되어도 돼요.

```js
// uchat.js — 프레임워크와 관계없이 쓰는 도우미
export function uchat(roomId) {
	const U = (window.U = window.U || {});
	U.events = U.events || [];
	if (!U.chat) {
		U.chat = (n) => ({
			on: (e, t) => U.events.push([n, e, t]),
			off: (e) => {
				for (let t = U.events.length; t > 0; )
					U.events[--t][0] == n && U.events[t][1] == e && U.events.splice(t, 1);
			}
		});
	}
	if (!document.querySelector('script[data-uchat]')) {
		const s = document.createElement('script');
		s.src = 'https://client.uchat.io/uchat.js';
		s.async = true;
		s.dataset.uchat = '';
		document.head.appendChild(s);
	}
	return U.chat(roomId);
}
```

React:

```jsx
import { useEffect } from 'react';
import { uchat } from './uchat';

export default function ChatRoom({ roomId }) {
	useEffect(() => {
		const name = 'after.message#my-widget-' + roomId;
		uchat('*').on(name, (room, data) => {
			if (room.id !== roomId) return;
			console.log(data.user.nick, data.content);
		});
		return () => uchat('*').off(name);
	}, [roomId]);

	// key를 바꾸면 태그를 새로 만들므로 다른 채팅방으로 바뀐다
	return <u-chat key={roomId} room={roomId} style={{ display: 'block', width: '100%', height: '500px' }} />;
}
```

- 이벤트는 `'*'`로 걸고 `room.id`로 나눠요. 채팅방 아이디로 건 이벤트는 태그가 지워질 때 함께 지워지므로, 다시 마운트할 때 순서가 꼬이지 않아요.
- 정리할 때는 `on`에 쓴 이름 전체를 `off`에 넘겨요. `uchat.js`가 아직 준비되지 않았어도 지워져요.
- `room` 속성만 바꾸면 채팅방이 바뀌지 않으므로 `key`로 태그를 새로 만들어요.

Vue는 `onMounted`에서 `on`, `onUnmounted`에서 `off`를 부르고, `u-chat`을 사용자 정의 요소로 알려 줘요([5분 설치](/doc/1.2#frameworks) 참고). Svelte는 `onMount`에서 `on`을 걸고 돌려주는 정리 함수에서 `off`를 불러요. 채팅방을 바꿀 때는 Vue는 `:key`, Svelte는 `{#key roomId}` 블록으로 태그를 새로 만들어요.

## 떠 있는 위젯 {#widget}

채팅방을 페이지 오른쪽 아래 단추로 붙이려면 npm 패키지 `@u-chat/embed`의 위젯을 써요. 닫혀 있을 때는 새 메시지를 미리보기와 안 읽은 수로 알려 주고, 누르면 채팅창이 열려요. 관리 페이지 설치 코드 화면의 **떠 있는 위젯** 탭에서도 이 코드를 만들 수 있고, 아래 속성과 위치·크기를 그 화면의 **위젯 설정**에서 골라요.

번들러 없이 쓰는 사이트(HTML, 그누보드, 워드프레스 등)는 `<script>` 한 줄로 불러와요. `uchat.js`를 이미 붙인 페이지에 그대로 더해도 돼요.

```html
<script src="https://cdn.jsdelivr.net/npm/@u-chat/embed@latest/dist/widget.global.js"></script>
<u-chat-widget launcher-text="현재 {user_count}명">
	<u-chat room="my-room"></u-chat>
</u-chat-widget>
```

채팅 서버에서 불러오려면 jsDelivr 주소 대신 `https://client.uchat.io/widget.js`를 써요. 독립 서버는 그 서버의 `widget.js`를 쓰고, 위젯이 같은 서버의 `uchat.js`에 붙어서 `src` 속성이 필요 없어요.

React(`npm install @u-chat/react`):

```jsx
import { UchatWidget } from '@u-chat/react';

export default function App() {
	return <UchatWidget room="my-room" launcherText="현재 {user_count}명" />;
}
```

Svelte 5(`npm install @u-chat/svelte`):

```svelte
<script>
	import { UchatWidget } from '@u-chat/svelte';
</script>

<!-- 중괄호가 Svelte 식으로 읽히지 않게 문자열을 {'...'}로 넘겨요 -->
<UchatWidget room="my-room" launcherText={'현재 {user_count}명'} />
```

Vue 3(`npm install @u-chat/vue`). `isCustomElement` 설정 없이 써요.

```vue
<script setup>
import { UchatWidget } from '@u-chat/vue';
</script>

<template>
	<UchatWidget room="my-room" launcher-text="현재 {user_count}명" />
</template>
```

위젯 속성(React·Svelte에서는 `launcherText`처럼 낙타 표기):

| 속성 | 설명 |
| --- | --- |
| `launcher-text` | 단추 글자. `{user_count}`는 접속자 수, `{unread_count}`는 안 읽은 수로 바뀌어요 |
| `variant` | `bubble`(기본, 둥근 단추), `bar`(아래쪽 긴 막대, 열면 아래에서 올라와요) 또는 `side`(오른쪽 벽면 사이드바. 처음부터 화면 높이 전체로 열려 페이지 본문을 왼쪽으로 밀고, 접으면 오른쪽 벽에 세로 탭만 남아요. 768px보다 좁은 화면은 접힌 채 시작하고 열면 덮어요) |
| `launcher-color`, `launcher-image` | 단추 배경색, 배경 그림 주소(그림이 우선) |
| `panel-title` | 열린 창 위의 제목 |
| `preview-duration` | 새 메시지 미리보기를 보여 주는 시간(ms, 기본 5000) |
| `lang` | 위젯 단추·안내 문구 언어(`ko`, `en`, `ja`). 없으면 안쪽 `<u-chat lang>` → 페이지 `<html lang>` → 브라우저 언어, 그 밖의 언어는 영어 |
| `src` | `uchat.js` 주소. 독립 서버는 그 서버의 주소를 넣어요 |

- 회원 연동 방은 안쪽 `<u-chat>`에 `user_data`를 넣어요(React·Svelte·Vue는 `userData`). 설치 코드 화면에서 **넣는 모양 → 떠 있는 위젯**을 고르면 서명까지 들어간 위젯 코드가 나와요.
- 페이지에 들어오면 바로 채팅방에 접속해요. 그래서 닫혀 있어도 미리보기와 접속자 수가 보여요. 지난 대화(`after.message`의 `past`)와 내가 보낸 메시지는 안 읽은 수에 넣지 않아요.
- 창의 `×`로 숨기면 접속을 끊고, 이 채팅방에 한해 숨긴 상태를 기억해요. 오른쪽 아래에 작은 켜기 단추가 남아요.
- 스크립트에서는 `open()`, `close()`, `toggle()`, `maximize()`, `restore()`, `hide()`, `show()`, `destroy()`를 불러요. React·Vue·Svelte는 컴포넌트에 `ref`(Svelte는 `bind:this`)를 걸고 같은 이름으로 불러요.
- `uchat.js`를 불러오지 못하면 위젯에 `uchat-widget-error` 이벤트가 와요(`event.detail`이 오류).

태그 대신 스크립트로 만들 때는 `createUchatWidget(넣을 요소, 설정)`을 써요. 한 줄 스크립트로 불러왔으면 `window.UchatEmbed`에, npm이면 `@u-chat/embed/widget`에 있어요.

```js
const widget = UchatEmbed.createUchatWidget(document.body, {
	chat: { room: 'my-room', user_data: signedUserData },
	widget: { launcherText: '현재 {user_count}명', variant: 'bar' },
});
widget.addEventListener('uchat-widget-error', (event) => console.error(event.detail));
widget.open();
```

자리와 크기는 CSS 변수로 바꿔요.

```css
u-chat-widget {
	--uchat-widget-right: 20px;
	--uchat-widget-bottom: 20px;
	--uchat-widget-panel-width: 380px;
	--uchat-widget-panel-height: 640px;
	--uchat-widget-side-width: 360px;
	--uchat-widget-accent: #ff7e25;
	--uchat-widget-z-index: 2147483000;
}
```
