본문 바로가기
AI, 클라우드, 협업, 교육, 문서, 업무자동화

React 챗봇(ChatBot) 페이지 코드 정리

by 아톨 2025. 7. 22.

코드는 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 컴포넌트의 기본적인 틀을 만듭니다.

  1. 파일 생성: ChatPage.jsx (또는 ChatPage.js) 파일과 ChatPage.css 파일을 만듭니다.
  2. 기본 임포트 및 컴포넌트 정의:
    • 사고 과정: "일단 화면에 챗봇 레이아웃을 보여줘야 하니, 헤더, 메시지 창, 입력 바 이렇게 세 부분으로 나눌 수 있겠다. 각 부분에 CSS 클래스를 미리 지정해두면 나중에 스타일링하기 편하겠지?"
  3. 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 상태로 관리합니다.

  1. useState 임포트: import React, { useState } from "react"로 변경합니다.
  2. input 상태 정의:
  3. const [input, setInput] = useState(""); // 사용자가 입력할 텍스트 상태
  4. <input> 요소에 바인딩:
    • 사고 과정: "사용자가 입력하는 텍스트는 계속 변하니 React의 상태로 관리해야 해. useState를 써서 input 변수에 현재 값을 저장하고, setInput 함수로 업데이트해야지. <input> 태그의 valueonChange를 연결하면 사용자가 입력할 때마다 input 상태가 자동으로 업데이트될 거야."
  5. // ... (return 문 내부) ... <div className="chat-input-bar"> <input type="text" value={input} // input 상태와 연결 onChange={e => setInput(e.target.value)} // 입력값 변경 시 상태 업데이트 placeholder="메시지를 입력하세요" /> </div>

3단계: 메시지 목록 표시 및 초기 메시지 설정

채팅 메시지들을 저장할 상태를 만들고, 이를 화면에 렌더링합니다.

  1. messages 상태 정의:
  2. const [messages, setMessages] = useState([{sender:"bot", text:"안녕하세요, 무엇을 도와드릴까요?"}]); // 메시지 목록 상태
  3. chat-window에 메시지 렌더링:
    • 사고 과정: "챗봇 대화는 여러 메시지가 순서대로 쌓이는 형태이니 배열로 관리하는 게 좋겠어. 초기에는 챗봇의 환영 메시지가 있어야겠지? map 함수를 써서 배열의 각 요소를 div로 만들고, sender에 따라 다른 스타일을 적용할 수 있도록 className을 동적으로 바꿔줘야겠다. 리스트 렌더링 시 key prop은 필수니까 i라도 넣어두자 (나중에 고유 ID로 바꾸는 게 좋겠지만)."
  4. // ... (return 문 내부) ... <div className="chat-window"> { messages.map((msg, i) => ( <div key={i} className={`chat-bubble ${msg.sender}`}> {msg.text} </div> )) } </div>

4단계: 메시지 전송 기능 (사용자 메시지 추가)

사용자가 입력한 메시지를 messages 배열에 추가하는 기능을 구현합니다.

  1. handleSend 함수 정의 (초기 버전):
  2. const handleSend = () => { const userMessage = {sender: "user", text: input}; setMessages(prev => [...prev, userMessage]); // 사용자 메시지 추가 setInput(""); // 입력 필드 초기화 };
  3. Enter 키 이벤트 핸들러:
    • 사고 과정: "메시지를 보내려면 버튼 클릭이나 Enter 키 입력이 필요해. handleSend 함수를 만들어서 현재 input 값을 사용자 메시지로 만들고 messages 배열에 추가해야지. 그리고 input 필드는 비워야 다음 메시지를 입력할 수 있으니까 setInput("")도 호출해야겠다. Enter 키로도 전송되게 onKeyDown 이벤트도 붙여야지."
  4. const handleKeyDown = (e) => { if(e.key === "Enter") handleSend(); // Enter 키 누르면 전송 }; // ... (input 태그에 onKeyDown={handleKeyDown} 추가)

5단계: 백엔드 API 연동 (Fetch API)

이제 백엔드 Flask 앱과 통신하여 챗봇 응답을 받아옵니다.

  1. handleSendasync 함수로 변경: const handleSend = async () => { ... }
  2. 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 변수에 값을 미리 저장하는 것이 좋습니다.
  3. 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단계: 추가 개선 사항 (선택 사항)

  • 스크롤 자동 내리기: 새 메시지가 추가될 때마다 채팅창이 자동으로 가장 아래로 스크롤되도록 useEffectscrollIntoView를 사용할 수 있습니다.
  • 로딩 인디케이터: 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 속성을 추가하고, 이 idmap 함수의 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 요소에 직접 접근할 수 있게 해주며, useEffectmessages 상태가 업데이트될 때마다 실행되어 scrollTop 속성을 scrollHeight로 설정함으로써 스크롤을 최하단으로 이동시킵니다.

2. 로딩 인디케이터

  • 적용 내용: isLoading 상태 변수를 추가하고, API 요청이 진행 중일 때 로딩 인디케이터를 표시합니다.
    • const [isLoading, setIsLoading] = useState(false);
    • handleSend 함수 시작 시 setIsLoading(true), try-catch-finally 블록의 finally에서 setIsLoading(false) 호출.
    • JSX 내부에 isLoading 상태에 따라 로딩 버블(dot-flashing CSS 애니메이션 포함)을 조건부 렌더링합니다.
    • 입력 필드의 placeholderdisabled 속성도 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 속성을 통해 로딩 중이거나 입력값이 비어있을 때는 버튼을 비활성화합니다.

이러한 개선 사항들을 통해 챗봇 애플리케이션의 사용자 경험, 안정성, 그리고 유지보수성이 크게 향상되었습니다.

반응형