react-optimistic-chat 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +907 -0
- package/dist/index.css +214 -0
- package/dist/index.d.mts +43 -55
- package/dist/index.d.ts +43 -55
- package/dist/index.js +169 -205
- package/dist/index.mjs +169 -205
- package/package.json +3 -6
- package/dist/style.css +0 -1
package/README.md
CHANGED
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img width="350" height="240" alt="Image" src="https://github.com/user-attachments/assets/de84f731-14ae-4023-9649-2c22f7747ed1"/>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
# react-optimistic-chat · [](https://www.npmjs.com/package/react-optimistic-chat) [](LICENSE)
|
|
6
|
+
|
|
7
|
+
<code>react-optimistic-chat</code>은 **React + TanStack Query** 기반으로
|
|
8
|
+
AI 챗봇 서비스에서 필요한 **채팅 캐시 관리 및 optimistic update, 채팅 UI**를 손쉽게 구현할 수 있도록 돕는 라이브러리입니다.
|
|
9
|
+
|
|
10
|
+
<br>
|
|
11
|
+
|
|
12
|
+
> 이 라이브러리는 AI 응답 생성 기능을 포함하지 않으며,
|
|
13
|
+
> 기존 API와 결합해 채팅 상태 관리와 UI 구현에만 집중합니다.
|
|
14
|
+
|
|
15
|
+
<br>
|
|
16
|
+
|
|
17
|
+
## 목차
|
|
18
|
+
#### **1.** [Install & Requirements](#install--requirements)
|
|
19
|
+
#### **2.** [Quick Start](#quick-start)
|
|
20
|
+
#### **3.** [Core Types](#core-types)
|
|
21
|
+
**\-** [Message](#message)
|
|
22
|
+
**\-** [VoiceRecognition](#voicerecognition)
|
|
23
|
+
#### **4.** [Hooks](#hooks)
|
|
24
|
+
**\-** [useChat](#usechat)
|
|
25
|
+
**\-** [useBrowserSpeechRecognition](#usebrowserspeechrecognition)
|
|
26
|
+
**\-** [useVoiceChat](#usevoicechat)
|
|
27
|
+
#### **5.** [Components](#components)
|
|
28
|
+
**\-** [Indicators](#indicators)
|
|
29
|
+
**\-** [ChatMessage](#chatmessage)
|
|
30
|
+
**\-** [ChatList](#chatlist)
|
|
31
|
+
**\-** [ChatInput](#chatinput)
|
|
32
|
+
**\-** [ChatContainer](#chatcontainer)
|
|
33
|
+
#### **6.** [Notes](#notes)
|
|
34
|
+
|
|
35
|
+
<br>
|
|
36
|
+
|
|
37
|
+
<h1 id="install--requirements">📦 Install & Requirements</h1>
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
```bash
|
|
41
|
+
npm install react-optimistic-chat
|
|
42
|
+
# or
|
|
43
|
+
yarn add react-optimistic-chat
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
<br>
|
|
47
|
+
|
|
48
|
+
## Peer Dependencies
|
|
49
|
+
이 라이브러리는 아래 패키지들을 **peer dependency**로 사용합니다.
|
|
50
|
+
프로젝트에 반드시 설치되어 있어야 합니다.
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"@tanstack/react-query": ">=5",
|
|
54
|
+
"react": ">=18",
|
|
55
|
+
"react-dom": ">=18"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
<br>
|
|
60
|
+
|
|
61
|
+
## styles
|
|
62
|
+
<code>react-optimistic-chat</code>의 **채팅 UI 컴포넌트**를 사용하려면
|
|
63
|
+
아래 스타일 파일을 반드시 import 해야 합니다.
|
|
64
|
+
```ts
|
|
65
|
+
import "react-optimistic-chat/style.css";
|
|
66
|
+
```
|
|
67
|
+
> React 프로젝트에서는 `App.tsx`에,
|
|
68
|
+
> Next.js(App Router)에서는 루트 `Layout.tsx`에서 import 하는 것을 권장합니다.
|
|
69
|
+
|
|
70
|
+
<br>
|
|
71
|
+
|
|
72
|
+
<h1 id="quick-start">🚀 Quick Start</h1>
|
|
73
|
+
|
|
74
|
+
아래 예제는 서버로부터 전달되는 Raw 채팅 데이터를
|
|
75
|
+
<code>useChat</code>과 <code>ChatContainer</code>를 조합해 **최소한의 설정으로 채팅 UI를 구성하는 방법**을 보여줍니다.
|
|
76
|
+
|
|
77
|
+
**Raw 데이터 → Message 타입 정규화 → 캐싱 → 렌더링**까지의 흐름을 한 번에 확인할 수 있습니다.
|
|
78
|
+
|
|
79
|
+
<br>
|
|
80
|
+
|
|
81
|
+
## 1️⃣ RawMessage
|
|
82
|
+
서버로부터 전달되는 채팅 데이터는 다음과 같은 형태라고 가정합니다.
|
|
83
|
+
```ts
|
|
84
|
+
type Raw = {
|
|
85
|
+
chatId: string;
|
|
86
|
+
sender: "ai" | "user";
|
|
87
|
+
body: string;
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
<br>
|
|
92
|
+
|
|
93
|
+
## 2️⃣ getChat & sendAI
|
|
94
|
+
채팅 목록을 불러오고, 사용자 메시지를 서버로 전송하는 함수는 다음과 같은 형태라고 가정합니다.
|
|
95
|
+
```ts
|
|
96
|
+
async function getChat(roomId: string, page: number): Promise<Raw[]> {
|
|
97
|
+
const res = await fetch(`/getChat?roomId=${roomId}&page=${page}`);
|
|
98
|
+
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
throw new Error("채팅 불러오기 실패");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const json = await res.json();
|
|
104
|
+
return json.result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function sendAI(content: string): Promise<Raw> {
|
|
108
|
+
const res = await fetch(`/sendAI`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ content }),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
throw new Error("AI 응답 실패");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const json = await res.json();
|
|
119
|
+
return json.result;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
<br>
|
|
124
|
+
|
|
125
|
+
## 3️⃣ ChatExample
|
|
126
|
+
<code>useChat</code> 훅으로 메시지 상태를 관리하고,
|
|
127
|
+
<code>ChatContainer</code> 컴포넌트에 전달해 채팅 UI + 무한 스크롤을 구성합니다.
|
|
128
|
+
|
|
129
|
+
이때 서버의 Raw 데이터를 Message 타입의
|
|
130
|
+
<code>id</code>, <code>role</code>, <code>content</code> 필드에 **정확히 매핑**합니다.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
export default function ChatExample() {
|
|
134
|
+
const roomId = "room-1";
|
|
135
|
+
const PAGE_SIZE = 8;
|
|
136
|
+
|
|
137
|
+
const {
|
|
138
|
+
messages,
|
|
139
|
+
sendUserMessage,
|
|
140
|
+
isPending,
|
|
141
|
+
fetchNextPage,
|
|
142
|
+
hasNextPage,
|
|
143
|
+
isFetchingNextPage,
|
|
144
|
+
} = useChat<Raw>({
|
|
145
|
+
queryKey: ["chat", roomId],
|
|
146
|
+
queryFn: (pageParam) => getChat(roomId, pageParam as number),
|
|
147
|
+
initialPageParam: 0,
|
|
148
|
+
|
|
149
|
+
getNextPageParam: (lastPage, allPages) =>
|
|
150
|
+
lastPage.length === PAGE_SIZE ? allPages.length : undefined,
|
|
151
|
+
|
|
152
|
+
mutationFn: sendAI,
|
|
153
|
+
|
|
154
|
+
map: (raw) => ({
|
|
155
|
+
id: raw.chatId,
|
|
156
|
+
role: raw.sender === "ai" ? "AI" : "USER",
|
|
157
|
+
content: raw.body,
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<ChatContainer
|
|
163
|
+
className="h-[80vh]"
|
|
164
|
+
messages={messages}
|
|
165
|
+
onSend={sendUserMessage}
|
|
166
|
+
isSending={isPending}
|
|
167
|
+
fetchNextPage={fetchNextPage}
|
|
168
|
+
hasNextPage={hasNextPage}
|
|
169
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
<br>
|
|
176
|
+
|
|
177
|
+
## 4️⃣ VoiceChatExample
|
|
178
|
+
음성 입력 기반 채팅을 사용하고 싶은 경우,
|
|
179
|
+
<code>useBrowserSpeechRecognition</code>을 생성한 뒤
|
|
180
|
+
<code>useVoiceChat</code>의 <code>voice</code> 옵션으로 전달하면 됩니다.
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
const voice = useBrowserSpeechRecognition();
|
|
184
|
+
|
|
185
|
+
const {
|
|
186
|
+
// 음성 제어용 API
|
|
187
|
+
startRecording,
|
|
188
|
+
stopRecording,
|
|
189
|
+
...
|
|
190
|
+
} = useVoiceChat<Raw>({
|
|
191
|
+
voice,
|
|
192
|
+
...
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
<br>
|
|
197
|
+
|
|
198
|
+
<h1 id="core-types">🧩 Core Types</h1>
|
|
199
|
+
|
|
200
|
+
<h2 id="message">🧩 Message</h2>
|
|
201
|
+
|
|
202
|
+
<code>react-optimistic-chat</code>은 채팅을 단순한 문자열 배열이 아닌
|
|
203
|
+
**일관된 Message 타입을 중심으로 관리**하도록 설계되었습니다.
|
|
204
|
+
|
|
205
|
+
모든 Hooks와 UI 컴포넌트는 이 Core Type을 기준으로 동작하며,
|
|
206
|
+
서버로부터 전달되는 다양한 형태의 Raw 데이터를 **예측 가능한 구조로 정규화**하는 것을 목표로 합니다.
|
|
207
|
+
```ts
|
|
208
|
+
type Message = {
|
|
209
|
+
id: number | string;
|
|
210
|
+
role: "USER" | "AI";
|
|
211
|
+
content: string;
|
|
212
|
+
isLoading?: boolean;
|
|
213
|
+
custom?: Record<string, unknown>;
|
|
214
|
+
};
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
| field | type | description |
|
|
218
|
+
|------|------|-------------|
|
|
219
|
+
| `id` | `number \| string` | 메시지를 식별하기 위한 고유 값 |
|
|
220
|
+
| `role` | `"USER" \| "AI"` | 메시지의 주체<br/>`"USER"`: 사용자가 입력한 메시지<br>`"AI"`: AI가 생성한 응답 메시지 |
|
|
221
|
+
| `content` | `string` | 메시지에 표시될 텍스트 내용 |
|
|
222
|
+
| `isLoading` | `boolean` _(optional)_ | AI 응답을 기다리는 중인 메시지임을 나타내는 플래그<br>optimistic update 시 UI 상태 표현에 사용 |
|
|
223
|
+
| `custom` | `Record<string, unknown>` | 서버에서 전달된 Raw 데이터 중 `id`, `role`, `content`에<br>포함되지 않은 모든 필드를 보존하는 객체 |
|
|
224
|
+
|
|
225
|
+
<br>
|
|
226
|
+
|
|
227
|
+
## Example: \<Raw> → \<Message> 정규화
|
|
228
|
+
```ts
|
|
229
|
+
type Raw = {
|
|
230
|
+
messageId: string;
|
|
231
|
+
sender: "user" | "assistant";
|
|
232
|
+
text: string;
|
|
233
|
+
createdAt: string;
|
|
234
|
+
model: string;
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
서버로부터 다음과 같은 <code>Raw</code> 채팅 데이터가 전달된다고 가정합니다.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
map: (raw: RawMessage) => ({
|
|
241
|
+
id: raw.messageId,
|
|
242
|
+
role: raw.sender === "user" ? "USER" : "AI",
|
|
243
|
+
content: raw.text,
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
Hook에서 필수로 제공하는 <code>map</code> 함수를 다음과 같이 정의하면
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
{
|
|
250
|
+
id: "abc123",
|
|
251
|
+
role: "AI",
|
|
252
|
+
content: "Hello! How can I help you?",
|
|
253
|
+
custom: {
|
|
254
|
+
createdAt: "2024-01-01T10:00:00Z",
|
|
255
|
+
model: "gpt-4o"
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
내부적으로 <code>Message</code>는 아래와 같이 정규화됩니다.
|
|
260
|
+
|
|
261
|
+
<br>
|
|
262
|
+
|
|
263
|
+
<h2 id="voicerecognition">🧩 VoiceRecognition</h2>
|
|
264
|
+
|
|
265
|
+
<code>react-optimistic-chat</code>은 음성 입력을 단순한 브라우저 API 호출이 아닌
|
|
266
|
+
**일관된 VoiceRecognition 인터페이스를 통해 추상화**하도록 설계되었습니다.
|
|
267
|
+
|
|
268
|
+
이를 통해 입력 방식(브라우저, 외부 SDK, 커스텀 STT 등)에 관계없이
|
|
269
|
+
<code>useVoiceChat</code> 훅과 <code>ChatInput</code> 컴포넌트에서 동일한 방식으로 음성 인식 상태를 제어할 수 있습니다.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
type VoiceRecognition = {
|
|
273
|
+
start: () => void;
|
|
274
|
+
stop: () => void;
|
|
275
|
+
isRecording: boolean;
|
|
276
|
+
onTranscript?: (text: string) => void;
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
| field | type | description |
|
|
281
|
+
| -------------- | ------------------------ | ------------------- |
|
|
282
|
+
| `start` | `() => void` | 음성 인식을 시작하는 함수 |
|
|
283
|
+
| `stop` | `() => void` | 음성 인식을 중단하는 함수 |
|
|
284
|
+
| `isRecording` | `boolean` | 현재 음성 인식이 진행 중인지 여부 |
|
|
285
|
+
| `onTranscript` | `(text: string) => void` | 인식된 음성 텍스트를 전달받는 콜백<br>• `useVoiceChat`에서는 필수<br>• `ChatInput`에서는 내부에서 자동으로 처리 |
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
<br>
|
|
290
|
+
|
|
291
|
+
<h1 id="hooks">🪝 Hooks</h1>
|
|
292
|
+
|
|
293
|
+
<h2 id="usechat">🪝 useChat</h2>
|
|
294
|
+
|
|
295
|
+
<code>useChat</code>은 **TanStack Query의 캐시를 기반으로**
|
|
296
|
+
AI 챗봇 서비스에 필요한 **채팅 히스토리 관리, optimistic update, 메시지 정규화**를 한 번에 제공하는 Hook입니다.
|
|
297
|
+
|
|
298
|
+
- <code>useInfiniteQuery</code> 기반 **채팅 히스토리 관리**
|
|
299
|
+
- 채팅 내역을 페이지 단위로 캐시에 저장
|
|
300
|
+
- 이미 로드된 페이지는 재요청 없이 캐시에서 즉시 복원
|
|
301
|
+
- 사용자 메시지 전송 시 **Optimistic Update 적용**
|
|
302
|
+
- 서버 응답을 기다리지 않고 UI에 즉시 반영
|
|
303
|
+
- AI 응답 대기 중 상태를 <code>isPending</code>으로 제공
|
|
304
|
+
- 서버로부터 받은 Raw 데이터를 **일관된 Message 구조로 정규화**
|
|
305
|
+
- <code>id</code>, <code>role</code>, <code>content</code>는 최상위 필드로 유지
|
|
306
|
+
- Message에 포함되지 않은 나머지 Raw 필드는 <code>custom</code> 영역에 자동 보존
|
|
307
|
+
- TanStack Query의 캐시 메커니즘을 활용한 **안정적인 상태 동기화**
|
|
308
|
+
- mutation 실패 시 이전 캐시 상태로 rollback
|
|
309
|
+
- <code>staleTime</code>, <code>gcTime</code>을 통한 캐시 수명 제어
|
|
310
|
+
|
|
311
|
+
<br>
|
|
312
|
+
|
|
313
|
+
### Usage
|
|
314
|
+
```ts
|
|
315
|
+
const {
|
|
316
|
+
messages,
|
|
317
|
+
sendUserMessage,
|
|
318
|
+
isPending,
|
|
319
|
+
isInitialLoading,
|
|
320
|
+
fetchNextPage,
|
|
321
|
+
hasNextPage,
|
|
322
|
+
isFetchingNextPage,
|
|
323
|
+
} = useChat({
|
|
324
|
+
queryKey: ["chat", roomId],
|
|
325
|
+
queryFn: getChat,
|
|
326
|
+
initialPageParam: 0,
|
|
327
|
+
getNextPageParam,
|
|
328
|
+
mutationFn: sendAI,
|
|
329
|
+
map: (raw) => ({
|
|
330
|
+
id: raw.chatId,
|
|
331
|
+
role: raw.sender === "ai" ? "AI" : "USER",
|
|
332
|
+
content: raw.body,
|
|
333
|
+
}),
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
<br>
|
|
338
|
+
|
|
339
|
+
### Returned Values
|
|
340
|
+
| name | type | description |
|
|
341
|
+
|------|------|-------------|
|
|
342
|
+
| `messages` | `Message[]` | 정규화된 메시지 배열 |
|
|
343
|
+
| `sendUserMessage` | `(content: string) => void` | 유저 메시지 전송 함수 |
|
|
344
|
+
| `isPending` | `boolean` | AI 응답 대기 상태 |
|
|
345
|
+
| `isInitialLoading` | `boolean` | `messages` 로딩 상태 |
|
|
346
|
+
| `fetchNextPage` | `() => Promise<unknown>` | 다음 채팅 페이지 요청 |
|
|
347
|
+
| `hasNextPage` | `boolean \| undefined` | 다음 페이지 존재 여부 |
|
|
348
|
+
| `isFetchingNextPage` | `boolean` | 페이지 로딩 상태 |
|
|
349
|
+
|
|
350
|
+
<br>
|
|
351
|
+
|
|
352
|
+
### Options
|
|
353
|
+
| name | type | required | description |
|
|
354
|
+
|------|------|----------|-------------|
|
|
355
|
+
| `queryKey` | `readonly unknown[]` | ✅ | 해당 채팅의 TanStack Query key |
|
|
356
|
+
| `queryFn` | `(pageParam: unknown) => Promise<Raw[]>` | ✅ | 기존 채팅 내역을 불러오는 함수 |
|
|
357
|
+
| `initialPageParam` | `unknown` | ✅ | 첫 페이지 요청 시 사용할 pageParam |
|
|
358
|
+
| `getNextPageParam` | `(lastPage: Message[], allPages: Message[][]) => unknown` | ✅ | 다음 페이지 요청을 위한 pageParam 계산 함수 |
|
|
359
|
+
| `mutationFn` | `(content: string) => Promise<Raw>` | ✅ | 유저 입력을 받아 AI 응답 1개를 반환하는 함수 |
|
|
360
|
+
| `map` | `(raw: Raw) => { id; role; content }` | ✅ | Raw 데이터를 Message 구조로 매핑하는 함수 |
|
|
361
|
+
| `onError` | `(error: unknown) => void` | ❌ | mutation 에러 발생 시 호출되는 콜백 |
|
|
362
|
+
| `staleTime` | `number` | ❌ | 캐시가 fresh 상태로 유지되는 시간 (ms) |
|
|
363
|
+
| `gcTime` | `number` | ❌ | 캐시가 GC 되기 전까지 유지되는 시간 (ms) |
|
|
364
|
+
|
|
365
|
+
<br>
|
|
366
|
+
|
|
367
|
+
### 🔁 useChat Flow
|
|
368
|
+
<table width="892" align="center" bgcolor="white">
|
|
369
|
+
<tr>
|
|
370
|
+
<td align="center" bgcolor="white">
|
|
371
|
+
<img
|
|
372
|
+
src="https://github.com/user-attachments/assets/6e61356b-0da3-45d9-8791-c11aa0d346e9"
|
|
373
|
+
width="690"
|
|
374
|
+
style="display: block; margin: 0 auto;"
|
|
375
|
+
alt="useChat 호출"
|
|
376
|
+
/>
|
|
377
|
+
</td>
|
|
378
|
+
</tr>
|
|
379
|
+
<tr>
|
|
380
|
+
<td align="center" bgcolor="white">
|
|
381
|
+
<b>useChat 호출</b>
|
|
382
|
+
</td>
|
|
383
|
+
</tr>
|
|
384
|
+
</table>
|
|
385
|
+
|
|
386
|
+
<table width="892" align="center" bgcolor="white">
|
|
387
|
+
<tr>
|
|
388
|
+
<td align="center" bgcolor="white">
|
|
389
|
+
<img
|
|
390
|
+
src="https://github.com/user-attachments/assets/10218350-8844-4bee-b78e-fe4564844e57"
|
|
391
|
+
width="690"
|
|
392
|
+
style="display: block; margin: 0 auto;"
|
|
393
|
+
alt="useChat 실행"
|
|
394
|
+
/>
|
|
395
|
+
</td>
|
|
396
|
+
</tr>
|
|
397
|
+
<tr>
|
|
398
|
+
<td align="center" bgcolor="white">
|
|
399
|
+
<b>useChat 실행</b>
|
|
400
|
+
</td>
|
|
401
|
+
</tr>
|
|
402
|
+
</table>
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
<br>
|
|
407
|
+
|
|
408
|
+
<h2 id="usebrowserspeechrecognition">🪝 useBrowserSpeechRecognition</h2>
|
|
409
|
+
|
|
410
|
+
<code>useBrowserSpeechRecognition</code>은 브라우저에서 제공하는
|
|
411
|
+
Speech Recognition API를 **React Hook 형태로 추상화한 훅**입니다.
|
|
412
|
+
|
|
413
|
+
이 훅은 음성 인식 로직을 직접 다루지 않고도, <code>useVoiceChat</code>이나 <code>ChatInput</code>과 같은 Hook/UI에서
|
|
414
|
+
**음성 입력 기능을 간편하게 사용하고 싶은 사용자**를 위해 제공됩니다.
|
|
415
|
+
|
|
416
|
+
- 브라우저 내장 음성 인식 API를 간단한 인터페이스로 제공
|
|
417
|
+
- 음성 인식 시작 / 종료 제어
|
|
418
|
+
- 현재 녹음 상태를 나타내는 <code>isRecording</code> 제공
|
|
419
|
+
- 음성 인식 결과(transcript)를 외부 로직으로 전달 가능
|
|
420
|
+
- 브라우저 미지원 환경에 대한 에러 처리 지원
|
|
421
|
+
|
|
422
|
+
<br>
|
|
423
|
+
|
|
424
|
+
### Usage
|
|
425
|
+
```ts
|
|
426
|
+
const voice = useBrowserSpeechRecognition();
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
<br>
|
|
430
|
+
|
|
431
|
+
### Returned Values
|
|
432
|
+
|
|
433
|
+
| name | type | description |
|
|
434
|
+
|------|------|-------------|
|
|
435
|
+
| `start` | `() => void` | 음성 인식 시작 |
|
|
436
|
+
| `stop` | `() => void` | 음성 인식 종료 |
|
|
437
|
+
| `isRecording` | `boolean` | 현재 음성 인식 진행 상태 |
|
|
438
|
+
| `onTranscript` | `(fn: (text: string) => void) => void` | 음성 인식 결과(transcript)를 처리할 콜백 |
|
|
439
|
+
|
|
440
|
+
<br>
|
|
441
|
+
|
|
442
|
+
### Options
|
|
443
|
+
|
|
444
|
+
| name | type | required | description |
|
|
445
|
+
|------|------|----------|-------------|
|
|
446
|
+
| `lang` | `string` | ❌ | 음성 인식에 사용할 언어 코드 (기본값: `"ko-KR"`) |
|
|
447
|
+
| `onStart` | `() => void` | ❌ | 음성 인식이 시작될 때 실행되는 콜백 |
|
|
448
|
+
| `onEnd` | `() => void` | ❌ | 음성 인식이 종료될 때 실행되는 콜백 |
|
|
449
|
+
| `onError` | `(error: unknown) => void` | ❌ | 음성 인식 중 에러가 발생했을 때 실행되는 콜백 |
|
|
450
|
+
|
|
451
|
+
<br>
|
|
452
|
+
|
|
453
|
+
<h2 id="usevoicechat">🪝 useVoiceChat</h2>
|
|
454
|
+
|
|
455
|
+
<code>useVoiceChat</code>은 <code>useChat</code>의 캐시 구조와 optimistic update 흐름을 그대로 유지하면서,
|
|
456
|
+
**음성 인식 기반 채팅** 경험을 제공하는 Hook입니다.
|
|
457
|
+
|
|
458
|
+
음성 인식 결과를 실시간으로 채팅 UI에 반영하고,
|
|
459
|
+
녹음 종료 시 최종 텍스트를 AI 요청으로 연결하는 흐름을 내부에서 관리합니다.
|
|
460
|
+
|
|
461
|
+
- <code>useInfiniteQuery</code> 기반 **채팅 히스토리 캐시 관리**
|
|
462
|
+
- <code>useChat</code>과 동일한 페이지 단위 캐싱 구조
|
|
463
|
+
- 기존 텍스트 채팅과 동일한 Message 정규화 방식 유지
|
|
464
|
+
- 음성 입력 기반 **Optimistic Update**
|
|
465
|
+
- 녹음 시작 시 USER 메시지를 즉시 캐시에 삽입
|
|
466
|
+
- 음성 인식 중간 결과를 실시간으로 메시지 content에 반영
|
|
467
|
+
- 음성 인식 종료 시 **AI 요청 트리거**
|
|
468
|
+
- 최종 transcript를 mutationFn으로 전달
|
|
469
|
+
- AI 응답 대기 상태를 <code>isPending</code>으로 제공
|
|
470
|
+
- TanStack Query의 캐시 메커니즘을 활용한 **안정적인 상태 동기화**
|
|
471
|
+
- 음성 입력 취소 또는 에러 발생 시 이전 캐시 상태로 rollback
|
|
472
|
+
- <code>staleTime</code>, <code>gcTime</code>을 통한 캐시 수명 제어
|
|
473
|
+
- 음성 인식 로직을 외부에서 주입 가능
|
|
474
|
+
- <code>useBrowserSpeechRecognition</code> 또는 커스텀 음성 인식 컨트롤러 사용 가능
|
|
475
|
+
|
|
476
|
+
<br>
|
|
477
|
+
|
|
478
|
+
### Usage
|
|
479
|
+
```ts
|
|
480
|
+
const voice = useBrowserSpeechRecognition();
|
|
481
|
+
|
|
482
|
+
const {
|
|
483
|
+
messages,
|
|
484
|
+
isPending,
|
|
485
|
+
isInitialLoading,
|
|
486
|
+
startRecording,
|
|
487
|
+
stopRecording,
|
|
488
|
+
fetchNextPage,
|
|
489
|
+
hasNextPage,
|
|
490
|
+
isFetchingNextPage,
|
|
491
|
+
} = useVoiceChat({
|
|
492
|
+
voice,
|
|
493
|
+
queryKey: ["chat", roomId],
|
|
494
|
+
queryFn: getChat,
|
|
495
|
+
initialPageParam: 0,
|
|
496
|
+
getNextPageParam,
|
|
497
|
+
mutationFn: sendAI,
|
|
498
|
+
map: (raw) => ({
|
|
499
|
+
id: raw.chatId,
|
|
500
|
+
role: raw.sender === "ai" ? "AI" : "USER",
|
|
501
|
+
content: raw.body,
|
|
502
|
+
}),
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
<br>
|
|
507
|
+
|
|
508
|
+
### Returned Values
|
|
509
|
+
| name | type | description |
|
|
510
|
+
| -------------------- | ------------------------ | ---------------------------------------- |
|
|
511
|
+
| `messages` | `Message[]` | 정규화된 메시지 배열 |
|
|
512
|
+
| `isPending` | `boolean` | AI 응답 대기 상태 |
|
|
513
|
+
| `isInitialLoading` | `boolean` | `messages` 로딩 상태 |
|
|
514
|
+
| `startRecording` | `() => Promise<void>` | 음성 인식 시작 함수 |
|
|
515
|
+
| `stopRecording` | `() => void` | 음성 인식 종료 및 최종 텍스트 전송 함수 |
|
|
516
|
+
| `fetchNextPage` | `() => Promise<unknown>` | 다음 채팅 페이지 요청 |
|
|
517
|
+
| `hasNextPage` | `boolean \| undefined` | 다음 페이지 존재 여부 |
|
|
518
|
+
| `isFetchingNextPage` | `boolean` | 페이지 로딩 상태 |
|
|
519
|
+
|
|
520
|
+
<br>
|
|
521
|
+
|
|
522
|
+
### Options
|
|
523
|
+
| name | type | required | description |
|
|
524
|
+
| ------------------ | --------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------- |
|
|
525
|
+
| `voice` | `VoiceRecognition` | ✅ | 음성 인식을 제어하는 컨트롤러 |
|
|
526
|
+
| `queryKey` | `readonly unknown[]` | ✅ | 해당 채팅의 TanStack Query key |
|
|
527
|
+
| `queryFn` | `(pageParam: unknown) => Promise<Raw[]>` | ✅ | 기존 채팅 내역을 불러오는 함수 |
|
|
528
|
+
| `initialPageParam` | `unknown` | ✅ | 첫 페이지 요청 시 사용할 pageParam |
|
|
529
|
+
| `getNextPageParam` | `(lastPage: Message[], allPages: Message[][]) => unknown` | ✅ | 다음 페이지 요청을 위한 pageParam 계산 함수 |
|
|
530
|
+
| `mutationFn` | `(content: string) => Promise<Raw>` | ✅ | 음성 인식 결과를 받아 AI 응답 1개를 반환하는 함수 |
|
|
531
|
+
| `map` | `(raw: Raw) => { id; role; content }` | ✅ | Raw 데이터를 Message 구조로 매핑하는 함수 |
|
|
532
|
+
| `onError` | `(error: unknown) => void` | ❌ | mutation 에러 발생 시 호출되는 콜백 |
|
|
533
|
+
| `staleTime` | `number` | ❌ | 캐시가 fresh 상태로 유지되는 시간 (ms) |
|
|
534
|
+
| `gcTime` | `number` | ❌ | 캐시가 GC 되기 전까지 유지되는 시간 (ms) |
|
|
535
|
+
|
|
536
|
+
<br>
|
|
537
|
+
|
|
538
|
+
### 🔁 useVoiceChat Flow
|
|
539
|
+
<table width="1001" align="center" bgcolor="white">
|
|
540
|
+
<tr>
|
|
541
|
+
<td align="center" bgcolor="white">
|
|
542
|
+
<img
|
|
543
|
+
src="https://github.com/user-attachments/assets/c80acf37-4886-4c36-a952-e629ccd73088"
|
|
544
|
+
width="690"
|
|
545
|
+
style="display: block; margin: 0 auto;"
|
|
546
|
+
alt="useVoiceChat 호출"
|
|
547
|
+
/>
|
|
548
|
+
</td>
|
|
549
|
+
</tr>
|
|
550
|
+
<tr>
|
|
551
|
+
<td align="center" bgcolor="white">
|
|
552
|
+
<b>useVoiceChat 호출</b>
|
|
553
|
+
</td>
|
|
554
|
+
</tr>
|
|
555
|
+
</table>
|
|
556
|
+
|
|
557
|
+
<table width="1001" align="center" bgcolor="white">
|
|
558
|
+
<tr>
|
|
559
|
+
<td align="center" bgcolor="white">
|
|
560
|
+
<img
|
|
561
|
+
src="https://github.com/user-attachments/assets/92b44439-5db4-474d-875e-8e62a09f157b"
|
|
562
|
+
width="690"
|
|
563
|
+
style="display: block; margin: 0 auto;"
|
|
564
|
+
alt="useVoiceChat 실행"
|
|
565
|
+
/>
|
|
566
|
+
</td>
|
|
567
|
+
</tr>
|
|
568
|
+
<tr>
|
|
569
|
+
<td align="center" bgcolor="white">
|
|
570
|
+
<b>useVoiceChat 실행</b>
|
|
571
|
+
</td>
|
|
572
|
+
</tr>
|
|
573
|
+
</table>
|
|
574
|
+
|
|
575
|
+
<br>
|
|
576
|
+
|
|
577
|
+
<h1 id="components">🎨 Components</h1>
|
|
578
|
+
|
|
579
|
+
<h2 id="indicators">🎨 Indicators</h2>
|
|
580
|
+
|
|
581
|
+
<code>Indicators</code>는 **로딩 상태를 시각적으로 표현하기 위한 컴포넌트 모음**입니다.
|
|
582
|
+
현재 아래 두 가지 컴포넌트를 제공합니다.
|
|
583
|
+
|
|
584
|
+
| <img src="https://github.com/user-attachments/assets/cd480e2f-5518-4588-bf90-e3461607bef1" alt="LoadingSpinner" width="120" /> | <img src="https://github.com/user-attachments/assets/0c30ce29-9535-480b-b74f-0f170a594951" alt="SendingDots" width="120" /> |
|
|
585
|
+
| :---------------: | :---------------: |
|
|
586
|
+
| **LoadingSpinner** | **SendingDots** |
|
|
587
|
+
|
|
588
|
+
<br>
|
|
589
|
+
|
|
590
|
+
### Usage
|
|
591
|
+
```tsx
|
|
592
|
+
<LoadingSpinner size="lg" />
|
|
593
|
+
```
|
|
594
|
+
```tsx
|
|
595
|
+
<SendingDots size="lg" />
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
<br>
|
|
599
|
+
|
|
600
|
+
### Props
|
|
601
|
+
| name | type | required | description |
|
|
602
|
+
| ------ | ------------------------------ |-----| ----------- |
|
|
603
|
+
| `size` | `"xs" \| "sm" \| "md" \| "lg"` | ❌ | 컴포넌트의 크기<br>(<code>default</code>: "md") |
|
|
604
|
+
|
|
605
|
+
<br>
|
|
606
|
+
|
|
607
|
+
<h2 id="chatmessage">🎨 ChatMessage</h2>
|
|
608
|
+
|
|
609
|
+
<code>ChatMessage</code>는 **단일 채팅 메시지를 렌더링하는 말풍선 컴포넌트**입니다.
|
|
610
|
+
메시지의 <code>role</code>에 따라 AI / USER 레이아웃을 자동으로 분기하며,
|
|
611
|
+
아이콘, 위치, 스타일을 유연하게 커스터마이징할 수 있도록 설계되었습니다.
|
|
612
|
+
|
|
613
|
+
| <img width="224" height="63" alt="Image" src="https://github.com/user-attachments/assets/e351d1f2-b476-41f5-a002-eee4119cf0a0" /> | <img width="174" height="61" alt="Image" src="https://github.com/user-attachments/assets/98a8e033-d364-49da-bdc1-bc0a9f842969" /> |
|
|
614
|
+
| :---------------: | :---------------: |
|
|
615
|
+
| **role="AI"** | **role="USER"** |
|
|
616
|
+
|
|
617
|
+
<br>
|
|
618
|
+
|
|
619
|
+
### Usage
|
|
620
|
+
```tsx
|
|
621
|
+
<ChatMessage
|
|
622
|
+
id="1"
|
|
623
|
+
role="AI"
|
|
624
|
+
content="안녕하세요! 무엇을 도와드릴까요?"
|
|
625
|
+
/>
|
|
626
|
+
|
|
627
|
+
<ChatMessage
|
|
628
|
+
id="2"
|
|
629
|
+
role="USER"
|
|
630
|
+
content="질문이 있어요."
|
|
631
|
+
/>
|
|
632
|
+
|
|
633
|
+
<ChatMessage
|
|
634
|
+
id="3"
|
|
635
|
+
role="AI"
|
|
636
|
+
isLoading
|
|
637
|
+
loadingRenderer={<SendingDots/>}
|
|
638
|
+
/>
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
<br>
|
|
642
|
+
|
|
643
|
+
### Props
|
|
644
|
+
| name | type | required | description |
|
|
645
|
+
| ----------------- | ----------------------------- | --------------- | --------------- |
|
|
646
|
+
| `id` | `string` | ✅ | 메시지의 고유 식별자 |
|
|
647
|
+
| `role` | `"AI" \| "USER"` | ✅ | 메시지 주체<br><code>AI</code>: 좌측 메시지<br> <code>USER</code>: 우측 메시지 |
|
|
648
|
+
| `content` | `string` | ✅ | 메시지 텍스트 |
|
|
649
|
+
| `isLoading` | `boolean` | ❌ | 로딩 상태 여부 |
|
|
650
|
+
| `wrapperClassName` | `string` | ❌ | 메시지 wrapper 커스텀 클래스 |
|
|
651
|
+
| `icon` | `React.ReactNode` | ❌ | AI 메시지에 표시할 커스텀 아이콘 |
|
|
652
|
+
| `aiIconWrapperClassName` | `string` | ❌ | AI 아이콘 wrapper 커스텀 클래스 |
|
|
653
|
+
| `aiIconColor` | `string` | ❌ | 기본 AI 아이콘 색상 클래스 |
|
|
654
|
+
| `bubbleClassName` | `string` | ❌ | 공통 말풍선 커스텀 클래스 |
|
|
655
|
+
| `aiBubbleClassName` | `string` | ❌ | AI 말풍선 커스텀 클래스 |
|
|
656
|
+
| `userBubbleClassName` | `string` | ❌ | User 말풍선 커스텀 클래스 |
|
|
657
|
+
| `position` | `"auto" \| "left" \| "right"` | ❌ | 말풍선 위치 설정 |
|
|
658
|
+
| `loadingRenderer` | `React.ReactNode` | ❌ | 로딩 상태 시 렌더링할 커스텀 UI<br>(<code>default</code>: \<LoadingSpinner/>) |
|
|
659
|
+
|
|
660
|
+
<br>
|
|
661
|
+
|
|
662
|
+
<h2 id="chatlist">🎨 ChatList</h2>
|
|
663
|
+
|
|
664
|
+
<code>ChatList</code>는 **채팅 메시지 목록을 렌더링하는 컴포넌트**입니다.
|
|
665
|
+
내부에서 <code>ChatMessage</code>를 사용해 메시지를 순서대로 나열하며,
|
|
666
|
+
메시지 매핑, 커스텀 렌더링을 통해 유연한 메시지 UI 구성이 가능합니다.
|
|
667
|
+
|
|
668
|
+
| <img width="524" height="450" alt="Image" src="https://github.com/user-attachments/assets/d55f54cc-22b3-4153-9982-5fe086aa5e31" /> |
|
|
669
|
+
| :---------------: |
|
|
670
|
+
| **ChatList** |
|
|
671
|
+
|
|
672
|
+
<br>
|
|
673
|
+
|
|
674
|
+
### Usage
|
|
675
|
+
```tsx
|
|
676
|
+
// 이미 Message 타입으로 정규화된 데이터를 사용하는 경우
|
|
677
|
+
<ChatList
|
|
678
|
+
messages={messages}
|
|
679
|
+
/>
|
|
680
|
+
|
|
681
|
+
// 서버에서 내려오는 Raw 데이터를 사용하는 경우
|
|
682
|
+
<ChatList
|
|
683
|
+
messages={messages}
|
|
684
|
+
messageMapper={(msg) => ({
|
|
685
|
+
id: Number(msg.chatId),
|
|
686
|
+
role: msg.sender === "bot" ? "AI" : "USER",
|
|
687
|
+
content: msg.body,
|
|
688
|
+
})}
|
|
689
|
+
/>
|
|
690
|
+
|
|
691
|
+
// 커스텀 메시지 UI 사용
|
|
692
|
+
<ChatList
|
|
693
|
+
messages={messages}
|
|
694
|
+
messageRenderer={(msg) => (
|
|
695
|
+
<CustomMessage key={msg.id} {...msg} />
|
|
696
|
+
)}
|
|
697
|
+
/>
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
<br>
|
|
701
|
+
|
|
702
|
+
### Props
|
|
703
|
+
| name | type | required | description |
|
|
704
|
+
| ----------------- | ---------------------------------------- | -------- | ----------------------------------- |
|
|
705
|
+
| `messages` | `Message[] \| Raw[]` | ✅ | 렌더링할 메시지 배열 |
|
|
706
|
+
| `messageMapper` | `(msg: Raw) => Message` | ❌ | Raw 데이터를 Message 구조로 매핑하는 함수 |
|
|
707
|
+
| `messageRenderer` | `(msg: Message) => React.ReactNode` | ❌ | 기본 `ChatMessage` 대신 사용할 커스텀 메시지 렌더러 |
|
|
708
|
+
| `className` | `string` | ❌ | 메시지 리스트 wrapper 커스텀 클래스 |
|
|
709
|
+
| `loadingRenderer` | `React.ReactNode` | ❌ | AI 메시지의 로딩 상태에 전달할 커스텀 로딩 UI<br>(<code>default</code>: \<LoadingSpinner/>) |
|
|
710
|
+
|
|
711
|
+
<br>
|
|
712
|
+
|
|
713
|
+
<h2 id="chatinput">🎨 ChatInput</h2>
|
|
714
|
+
|
|
715
|
+
<code>ChatInput</code>은 **텍스트 입력과 음성 입력을 모두 지원하는 채팅 입력 컴포넌트**입니다.
|
|
716
|
+
**textarea** 기반 입력창과 전송 버튼을 제공하며,
|
|
717
|
+
마이크 버튼을 통해 음성을 텍스트로 변환해 입력할 수 있도록 설계되었습니다.
|
|
718
|
+
|
|
719
|
+
기본적으로 브라우저 음성 인식 기능을 사용한
|
|
720
|
+
<code>useBrowserSpeechRecognition</code> 훅이 설정되어 있으며,
|
|
721
|
+
다른 음성 인식 로직을 사용하고 싶은 경우 **voice** 옵션으로 교체할 수 있습니다.
|
|
722
|
+
|
|
723
|
+
| <img width="534" height="69" alt="Image" src="https://github.com/user-attachments/assets/15c90e44-1a44-4243-87ec-7755610eafb2" />|
|
|
724
|
+
| :---------------: |
|
|
725
|
+
| **ChatInput** |
|
|
726
|
+
|
|
727
|
+
<br>
|
|
728
|
+
|
|
729
|
+
### Usage
|
|
730
|
+
```tsx
|
|
731
|
+
<ChatInput
|
|
732
|
+
onSend={(value) => {
|
|
733
|
+
console.log(value);
|
|
734
|
+
}}
|
|
735
|
+
isSending={isPending}
|
|
736
|
+
/>
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
<br>
|
|
740
|
+
|
|
741
|
+
### Props
|
|
742
|
+
| name | type | required | description |
|
|
743
|
+
| ----------------- | ------------------------------------------ | -------- | ----------------------------- |
|
|
744
|
+
| `onSend` | `(value: string) => void \| Promise<void>` | ✅ | 메시지 전송 시 호출되는 콜백 |
|
|
745
|
+
| `isSending` | `boolean` | ✅ | 메시지 전송 중 상태 여부 |
|
|
746
|
+
| `voice` | `boolean \| VoiceRecognition` | ❌ | 음성 인식 사용 여부 또는 커스텀 음성 인식 컨트롤러<br>(<code>default</code>: true) |
|
|
747
|
+
| `placeholder` | `string` | ❌ | 입력창 placeholder 텍스트 |
|
|
748
|
+
| `className` | `string` | ❌ | 전체 wrapper 커스텀 클래스 |
|
|
749
|
+
| `inputClassName` | `string` | ❌ | textarea 커스텀 클래스 |
|
|
750
|
+
| `micButton` | `{ className?: string; icon?: ReactNode }` | ❌ | 마이크 버튼 커스터마이징 |
|
|
751
|
+
| `recordingButton` | `{ className?: string; icon?: ReactNode }` | ❌ | 녹음 중 버튼 커스터마이징 |
|
|
752
|
+
| `sendButton` | `{ className?: string; icon?: ReactNode }` | ❌ | 전송 버튼 커스터마이징 |
|
|
753
|
+
| `sendingButton` | `{ className?: string; icon?: ReactNode }` | ❌ | 전송 중 버튼 커스터마이징 |
|
|
754
|
+
| `maxHeight` | `number` | ❌ | textarea 최대 높이(px) |
|
|
755
|
+
| `value` | `string` | ❌ | 컨트롤드 모드 입력값 |
|
|
756
|
+
| `onChange` | `(value: string) => void` | ❌ | 컨트롤드 모드 입력 변경 핸들러 |
|
|
757
|
+
| `submitOnEnter` | `boolean` | ❌ | Enter 키로 전송할지 여부 |
|
|
758
|
+
|
|
759
|
+
<br>
|
|
760
|
+
|
|
761
|
+
<h2 id="chatcontainer">🎨 ChatContainer</h2>
|
|
762
|
+
|
|
763
|
+
<code>ChatContainer</code>는 **채팅 UI를 빠르게 구성하고 싶은 사용자를 위한 채팅 컨테이너 컴포넌트**입니다.
|
|
764
|
+
<code>ChatList</code>와 <code>ChatInput</code>을 내부에서 함께 렌더링하며,
|
|
765
|
+
<code>useChat</code>, <code>useVoiceChat</code>과 자연스럽게 결합할 수 있도록 설계되었습니다.
|
|
766
|
+
|
|
767
|
+
또한 <code>fetchNextPage</code>, <code>hasNextPage</code>, <code>isFetchingNextPage</code>를 props로 받아
|
|
768
|
+
스크롤을 최상단으로 올리면 과거 채팅 내역을 자동으로 로딩합니다.
|
|
769
|
+
|
|
770
|
+
| <img width="438" height="532" alt="Image" src="https://github.com/user-attachments/assets/b1713f34-419e-40cf-a79c-016c57145920" />|
|
|
771
|
+
| :---------------: |
|
|
772
|
+
| **ChatContainer** |
|
|
773
|
+
|
|
774
|
+
- 메시지 추가 시 스크롤이 하단에 고정됨
|
|
775
|
+
- 스크롤 최상단 도달 시 과거 메시지 페이지 로딩
|
|
776
|
+
- 하단에 도달하지 않은 상태에서는 "scroll to bottom" 버튼 노출
|
|
777
|
+
|
|
778
|
+
<br>
|
|
779
|
+
|
|
780
|
+
### Usage
|
|
781
|
+
```tsx
|
|
782
|
+
// 이미 Message 타입으로 정규화된 데이터를 사용하는 경우
|
|
783
|
+
<ChatContainer
|
|
784
|
+
messages={messages}
|
|
785
|
+
onSend={sendMessage}
|
|
786
|
+
isSending={isPending}
|
|
787
|
+
/>
|
|
788
|
+
|
|
789
|
+
// 서버에서 내려오는 Raw 데이터를 사용하는 경우
|
|
790
|
+
<ChatContainer
|
|
791
|
+
messages={rawMessages}
|
|
792
|
+
messageMapper={(raw) => ({
|
|
793
|
+
id: raw.id,
|
|
794
|
+
role: raw.sender === "user" ? "USER" : "AI",
|
|
795
|
+
content: raw.text,
|
|
796
|
+
})}
|
|
797
|
+
onSend={sendMessage}
|
|
798
|
+
isSending={isPending}
|
|
799
|
+
/>
|
|
800
|
+
|
|
801
|
+
// useChat, useVoiceChat과 함께 사용하는 경우
|
|
802
|
+
<ChatContainer
|
|
803
|
+
messages={messages}
|
|
804
|
+
onSend={sendMessage}
|
|
805
|
+
isSending={isPending}
|
|
806
|
+
fetchNextPage={fetchNextPage}
|
|
807
|
+
hasNextPage={hasNextPage}
|
|
808
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
809
|
+
/>
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
<br>
|
|
813
|
+
|
|
814
|
+
### Props
|
|
815
|
+
| name | type | required | description |
|
|
816
|
+
| -------------------- | ------------------------------------------ | -------- | ----------------------------------- |
|
|
817
|
+
| `messages` | `Message[] \| Raw[]` | ✅ | 렌더링할 메시지 배열 |
|
|
818
|
+
| `onSend` | `(value: string) => void \| Promise<void>` | ✅ | 메시지 전송 시 호출되는 콜백 |
|
|
819
|
+
| `isSending` | `boolean` | ✅ | 메시지 전송 중 상태 여부 |
|
|
820
|
+
| `messageMapper` | `(msg: Raw) => Message` | ❌ | Raw 데이터를 `Message`구조로 매핑하는 함수 |
|
|
821
|
+
| `messageRenderer` | `(msg: Message) => React.ReactNode` | ❌ | 기본 `ChatMessage` 대신 사용할 커스텀 메시지 렌더러 |
|
|
822
|
+
| `loadingRenderer` | `React.ReactNode` | ❌ | 메시지 로딩 상태에 사용할 커스텀 UI |
|
|
823
|
+
| `listClassName` | `string` | ❌ | `ChatList` wrapper 커스텀 클래스 |
|
|
824
|
+
| `disableVoice` | `boolean` | ❌ | 음성 입력 기능 비활성화 여부 |
|
|
825
|
+
| `placeholder` | `string` | ❌ | 입력창 placeholder 텍스트 |
|
|
826
|
+
| `inputClassName` | `string` | ❌ | `ChatInput` 커스텀 클래스 |
|
|
827
|
+
| `fetchNextPage` | `() => void` | ❌ | 다음 채팅 페이지를 요청하는 함수 |
|
|
828
|
+
| `hasNextPage` | `boolean` | ❌ | 다음 페이지 존재 여부 |
|
|
829
|
+
| `isFetchingNextPage` | `boolean` | ❌ | 다음 페이지 로딩 상태 |
|
|
830
|
+
| `className` | `string` | ❌ | 전체 컨테이너 wrapper 커스텀 클래스 |
|
|
831
|
+
|
|
832
|
+
<br>
|
|
833
|
+
|
|
834
|
+
<h1 id="notes">📝 Notes</h1>
|
|
835
|
+
|
|
836
|
+
## 1. 서버 사이드 페이지네이션은 필수입니다
|
|
837
|
+
이 라이브러리는 채팅 데이터를 **무한 스크롤** 기반으로 관리합니다.
|
|
838
|
+
따라서 서버는 반드시 **page 단위로 과거 채팅 내역을 조회**할 수 있어야 합니다.
|
|
839
|
+
|
|
840
|
+
<br>
|
|
841
|
+
|
|
842
|
+
## 2. 페이지 기준은 "과거 → 최신” 순서를 권장합니다
|
|
843
|
+
각 페이지는 **시간 오름차순(과거 → 최신)** 으로 정렬된 데이터를 반환해야 합니다.
|
|
844
|
+
이 구조를 기준으로 스크롤 위치를 유지하며 이전 페이지를 자연스럽게 연결합니다.
|
|
845
|
+
```ts
|
|
846
|
+
pages = [
|
|
847
|
+
page[0], // 가장 최근 페이지
|
|
848
|
+
page[1],
|
|
849
|
+
page[2],
|
|
850
|
+
page[3], // fetchNextPageParam으로 불러온 과거 채팅
|
|
851
|
+
];
|
|
852
|
+
```
|
|
853
|
+
```ts
|
|
854
|
+
page[0] = [
|
|
855
|
+
{ chatId: 0, time: "12:00" }, // 과거
|
|
856
|
+
{ chatId: 1, time: "12:10" },
|
|
857
|
+
{ chatId: 2, time: "12:20" },
|
|
858
|
+
{ chatId: 3, time: "12:30" }, // 최신
|
|
859
|
+
];
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
<br>
|
|
863
|
+
|
|
864
|
+
## 3. Optimistic Message는 서버 응답으로 교체되는 구조입니다
|
|
865
|
+
메시지 전송 시
|
|
866
|
+
|
|
867
|
+
**1.** 사용자 메시지를 즉시 캐시에 추가
|
|
868
|
+
**2.** 서버 응답 성공 → 해당 메시지를 실제 응답 메시지로 교체
|
|
869
|
+
**3.** 실패 시 → optimistic 메시지 롤백 + onError 호출
|
|
870
|
+
|
|
871
|
+
이 구조를 전제로 UI가 설계되어 있으므로
|
|
872
|
+
서버는 **단일 메시지 단위 응답**을 반환하는 것을 권장합니다.
|
|
873
|
+
|
|
874
|
+
<br>
|
|
875
|
+
|
|
876
|
+
## 4. ChatContainer는 “빠른 구현용” 컴포넌트입니다
|
|
877
|
+
<code>ChatContainer</code>는 다음을 한 번에 제공합니다
|
|
878
|
+
- 메시지 리스트 렌더링
|
|
879
|
+
- 입력창 + 전송 처리
|
|
880
|
+
- 상단 스크롤 기반 과거 메시지 로딩
|
|
881
|
+
- 스크롤 위치 자동 보정
|
|
882
|
+
|
|
883
|
+
보다 세밀한 UI 제어가 필요한 경우에는
|
|
884
|
+
<code>ChatList</code> + <code>ChatInput</code>을 직접 조합해 사용하는 것을 권장합니다.
|
|
885
|
+
|
|
886
|
+
<br>
|
|
887
|
+
|
|
888
|
+
# 📄 License
|
|
889
|
+
MIT License © 2025
|
|
890
|
+
See the [LICENSE](./LICENSE) file for details.
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
|