최근에 OpenAI API를 사용해보게 되었는데,
앞으로 진행하게될 프로젝트에 굉장히 유용하게 사용할 수 있을 거 같아
NextJS에서 OpenAI API를 연동하고 활용하여
간단한 문장요약 사이트를 만드는 과정에 대해 기록해보고자 한다.
우선 아래와같이 NextJS 프로젝트를 생성하였다.
npx create-next-app@latest text-summary-demo
아래와 같이 이후 개발에 필요한 라이브러리를 설치한다.
npm install openai
npm install react-query@latest --legacy-peer-deps
npx shadcn@latest add button
npx shadcn@latest add form
npx shadcn@latest add textarea
OpenAI platform에 접속한 후 프로젝트를 생성하고 api 키를 발급받는다.
(참고로 5달러 이상 충전해야 OpenAI를 사용할 수 있다. )
API 키를 복사한 후 .env 파일(없으면 새로 생성)에 아래와같이 추가한다
OPENAI_API_KEY=<복사한 API KEY>
OpenAI API를 호출하기 위해서는 위에서 추가한 OPENAI_API_KEY 환경변수가 필요하다.
하지만 이 환경변수는 서버에서만 접근이 가능해야 한다.
따라서 우리는 서버에 OpenAI API를 호출하는 API를 구현한 후
프론트엔드에서 해당 API를 호출하여 OpenAI API 사용하도록 구현할 것이다.
/app/api 폴더 아래에 route.ts 파일을 생성한 후 다음과 같이 작성한다.
import OpenAI from "openai";
import {NextResponse} from "next/server";
const openai = new OpenAI();
const SYSTEM_PROMPT = `
사용자 입력값 요약.
100자 이하, 코멘트 없이 요약한 내용만 반환.`;
export interface SummaryResponse{
summary: string;
}
export async function POST(request: Request) {
const body: {inputText: string} = await request.json();
const {inputText} = body;
if (!inputText){
throw new Error("input text is null");
}
try{
const completion = await openai.chat.completions.create({
model: "gpt-4.1",
messages: [
{
role: "system",
content: SYSTEM_PROMPT,
},
{
role: "user",
content: inputText,
},
],
});
if (!completion.choices) {
throw new Error("completion.choices is empty");
}
return NextResponse.json(
{
summary: completion.choices[0].message.content
}
);
}catch (error){
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 }
);
}
}
OpenAI 클라이언트인 openai 객체를 생성하고 openai.chat.completions.create 메서드로 api를 호출하는 방식이다.
system 프롬프트와 사용자 입력값으로 요청 후 반환값을 파싱하여 반환한다.
SYSTEM_PROMPT는 원하는대로 커스텀하면 된다.
/app에 존재하는 layout.tsx를 다음과 같이 작성한다.
"use client";
import "./globals.css";
import {QueryClient, QueryClientProvider} from "react-query";
const queryClient = new QueryClient();
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>
) {
return (
<html lang="en">
<body
className={`antialiased bg-gray-200`}
>
<div className="flex w-full flex-col text-stone-900 h-screen">
<main>
<div className="flex justify-center ">
<div className="w-full min-h-screen md:w-[60%] p-10 bg-white">
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</div>
</div>
</main>
</div>
</body>
</html>
);
}
page.tsx에서 useMutation 훅을 사용할 예정이라 QueryClientProvider를 추가하였다.
/app/page.tsx를 아래와같이 작성한다.
export interface SummaryFormData {
inputText: string;
summary: string;
}
export default function Home() {
const form = useForm<SummaryFormData>();
return (
<>
<h2 className="text-2xl font-bold mb-4 text-center">
AI가 요약해드립니다~
</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(() => {})}>
{/* 요약할 내용 textarea */}
<FormField
control={form.control}
name="inputText"
render={({field}) => (
<FormItem>
<FormLabel className="text-lg">요약할 내용</FormLabel>
<FormControl>
<Textarea
{...field}
value={field.value ?? ""}
className="h-80"
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
{/* 요약하기 button */}
<div className="flex justify-end mt-5">
<Button
type="submit"
variant="default"
>
요약하기
</Button>
</div>
{/* 요약 결과 textarea(readonly) */}
<FormField
control={form.control}
name="summary"
render={({field}) => (
<FormItem className="mt-5">
<FormLabel className="text-xl">요약 결과</FormLabel>
<FormControl>
<Textarea
{...field}
value={field.value ?? ""}
className="h-50"
readOnly
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
{/* 초기화 button */}
<div className="flex justify-end mt-5">
<Button
type="button"
variant="destructive"
onClick={() => form.reset()}
>
초기화
</Button>
</div>
</form>
</Form>
</>
);
}
사용자로부터 입력받을 textarea와 해당 내용을 요약한 값을 출력할 readonly textarea, 요약하기 버튼, 초기화 버튼으로 구성된
form을 생성한다.
이제 요약하기 버튼을 클릭하면 사용자가 입력한 내용을 요약한 후
요약 결과에 보여주도록 submit 핸들러를 구현해볼 것이다.
이에 앞서 요약하기 버튼을 클릭한 후 응답을 받을 때까지는 다시 요약하기 버튼을 클릭하지 못하도록
useMutation 훅을 사용하여 요약하기 버튼의 활성화 여부를 처리하려 한다.
그래서 summary라는 mutation을 생성한 후 mutation의 상태에 따라 버튼을 활성화할 것이다.
아래와같이 Home 컴포넌트에 onSubmit, summary, fetchSummary를 구현한다.
const fetchSummary = async (inputText: string) => {
const response = await fetch('/api/summary', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({inputText}),
});
return response.json();
};
const summary = useMutation(fetchSummary, {
onSuccess: (response: SummaryResponse) => {
if (response.summary) {
form.setValue('summary', response.summary);
}
},
onError: (error) => {
console.log(error);
},
});
const onSubmit: SubmitHandler<SummaryFormData> = (formData) => {
const { inputText } = formData;
if (!inputText) {
return;
}
summary.mutate(inputText);
};
그리고 다음과같이 form을 수정한다.
<form onSubmit={form.handleSubmit(onSubmit)}> {/* ✅ submit 핸들러 등록 */}
{/* 요약할 내용 textarea */}
...
{/* 요약하기 button */}
<div className="flex justify-end mt-5">
<Button
type="submit"
variant="default"
disabled={summary.isLoading || !form.watch('inputText')} {/* ✅ disabled 처리 */}
>
{summary.isLoading
? <Loader2Icon className="animate-spin"/>
: <>요약하기</> {/* ✅ 로딩아이콘 처리 */}
}
</Button>
</div>
{/* 요약 결과 textarea(readonly) */}
...
{/* 초기화 button */}
<div className="flex justify-end mt-5">
<Button
...
disabled={!form.watch('summary')} {/* ✅ disabled 처리 */}
>
초기화
</Button>
</div>
</form>
gif로 변환해서 화질이 많이 깨졌지만 정상적으로 요약이 되는 것을 확인할 수 있다.
이번포스팅에서는 OpenAI API를 사용하여 간단한 사이트를 구현해보았다.
앞으로 구현하게될 기능에 적극적으로 활용해보려 한다.
[IntelliJ + Claude] 초간단하게 SpringBoot API 서버 구현하기 (1) | 2025.06.25 |
---|---|
IntelliJ에 Claude를 붙여 더 intelligent하게 만들어보쟛! (0) | 2025.05.16 |
[GPT 활용기] 파이썬 코드 리팩토링 (0) | 2025.04.15 |