[이마고웍스] 사용자 로그 수집 라이브러리 개발: 구글 스프레드시트를 사용한 스크립트 생성 자동화

Ji-Heon Park·2023년 10월 13일
3

Imagoworks

목록 보기
8/10

Imagoworks의 클라우드 팀에서 사용자의 로그 데이터를 체계적이고 효율적으로 수집하고 활용하기 위한 라이브러리 개발 작업을 맡게 되었습니다.

라이브러리의 요구사항은 다음과 같았습니다.

  • 문서를 바탕으로 바디값을 담아 서버에 post 요청을 보내는 메서드
  • 타입추론으로 자동완성을 가능하게하여 직접 문서를 확인하지 않아야 한다.

제한된 시간 내에서 데이터 크기(139 x 1000)를 감안할 때 수동 작업은 비효율적이라고 판단되었습니다. 효율성을 최대화하기 위해 자동화 작업을 우선적으로 진행하기로 결정하였습니다.

구글 스프레드시트와 구글 API를 활용한 코드 생성 자동화과정

팀 내 데이터엔지니어님께서 정리한 엑셀 파일을 받았지만 완성본이 아니기에 변경사항이 종종 생겼습니다. 변화에 유연하게 대응하고 관련 API를 활용하기 위해 구글 스프레드시트 공유문서로 툴을 먼저 변경했습니다.

1. Node.js에서 Google Spread Sheet를 연동

  1. Google Spread Sheet API 생성:

    • Google Developers Console에 접속하여 새로운 프로젝트를 생성합니다.
    • Google Sheets API를 활성화합니다.
  2. 서비스 계정 키 생성 및 권한 설정:

    • 서비스 계정 키를 생성하고, JSON 파일로 다운로드합니다.
    • 생성한 스프레드 시트에 서비스 계정 키의 메일 주소에 읽기 및 쓰기 권한을 부여합니다.
  3. Node.js 연동:

    • 모듈 설치:
      npm install google-spreadsheet
    • 서비스 계정 키의 JSON 파일과 스프레드 시트의 ID 값을 사용하여 Node.js와 연동합니다.
// controllers/spreadsheetController.js

const createAuthClient = () => {
  return new JWT({
    email: creds.client_email,
    key: creds.private_key,
    scopes: ['https://www.googleapis.com/auth/spreadsheets'],
  });
};

const loadSpreadsheet = async (authClient) => {
  const doc = new GoogleSpreadsheet(SPREADSHEET_DOC_ID, authClient);
  await doc.loadInfo();
  const targetSheet = doc.sheetsByIndex[0];
  return targetSheet;
};

2. 데이터 파싱 및 정제

불러오는 시트의 모양은 대략 다음과 같습니다:

위 시트를 토대로 스크립트 작성 자동화를 위해 필요한 데이터를 만들어야 합니다. 플로우는 다음과 같습니다.

  1. 이벤트의 시작열부터 순회한다. (eventName, flow, description 수집)
  2. 해당 열의 파라미터 행을 순회한다. (파라미터 이름, 파라미터 타입, note를 각각 수집)

이렇게해서 자동화를 위한 데이터의 형태를 갖출 수 있었습니다.

// controllers/dataController.js

const processRawDataToEventMeta = async (targetSheet) => {
  const rowCount = targetSheet.rowCount;
  const colCount = targetSheet.columnCount;

  await targetSheet.loadCells({
    startRowIndex: 0,
    endRowIndex: rowCount,
    startColumnIndex: 0,
    endColumnIndex: colCount,
  });

  let eventMeta = [];
  let seenEventNames = new Set();

  for (let colIndex = EVENT_LIST_START_COL_INDEX; colIndex < colCount; colIndex++) {
    const eventName = targetSheet.getCell(EVENT_NAME_ROW_INDEX, colIndex).value;

    if (seenEventNames.has(eventName)) continue;

    seenEventNames.add(eventName);

    let types = {};
    for (let rowIndex = PARAMS_START_ROW_INDEX; rowIndex < rowCount; rowIndex++) {
      const currentCell = targetSheet.getCell(rowIndex, colIndex).value;
      if (currentCell === 'O') {
        types = {
          ...types,
          ...extractFieldData(targetSheet, rowIndex, colIndex),
        };
      }
    }
    eventMeta.push(extractEventMeta(targetSheet, colIndex, types));
  }

  return eventMeta;
};

// ...other functions

3. 스크립트 생성

위에서 필요한 데이터 정제가 잘 되어 있어 이 부분은 간단히 끝납니다. 만들어진 데이터를 토대로 템플릿 리터럴을 사용해 .ts 스크립트를 작성합니다. 요구사항에 없었지만 시트에 이벤트와 파라미터의 설명을 보고 개발단에서 이를 파악하기 쉽도록 jsDoc 주석을 추가하였습니다.

// controller/scriptGenerator.js
const generateTSFile = (eventMeta) => {
  const content = buildTSContent(eventMeta);
  const filePath = path.join(__dirname, '../../index.ts');
  fs.writeFileSync(filePath, content, 'utf8');
  console.log('index.ts file generated successfully!');
};

const buildTSContent = (eventMeta) => {
  const flows = groupByFlow(eventMeta);
  return `
import ...

// ... 필요한 공통 로직들

const ImagoEvent = {
  ${Object.entries(flows)
    .map(
      ([flow, methods]) => `
  ${flow}: {
    ${methods.map(buildTSMethod).join(',\n')}
  },`
    )
    .join('\n')}
};
export default ImagoEvent;
  `;
};

const buildTSMethod = (event) => {
  const types = Object.entries(event.types)
    .map(([key, value]) => `${key}: ${value.type}`)
    .join(',\n    ');

  const jsdocTypes = Object.entries(event.types)
    .map(([key, value]) => `* @param params.${key} - ${value.note}`)
    .join('\n    ');

  return `
  /**
   * ${event.description}
   ${jsdocTypes}
   */
  ${event.eventName}: async (
    params: {
    ${types}
  }) => {
      try {
          const response = await axios.post(eventServerUrl, {...params, ...commonParams});
          return response.data;
      } catch (error) {
          console.error("Error in ${event.eventName}:", error);
          throw error;
      }
  }`;
};

결과물 및 예제

완성된 프로젝트의 구조는 아래와 같습니다:

.
├── README.md
├── build
│   ├── index.d.ts
│   └── index.js
├── index.ts
├── package-lock.json
├── package.json
├── src
│   ├── app.ts
│   ├── constants
│   │   ├── config.ts
│   │   └── tableConfig.ts
│   ├── controllers
│   │   ├── dataController.ts
│   │   ├── scriptGenerator.ts
│   │   └── spreadsheetController.ts
│   └── utils
│       └── transfomer.ts
└── tsconfig.json

스크립트 생성 과정을 모듈(controllers)로 분리하여 관리합니다. 각 모듈은 특정한 역할과 책임을 가지며, 다른 모듈들과 상호작용합니다. app.js 에서 각각의 모듈은 순차적으로 실행되며, 전체적인 자동화 프로세스를 구성합니다.

// app.js
const app = async () => {
  try {
    const authClient = createAuthClient();
    const targetSheet = await loadSpreadsheet(authClient);
    const eventMeta = await processRawDataToEventMeta(targetSheet);
    generateTSFile(eventMeta);
  } catch (error) {
    console.error('Error:', error);
  }
};

app();

위 app을 실행하면 다음과 서비스에서 사용될 코드가 생성됩니다. 전체 코드의 길이는 4000줄 이상입니다. 직접 만들었을 생각을 하면 머리가 아픕니다. 😂 string, boolean 같은 단순타입은 물론 union, object, array까지 잘 만들어졌습니다.

...

/**
* 구독 취소
* @param params.isImagoworks - 메일 계정이 Imagoworks.ai인 경우 True
* @param params.sourceType -
* @param params.balanceFinal -
* @param params.creditWalletId - 보낼 수 없는 경우 null
*/
cancelSubscription: async (params: {
isImagoworks: boolean;
sourceType: 'voucher' | 'giftcard' | 'export' | 'storage';
balanceFinal: number;
creditWalletId: string;
    }) => {
      try {
        const response = await axios.post(eventServerUrl, { ...params, ...commonParams });
        return response.data;
      } catch (error) {
        console.error('Error in cancelSubscription:', error);
        throw error;
      }
    },
     
    ...

라이브러리를 사용하여 로깅 함수를 서비스단에 심는 예시는 다음과 같습니다:

예시를 보면 3가지 특징이 있습니다

  • jsDoc의 활용: 함수의 설명 및 매개변수에 대한 설명을 제공함으로써 이해를 쉽게 합니다.
  • 타입추론 및 자동완성을 지원하여 개발 효율성을 높입니다.
  • 공통 파라미터의 자동 수집: clientDt, os, device, userAgent와 같은 기본적인 데이터는 라이브러리가 자동으로 수집하므로, 별도의 설정이나 입력 없이 바로 사용할 수 있습니다.

결론

이 작업을 통해 4000줄 이상의 코드 생성을 자동화하여 개발에 소요되는 시간을 크게 절약하고 실수로 인한 오류를 줄여 생산성을 높일 수 있었습니다. 이후 프로젝트 회고 시간에서 동료들로부터 편리성과 문서 기반 자동화로 인한 빠른 피드백 제공으로 호평을 받았습니다.

profile
Frontend Developer | 기록되지 않은 것은 기억되지 않는다

0개의 댓글

관련 채용 정보