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부터 가능(유료)
- 암시적Implicit 캐싱: 내부적으로 비슷한 요청 처리 과정에서 효율을 높이는 방향으로 동작하는 개념
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시간
),
)'Study Notes' 카테고리의 다른 글
| Flask로 Gemini API 붙이기: /sendMessage(GET)로 한 줄 Q&A 만들기 (0) | 2026.03.01 |
|---|---|
| Flask로 HTTP 요청 이해하기: GET/POST, Query String, Content-Type (0) | 2026.02.25 |
| Git/GitHub · Vercel · DNS : ‘배포 URL’에서 ‘내 도메인’으로 (0) | 2026.02.01 |
| 포트폴리오 배포 준비 ② ㅡ Vercel 배포 & 도메인 구조 이해하기 (0) | 2026.02.01 |
| 포트폴리오 배포 준비 ① ㅡ GitHub 업로드까지 (Windows / Cursor) (0) | 2026.01.30 |