13.2 실시간 챗 UI 만들기
물론입니다. 제목에 맞추어 실제 프로덕션에 도입 가능한 수준의 기술 설명과 예제를 포함한 실시간 챗 UI 구성 절을 자세히 집필해드리겠습니다.
13.2 실시간 챗 UI 만들기
OpenAI API를 활용한 애플리케이션에서는 대화형 경험이 핵심인 경우가 많습니다. 특히 사용자가 텍스트를 입력하고, GPT 사용 결과를 실시간으로 받아보는 "챗 UI(chat user interface)"는 다양한 서비스(예: 고객지원, 개인 비서, 교육 플랫폼 등)의 중심 인터페이스로 자리잡고 있습니다.
이번 절에서는 React 기반의 웹 프론트를 토대로 실시간 챗 UI를 단계별로 구현하는 과정을 소개합니다. 주요 구현 요소에는 다음이 포함됩니다:
React 컴포넌트 설계 및 상태 관리
OpenAI API와의 비동기 통신
사용자 입력 및 응답 흐름 렌더링
Streaming 응답 처리 (서버 측 EventSource or WebSocket 대체 방식 등)
UX 향상을 위한 최적 기능 (로딩 인디케이터, 키보드 입력 핸들링 등)
1. 설계 개요
우리가 만들 챗 UI는 다음 구조를 기본으로 합니다:
▲ 상단: "Chat with GPT" 제목 표시
▒ 가운데: 메시지 대화 영역 (user / assistant 발화가 시간 순으로 나열)
▼ 하단: 사용자 입력용 textarea & 전송 버튼
React에서는 하나의 ChatApp 컴포넌트를 만들고, 필요한 하위 컴포넌트를 분리 구성합니다.
구현에 사용할 대표 기술 스택:
Frontend: React (Hook 기반)
OpenAI API 호출 방식: fetch
Streaming (옵션): ReadableStream 기반 처리
CSS 프레임워크: Tailwind CSS (또는 순수 CSS 기준 예시 제공)
2. 프로젝트 초기화
먼저 React 프로젝트를 생성하고 필요한 라이브러리를 설치합니다.
npx create-react-app openai-chat-ui --template cra-template-pwa
cd openai-chat-ui
npm install
.env 파일에 OpenAI API Key를 넣어두면 좋습니다 (단, 실제 배포 시 클라이언트에 노출되지 않도록 주의!)
REACT_APP_OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
⚠️ 클라이언트에서 직접 API Key를 호출하는 것은 보안상 위험하므로 후반부에서 서버 프록시 혹은 백엔드 API 패턴으로 개선합니다.
3. 기본 챗 컴포넌트 구성
다음은 기본적인 컴포넌트 구조입니다:
App.js
: 루트 렌더링ChatApp.jsx
: 전체 챗 인터페이스 UIMessageList.jsx
: 메시지 목록 표시MessageInput.jsx
: 사용자 입력창
예시 디렉토리 구조:
src/
├─ components/
│ ├─ ChatApp.jsx
│ ├─ MessageList.jsx
│ └─ MessageInput.jsx
4. 상태 관리 구조
우리는 다음 상태들을 관리합니다:
const [messages, setMessages] = useState([
{ role: "system", content: "You are a helpful assistant." },
{ role: "assistant", content: "안녕하세요! 무엇을 도와드릴까요?" }
]);
const [currentInput, setCurrentInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
5. 메시지 입력과 전송 처리
사용자의 입력을 받아 messages
배열에 추가하고 OpenAI API로 전달합니다.
MessageInput.jsx:
const MessageInput = ({ onSend, isLoading }) => {
const [input, setInput] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
onSend(input);
setInput("");
};
return (
<form onSubmit={handleSubmit} className="flex gap-2 mt-2">
<textarea
rows={2}
value={input}
disabled={isLoading}
onChange={(e) => setInput(e.target.value)}
placeholder="메시지를 입력하세요..."
className="flex-1 border p-2 rounded"
/>
<button
type="submit"
disabled={isLoading}
className="bg-blue-500 text-white px-4 rounded"
>
전송
</button>
</form>
);
};
6. OpenAI API 호출 로직 (기본 응답 모드)
ChatApp.jsx 내부에서 메시지를 서버로 전송하고 응답을 받아온 후 상태를 갱신합니다.
const sendMessage = async (userInput) => {
const newMessages = [
...messages,
{ role: "user", content: userInput }
];
setMessages(newMessages);
setIsLoading(true);
try {
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages: newMessages
})
});
const data = await res.json();
const reply = data.choices[0].message;
setMessages([...newMessages, reply]);
} catch (err) {
console.error("API 호출 오류", err);
} finally {
setIsLoading(false);
}
};
7. 메시지 목록 렌더링
const MessageList = ({ messages }) => {
return (
<div className="flex flex-col gap-2 p-4 overflow-y-auto" style={{ height: "400px" }}>
{messages.map((msg, idx) => (
<div key={idx} className={`p-2 rounded ${msg.role === "user" ? 'bg-gray-200 text-right' : 'bg-green-100 text-left'}`}>
<strong>{msg.role === "user" ? "👤 사용자" : "🤖 GPT"}:</strong> {msg.content}
</div>
))}
</div>
);
};
8. 실시간 응답 스트리밍 처리 (선택 기능)
OpenAI의 GPT-4와 GPT-3.5-turbo는 스트리밍 응답 옵션(stream: true)을 지원합니다.
이 기능을 활용하면 답변을 한 번에 받는 것이 아니라, 조각을 순차적으로 받아 사용자에게 "실시간" 느낌을 줄 수 있습니다.
const handleStreamingResponse = async (userInput) => {
const newMessages = [...messages, { role: "user", content: userInput }];
setMessages(newMessages);
setIsLoading(true);
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`
},
body: JSON.stringify({
model: "gpt-4",
messages: newMessages,
stream: true
})
});
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let fullText = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n");
for (let line of lines) {
if (line.startsWith("data: ")) {
const dataStr = line.replace("data: ", "");
if (dataStr === "[DONE]") break;
const data = JSON.parse(dataStr);
const delta = data.choices[0].delta.content || "";
fullText += delta;
setMessages([
...newMessages,
{ role: "assistant", content: fullText }
]);
}
}
}
setIsLoading(false);
};
🔔 주의: 위 로직에서는 스트리밍 응답을 실시간 렌더링 하기 위해 반복적으로 setState를 호출하게 되는데, 이는 성능 저하를 유발할 수 있으므로 throttle/debounce 또는 progressive rendering 최적화 기법이 필요할 수 있습니다.
9. UX 최적화 포인트
전송 버튼 비활성화 처리 (입력 공백 혹은 로딩 중)
assistant 메시지에 타이핑 애니메이션 추가 (CSS 애니메이션 or 텍스트 점진 출력)
자동 스크롤:
useEffect(() => {
const container = document.getElementById("chat-scroll");
container?.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
}, [messages]);
10. 보안상의 주의점
API 키를 React 내에 직접 노출하는 것은 매우 위험하니 반드시 Backend를 중계하게 합니다 (예: Express API 경유)
production 배포 시 HTTP 대신 HTTPS 필수
악성 입력 방지 필터 (예: 프롬프트 인젝션)
간단한 Express.js 백엔드 예시:
app.post("/api/chat", async (req, res) => {
const { messages } = req.body;
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages,
})
});
const data = await response.json();
res.json(data);
});
정리
React를 기반으로 OpenAI API와 통신하는 실시간 챗 UI를 구현하는 핵심 과정은 다음과 같습니다:
사용자 메시지를 수집 → 상태 업데이트
OpenAI API 호출 → assistant 응답 수신
스트리밍 응답의 경우 응답 흐름을 처리하여 인터랙션 향상
메시지를 시간 순서대로 프론트에 출력
UX를 위한 스크롤/로딩 처리 등 보강
이 UI는 추후 LangChain, RAG, embedding 기반 검색 등 다양한 기능과도 결합할 수 있는 강력한 기초 구조를 제공합니다.
다음 절에서는 이 인터페이스를 기반으로 한 Gradio, Streamlit 등의 프론트엔드 프레임워크 통합 방식을 살펴봅니다.
Last updated