2026년 React AI 채팅 컴포넌트 구축하기: 완전 가이드
요약
본 가이드는 React를 사용하여 프로덕션 수준의 AI 채팅 인터페이스를 구축하는 방법을 다룹니다. 단순히 메시지를 표시하는 것을 넘어, 스트리밍 응답 처리, 마크다운 렌더링, 코드 하이라이팅, 그리고 견고한 에러 핸들링 기능을 포함합니다. 제공된 코드는 `ChatWindow` 컴포넌트를 통해 사용자 입력 및 AI 응답을 관리하며, API 호출 시 실시간으로 내용을 업데이트하는 스트리밍 로직을 구현하고 있습니다.
핵심 포인트
- React를 활용하여 복잡한 AI 채팅 UI를 구축할 수 있다.
- 스트리밍(Streaming) 응답 처리를 위해 `fetch`와 `TextDecoder`, `ReadableStream` API를 사용한다.
- 메시지 내용에 마크다운 렌더링 및 코드 하이라이팅 기능을 통합해야 한다.
- 사용자 경험(UX)을 개선하기 위해 로딩 상태, 에러 핸들링, 자동 스크롤 기능 등을 구현했다.
React에서 프로덕션 수준의 AI 채팅 인터페이스를 구축하려면 단순히 메시지를 표시하는 것 이상의 작업이 필요합니다. 스트리밍 응답 (streaming responses), 마크다운 렌더링 (markdown rendering), 코드 하이라이팅 (code highlighting), 에러 핸들링 (error handling), 그리고 세련된 UX가 필요합니다. 여기 전체 구현 방법이 있습니다. 핵심 채팅 컴포넌트 typescript // ChatWindow.tsx import React, { useState, useRef, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; interface Message { id: string; role: 'user' | 'assistant'; content: string; timestamp: Date; } interface ChatWindowProps { apiKey: string; model?: string; } export function ChatWindow({ apiKey, model = 'claude-3-5-sonnet-20241022' }: ChatWindowProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const messagesEndRef = useRef(null); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages, isLoading]); const sendMessage = async () => { if (!input.trim() || isLoading) return; const userMessage: Message = { id: crypto.randomUUID(), role: 'user', content: input.trim(), timestamp: new Date() }; setMessages(prev => [...prev, userMessage]); setInput(''); setIsLoading(true); setError(null); try { const response = await fetch(' https://api.ofox.ai/v1/chat/completions ', { method: 'POST', headers: { 'Authorization': Bearer ${apiKey}, 'Content-Type': 'application/json' }, body: JSON.stringify({ model, messages: [...messages, userMessage].map(m => ({ role: m.role, content: m.content })), stream: true }) }); if (!response.ok) { throw new Error(API error: ${response.status}`); } // 스트리밍 응답 처리 const reader = response.body?.getReader(); const decoder = new TextDecoder(); let assistantContent = ''; const assistantMessage: Message = { id: crypto.randomUUID(), role: 'assistant', content: '',
timestamp: new Date() }; setMessages(prev => [...prev, assistantMessage]); while (reader) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); if (data.choices[0].delta.content) { assistantContent += data.choices[0].delta.content; setMessages(prev => prev.map(m => m.id === assistantMessage.id ? { ...m, content: assistantContent } : m )); } } } } } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); setMessages(prev => prev.filter(m => m.id !== userMessage.id)); } finally { setIsLoading(false); } }; return ( {messages.map(msg => ( <div key={msg.id}> {msg.role === 'user' ? 'You' : 'Claude'} </div> <div className="message-content"> {msg.content} </div> ))} {isLoading && ( <div> Claude ... </div> )} {error && ( <div className="error"> {error} </div> )} <input value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }} placeholder="Ask Claude..." disabled={isLoading} /> <button onClick={sendMessage} disabled={isLoading}> Send </button> ); } Streaming vs Non-Streaming
typecript
// Non-streaming (더 간단하며 짧은 응답에 적합)
async function chat(apiKey: string, messages: Message[]) {
const response = await fetch(' https://api.ofox.ai/v1/chat/completions ', {
method: 'POST',
headers: {
'Authorization': Bearer ${apiKey},
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
messages: messages.map(m => ({ role: m.role, content: m.content }))
})
});
const data = await response.json();
return data.choices[0].message.content;
}
// Streaming (긴 응답에 대해 더 나은 UX 제공)
async function* chatStream(apiKey: string, messages: Message[]) {
const response = await fetch(' https://api.ofox.ai/v1/chat/completions ', {
method: 'POST',
headers: {
'Authorization': Bearer ${apiKey},
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
messages: messages.map(m => ({ role: m.role, content: m.content })), stream: true }) }); const reader = response.body?.getReader(); const decoder = new TextDecoder(); while (reader) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); for (const line of chunk.split('\n')) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); if (data.choices[0].delta.content) { yield data.choices[0].delta.content; } } } } } 현대적인 React 패턴으로 구축하기typescript // useChat hook 함수 function useChat(initialMessages: Message[] = []) { const [messages, setMessages] = useState(initialMessages); const [isLoading, setIsLoading] = useState(false); const send = async (content: string) => { const userMessage: Message = { id: crypto.randomUUID(), role: 'user', content, timestamp: new Date() }; setMessages(prev => [...prev, userMessage]); setIsLoading(true); // ... 스트리밍 로직 (streaming logic) setIsLoading(false); }; const clear = () => setMessages([]); return { messages, send, clear, isLoading }; } // 사용법 function App() { const { messages, send, isLoading } = useChat(); return ; } ` 시작하기 Power your React chat app with ofox.ai — Claude 모델을 지원하는 OpenAI 호환 API입니다. 가입하여 API 키를 받고 구축을 시작하세요. 👉 ofox.ai에서 시작하기 이 기사에는 제휴 링크가 포함되어 있습니다. Tags: react,javascript,ai,programming,webdev Canonical URL: https://dev.to/zny10289
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기