잊지 않기 위해 포스팅 - 채팅 메시지를 json으로 만들기.

Sal Jeong·2023년 6월 26일
0

나는 무언가를 데이터 형식으로 정리하는 걸 좋아한다. 길게 늘어진 html을 리액트식으로 하면 .map으로 바꾸는 것을 좋아하는데,(시간이 허락하는 한에서)

지난 프로젝트에서 받은 태스크에 대해서 잊지 않기 위해 기록해 볼까 함.

위 프로젝트는 openAI 요즘 핫한 chatGPT의 api를 사용해 이른바 '환자 정보'를 만들어내는 것으로, 아래의 로직은 모두 파이썬 백엔드에서 완성했지만, 데이터형을 한 번 바꾸고 자바스크립트 내부에서 다시 한 번 코딩하는 것으로 기록하려 한다.

플로우는

  1. 서버에서 프론트의 데이터(가상환자를 만드는데 필요한 데이터)를 받음
  2. 서버에서 openAI API로 위 데이터를 호출함
  3. openAI에서 내려준 데이터(대화문 형식의 스트링)을 프론트로 보냄
  4. 프론트에서는 해당 스트링을 받아 파싱하여 json 형식으로 만들어서 위의 카드 형식으로 렌더링하고 수정이 가능하도록 html 형식으로 만듬.

의 파이프라인을 구상해야 했다.

어떤 스트링이였는가?

현재 적용된 내용을 그대로 적을 수는 없으니, 좀 간단하게 조건을 바꾸어서 보면

  1. 대제목은 '-'으로 감싸져 있다.
  2. 이하 항목은 numbered list로 되어 있다.

이것을 서버에서 받은 escaped string으로 바꾸면 이런 형태가 된다.

따라서 이 부분을 파싱해서 그려주면 되었다.

어떤 형식으로 파싱할 것인가?

프론트에서만 사용할 것이기 때문에, 가장 그리기 쉬운 형태로 만들기로 했다.

  const resultObj: { [key: string]: { phrases: string[]; hide: boolean } } = {};

대략적으로 구상해 보았을 때 이런 형태면 된다고 생각했다.

  1. 생성되는 텍스트는 1차원이다.(1.2.3... 은 있지만 1-1, 1-a는 없는 것으로
  2. 렌더할 때 .map으로 돌릴 수 있도록 한다.(배열이든, 오브젝트이든)
export const parseGPTToJSON = (string: string) => {
  const gptArr = string.split('\n').filter((e) => e.length); // 1. 먼저 newline을 날림.
  const resultObj: GptJsontype = {}; // 예상 완성 형태.
  try {
    let currentTitle = ''; // 오브젝트의 키값을 담아두기 위한 변수
    let numberedEntry = ''; // 제목 하위 번호 소제목을 담기 위한 변수
    for (let i = 0; i < gptArr.length; i++) {
      if (gptArr[i].endsWith('-')) {
        // 만약 -가 포함되어있다면, 대제목으로 간주함.
        currentTitle = gptArr[i].trim().replaceAll('-', ''); // 대제목에서 - 과 공백을 제거해서 키값으로 만듬.
        resultObj[currentTitle] = { phrases: [], hide: false };
      } else {
        // - 가 없다면, 제목이 아니라 내용으로 간주
        const contentArr = gptArr[i].split('.'); // 하위에서의 string 형식은 n. content로 간주해서 먼저 숫자 번호와 내용을 분리함
        contentArr.forEach((e) => {
          if (e) {
            if (/^[1-9]+$/.test(e)) {
              // 번호를 기록하기 위한 if
              return (numberedEntry = e);
            } else {
              // 번호가 아니라면 컨텐츠로 한다.
              if (numberedEntry) {
                // 주어진 번호가 있다면 해당 번호와 함께 컨텐츠를 넣어주고, numberedEntry를 버림.
                resultObj[currentTitle]['phrases'] = [
                  ...resultObj[currentTitle]['phrases'],
                  numberedEntry + '. ' + e.trim(),
                ];
                return (numberedEntry = '');
              } else {
                // numberedEntry가 없다면, 그냥 해당 스트링 자체만 넣음.
                return (resultObj[currentTitle]['phrases'] = [
                  ...resultObj[currentTitle]['phrases'],
                  e.trim(),
                ]);
              }
            }
          }
        });
      }
    }
    return resultObj;
  } catch {
    console.log('err');
  }
};

의 형태로 파싱을 해보면

이러한 형태로 만들어지는 것을 확인할 수 있다.

직접 렌더를 해 본다면,

        {Object.entries(
          parseGPTToJSON(
            gptResponseString
          )
        ).map(([key, { phrases }], i) => {
          return (
            <div key={'heading' + i}>
              {key}
              {phrases.map((p) => {
                return <div key={p}>{p}</div>;
              })}
            </div>
          );
        })}

이런식으로 두 가지 정도의 문제점이 보인다.

  1. example.com 의 경우 .로 스플릿하기 때문에 줄나눔이 되어버린 모습
    1. 0으로 끝나는 숫자의 경우 위의 regex에서 1-9를 체크하기 때문에 또 줄바꿈이 되어버렸다.
const contentArr = gptArr[i].split('. '); // 하위에서의 string 형식은 n. content로 간주해서 먼저 숫자 번호와 내용을 분리함

// 이 부분에서 gpt가 형성하는 스트링에서 numbered list는 '1. '의 형태로 나오는 것을 이용해, 스플릿을 .가 아니라 . 으로 해준다.

if (e) {
            if (!isNaN(Number(e))) {
              // 번호를 기록하기 위한 if
              return (numberedEntry = e);
            }
// 2. 은 1-9가 아니라 !isNaN으로 숫자인지를 체크해준다.

위의 방법으로 두 가지를 해결할 수 있었다.

최근 이런 문제를 할 일이 거의 없었는데 생각보다 재미있게 해결할 수 있었다.

profile
Can an old dog learn new tricks?

0개의 댓글