티스토리 뷰
[8주차 추가과제 - 프롬프트 수정]
# 항상 고정된 JSON 형태로 응답
- 사용자 질문에 대한 답변
- 답변과 관련된 추가 질문 추천 리스트
하려는 방식 (responseSchema로 강제)
→ Gemini가 토큰을 생성하는 단계에서부터 이 스키마를 벗어나지 못하게 막음
// 인사말, 마크다운, 설명 같은 거 절대 못 붙임. 무조건 깔끔한 JSON만 나오게
- 기존 방식: 친구한테 "JSON으로 답해줘~" 하고 부탁 → 친구가 까먹고 인사말 붙일 수도 있음
- 새 방식: 친구한테 빈칸 뚫린 양식지를 주고 "여기에만 채워" → 양식 벗어날 방법이 없음
[순서]
STEP 1. 백엔드 수정 — route.js 코드 분석
- 시스템 프롬프트에 추천 질문 규칙 추가
- JSON 응답 스키마 정의 (responseSchema)
- generationConfig에 스키마 적용
- 응답 처리 로직 변경 (reply → answer + suggestedQuestions)
STEP 2. 프론트엔드 수정 — 응답 구조 맞추기
- API 응답 키 변경 반영 (data.reply → data.answer)
- 답변을 화면에 렌더링하는 부분 수정
STEP 3. 프론트엔드 추가 — 추천 질문 UI
- 추천 질문을 버튼 형태로 표시
- 버튼 클릭 시 자동으로 질문 전송
- 로딩/에러 상태 처리
STEP 4. 동작 테스트
- 한국어 질문 → 한국어 답변 + 한국어 추천 질문 확인
- 영어 질문 → 영어 답변 + 영어 추천 질문 확인
- 추천 질문 클릭 → 자동 전송 동작 확인
- 이력서 외 질문 → "모른다" 답변 + 적절한 추천 질문 확인
generationConfig
-> Gemini 모델한테 "답변을 어떻게 생성할지"에 대한 설정을 넘기는 객체
예시)

[STEP 1]
1. import에 SchemaType 추가(스키마 타입 지정)
import { GoogleGenerativeAI, SchemaType } from '@google/generative-ai'
2. 시스템 프롬프트에 "추천 질문 규칙" 추가
JSON 출력은 스키마가 강제하지만, 추천 질문의 품질(말투, 길이, 시점)은 프롬프트에서 잡아줘야 함
스키마 = "구조"를 잡는다 (필드 이름, 타입, 개수)
프롬프트 = "내용 품질"을 잡는다 (말투, 시점, 구체성)

3. responseSchema + responseMimeType 추가
generationConfig: {
responseMimeType: 'application/json',
responseSchema: responseSchema,
}
-> JSON 안 깨지도록(핵심)
4. 응답 처리 변경
text()로 받은 문자열을 JSON.parse() → 안전하게 파싱(읽고 해석할 수 있게 만드는 작업)
응답 구조: { reply } → { answer, suggestedQuestions }
[STEP 2] 기존 동작 유지 + 추천 질문 데이터 받기
현재 프론트엔드에서 API를 호출하고 답변을 화면에 뿌리는 부분의 코드 필요
-> 나의 경우엔 ChatWidget.jsx 파일
const res = await fetch('/api/chat'
-> 수정할 부분
const data = await res.json()
setMessages(prev => [...prev, { role: 'assistant', content: data.reply }])
응답 키 맞추기
data.reply → data.answer
// 추천 질문은 받아서 state에 저장만 함 (UI에는 아직 표시 X)
// 목표: 기존 동작이 안 깨지게 유지하기
1. 추천 질문용 state 추가
const [suggestedQuestions, setSuggestedQuestions] = useState([])
2. 새 질문 보낼 때 이전 추천 질문 비우기
setSuggestedQuestions([])
답변 생성 중인데 옛날 추천 질문이 떠있으면 사용자가 헷갈림
로딩 = 추천 질문도 사라지게
3. 응답 처리
// Before
setMessages(prev => [...prev, { role: 'assistant', content: data.reply }])
// After
setMessages(prev => [...prev, { role: 'assistant', content: data.answer }])
setSuggestedQuestions(data.suggestedQuestions || [])
[STEP 3]
- 추천 질문을 버튼 형태로 표시 → map()으로 렌더링
- 버튼 클릭 시 자동으로 질문 전송 → handleSuggestedClick
- 로딩/에러 상태 처리 → !isLoading && length > 0 조건
1. send 함수를 sendMessage(text)로 분리
기존 send는 input state에서만 텍스트를 가져옴.
그런데 추천 질문 버튼을 클릭했을 때는 사용자가 input에 타이핑한 게 아니라 버튼에 적힌 텍스트를 보내야함.
→ 텍스트의 출처가 2개(직접 입력 / 버튼 클릭)가 됐으므로,
어디서 오든 받아서 처리할 수 있게 인자로 받는 형태로 분리
(=> 함수의 재사용성 : 같은 로직을 여러 곳에서 호출할 수 있게 만드는 패턴)
const sendMessage = async (text) => { // ← text를 인자로 받음
if (!text || isLoading) return // ...fetch 로직
}
const send = () => sendMessage(input.trim()) // 직접 입력용
2. handleSuggestedClick 함수 추가
const handleSuggestedClick = (question) => sendMessage(question)
// 추천 질문 버튼을 누르면 그 질문 텍스트를 그대로 sendMessage에 넘겨서 전송하게 하는 함수
(확장성, 가독성, 관심사 분리: 서로 다른 역할 하는 코드를 분리해서, 각자 자기일만 하게 만들기)
3. 추가 질문 버튼 렌더링 영역 추가
{!isLoading && suggestedQuestions.length > 0 && (
<div className='flex flex-col gap-1.5 mt-1'>
{suggestedQuestions.map((q, i) => (
<button key={i}
onClick={() => handleSuggestedClick(q)}
className={`...`}>
{q}
</button>
))}
</div>
)}
key는 React가 리스트의 각 요소를 구별하기 위해 필요. 없으면 콘솔 경고.
&& 연결로 둘 다 true일 때만 표시
- !isLoading : 로딩 중이 아닐 때
- suggestedQuestions.length > 0 : 추천 질문이 1개 이상 있을 때
*MAP의 역할
추천 질문 배열을 → 클릭 가능한 버튼들로 화면에 그리기
(추천 질문 3개를 사용자가 클릭만 하면 자동 전송되게)
{suggestedQuestions.map((q, i) => (
<button key={i} onClick={() => handleSuggestedClick(q)}>
{q}
</button>
))}
[STEP 4] 동작테스트
한국어질문, 영어질문, 추천 질문 클릭, 이력서 외 질문
이렇게 4가지 테스트


이런 식으로 잘 대답하고 있음. 모르는 내용은 모른다고 함.
근데 하나 문제가 있다면 추천 질문을 클릭했을 때
내가 입력한 프롬포트의 양이 많지 않다보니 답변을 제대로 못하는 경우가 발생
이는 추후 수정할 예정이고 git허브에 push 해두고 끝.
'AI스쿨 > 알토르과정' 카테고리의 다른 글
| [알토르] 9주차(2) : CI/CD(Github Action 적용 + 디스코드 알림) (0) | 2026.04.30 |
|---|---|
| [알토르] 9주차(1) : 포트폴리오에 TDD 적용해보기 (0) | 2026.04.30 |
| [알토르] 8주차 과제 : Next.js에서 API 만들기 (0) | 2026.04.22 |
| [알토르] 7주차 : Flask API 서버 배포 및 Vercel 연동 (0) | 2026.04.19 |
| [알토르] 6주차 추가과제 : 서브도메인 생성 & SSL 인증서 적용 (0) | 2026.04.11 |
