코드는 React를 사용하여 간단한 웹 챗봇 인터페이스를 구현한 것입니다. 사용자가 메시지를 입력하고 전송하면, 해당 메시지가 화면에 표시되고 백엔드 API(http://localhost:5500/searchChat)와 통신하여 챗봇의 응답을 받아 다시 화면에 표시하는 기능을 합니다.
import React, {useState} from "react" // React 라이브러리와 React 훅인 useState를 가져옵니다.
// useState: 컴포넌트 내에서 상태(데이터)를 관리할 수 있게 해주는 훅입니다.
import "./ChatPage.css" // ChatPage 컴포넌트의 스타일을 정의하는 CSS 파일을 가져옵니다.
const ChatPage =() => { // ChatPage라는 이름의 함수형 컴포넌트를 정의합니다. React 컴포넌트 이름은 대문자로 시작해야 합니다.
// --- 1. 상태(State) 변수 정의 ---
// React 컴포넌트의 UI나 동작에 영향을 미치는 데이터는 '상태'로 관리해야 합니다.
// input: 사용자가 입력 필드에 타이핑하는 현재 텍스트를 저장하는 상태 변수입니다.
// setInput: input 상태를 업데이트하는 함수입니다.
const [input, setInput] = useState("") // input의 초기값은 빈 문자열("")입니다.
// messages: 채팅창에 표시될 메시지들의 배열을 저장하는 상태 변수입니다.
// 각 메시지는 {sender: "user" | "bot", text: "메시지 내용"} 형태의 객체입니다.
// setMessages: messages 상태를 업데이트하는 함수입니다.
const [messages, setMessages] = useState([{sender:"bot", text:"안녕하세요, 무엇을 도와드릴까요?"}])
// messages의 초기값으로 챗봇의 환영 메시지를 포함합니다.
// --- 2. 이벤트 핸들러 함수 정의 ---
// handleSend: 메시지 전송 버튼 클릭 또는 Enter 키 입력 시 실행될 비동기 함수입니다.
const handleSend = async() => { // async 키워드는 이 함수 내에서 await를 사용하여 비동기 작업을 기다릴 수 있게 합니다.
// 1. 사용자 메시지 생성 및 화면에 즉시 추가
const userMessage = {sender: "user", text: input} // 현재 input 상태의 텍스트로 사용자 메시지 객체를 생성합니다.
setMessages(prev => [...prev, userMessage]) // setMessages를 사용하여 messages 상태를 업데이트합니다.
// prev: 이전 messages 상태를 참조합니다.
// [...prev, userMessage]: 이전 메시지들(prev)을 모두 복사하고, 그 뒤에 새로운 userMessage를 추가하여 새로운 배열을 만듭니다.
// 이렇게 해야 React의 불변성(immutability) 원칙을 지키면서 상태를 업데이트할 수 있습니다.
// 2. 입력 필드 초기화
setInput("") // 메시지를 보낸 후 input 필드를 비웁니다.
// 3. 백엔드 API 호출 (비동기 통신)
try{ // 네트워크 요청은 실패할 수 있으므로 try-catch 블록으로 에러를 처리합니다.
const response = await fetch("http://localhost:5500/searchChat",{ // fetch API를 사용하여 백엔드(Flask 앱)의 /searchChat 엔드포인트로 POST 요청을 보냅니다.
method: "POST", // HTTP 요청 메서드를 POST로 지정합니다.
headers : { // 요청 헤더를 설정합니다.
"Content-Type": "application/json", // 보내는 데이터가 JSON 형식임을 서버에 알립니다.
},
body: JSON.stringify({inputText : input}) // input 상태의 텍스트를 JSON 문자열로 변환하여 요청 본문에 담아 보냅니다.
// 주의: 여기서 input은 handleSend 함수가 호출될 당시의 input 값입니다.
// setInput("")으로 input이 초기화되기 전의 값입니다.
})
// 4. 백엔드 응답 처리
const data = await response.text() // 서버로부터 받은 응답을 텍스트 형태로 파싱합니다. (서버가 JSON이 아닌 일반 텍스트를 반환할 경우)
const chatBotMessage = {sender: "bot", text:data} // 파싱된 텍스트로 챗봇 메시지 객체를 생성합니다.
// 5. 챗봇 메시지를 messages 상태에 추가
setMessages(prev => [...prev, chatBotMessage]) // 이전 메시지들 뒤에 챗봇 메시지를 추가하여 상태를 업데이트합니다.
}catch(error){ // fetch 요청 또는 응답 처리 중 에러가 발생하면 이 블록이 실행됩니다.
console.error("메시지 전송 중 오류 발생:", error) // 콘솔에 에러를 출력합니다. (사용자에게는 다른 방식으로 보여줄 수 있음)
setMessages(prev => [...prev, {sender: "bot", text: "오류가 발생했습니다. 다시 시도해주세요."}]); // 오류 메시지를 챗봇 메시지로 표시
}
}
// handleKeyDown: 입력 필드에서 키보드 이벤트 발생 시 실행될 함수입니다.
const handleKeyDown = (e) => {
if(e.key === "Enter") { // 눌린 키가 "Enter" 키인지 확인합니다.
handleSend(); // Enter 키가 눌렸으면 메시지 전송 함수를 호출합니다.
}
}
// --- 3. 컴포넌트 UI (JSX) 렌더링 ---
return( // 컴포넌트가 화면에 렌더링할 JSX를 반환합니다.
<div className = "chat-fullscreen"> {/* 전체 채팅 페이지를 감싸는 컨테이너. CSS 클래스 적용. */}
<div className = "chat-header"> {/* 채팅 헤더 영역 */}
챗봇 만들기
</div>
<div className = "chat-window"> {/* 메시지들이 표시될 채팅창 영역 */}
{
// messages 배열을 순회하며 각 메시지에 대해 <div> 요소를 생성합니다.
messages.map((msg, i) => (
// key={i}: React에서 리스트를 렌더링할 때 각 요소에 고유한 'key' prop을 부여해야 합니다.
// 여기서는 인덱스(i)를 사용했지만, 실제 앱에서는 메시지 ID와 같은 고유한 값을 사용하는 것이 좋습니다.
// className={`chat-bubble ${msg.sender}`}: 각 메시지에 'chat-bubble' 클래스와 함께
// 'user' 또는 'bot' 클래스를 동적으로 적용하여 스타일을 다르게 합니다.
<div key={i} className={`chat-bubble ${msg.sender}`}>
{msg.text} {/* 메시지 내용을 표시합니다. */}
</div>
))
}
</div>
<div className="chat-input-bar"> {/* 메시지 입력 및 전송 영역 */}
<input
type="text" // 입력 필드 타입
value={input} // input 상태 변수와 입력 필드의 값을 바인딩합니다. (제어 컴포넌트)
onChange={e => setInput(e.target.value)} // 입력 필드 내용이 변경될 때마다 input 상태를 업데이트합니다.
placeholder="메시지를 입력하세요" // 입력 필드에 표시될 안내 텍스트
onKeyDown = {handleKeyDown} // 키보드 눌림 이벤트 발생 시 handleKeyDown 함수 호출
></input>
{/* 전송 버튼을 추가할 수도 있습니다. */}
{/* <button onClick={handleSend}>전송</button> */}
</div>
</div>
)
}
export default ChatPage; // ChatPage 컴포넌트를 외부에서 가져다 사용할 수 있도록 내보냅니다.
React ChatPage 컴포넌트 코딩 순서 및 사고 과정
이 챗봇 페이지를 처음부터 코딩해 나간다고 가정하고, 어떤 순서로 어떤 부분을 추가하며 생각해야 하는지 단계별로 설명해 드리겠습니다.
1단계: 기본 React 컴포넌트 구조 잡기
가장 먼저, React 컴포넌트의 기본적인 틀을 만듭니다.
- 파일 생성: ChatPage.jsx (또는 ChatPage.js) 파일과 ChatPage.css 파일을 만듭니다.
- 기본 임포트 및 컴포넌트 정의:
- 사고 과정: "일단 화면에 챗봇 레이아웃을 보여줘야 하니, 헤더, 메시지 창, 입력 바 이렇게 세 부분으로 나눌 수 있겠다. 각 부분에 CSS 클래스를 미리 지정해두면 나중에 스타일링하기 편하겠지?"
- import React from "react"; // React를 사용하기 위해 필수 import "./ChatPage.css"; // 스타일 시트 임포트 const ChatPage = () => { // 컴포넌트 로직이 들어갈 곳 return ( // JSX (UI)가 들어갈 곳 <div className="chat-fullscreen"> <div className="chat-header">챗봇 만들기</div> <div className="chat-window"></div> <div className="chat-input-bar"></div> </div> ); }; export default ChatPage; // 외부에서 이 컴포넌트를 사용할 수 있도록 내보내기
2단계: 사용자 입력 필드 구현 (상태 관리 시작)
사용자가 메시지를 입력할 수 있는 <input> 요소를 추가하고, 그 값을 React 상태로 관리합니다.
- useState 임포트: import React, { useState } from "react"로 변경합니다.
- input 상태 정의:
- const [input, setInput] = useState(""); // 사용자가 입력할 텍스트 상태
- <input> 요소에 바인딩:
- 사고 과정: "사용자가 입력하는 텍스트는 계속 변하니 React의 상태로 관리해야 해. useState를 써서 input 변수에 현재 값을 저장하고, setInput 함수로 업데이트해야지. <input> 태그의 value와 onChange를 연결하면 사용자가 입력할 때마다 input 상태가 자동으로 업데이트될 거야."
- // ... (return 문 내부) ... <div className="chat-input-bar"> <input type="text" value={input} // input 상태와 연결 onChange={e => setInput(e.target.value)} // 입력값 변경 시 상태 업데이트 placeholder="메시지를 입력하세요" /> </div>
3단계: 메시지 목록 표시 및 초기 메시지 설정
채팅 메시지들을 저장할 상태를 만들고, 이를 화면에 렌더링합니다.
- messages 상태 정의:
- const [messages, setMessages] = useState([{sender:"bot", text:"안녕하세요, 무엇을 도와드릴까요?"}]); // 메시지 목록 상태
- chat-window에 메시지 렌더링:
- 사고 과정: "챗봇 대화는 여러 메시지가 순서대로 쌓이는 형태이니 배열로 관리하는 게 좋겠어. 초기에는 챗봇의 환영 메시지가 있어야겠지? map 함수를 써서 배열의 각 요소를 div로 만들고, sender에 따라 다른 스타일을 적용할 수 있도록 className을 동적으로 바꿔줘야겠다. 리스트 렌더링 시 key prop은 필수니까 i라도 넣어두자 (나중에 고유 ID로 바꾸는 게 좋겠지만)."
- // ... (return 문 내부) ... <div className="chat-window"> { messages.map((msg, i) => ( <div key={i} className={`chat-bubble ${msg.sender}`}> {msg.text} </div> )) } </div>
4단계: 메시지 전송 기능 (사용자 메시지 추가)
사용자가 입력한 메시지를 messages 배열에 추가하는 기능을 구현합니다.
- handleSend 함수 정의 (초기 버전):
- const handleSend = () => { const userMessage = {sender: "user", text: input}; setMessages(prev => [...prev, userMessage]); // 사용자 메시지 추가 setInput(""); // 입력 필드 초기화 };
- Enter 키 이벤트 핸들러:
- 사고 과정: "메시지를 보내려면 버튼 클릭이나 Enter 키 입력이 필요해. handleSend 함수를 만들어서 현재 input 값을 사용자 메시지로 만들고 messages 배열에 추가해야지. 그리고 input 필드는 비워야 다음 메시지를 입력할 수 있으니까 setInput("")도 호출해야겠다. Enter 키로도 전송되게 onKeyDown 이벤트도 붙여야지."
- const handleKeyDown = (e) => { if(e.key === "Enter") handleSend(); // Enter 키 누르면 전송 }; // ... (input 태그에 onKeyDown={handleKeyDown} 추가)
5단계: 백엔드 API 연동 (Fetch API)
이제 백엔드 Flask 앱과 통신하여 챗봇 응답을 받아옵니다.
- handleSend를 async 함수로 변경: const handleSend = async () => { ... }
- fetch API 호출:
- 사고 과정: "챗봇 응답을 받으려면 백엔드 API를 호출해야 해. fetch API를 사용하면 비동기적으로 HTTP 요청을 보낼 수 있지. await를 쓰려면 handleSend 함수를 async로 만들어야 해. POST 요청이니까 method, headers, body를 설정해야 하고, inputText는 JSON 형태로 보내야 하니까 JSON.stringify를 써야겠다. 네트워크 요청은 실패할 수 있으니 try-catch로 감싸서 에러 처리도 해줘야지. 서버 응답을 받으면 그걸 챗봇 메시지로 만들어서 messages에 추가해야 해."
- 주의 사항: body: JSON.stringify({inputText : input})에서 input을 직접 사용하면, setInput("")으로 input 상태가 초기화된 후 fetch 요청이 보내질 때 input이 빈 문자열이 될 수 있습니다. 이를 방지하기 위해 currentInput 변수에 값을 미리 저장하는 것이 좋습니다.
- const handleSend = async () => { const userMessage = {sender: "user", text: input}; setMessages(prev => [...prev, userMessage]); const currentInput = input; // fetch 요청에 사용될 input 값을 미리 저장 (setInput으로 초기화되기 전) setInput(""); try { const response = await fetch("http://localhost:5500/searchChat", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({inputText: currentInput}) // 저장된 input 값 사용 }); const data = await response.text(); // 서버 응답 파싱 const chatBotMessage = {sender: "bot", text: data}; setMessages(prev => [...prev, chatBotMessage]); // 챗봇 메시지 추가 } catch (error) { console.error("메시지 전송 중 오류 발생:", error); setMessages(prev => [...prev, {sender: "bot", text: "오류가 발생했습니다. 다시 시도해주세요."}]); } };
6단계: CSS 스타일링
ChatPage.css 파일을 작성하여 챗봇 UI에 시각적인 스타일을 적용합니다.
/* ChatPage.css */
.chat-fullscreen {
display: flex;
flex-direction: column; /* 세로 방향으로 요소들을 정렬 */
height: 100vh; /* 전체 뷰포트 높이 사용 */
max-width: 600px; /* 최대 너비 설정 */
margin: 0 auto; /* 중앙 정렬 */
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden; /* 내용이 넘칠 경우 숨김 */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.chat-header {
background-color: #4CAF50; /* 헤더 배경색 */
color: white;
padding: 15px;
font-size: 1.2em;
font-weight: bold;
text-align: center;
}
.chat-window {
flex-grow: 1; /* 남은 공간을 모두 차지하도록 설정 */
padding: 15px;
overflow-y: auto; /* 내용이 넘칠 경우 스크롤바 생성 */
background-color: #f9f9f9;
display: flex;
flex-direction: column;
gap: 10px; /* 메시지 버블 간 간격 */
}
.chat-bubble {
max-width: 70%; /* 버블의 최대 너비 */
padding: 10px 15px;
border-radius: 18px;
line-height: 1.4;
word-wrap: break-word; /* 긴 단어 자동 줄바꿈 */
}
.chat-bubble.user {
align-self: flex-end; /* 사용자 메시지는 오른쪽 정렬 */
background-color: #DCF8C6; /* 사용자 버블 색상 */
color: #333;
border-bottom-right-radius: 4px; /* 모서리 조절 */
}
.chat-bubble.bot {
align-self: flex-start; /* 봇 메시지는 왼쪽 정렬 */
background-color: #E0E0E0; /* 봇 버블 색상 */
color: #333;
border-bottom-left-radius: 4px; /* 모서리 조절 */
}
.chat-input-bar {
display: flex;
padding: 15px;
border-top: 1px solid #eee;
background-color: white;
}
.chat-input-bar input {
flex-grow: 1; /* 입력 필드가 남은 공간을 모두 차지 */
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 20px;
font-size: 1em;
outline: none; /* 포커스 시 외곽선 제거 */
}
.chat-input-bar input:focus {
border-color: #4CAF50; /* 포커스 시 테두리 색상 변경 */
}
- 사고 과정: "챗봇 UI는 보통 세로로 쌓이는 구조니까 flex-direction: column을 사용해야겠다. 헤더는 고정, 메시지 창은 스크롤 가능하게, 입력 바는 하단에 고정해야지. 메시지 버블은 sender에 따라 색깔과 정렬을 다르게 하고, 입력 필드도 예쁘게 꾸며야겠다."
7단계: 추가 개선 사항 (선택 사항)
- 스크롤 자동 내리기: 새 메시지가 추가될 때마다 채팅창이 자동으로 가장 아래로 스크롤되도록 useEffect와 scrollIntoView를 사용할 수 있습니다.
- 로딩 인디케이터: API 요청 중임을 사용자에게 알리기 위해 로딩 스피너를 추가할 수 있습니다.
- 에러 메시지 UI: catch 블록에서 console.error 대신 사용자에게 보이는 에러 메시지를 messages 배열에 추가하거나 별도의 알림 UI를 사용할 수 있습니다.
- 입력값 유효성 검사: 빈 메시지 전송 방지 등.
- 백엔드 URL 설정: http://localhost:5500을 환경 변수나 설정 파일로 관리하여 배포 환경에 따라 쉽게 변경할 수 있도록 합니다.
이러한 단계들을 통해 React 챗봇 컴포넌트를 체계적으로 개발하고 완성할 수 있습니다. 각 단계에서 필요한 React 개념과 웹 개발 원칙들을 적용하며 코드를 작성하는 것이 중요합니다.
React ChatPage 컴포넌트 최종 완성 코드 및 해설
이 문서는 이전 React ChatPage 컴포넌트 상세 해설 Canvas의 내용을 바탕으로, 7단계 추가 개선 사항을 적용하고 메시지 key 값을 고유 ID로 변경한 최종 완성 코드와 그 상세 해설을 제공합니다.
1. 최종 완성 코드 (ChatPage.jsx)
import React, { useState, useEffect, useRef } from "react"; // useRef 훅을 추가로 가져옵니다.
import "./ChatPage.css"; // ChatPage 컴포넌트의 스타일을 정의하는 CSS 파일을 가져옵니다.
// 백엔드 URL을 환경 변수에서 가져오거나 기본값을 설정합니다.
// 실제 프로젝트에서는 .env 파일에 REACT_APP_BACKEND_URL=http://your-ec2-public-ip:5500 과 같이 설정합니다.
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || "http://localhost:5500";
const ChatPage = () => {
// --- 1. 상태(State) 변수 정의 ---
const [input, setInput] = useState("");
// 각 메시지에 고유 ID를 부여하기 위해 id 속성을 추가했습니다.
const [messages, setMessages] = useState([
{ id: Date.now(), sender: "bot", text: "안녕하세요, 무엇을 도와드릴까요?" }
]);
const [isLoading, setIsLoading] = useState(false); // 로딩 상태를 관리하는 상태 변수
// --- 2. useRef 훅 정의 ---
// chatWindowRef: 메시지 창의 DOM 요소에 직접 접근하여 스크롤을 제어하기 위한 ref입니다.
const chatWindowRef = useRef(null);
// --- 3. useEffect 훅 정의 ---
// 메시지가 추가될 때마다 채팅창을 자동으로 가장 아래로 스크롤합니다.
useEffect(() => {
if (chatWindowRef.current) {
chatWindowRef.current.scrollTop = chatWindowRef.current.scrollHeight;
}
}, [messages]); // messages 상태가 변경될 때마다 이 훅이 실행됩니다.
// --- 4. 이벤트 핸들러 함수 정의 ---
// handleSend: 메시지 전송 버튼 클릭 또는 Enter 키 입력 시 실행될 비동기 함수입니다.
const handleSend = async () => {
// 입력값이 비어있거나, 이미 로딩 중인 경우 함수 실행을 중단합니다.
if (!input.trim() || isLoading) {
return;
}
const userMessageText = input.trim(); // 사용자 메시지에서 앞뒤 공백 제거
const userMessage = { id: Date.now(), sender: "user", text: userMessageText }; // 고유 ID 부여
// 사용자 메시지를 즉시 화면에 추가하고 입력 필드를 초기화합니다.
setMessages(prev => [...prev, userMessage]);
setInput("");
setIsLoading(true); // 로딩 상태 시작
try {
const response = await fetch(`${BACKEND_URL}/searchChat`, { // 백엔드 URL 변수 사용
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ inputText: userMessageText }) // 사용자 메시지 텍스트를 body에 담아 보냅니다.
});
if (!response.ok) { // HTTP 응답 상태 코드가 200번대가 아닐 경우 에러 처리
const errorData = await response.text();
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData}`);
}
const data = await response.text();
const chatBotMessage = { id: Date.now() + 1, sender: "bot", text: data }; // 챗봇 메시지에도 고유 ID 부여
setMessages(prev => [...prev, chatBotMessage]);
} catch (error) {
console.error("메시지 전송 중 오류 발생:", error);
// 사용자에게 보이는 오류 메시지 추가
setMessages(prev => [...prev, { id: Date.now() + 1, sender: "bot", text: "오류가 발생했습니다. 다시 시도해주세요." }]);
} finally {
setIsLoading(false); // 로딩 상태 종료
}
};
// handleKeyDown: 입력 필드에서 키보드 이벤트 발생 시 실행될 함수입니다.
const handleKeyDown = (e) => {
if (e.key === "Enter") {
handleSend();
}
};
// --- 5. 컴포넌트 UI (JSX) 렌더링 ---
return (
<div className="chat-fullscreen">
<div className="chat-header">
챗봇 만들기
</div>
<div ref={chatWindowRef} className="chat-window"> {/* ref를 메시지 창에 연결 */}
{
messages.map((msg) => ( // key에 msg.id 사용
<div key={msg.id} className={`chat-bubble ${msg.sender}`}>
{msg.text}
</div>
))
}
{isLoading && ( // 로딩 중일 때 로딩 인디케이터 표시
<div className="chat-bubble bot loading">
<div className="dot-flashing"></div>
</div>
)}
</div>
<div className="chat-input-bar">
<input
type="text"
value={input}
onChange={e => setInput(e.target.value)}
placeholder={isLoading ? "답변을 기다리는 중..." : "메시지를 입력하세요"} // 로딩 상태에 따라 placeholder 변경
onKeyDown={handleKeyDown}
disabled={isLoading} // 로딩 중일 때 입력 필드 비활성화
></input>
<button onClick={handleSend} disabled={isLoading || !input.trim()}>전송</button> {/* 전송 버튼 추가 및 비활성화 조건 */}
</div>
</div>
);
}
export default ChatPage;
2. ChatPage.css (추가 및 수정된 스타일)
/* ChatPage.css */
.chat-fullscreen {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 600px;
margin: 0 auto;
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.chat-header {
background-color: #4CAF50;
color: white;
padding: 15px;
font-size: 1.2em;
font-weight: bold;
text-align: center;
}
.chat-window {
flex-grow: 1;
padding: 15px;
overflow-y: auto; /* 스크롤 가능하게 */
background-color: #f9f9f9;
display: flex;
flex-direction: column;
gap: 10px;
}
.chat-bubble {
max-width: 70%;
padding: 10px 15px;
border-radius: 18px;
line-height: 1.4;
word-wrap: break-word;
}
.chat-bubble.user {
align-self: flex-end;
background-color: #DCF8C6;
color: #333;
border-bottom-right-radius: 4px;
}
.chat-bubble.bot {
align-self: flex-start;
background-color: #E0E0E0;
color: #333;
border-bottom-left-radius: 4px;
}
.chat-input-bar {
display: flex;
padding: 15px;
border-top: 1px solid #eee;
background-color: white;
gap: 10px; /* 입력 필드와 버튼 사이 간격 */
}
.chat-input-bar input {
flex-grow: 1;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 20px;
font-size: 1em;
outline: none;
}
.chat-input-bar input:focus {
border-color: #4CAF50;
}
.chat-input-bar button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease;
}
.chat-input-bar button:hover:not(:disabled) {
background-color: #45a049;
}
.chat-input-bar button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
/* 로딩 인디케이터 스타일 */
.chat-bubble.loading {
background-color: #f0f0f0;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
min-width: 50px; /* 로딩 버블 최소 너비 */
min-height: 20px; /* 로딩 버블 최소 높이 */
}
.dot-flashing {
position: relative;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #888;
color: #888;
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 0.5s;
}
.dot-flashing::before, .dot-flashing::after {
content: '';
display: inline-block;
position: absolute;
top: 0;
}
.dot-flashing::before {
left: -15px;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #888;
color: #888;
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 0s;
}
.dot-flashing::after {
left: 15px;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #888;
color: #888;
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 1s;
}
@keyframes dotFlashing {
0% {
background-color: #888;
}
50%, 100% {
background-color: #ccc;
}
}
3. 변경 사항 상세 해설
이전 코드에서 다음과 같은 주요 개선 사항들이 적용되었습니다.
3.1. key={i}를 고유 ID로 변경
- 변경 내용: messages 상태의 각 메시지 객체에 id 속성을 추가하고, 이 id를 map 함수의 key prop으로 사용했습니다.
- const [messages, setMessages] = useState([{ id: Date.now(), sender: "bot", text: "안녕하세요, 무엇을 도와드릴까요?" }])
- const userMessage = { id: Date.now(), sender: "user", text: userMessageText };
- const chatBotMessage = { id: Date.now() + 1, sender: "bot", text: data };
- <div key={msg.id} className={chat-bubble ${msg.sender}}>
- 해설: React에서 리스트를 렌더링할 때 key prop은 매우 중요합니다. key는 React가 리스트의 아이템들을 식별하고, 변경(추가, 삭제, 재정렬)이 일어났을 때 어떤 아이템이 변경되었는지 효율적으로 추적하는 데 사용됩니다. i (배열 인덱스)를 key로 사용하는 것은 리스트의 아이템 순서가 변경되거나 아이템이 추가/삭제될 때 문제가 발생할 수 있습니다. Date.now()를 사용하여 각 메시지에 고유한 타임스탬프 기반 ID를 부여함으로써, React가 메시지들을 더 정확하게 식별하고 UI를 효율적으로 업데이트할 수 있게 됩니다. (실제 프로덕션에서는 UUID 같은 더 강력한 고유 ID 생성 방법을 권장합니다.)
3.2. 7단계 추가 개선 사항 적용
1. 스크롤 자동 내리기
- 적용 내용: useRef 훅과 useEffect 훅을 사용하여 새 메시지가 추가될 때마다 채팅창이 자동으로 가장 아래로 스크롤되도록 했습니다.
- const chatWindowRef = useRef(null);
- <div ref={chatWindowRef} className="chat-window">
- useEffect(() => { if (chatWindowRef.current) { chatWindowRef.current.scrollTop = chatWindowRef.current.scrollHeight; } }, [messages]);
- 해설: useRef는 DOM 요소에 직접 접근할 수 있게 해주며, useEffect는 messages 상태가 업데이트될 때마다 실행되어 scrollTop 속성을 scrollHeight로 설정함으로써 스크롤을 최하단으로 이동시킵니다.
2. 로딩 인디케이터
- 적용 내용: isLoading 상태 변수를 추가하고, API 요청이 진행 중일 때 로딩 인디케이터를 표시합니다.
- const [isLoading, setIsLoading] = useState(false);
- handleSend 함수 시작 시 setIsLoading(true), try-catch-finally 블록의 finally에서 setIsLoading(false) 호출.
- JSX 내부에 isLoading 상태에 따라 로딩 버블(dot-flashing CSS 애니메이션 포함)을 조건부 렌더링합니다.
- 입력 필드의 placeholder와 disabled 속성도 isLoading 상태에 따라 변경됩니다.
- 해설: 사용자에게 API 요청이 처리 중임을 시각적으로 알려주어 사용자 경험을 향상시킵니다. disabled 속성을 통해 로딩 중에는 중복 전송을 방지합니다.
3. 에러 메시지 UI
- 적용 내용: handleSend 함수의 catch 블록에서 에러 발생 시, 사용자에게 보이는 "오류가 발생했습니다. 다시 시도해주세요." 메시지를 messages 배열에 챗봇 메시지 형태로 추가합니다.
- setMessages(prev => [...prev, { id: Date.now() + 1, sender: "bot", text: "오류가 발생했습니다. 다시 시도해주세요." }]);
- console.error를 사용하여 개발자 콘솔에도 상세 에러를 출력합니다.
- 해설: 백엔드 통신 오류 시 사용자에게 명확한 피드백을 제공하여, 앱이 멈춘 것처럼 보이지 않게 합니다.
4. 입력값 유효성 검사
- 적용 내용: handleSend 함수 시작 부분에서 if (!input.trim() || isLoading) 조건을 추가하여, 입력값이 비어있거나 공백만 있는 경우, 또는 이미 로딩 중인 경우에는 메시지 전송을 막습니다.
- input.trim(): 입력된 문자열의 앞뒤 공백을 제거합니다.
- 해설: 불필요한 빈 메시지 전송이나 중복 전송을 방지하여 앱의 안정성을 높입니다.
5. 백엔드 URL 설정 외부화
- 적용 내용: BACKEND_URL 상수를 정의하고 process.env.REACT_APP_BACKEND_URL을 사용하여 환경 변수에서 URL을 가져오도록 했습니다.
- const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || "http://localhost:5500";
- fetch(${BACKEND_URL}/searchChat, { ... });
- 해설: 개발 환경(localhost)과 배포 환경(AWS EC2 퍼블릭 IP)에서 백엔드 URL이 달라질 수 있으므로, 코드를 수정하지 않고 환경 변수만 변경하여 유연하게 대응할 수 있도록 합니다. (Create React App 기준으로 REACT_APP_ 접두사를 사용합니다.)
6. 전송 버튼 추가
- 적용 내용: <input> 태그 옆에 명시적인 "전송" 버튼을 추가했습니다.
- <button onClick={handleSend} disabled={isLoading || !input.trim()}>전송</button>
- 해설: Enter 키 외에 버튼 클릭으로도 메시지를 보낼 수 있도록 하여 사용자 편의성을 높입니다. disabled 속성을 통해 로딩 중이거나 입력값이 비어있을 때는 버튼을 비활성화합니다.
이러한 개선 사항들을 통해 챗봇 애플리케이션의 사용자 경험, 안정성, 그리고 유지보수성이 크게 향상되었습니다.
'AI, 클라우드, 협업, 교육, 문서, 업무자동화' 카테고리의 다른 글
간단한 RAG(Retrieval Augmented Generation) 기반 챗봇 서비스 (0) | 2025.07.20 |
---|---|
벡터DB, Qdrant를 활용한 벡터 검색 워크플로우 정리 (0) | 2025.07.20 |
벡터DB, Qdrant 컬렉션 검색 코드 설명 (0) | 2025.07.20 |
Qdrant 컬렉션에 포인트(Vector & Payload) 삽입 코드 설명 (0) | 2025.07.20 |
Qdrant 벡터 데이터베이스에 새로운 컬렉션(Collection)을 생성하는 예시 (0) | 2025.07.20 |