본문 바로가기
Programming/FrontEnd

[OpenAI] 음악 추천 서비스 만들어보기 (React) 2.5부

by devpine 2023. 3. 21.
반응형

오늘은 2부에 이어서 가볍게 2.5부로 리팩토링을 진행해보도록 하겠습니다. 원래 다음 포스팅은 3부로 기분 / 날씨 / 장르 등 키워드를 선택하여 노래 추천받는 기능 및 디자인까지 마무리 지으려고 했는데, 중간점검할 겸 조금이나마 정리된 코드를 공유하려고 합니다. 2부 포스팅은 아래를 클릭하면 볼 수 있습니다.

 

[OpenAI] 음악 추천 서비스 만들어보기 (React) 2부

오늘은 저번에 이어서 OpenAI 를 사용한 서비스를 만들어보겠습니다. 1부 포스팅은 여기에서 볼 수 있습니다. 1부: https://bolob.tistory.com/entry/OpenAI-%EC%9D%8C%EC%95%85-%EC%B6%94%EC%B2%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%E

bolob.tistory.com

 

리팩토링

지금 모든 코드가 App.js 에 들어가 있기 때문에 더 복잡해지지 않기 위해서는 중간에 리팩토링이 한번 들어가야 할 것 같습니다. 따라서 Api와 Hooks로 로직과 컴포넌트를 나누어주도록 하겠습니다.

호출하고 있는 Api는 두 개이며, Api 폴더를 만든 뒤 하위에 OpenAI 관련 폴더 하나, 음악 검색 관련 폴더 하나 만들어주겠습니다.

API

src/api/MusicSearchAPI 추가

export default class MusicSearchAPI {
  // maniaDB 음악 키워드 검색
  static fetchMusicSearch(params) {
    const { keyword, sr, display, v } = params;

    return fetch(`/api/search/${keyword}/?sr=${sr}&display=${display}&v=${v}`);
  }
}

src/api/OpenAIAPI 추가

import { Configuration, OpenAIApi } from 'openai';

export default class OpenAIAPI {
  // OpenAI Api
  static fetchOpenAI(params) {
    const { apiKey, prompt } = params;
    const configuration = new Configuration({ apiKey });

    return new OpenAIApi(configuration).createCompletion({
      model: 'text-davinci-003',
      prompt: prompt,
      temperature: 0,
      max_tokens: 150,
    });
  }
}

Api에 넘길 params도 interface를 만들어주어 types.ts로 관리해줍시다.

src/api/types 추가

export interface MusicSearchParams {
  keyword: string;
  sr: string;
  display: number;
  v: number;
}

export interface OpenAIParams {
  apiKey: string;
  prompt: string;
}

 

Custom Hooks

그럼 Api 세팅까지는 완료되었으니, Api 호출 로직을 따로 커스텀 훅을 만들어서 분리해주겠습니다.

이 커스텀 훅의 역할은 호출했을 때 1. OpenAI API 호출 > 2. 응답(노래 데이터) 을 음악 검색 API에 파라미터로 넘겨서 호출 > 3. 응답 파싱 > 4. 노래 제목, 가수, 앨범 커버를 리턴하도록 합니다.

src/hooks/useMusicSearch 추가

import { useCallback, useState } from 'react';

import MusicSearchAPI from 'api/MusicSearchAPI';
import OpenAIAPI from 'api/OpenAIAPI';

import XMLParser from 'react-xml-parser';

const OPENAI_API_KEY = process.env.REACT_APP_OPENAI_API_KEY;

function useMusicSearch() {
  const [music, setMusic] = useState({ title: '', artist: '', img: '' });

  const fetchMusicSearch = useCallback(async (keyword: string) => {
    const params = { keyword, sr: 'album', display: 1, v: 0.5 };
    const regExp = /[\{\}\[\]\/?.,;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g; // eslint-disable-line

    try {
      await MusicSearchAPI.fetchMusicSearch(params)
        .then((response) => response.text())
        .then((str) => new XMLParser().parseFromString(str))
        .then((data) => {
          const { children } = data.children[0];
          const item = children.filter((child) => child.name === 'item')[0]
            .children;

          const text = item.filter((el) => el.name === 'title')[0].value;
          const img = item
            .filter((el) => el.name === 'image')[0]
            .value.replace('>', '');
          const [artist, title] = text.split(regExp);

          setMusic({ title, artist, img });
        });
    } catch (error) {
      console.log(`Error: ${error}`);
    }
  }, []);

  const fetchOpenAI = useCallback(
    async (prompt: string) => {
      try {
        await OpenAIAPI.fetchOpenAI({
          apiKey: OPENAI_API_KEY,
          prompt,
        }).then((res) => {
          const { choices } = res.data;
          const [title] = choices[0].text.split('by');

          fetchMusicSearch(title);
        });
      } catch (error) {
        console.log(`Error: ${error}`);
      }
    },
    [fetchMusicSearch]
  );

  return { fetchOpenAI, music } as const;
}

export default useMusicSearch;

 

로직을 분리했으니 App.js에서는 해당 로직들을 모두 제거하고, 호출해서 결과값만 받아오도록 수정합니다.

 

src/App.js 수정

import { useCallback, useEffect } from 'react';
import './App.css';

import useMusicSearch from 'hooks/useMusicSearch';

function App() {
  const { fetchOpenAI, music } = useMusicSearch();

  const recommendMusic = useCallback(() => {
    fetchOpenAI('recommend me one male indie song');
  }, [fetchOpenAI]);

  useEffect(() => {
    recommendMusic(); // Mount 시 호출한다.
  }, []);

  const { title, artist, img } = music;

  return (
    <div className="App">
      <img className="Thumbnail" src={img} alt="Thumbnail" />
      {title} {artist}
    </div>
  );
}

export default App;

 

현재까지 App.js에 넣어주었던 Api와 호출부, 관련 로직을 Api, types, custom hooks로 분리해주어 가독성도 높이고 다른 페이지에서도 필요하다면 재사용할 수 있도록 수정하는 작업까지 해보았습니다.

그럼 이제 다음 3부에서는 기분, 날씨, 장르 등 원하는 키워드를 선택하는 기능을 만들어서, 디자인까지 진행해보도록 하겠습니다. 그럼 읽어주셔서 감사하고 잘 마무리해서 다음 포스팅까지 올려보겠습니다!

 

반응형

댓글