Study Notes

채팅창 입력이 AI 응답으로 돌아오기까지: 프론트엔드, 백엔드, API 호출 흐름 정리

callmeVANTA 2026. 3. 15. 12:27

1. 전체 시스템 구조

사용자 입력
    │
    ▼
프론트엔드 (Next.js / React 채팅 UI)
    │
    │ fetch로 /api/chat 요청 전송
    ▼
백엔드 (Flask API 서버)
    │
    │ 사용자 메시지 정리 + 프롬프트 생성
    │ Gemini API 호출
    ▼
AI 모델 (Gemini)
    │
    ▼
백엔드 (Flask JSON 응답 반환)
    │
    ▼
프론트엔드 (React 채팅창 렌더링)
  • 프론트엔드는 사용자의 입력을 받고, 백엔드에 요청을 보내고, 돌아온 응답을 화면에 표시한다.
  • 백엔드는 프론트엔드가 보낸 요청을 받아 내용을 정리한 뒤, AI API를 호출하고 그 결과를 다시 JSON 형태로 프론트엔드에 돌려준다.
  • AI 모델은 실제로 답변을 생성하는 역할을 한다.

이전까지는 프론트와 백엔드가 각각 어떤 일을 하는지 따로따로 정리했다면, 이번에는 그 둘을 실제로 연결해서 하나의 요청-응답 흐름으로 보는 단계였다. 프론트엔드와 백엔드를 연결할 때 중요한 것은 전체 구조에서 어느 부분을 다루고 있는지 이해하는 것이다. 흐름을 정확히 이해하고 진행해야 길을 잃지 않을 수 있다.


2. Frontend: 채팅 입력/Flask API 호출

2-1. 사용자가 채팅창에 입력

  • 프론트엔드 내부 변수에 들어감
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");

 

2-2. 백엔드 API 호출

  • 프론트엔드가 백엔드 API에 HTTP 요청을 보내는 도구
  • fetch: 브라우저에 기본 내장된 Web API
    • 별도 설치 없이 사용 가능, 네트워크에 요청을 보내면 Promise를 반환함
    • 응답은 Response 객체, JSON 본문을 쓰려면 response.json()을 한 번 더 호출해야 함
      ㅡJSON 파싱을 직접 해야 함
    • 네트워크 자체가 성공하면 일단 response를 주기 때문에, 상태를 직접 확인해야 함(response.ok/response.status)
      ㅡ에러 처리를 조금 더 직접 해야 함
  • fetch 사용 예시
const response = await fetch("http://127.0.0.1:5000/api/chat", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ message: "안녕하세요" }),
});

const data = await response.json();
console.log(data);
  • axios: 브라우저와 Node.js용 Promise 기반 HTTP 클라이언트 라이브러리
    • 같은 요청을 보내는 역할이지만, 브라우저 기본 내장이 아니라 설치해서 쓰는 도구
    • 응답 객체에 data/status/statusText/headers 등이 들어오기 때문에, 보통 response.data로 바로 사용함
    • 기본적으로 2xx 범위를 벗어난 상태코드면 에러 흐름으로 빠지는 구조ㅡ에러 흐름이 더 직관적
  • axios 설치
npm install axios
  • axios 사용 예시
import axios from "axios";

const response = await axios.post("http://127.0.0.1:5000/api/chat", {
  message: "안녕하세요",
});

console.log(response.data);
항목 fetch axios
기본 제공 브라우저 라이브러리
설치 필요 없음 npm 필요
JSON 처리 직접 자동

 

  • 내 실제 코드
const res = await fetch(CHAT_ENDPOINT, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ message: trimmed }),
});

const data = await res.json();

if (!res.ok || !data.ok) {
        throw new Error(data.error || "Failed to load a response.");
}

3. Backend: 요청 수집/AI API 호출

3-0. CORS 설정하기

  • CORS: Cross-Origin Resource Sharing
    서로 다른 출처 간 요청을 허용한다고 응답 헤더로 알려주는 방식
  • 브라우저에는 동일 출처 정책 (Same-Origin Policy)라는 기본 보안 규칙 존재
  • 이 규칙 때문에 출처origin가 다른 곳으로 보내는 요청은 자유롭게 허용 불가
    • 프로토콜: http/https
    • 도메인: localhost/127.0.0.1/example.com
    • 포트: 3000, 5000
  • 프론트엔드와 백엔드는 출처가 다르기 때문에 브라우저가 요청을 차단할 수 있음 
from flask_cors import CORS

def parse_cors_origins():
    raw = os.getenv("CORS_ALLOWED_ORIGINS", "")
    origins = [origin.strip() for origin in raw.split(",") if origin.strip()]
    return origins or [
        "http://localhost:3000",
        "http://127.0.0.1:3000",
    ]

CORS(
    app,
    resources={r"/api/*": {"origins": parse_cors_origins()}},
)

3-1. Flask에서 요청 받기

@app.post("/api/chat")
def chat():
	data = request.get_json(silent=True) or {} # HTTP 요청 body(JSON)를 파싱
    user_message = str(data.get("message", "")).strip() # JSON에서 message 값 가져오기

3-2. profile.txt 로드

  • profile.txt 파일을 읽어서 문자열로 가져옴
def load_profile_text():
    if not PROFILE_PATH.exists():
        raise FileNotFoundError(...)
    return PROFILE_PATH.read_text(encoding="utf-8")

3-3. 프롬프트 생성

  • 내부에 입력한 규칙과 profile.txt 내용을 합쳐서 프롬프트 생성
  • 단순 전달 뿐만 아니라 AI에게 어떤 맥락으로 질문을 보낼지 조립하는 역
def build_prompt(user_message, profile_text):
    return f"""
    
    [profile.txt]
    {profile_text}

    [사용자 질문]
    {user_message}
    """.strip()

3-4. Gemini 호출, 응답, 텍스트 추출

def generate_answer(prompt): # Gemini에게 요청(호출)
    response = client.models.generate_content( # Gemini 응답을 response로 받음
        model=MODEL,
        contents=prompt,
    )

    text = response.text if response.text else None # 답변을 텍스트만 꺼냄
    if not text:
        raise RuntimeError("Failed to generate a response.")

    return text.strip()

3-5. 받은 답변을 프론트엔드에 반환

  • Gemini 응답 결과를 변수에 받아서
  • 프론트엔드에 JSON으로 반환
answer = generate_answer(prompt)

return jsonify({
	"ok": True,
     "answer": answer,
}), 200

4. Frontend: 받은 답변으로 화면 렌더링

4-1. 백엔드에서 답변 받기

const data = await res.json(); # 백엔드가 돌려준 JSON 받기

if (!res.ok || !data.ok) { # 정상인지 검사
  throw new Error(data.error || "Failed to load a response.");
}

setMessages((prev) => [...prev, { role: "assistant", text: data.answer }]); # 상태 변경

4-2. 화면 렌더링

messages.map((m, idx) => (
  ...
))

5. 캐싱

  • 캐싱: 자주 사용하는 내용을 매번 처음부터 다시 처리하지 않고,
    빠르게 접근할 수 있는 곳에 임시로 저장해두었다가 사용하는 방식
  • 목적: 속도 향상, 서버/시스템 부하 감소
    • 암시적Implicit 캐싱: 내부적으로 비슷한 요청 처리 과정에서 효율을 높이는 방향으로 동작하는 개념
      • 플랫폼이 자동으로 캐싱하는 방식
      • 동일 프롬프트 기준으로 재사용
      • latency 감소, 비용 감소
      • 첫 요청: Client > Gemini 서버 > 모델 추론 > 응답 반환
      • 두번째 동일 요청: Client > Gemini 서버 > 캐시 확인 > 캐시 응답 반환
    • 명시적Explicit 캐싱: 개발자가 캐시할 내용을 직접 정해서 저장하고, 이후 요청에서 그 캐시를 참조하는 방식
      • 개발자가 직접 캐시를 관리하는 방식
      • TTL / invalidation 관리 필요 
      • 첫 요청(컨텍스트 업로드): Client > Document upload > upload_context > cache_id 생성
      • 이후 요청: prompt + cache_id로 호출
      • Gemini에서는 paid tier부터 가능(유료)
def create_profile_cache():
    global profile_cache_name

    profile_text = load_profile_text()

    cache = client.caches.create(
        model=MODEL,
        config=types.CreateCachedContentConfig(
            display_name="portfolio-profile-cache",
            system_instruction=(
                "You are an assistant that answers based on the portfolio profile."
            ),
            contents=[profile_text],
            ttl="3600s",  # 1시간
        ),
    )