Imagoworks의 클라우드 팀에서 사용자의 로그 데이터를 체계적이고 효율적으로 수집하고 활용하기 위한 라이브러리 개발 작업을 맡게 되었습니다.
라이브러리의 요구사항은 다음과 같았습니다.
제한된 시간 내에서 데이터 크기(139 x 1000)를 감안할 때 수동 작업은 비효율적이라고 판단되었습니다. 효율성을 최대화하기 위해 자동화 작업을 우선적으로 진행하기로 결정하였습니다.
팀 내 데이터엔지니어님께서 정리한 엑셀 파일을 받았지만 완성본이 아니기에 변경사항이 종종 생겼습니다. 변화에 유연하게 대응하고 관련 API를 활용하기 위해 구글 스프레드시트 공유문서로 툴을 먼저 변경했습니다.
Google Spread Sheet API 생성:
서비스 계정 키 생성 및 권한 설정:
Node.js 연동:
npm install google-spreadsheet
// 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;
};
불러오는 시트의 모양은 대략 다음과 같습니다:
위 시트를 토대로 스크립트 작성 자동화를 위해 필요한 데이터를 만들어야 합니다. 플로우는 다음과 같습니다.
이렇게해서 자동화를 위한 데이터의 형태를 갖출 수 있었습니다.
// 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
위에서 필요한 데이터 정제가 잘 되어 있어 이 부분은 간단히 끝납니다. 만들어진 데이터를 토대로 템플릿 리터럴을 사용해 .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가지 특징이 있습니다
이 작업을 통해 4000줄 이상의 코드 생성을 자동화하여 개발에 소요되는 시간을 크게 절약하고 실수로 인한 오류를 줄여 생산성을 높일 수 있었습니다. 이후 프로젝트 회고 시간에서 동료들로부터 편리성과 문서 기반 자동화로 인한 빠른 피드백 제공으로 호평을 받았습니다.