오늘은 쳇지피티 검색페이지 제작과 배포까지 진행하는 방법에 대해 수업을 들었다. 아직 모르는부분이 많은데도 불구하고 코드가 조금씩 보이는것 보면 매니저님들이나 클매님들이 왜 차츰 익숙해질거라고 하셨는지 조금은 그 말씀이 이해가 된다. 정말 어려운데도 맨날 코드만 보니까 익숙해지는 느낌? 어렵지만 불편하지는 않은느낌? ㅋㅋ 약간 그런 단계가 아닌가 싶다. 오늘은 코드 리뷰를 해보고 시간이 된다면 배포했던 부분까지 간단하게 정리를 해보도록 하겠다.
Front-end <App.jsx> import axios from "axios"; import { useEffect } from "react"; import { useState } from "react"; function App() { const [content, setContent] = useState(""); const [result, setResult] = useState(""); const [isLoading, setIsLoading] = useState(false); const onSubmitChat = async (e) => { try { e.preventDefault(); if (!content) return; setIsLoading(true); const response = await axios.post( `${process.env.REACT_APP_BACKEND_URL}/chat`, { content, }, { headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.REACT_APP_SECRET_KEY}`, }, } ); setResult(response.data.result); setIsLoading(false); } catch (error) { console.error(error); setIsLoading(false); } }; useEffect(() => { console.log(process.env.REACT_APP_SECRET_KEY); }, []); return ( <div className="max-w-screen-md mx-auto min-h-screen flex flex-col justify-start items-center pt-16 px-4"> <form className="flex w-full" onSubmit={onSubmitChat}> <input className={`grow border-2 px-2 py-1 border-gray-300 rounded-lg focus:outline-main shadow-lg ${ isLoading && "bg-gray-200 text-gray-400" }`} type="text" value={content} onChange={(e) => setContent(e.target.value)} disabled={isLoading} /> <input className={`w-24 ml-4 px-2 py-1 border-2 border-main text-main rounded-lg shadow-lg ${ isLoading && "bg-main text-white" }`} type="submit" disabled={isLoading} value={isLoading ? "검색중..." : "검색"} /> </form> {result && <div className="mt-16 bg-main p-4 text-gray-50">{result}</div>} </div> ); } export default App;
import로 axios, useEffect, useState를 연결.
useState를 이용해 각각 content, result, isLoading 세 가지 상태 변수를 선언하고, 초기값으로 "", "", false를 할당했다.
content는 사용자가 입력한 검색어나 질문을 저장하는 변수
result는 검색 결과나 대답을 저장하는 변수
isLoading은 검색 요청이 진행 중인지 여부를 나타내는 변수로, true이면 현재 검색 중이라는 의미고 false이면 검색이 완료되었다는 의미
onSubmitChat은 "검색" 버튼을 눌렀을 때 실행되는 함수다.
axios.post 메소드를 사용해 입력한 내용인 content를 백엔드 서버에 보낸다.
그리고 백엔드 서버에서 반환된 결과인 response.data.result를 setResult 함수를 사용하여 result 상태 변수에 저장한다.
그리고 마지막으로 로딩 중임을 알리는 isLoading 상태 변수를 false로 설정한다.
또한, try-catch문으로 오류 처리를 수행하고 만약 axios.post 메소드에서 오류가 발생하면, 오류를 콘솔에 출력하고 로딩 중임을 알리는 isLoading 상태 변수를 false로 설정하도록 했다.
useEffect는 두 개의 인자를 받는데 첫째는 Side Effect 함수, 두번째는 의존성 배열(Dependency Array)이다.
의존성 배열에 포함된 값이 변경되었을 때만 Side Effect 함수가 호출되며, 빈 배열 []을 전달하면 마운트 된 후 한 번만 호출된다.
위 코드에서는 의존성 배열이 빈 배열로 전달되었기때문에 컴포넌트가 처음 한 번만 실행된다.
Side Effect 함수는 console.log(process.env.REACT_APP_SECRET_KEY)이라는 명령어로, process.env.REACT_APP_SECRET_KEY라는 이름의 환경 변수 값을 콘솔에 출력한다.
- return문.
div 태그에 className 속성을 사용해 스타일링 클래스를 적용.
form 태그에 className 속성을 사용하여 스타일링 클래스를 추가하고, onSubmit 핸들러 함수를 등록. 이 폼에는 사용자의 질문을 입력할 수 있는 input 요소와 검색 버튼을 나타내는 input 요소가 포함되어있다.
첫번째 input에는 className 속성을 사용하여 스타일링 클래스를 추가하고, 각각 type, value, onChange, disabled 속성을 설정했다.
첫번째 input은 사용자의 질문을 입력하는 검색창이다.
value 속성은 content 변수의 값을 사용하고, onChange 속성은 setContent 함수를 호출한다.
disabled 속성은 isLoading 값이 true일 경우 true를 반환한다.
두번째 input에는 className 속성을 사용하여 스타일링 클래스를 추가하고, 각각 type, disabled, value 속성을 설정했다.
두번째 input은 검색 버튼이다.
disabled 속성은 isLoading 값이 true일 경우 true를 반환한다.
value 속성은 isLoading 값이 true일 경우 "검색중...", 아닐 경우 "검색"으로 설정되었다.
{result && div: result 값이 null이 아닐 경우에만 해당하는 div 요소를 렌더링하도록 설정되었다.
require("dotenv").config(); const express = require("express"); const cors = require("cors"); const axios = require("axios"); const app = express(); const port = process.env.PORT; app.use(cors()); app.use(express.json()); app.get("/", (req, res) => { res.send("Hello, Express!"); }); app.post("/chat", async (req, res) => { const { content } = req.body; try { const bearerToken = req.headers.authorization?.substring(7); if (bearerToken !== process.env.SECRET_KEY) { return res .status(400) .json({ ok: false, error: "올바른 키를 입력해주세요." }); } if (!content) { return res.status(400).json({ ok: false, error: "질문을 입력해주세요." }); } const response = await axios.post( "https://api.openai.com/v1/chat/completions", { model: "gpt-3.5-turbo", messages: [{ role: "user", content }], }, { headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.OPENAI_KEY}`, }, } ); console.log(response.data.choices[0].message.content); res.json({ ok: true, result: response.data.choices[0].message.content }); } catch (error) { console.error(error); res.json({ ok: false, error }); } }); app.listen(port, () => { console.log(`Server listening on port: ${port} 🚀`); });
require("dotenv").config(); : .env 파일에서 환경 변수를 읽어온다.
const express = require("express"); : Express 프레임워크를 불러온다.
const cors = require("cors"); : CORS 미들웨어를 불러온다.
const axios = require("axios"); : HTTP 클라이언트 라이브러리 Axios를 불러온다.
const app = express(); : Express 애플리케이션 객체를 생성.
const port = process.env.PORT; : .env 파일에 설정된 PORT 환경 변수를 불러온다.
app.use(cors()); : CORS 미들웨어를 Express 애플리케이션에 등록.
app.use(express.json()); : Express 애플리케이션에서 JSON 파싱을 가능하게 해주는 미들웨어를 등록.
app.get("/", (req, res) => {...}) : "/" 경로로 GET 요청이 들어오면 "Hello, Express!"를 반환.
app.post("/chat", async (req, res) => {const { content } = req.body;
"/chat" 경로로 POST 요청이 들어오면 OpenAI API를 사용하여 챗봇 기능을 쓸 수 있도록 함.
요청 바디에서 content 필드를 받아와서 OpenAI API로 전송하고, 결과를 클라이언트로 응답.
이때, 요청 헤더에서 인증에 필요한 Authorization 필드를 검증하여 올바른 키인 경우에만 API를 호출할 수 있도록 설정.
- 서버 측에서 요청을 처리하는 부분 if문으로 정리.
첫번째 줄에서는 Authorization 헤더에서 Bearer 토큰을 추출한다. 이 토큰은 클라이언트가 요청을 보낼 때 함께 전달된다. 또한, substring(7);을 작성해 앞에 7글자를 날리도록 지시.
두번째 줄에서는 추출한 토큰과 process.env.SECRET_KEY를 비교하여 올바른 키인지 확인. 올바른 키가 아닐 경우 에러 응답을 반환.
세번째 줄에서는 content 변수가 존재하지 않을 경우 에러 응답을 반환.
axios.post를 사용해 "https://api.openai.com/v1/chat/completions" 엔드포인트에 POST 요청을 보낸다.
요청 헤더에는 insomnia에 있던 Content-Type과 application/json을 넣었다.
Content-Type과 application/json은 content타입이 json이다라고 인식할 수 있도록 해준다.
Bearer Token을 보내기 위해서 Authorization을 적고, Bearer와 함께 Token을 작성한다. 다만, Token은 .env에 보관되어있기 때문에 Token을 ${process.env.OPENAI_KEY}로 작성해준다.
응답은 response 변수에 할당되며, 그 중 response.data.choices[0].message.content는 API가 생성한 답변을 의미한다.
res.json({ ok: true, result: response.data.choices[0].message.content });는 챗봇의 응답 결과를 클라이언트에게 JSON 형식으로 반환한다.
res.json({ ok: false, error });는 무한로딩에 빠지는 상황을 방지한다.
app.listen(port, () => { console.log(Server listening on port: ${port} 🚀); });는 서버를 시작하고 해당 포트에서 요청을 대기하도록 설정.
배포를 하기위한 설치와 과정까지 올려볼까 했지만 도저히 정리할 자신이 없어서 포기했다.
오늘은 코드를 보고 머릿속에 정리한것만으로도 머리가 아프다.
내일은 또 새로운 과정인 솔리디티와 관련된 과정을 배운다고 하니 다시 머릿속을 비워야겠다.
하루가 길다..