상세 컨텐츠

본문 제목

[Next.js] OpenAI API로 문장요약 사이트 만들기

Development Study/AI 개발 활용기

by yooputer 2025. 7. 3. 16:23

본문

최근에 OpenAI API를 사용해보게 되었는데, 

앞으로 진행하게될 프로젝트에 굉장히 유용하게 사용할 수 있을 거 같아 

NextJS에서 OpenAI API를 연동하고 활용하여

간단한 문장요약 사이트를 만드는 과정에 대해 기록해보고자 한다. 


Setup

 

NextJS 프로젝트 생성

우선 아래와같이 NextJS 프로젝트를 생성하였다. 

npx create-next-app@latest text-summary-demo

 

npm install

아래와 같이 이후 개발에 필요한 라이브러리를 설치한다. 

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 API KEY 발급

OpenAI platform에 접속한 후 프로젝트를 생성하고 api 키를 발급받는다. 

(참고로 5달러 이상 충전해야 OpenAI를 사용할 수 있다. )

 

 

API 키를 복사한 후 .env 파일(없으면 새로 생성)에 아래와같이 추가한다

OPENAI_API_KEY=<복사한 API KEY>

API 구현

OpenAI API를 호출하기 위해서는 위에서 추가한 OPENAI_API_KEY 환경변수가 필요하다. 

하지만 이 환경변수는 서버에서만 접근이 가능해야 한다. 

따라서 우리는 서버에 OpenAI API를 호출하는 API를 구현한 후 

프론트엔드에서 해당 API를 호출하여 OpenAI API 사용하도록 구현할 것이다. 

 

route.ts 작성

/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는 원하는대로 커스텀하면 된다. 


화면 구현

Layout.tsx 작성

/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를 추가하였다. 

 


Form 구현하기

/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 핸들러 구현

이제 요약하기 버튼을 클릭하면 사용자가 입력한 내용을 요약한 후

요약 결과에 보여주도록 submit 핸들러를 구현해볼 것이다. 

 

이에 앞서 요약하기 버튼을 클릭한 후 응답을 받을 때까지는 다시 요약하기 버튼을 클릭하지 못하도록

useMutation 훅을 사용하여 요약하기 버튼의 활성화 여부를 처리하려 한다. 

 

그래서 summary라는 mutation을 생성한 후 mutation의 상태에 따라 버튼을 활성화할 것이다. 


onSubmit, mutation, mutation function 구현

아래와같이 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를 사용하여 간단한 사이트를 구현해보았다. 

앞으로 구현하게될 기능에 적극적으로 활용해보려 한다. 

 

관련글 더보기