본문 바로가기
공부낙서장/emotional

[emotional] 배포 환경 변경 및 그에 따른 변화

by 곰인간 2024. 8. 24.

기존 emotional은 OpeanAI와 YouTube API가 클라이언트 단에서 두 API를 호출 하였을 때 네트워크 탭을 확인하면 누구든 손쉽게 API 키값을 확인할 수 있었다.

클라이언트 단에서의 API 키 노출 이슈를 방지하기 위해 서버 측에서 API를 실행시켜 클라이언트에 필요한 데이터만 반환하게 하였어야했는데, 나는 딱히 백엔드의 지식이 없었기 때문에 당시에는 Node.js랑 express를 사용해야하나 라고 생각을 하게 되었다.

하지만 Next.js에서 next server를 사용하여 서버리스 함수를 만들 수가 있었는데 그게 route handler를 이용한 방법이었다.

import { NextRequest } from 'next/server';
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
});

export const POST = async (req: NextRequest) => {
  try {
    const searchParams = req.nextUrl.searchParams;
    const prompt = searchParams.get('prompt');

    if (!prompt) {
      return new Response(
        JSON.stringify({ message: '프롬프트를 입력해주세요.' }),
        {
          status: 500,
        },
      );
    }

    const completion = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: [
        { role: 'user', content: `일기: ${prompt}` },

        {
          role: 'system',
          content:
            'prompt에 적힌 글을 분석하여 유튜브에서 playlist를 검색을 하려고하니깐 prompt를 상세하게 분석해서 검색이 될만한 하나의 키워드를 알려줘, 다만 아무 의미 없는 글이면 사족을 붙이지말고 아무 단어로 알려줘',
        },
      ],
      max_tokens: 180,
      temperature: 1,
      presence_penalty: 0,
      frequency_penalty: 0,
    });

    const keyword = completion.choices[0].message.content;

    return new Response(JSON.stringify({ keyword }), {
      headers: { 'Content-Type': 'application/json' },
    });
  } catch (error) {
    console.error(error);
    return new Response(
      JSON.stringify({ message: '서버 오류가 발생했습니다.' }),
      {
        status: 500,
        headers: { 'Content-Type': 'application/json' },
      },
    );
  }
};

OpenAI를 서버 측에서 호출하는 route handler 로직이다.

기존 page router를 사용했을 때는 명시적으로 page/api 디렉토리 아래에 hello.js 이런식으로 만들어 호출해서 사용했다면,

app router에서는 app 디렉토리 아래에 route.ts라고 파일을 명시하면 된다. 하지만 좀 더 명확하게 구분하기 위해 개인적으로는 app/api/hello/route.ts 이런식으로 사용하고 있다.

이렇게 만들어 놓은 로직을 vercel 환경에서 배포해서 쉽게 사용하였는데, 이게 또 항상 배포를 할 때 vercel에 너무 의존하는게 아닌가 하는 생각에 aws를 사용하기로 해보았다.

그중에는 vercel과 비슷한 aws 서비스인 amplify로 배포를 하였다. 

amplify는 vercel과 비슷하게 프론트 코드를 서버리스 환경에서 쉽게 호스팅하게 해준다.

물론 프론트 뿐만 아니라 백엔드 코드도 같이 배포가 가능하다고 한다.

하지만 어차피 나는 서버 동작하는거라고 해봤자 route handler를 이용한 API밖에 없기 때문에 깃헙 레포지토리를 연결해서 배포하였고, 이때 amplify의 개인적으로 최대 장점이라고 생각되는 부분이 깃헙 레포지토리에서 배포한 브랜치에 변경 사항이 있을 시에 CI/CD를 따로 구성하지 않아도 자동으로 처리해주는게 맘에 들었다.

이렇게 배포를 하고 가비아에서 구매한 도메인도 route53에서 따로 설정할 필요없이 amplify 내부 환경에서 연결이 가능하다.

aws라고 생각하면 복잡하다고 생각했는데 이렇게 간단하게 배포를 할 수 있는 서비스가 있다니 생각보다 신기했다.

하지만 배포하고 일주일 쯤 되었을까?

뭔가 그냥 vercel에 배포하는 방법이나 amplify를 사용하는 방법이나  별 차이가 없다고 생각이 들어서 S3를 이용해서 정적 웹 호스팅에 도전하기로 했다.

이때 제일 먼저 들었던 생각은 route handler를 사용한 API호출에 관한 문제였다.

이거만 따로 배포해서 엔드포인트만 긁어와볼까 생각을 했지만 aws에서는 막강한 서비스들이 있다.

lambda를 사용해서 서버리스 함수를 생성하고 API Gateway를 사용하여 만들어둔 lmabda 함수를 가져와 사용할 수 있다.

내가 제일 먼저 했던 것은 기존 emotional의 코드에서  route handler에 관한 부분을 lambda로 변경하는 것이었다.

lambda로 생성한 함수

const OpenAI = require("openai");

const openai = new OpenAI({
  apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
});

const allowedOrigins = [
  "https://www.emotional.today",
  "https://emotional.today",
];

exports.handler = async (event) => {
  const origin = event.headers && event.headers.origin ? event.headers.origin : null;
  const headers = {
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Origin": origin, 
  };

  if (event.requestContext.http.method === "OPTIONS") {
    return {
      statusCode: 200,
      headers,
      body: JSON.stringify({}),
    };
  }

  try {
    const body = JSON.parse(event.body);
    const prompt = body.prompt;

    if (!prompt) {
      return {
        statusCode: 400,
        headers,
        body: JSON.stringify({ message: "프롬프트를 입력해주세요." }),
      };
    }

    const completion = await openai.chat.completions.create({
      model: "gpt-4",
      messages: [
        { role: "user", content: `일기: ${prompt}` },
        {
          role: "system",
          content:
            "prompt에 적힌 글을 분석하여 유튜브에서 playlist를 검색할 수 있는 하나의 키워드를 알려줘",
        },
      ],
      max_tokens: 180,
      temperature: 1,
      presence_penalty: 0,
      frequency_penalty: 0,
    });

    const keyword = completion.choices[0].message.content;

    return {
      statusCode: 200,
      headers,
      body: JSON.stringify({ keyword }),
    };
  } catch (error) {
    return {
      statusCode: 500,
      headers,
      body: JSON.stringify({
        message: "서버 오류가 발생했습니다.",
        error: error.message,
      }),
    };
  }
};

기존 코드에서 변경된거라고는 거의 없지만 프리플라이트 요청 옵션을 추가하였다.

그리고 필요한 라이브러리 같은 경우 node_moudules와 package를 zip파일로 압축하여 lambda layer에 넣었다.

다음엔 람다 함수를 사용하기 위해서 API Gateway를 설정해 람다 함수의 엔드포인트를 설정하고, CORS설정까지 하였다. 

이렇게 하고 보니 안그래도 간단한 프로젝트 코드가 더욱 간단해져버렸고, 기존 route handler에서 api를 호출하는 코드를 람다에서 호출하게만 변경하였다.

그리고

const nextConfig = { output: 'export' };

export 설정으로 빌드할 때 static 파일로 빌드하게 하였고, 이를 S3 버킷에 업로드하여 호스팅을 했다.

추가적으로 cloudfront를 사용하여 엣지 로케이션에 콘텐츠를 캐싱해 지연 시간을 줄이고 전송 속도를 향상 시키고자 하였고, certificate manager와 cloudfront를 연결하면 SSL/TLS 인증서를 발급해서 보안을 강화하였다.

마지막으로는 가비아와 route53을 연동하여 도메인을 설정하면 끝.

댓글