next

next.js, gemini AI를 이용한 한국인 캐릭터 생성기 - 5

weaklion 2025. 3. 27. 15:11

오늘은 gemini 연결하는 방법과 나머지에 대해서 알아보도록 하겠습니다.

 

 캐릭터는 gemini에게 프롬프트를 통해서 물어본 뒤, 그 답을 가져와서 뿌리게 되는데 그러기 위해선 gemini의 api 키가 필요합니다.

 

api key는 google ai studio에서 가져올 수 있습니다.

https://ai.google.dev

 

Gemini Developer API | Gemma open models  |  Google AI for Developers

Build with Gemini 2.0 Flash, 2.0 Pro, and Gemma using the Gemini API and Google AI Studio.

ai.google.dev

 

 

위 버튼을 누르고 하라는대로만 하면 api key를 쉽게 얻을 수 있습니다.

 

 

이후 api key를 집어넣고 연동합니다.

저는 .env에 api key를 집어넣었고 action에서 가져오는 식으로 사용했습니다.

 

npm install @google/generative-ai

 

google 에서 지원하는 generative-ai 라이브러리를 설치 후 ai를 연동합니다.

"use server";

import { GoogleGenerativeAI } from "@google/generative-ai";

const generativeAI = new GoogleGenerativeAI(process.env.API_KEY || "");

const model = generativeAI.getGenerativeModel({
  model: "gemini-1.5-flash",
  generationConfig: { responseMimeType: "application/json" },
});
{
}
export async function getData(count: number, gender: string) {
  try {
    const result = await model.generateContent(
      `웹소설에 등장하는 가상의 한국인 캐릭터들을 생성해주세요. 성별이 **반드시** ${gender}인 ${count}명의 캐릭터를 생성하여 characters 배열에 넣고 JSON 형식으로 출력해주세요. 각 캐릭터는 다음 속성을 가져야 합니다.

- name: 무작위로 선택한 한국인 이름 (문자열)
- age: 나이 (문자열, 예: "25세") (다양한 나이대를 섞어서 무작위로 선택)
- gender: 성별 (문자열, **반드시** "${gender}"로 지정)
- appearance: 외형적 특징을 **상세하게** 설명합니다. 눈 색깔, 머리 스타일, 키, 체형, 옷 스타일 등을 포함하여 구체적으로 묘사 (문자열). (각 캐릭터마다 다른 특징을 갖도록 무작위로 선택)
- background: 캐릭터의 간단한 배경 설명을 구체적으로 작성합니다. 성장 환경, 가족 관계, 특별한 경험 등을 포함하여 각 캐릭터마다 고유한 스토리를 부여 (문자열).
- occupation: 직업 (문자열). (다양한 직업을 무작위로 선택)

출력 형식:
{
  "characters": [
    {
      "name": string,
      "age": string,
      "gender": string,
      "appearance": string,
      "background": string,
      "occupation": string
    },
    ...
  ]
}

각 필드에 대해 구체적이고 다양한 설명을 제공해주세요. 캐릭터들은 서로 다른 특징과 배경을 가져야 하며, 성별은 반드시 프롬프트에 지정된 ${gender}로 생성되어야 합니다. 각 캐릭터는 다른 나이, 직업을 갖도록 해주세요.`
    );
    const response = await result.response;
    const text = await response.text();
    if (text) {
      console.log(text, "결과 텍스트 ");
      return text;
    }
  } catch (e) {
    console.error(e, "error");
  }
}

export async function getList(name: string) {
  try {
    const result = await model.generateContent(
      `앞서 생성된 캐릭터 중 ${name}의 상세한 배경을 제공해주세요. 다음 요소들을 포함하여 자세히 설명해주세요:

1. 가족 관계
2. 성장 환경
3. 교육 배경
4. 주요 삶의 경험이나 전환점
5. 현재 상황에 이르게 된 과정
6. 성격 형성에 영향을 준 사건들
7. 주요 인간관계
8. 꿈과 목표
9. 강점과 약점
10. 특별한 기술이나 재능

형식은 다음과 같이 JSON으로 출력해주세요:

{
  "characterBackground": {
    "name": "${name}",
    "detailedBackground": {
      "familyRelations": string,
      "growthEnvironment": string,
      "educationalBackground": string,
      "lifeExperiences": string,
      "pathToCurrentSituation": string,
      "personalityShapingEvents": string,
      "keyRelationships": string,
      "dreamsAndGoals": string,
      "strengthsAndWeaknesses": string,
      "specialSkillsOrTalents": string
    }
  }
}

각 항목에 대해 2-3문장 정도의 구체적인 설명을 제공해주세요. 캐릭터의 기존 정보와 일관성을 유지하면서, 풍부하고 현실감 있는 배경을 만들어주세요.`
    );
    const response = await result.response;
    const text = await response.text();
    if (text) {
      console.log(text, "결과");
      return text;
    }
  } catch (e) {
    console.error(e, "error");
  }
}

//app/detail/actions.ts

 

이후 서버 액션에서 설치한 라이브러리에 가져온 api key를 넣고 모델을 만듭니다. 저는 gemeni-1.5를 이용했는데 모델은 다른 것을 이용해도 상관없습니다.

다만 모델에 따라 구글에서 제공하는 가격정책이 다 다르니 이부분은 아래 참조해주세요.

https://ai.google.dev/gemini-api/docs/pricing?hl=ko

 

Gemini Developer API 가격 책정  |  Gemini API  |  Google AI for Developers

Gemini Developer API 가격 책정

ai.google.dev

 

api key를 통해서 통신할 때 프롬프트는 정확해야 합니다. 원하는 출력 형식과 인풋을 명확하게 알려줘야 원하는 값을 주기 때문에 되도록 프롬프트를 정확하게 설정합니다.

 

이 부분은 사람에 따라 많은 팁이 있겠지만 개인적으론 물어볼 모델에게 대략적인 베이스를 물어본 뒤 받은 프롬프트를 다시 모델에게 물어 정확도를 얻는게 가장 결과가 만족스러웠습니다.

 

이제 서버 액션에 있는 api를 메인에 있는 버튼에 연결하면 끝입니다. 코드엔 react에서 제공하는 suspense를 붙여 ux를 좋게 만들려고 했습니다. 

 

근데 ai에 api로 요청하면 response를 200으로 만든 뒤, 데이터가 나올때 까지 기다리더라구요. 그래서 그냥 loading 상태값 만들어서 loading이 false면 데이터 안보여주도록 만들어 줬습니다.

 

이후 상세페이지와 리스트엔 요청한 모델에 대한 값을 그대로 보여주면 끝.

 

export interface Character {
  name: string;
  age: number;
  gender: string;
  appearance: string;
  background: string;
  occupation: string;
}

export interface CharacterBackground {
  name: string;
  detailedBackground: {
    familyRelations: string;
    growthEnvironment: string;
    educationalBackground: string;
    lifeExperiences: string;
    pathToCurrentSituation: string;
    personalityShapingEvents: string;
    keyRelationships: string;
    dreamsAndGoals: string;
    strengthsAndWeaknesses: string;
    specialSkillsOrTalents: string;
  };
}

 

"use client";

import { CharacterBackground } from "@/app/model";

export default function Client({
  background,
}: {
  background: CharacterBackground;
}) {
  const detail = background;

  return (
    <main className="flex flex-col items-center justify-center size-full">
      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        이름 : {detail.name}
      </h3>
      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        가족 관계 : {detail.detailedBackground.familyRelations}
      </h3>
      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        성장 환경 : {detail.detailedBackground.growthEnvironment}
      </h3>

      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        교육 배경 : {detail.detailedBackground.educationalBackground}
      </h3>
      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        주요 삶의 경험이나 전환점 : {detail.detailedBackground.lifeExperiences}
      </h3>
      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        현재 상황에 이르게 된 과정 :
        {detail.detailedBackground.pathToCurrentSituation}
      </h3>
      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        성격 형성에 영향을 준 사건들 :{" "}
        {detail.detailedBackground.personalityShapingEvents}
      </h3>
      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        주요 인간관계: {detail.detailedBackground.keyRelationships}
      </h3>

      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        꿈과 목표: {detail.detailedBackground.dreamsAndGoals}
      </h3>
      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        강점과 약점: {detail.detailedBackground.strengthsAndWeaknesses}
      </h3>

      <h3 className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight">
        특별한 기술이나 재능 :{detail.detailedBackground.specialSkillsOrTalents}
      </h3>
    </main>
  );
}

 

딱 보면 알죠?

네 이게 끝입니다. 사실 이번 프로젝트는 ai를 어떻게 사용할지에 대해 초점을 둔지라 ui에 대해선 깊게 생각하지 않았습니다.

덕분에 ai에 대해 공부할 수 있게 됐네요. next는 안 쓸 거 같습니다.

 

사실 이 프로젝트를 만들고 이미 5~6개월 정도가 흘렀습니다. 만드는데는 1주일도 안걸린 게 설명하려다 보니 더 오랜 시간이 걸렸네요.

제 게으름을 좀 반성해야겠습니다.

 

여담인데, 이 이후로 next를 실제로 안 썼습니다. 대신 react router v7이 나왔다고 해서 거기 문서나 조금 읽어본 게 다네요. 만약 다음에 react에 ssr이 필요하다면 react router 7을 써보지 않을까 싶습니다. 실제로 next에 불편함을 느낀 사람이 많이 쓰고 있다고도 하고요.

 

어쨌든 이제 코드에 대한 설명은 끝났습니다. 최종 코드는 아래 repo를 참조해주세요.

http://git@github.com:weaklion/k-character-generator.git