openAI로 이름 짓는 Web App 만들기 2

차차·2023년 3월 21일
2

이전에 Udemy 강의를 들으면서 openAI 를 api 로 가져와서 반려동물 이름을 짓는 웹을 구현했었다. 회고하면서 이거도 추가하고 저거도 추가하고... 등등 이런 저런 계획을 세웠었는데, 그걸 통틀어서 여러 이름을 지어주는 AI 작명소를 구현해보았다.


기능 계획 짜보기

✅ 계획했던 기능들

  1. 한글로 기능하도록 하기
  2. CSS 개선하기 (기능은 아니지만서도 ..)
  3. input을 여러개로 나누기
  4. 꼭 반려동물 이름이어야 하는가?
  5. fetch 말고 axios 로도 해보기

✅ AI 작명소 기능

  1. 한글로 기능하게 하기
  2. CSS를 포함하여 완성된 웹으로 구현하기
  3. input : (설명 + 이름에 들어가야하는 문자열)로 확장하기
  4. 이름 카테고리를 여러개 두기
  5. axios 로 작동하게 하기

일단 이렇게 계획을 짰었다. 모종의 이유(?)들로 인해 겁먹은것보다 막 어렵지는 않았다!
모종의 이유는 아래 과정에서 써나가겠다.


과정

✅ 디자인 & 퍼블리싱


figma 를 활용해서 위와 같이 간단하게 디자인을 구성했다.
사용자는 원하는 카테고리를 고르고, 이름에 대한 설명 + 포함해야 하는 문자열을 입력한다.
마지막으로는 이름 짓기 버튼을 누르면 작명 결과가 보여지도록 했다.

1편에서 했던 것 처럼, NextJS 로 개발을 진행했다. (vercel 로 배포하기 위한 빅픽쳐이기도 함)
페이지 1개에서 state 를 조정해서 기능들을 넣을 예정이었기 때문에, 페이지나 라우터 구성은 따로 하지 않았다.

  • NextJS 에서 style jsx 컴포넌트 기능을 제공한다.
  • 일반 css 작성 때 처럼 css 파일을 작성한 후, 백틱으로 묶어서 변수에 저장하고 내보내기만 하면 되는 아주 편리한 기능이다.
  • 스타일을 적용할 때도, 원래 방식처럼 class 를 참조하면 된다.
// index.js
import styleSheet from './style.jsx';
export default function Home(){
  return(
    <div className = 'container'>
      <style jsx>{styleSheet}</style>
    </div>
  )
}
// style.jsx
const styleSheet = `
  .container{
    display: flex;
    flex-direction: column;
  }
`

✅ 기능 요구사항 적어보기

일전에 우아한테크코스 프리코스에 참여했던 경험 + Udemy Maker Jun 님의 블랙커피 강의를 수강한 경험으로, 기능들을 즉석에서 구현했을 때에 착오를 방지하기 위해서 기능 요구사항을 정리해보았다. 더 세세하게 작성하는 습관을 들이고 싶다..!

기능 요구사항

  1. 카테고리 버튼을 클릭하면 클릭한 카테고리를 state에 저장한다
    1-1. emoji, text, color 를 각각 저장한다.
    1-2. 저장된 state 값들을 바탕으로 editor 에 변화를 준다.
  2. detail input 값을 state 에 저장한다.
  3. include input 값을 state 에 저장한다.
  4. 이름짓기 버튼을 클릭하면 저장된 데이터를 바탕으로 명령을 종합한다.
    4-1. 설명란이 공백일 경우, 설명란을 입력해달라는 알림창을 띄운다.
    4-2. 포함되어야 하는 문자열이 공백일 경우, confirm 메소드로 포함되어야 하는 문자열이 없는지 확인받다.
  5. submit 버튼을 누르면 axios 라이브러리로 서버에 post 요청을 보낸다.
  6. axios api 통신중 및 로드중일 때, 로딩중 UI를 띄운다.
  7. 결과 UI 를 사용자에게 보여준다.

✅ 구현하기

나중에 내가 이 포스팅을 봤을 때, 아 맞아 이렇게도 했었지! 할 수 있는 주요 기능들만 정리해보겠다!

⭐ 카테고리 선택 로직

  • 카테고리 데이터는 객체요소가 들어있는 배열 형태로 구성했다.

  • map 메서드를 통해 emoji,text,color 를 사용해 버튼들을 렌더링한다.

  • 카테고리 버튼을 클릭하면, category state 는 해당 카테고리로 업데이트된다.

  • 원래는 emoji, text, color 등등의 프로퍼티를 각각의 state 로 두기로 했었지만, 그렇게 되면 state 가 너무 많아지기 때문에 이를 최소화 시키기 위해 하나의 category 를 state 로 두고, category.emoji 이런식으로 참조하는 방식으로 진행했다.

const categoryData = [
  // type : 1 = '~의 이름 짓기', 0 = '~ 짓기', -1 = 아무개
  {emoji:'🐶😺', text: '반려동물', color: 'pupple', type: 1},
  {emoji:'🕹️', text: '별명', color: 'mint', type: 0},
  {emoji:'📦', text: '상품', color: 'yellow', type: 1},
  {emoji:'📜', text: '글 제목', color: 'blue', type: 0},
  {emoji:'📱', text: '서비스', color: 'pink', type:1},
  {emoji:'👥', text: '팀/그룹', color: 'green', type:1},
  {emoji:'❓', text: '아무개', color: 'gray', type:-1},
]
  • 요기서 type 프로퍼티는 코드를 짜다가 하드코딩이 싫어서 만든 것이다.

  • 예를 들어, 반려동물의 경우 반려동물 이름 짓기 라는 제출 버튼이 나와야 한다. 하지만 별명이라면 별명 짓기, 아무개라면 이름 짓기 이다.

  • 위와 같이, category.text 를 활용하여 UI 를 동적으로 구성한다. 동적으로 변화하는 텍스트를 만들어주는 util인 MakeText를 아래와 같이 따로 빼서 작성하였다.

  • 버튼에 들어가는 텍스트, 설명란 위에 보여지는 텍스트, api에 전송하는 prompt에 들어가는 텍스트를 반환한다.

class MakeText{
  constructor(category){
    this.text = category.text;
    this.type = category.type;
  }
  buttonText = () => { //버튼 텍스트 반환
    if(this.type===-1){
      return '이름 짓기'
    }
    if(this.type===0){
      return `${this.text} 짓기`
    }
    return `${this.text} 이름 짓기`
  }
  detailExplainText = () => { //설명 입력 요청 텍스트 반환
    const baseExplain = '설명을 입력해 주세요'
    return(
      this.type<0?baseExplain:this.text+'에 대한 '+baseExplain
    );
  }
  commandText = (detail, include) => { //서버에 요청할 명령 텍스트 반환
    let baseCommand = ''
    if(this.type===0){
      baseCommand = `${this.text}을 한글로 추천해주세요.\n`
    }
    else if(this.type===1){
      baseCommand = `${this.text} 한글 이름을 추천해주세요.\n`
    }
    else{
      baseCommand = '한글 이름을 아무거나 추천해주세요.\n'
    }
    return(
      include?
      baseCommand+`설명: ${detail}.\n포함되어야 하는 문자열:${include}\n\n`:
      baseCommand+`설명: ${detail}\n`
    )
  }
}

그리고 아래와 같이, category 가 변할 때 마다 MakeText 를 새로 생성하도록 하였다.

useEffect(()=>{
    setSubmitState(0);
    makeText = new MakeText(category);
    initAllStates();
  }, [category]);

이렇게 카테고리 버튼을 클릭하면 입력하는 UI 부분이 카테고리 state 프로퍼티를 참조하여 변화하는 형태이다. 그 과정에서 각종 문자열 포맷을 위해 MakeText 를 거쳐서 나온다.

⭐ 로딩중 화면 띄우기 (Spinner)

  • 이름 짓기 버튼을 누른 후, 결과가 나오기 전에 아래와 같은 화면이 나오도록 했다.

  • 생각보다 기다리는 시간이 길어질 때가 있어서 사용자의 입장에서 최대한 완성도를 높이고자 넣은 기능이다.

  • submitState 라는 state 변수를 사용하여 구현하였다.

  • submitState = 0 : 이름 짓기 요청 들어가기 전
    submitState = 1 : 이름 짓기 요청 들어온 후, 결과 받아오기 전
    submitState = 2 : 결과 받아온 후

const handleSubmit = async() => {
    if(submitState===0){
      if(detail.trim().length<1){
        alert('설명을 입력해주세요');
        return;
      }
      if(include.trim().length<1){
        if(!confirm('포함되어야 하는 문자열이 없나요?')){
          return;
        }
      }
    }
    setSubmitState(1); //로딩 상태로 전환
    const command = makeText.commandText(detail, include.trim());
    try{
      //생략
      }
      setSubmitState(2); //결과 상태로 전환
      setResult(data.result);
    }
    catch(error){
      //생략
      setSubmitState(0); //초기 상태로 전환
    }
  }

⬆️ submitState 업데이트

  • 사용자가 이름 짓기 버튼을 클릭하면 handleSubmit 함수가 호출된다.
  • handleSubmit 함수에서 api 통신 요청 직전에 submitState 를 1로 업데이트한다.
  • 무사히 결과를 받아오면 submitState 를 2로 업데이트한다.
  • 에러가 나면 다시 초기 상태인 0으로 업데이트한다.
const MainBox = () => {
    if(submitState===1){
      return(
        <LoadingBox/>
      )
    }
    if(submitState===2){
      return(
        <ResultBox
          result = {result}
          category = {category}
          setSubmitState = {setSubmitState}
        ></ResultBox>
      )
    }
  }

⬆️ submitState 에 따른 화면 구현

  • 0 : 입력 화면 (default)
  • 1 : 로딩 화면
  • 2 : 결과 화면
  • default 값인 0인 경우는 return( ) 안에 작성해주었고, 그 이외의 화면은 위와 같이 MainBox 함수 안에 따로 작성한 컴포넌트를 넣어주었다.

중간 정리&회고

쓰다보니 너무 길어지기도 하고, 배포랑 openAI 관련해서 기록하고 싶은 내용들이 더 남아있어서 다음에 이어서 작성하도록 하겠다!

✅ 돌아보기

  • 간단한 구현이라도 기능 요구 사항을 정리하고 시작하니까 개발하는 시간이 훨씬 줄어들었다.
    또한 갈아엎을 상황이 잘 오지 않아서 좋은 것 같다.

  • 원래는 모든 함수들을 index.jsx 에 다 때려박는 안좋은 습관이 있었는데,
    이번에 그걸 고치려고 노력했다.
    MakeText 를 밖으로 빼고, categoryData 도 밖으로 뺐다. 로딩화면과 결과화면 컴포넌트도 탈출시켰다. 나중에 유지보수를 생각하면 훨씬 나아졌다고 생각한다.

  • style jsx 도 처음 알게된 기능이다. 개인적으로는 module.css 보다 쓰기 편했다!

0개의 댓글