일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- es6
- CSS
- VUE
- 블록체인
- 알고리즘
- HTML
- 프론트엔드
- docker
- BFS
- 클라우드
- next.js
- 백엔드
- 이더리움
- react
- AWS
- 웹
- TypeScript
- 백준
- 가상화
- 타입스크립트
- 파이썬
- 솔리디티
- 리액트
- JavaScript
- kubernetes
- 자바스크립트
- 이슈
- 컴퓨터공학
- 쿠버네티스
- k8s
- Today
- Total
즐겁게, 코드
CLI로 반복 작업 개선하기 본문
지난 프로젝트를 회고하며 부족했던 점들을 돌아봤을 때, 새로운 서비스를 추가할 때마다 기존 코드를 복사 - 붙여넣기 후 새로운 프로젝트를 시작하는 것에 좋지 않은 경험을 느꼈던 것 같다.
어떤 경로로 진입했을 때 어떤 페이지를 렌더할지 설정하는 modules/[모듈명]/route.ts
를 작성하는 상황을 예로 들어보겠다.
import type { RouteRecordRaw } from 'vue-router';
export const accountRoutes: RouteRecordRaw[] = [];
accountRoutes.push({
path: '/account/signUp',
name: 'accountSignup',
component: () => import('@/pages/account/AccountSignUp.vue'),
});
accountRoutes.push({
path: '/account/signIn',
name: 'accountSignIn',
component: () => import('@/pages/account/AccountSignIn.vue'),
});
accountRoutes.push({
path: '/account/withdraw',
name: 'accountWithdraw',
component: () => import('@/pages/account/AccountWithdraw.vue'),
});
그리고 새로운 모듈(또는 서비스)을 추가하는 상황이 되면 관성적으로 위 파일을 복사한 다음, 새로운 폴더에 붙여넣고 불필요한 코드를 잘라내면서 작업을 진행해왔다.
/**
* 예시 : point 라는 새로운 기능을 추가하게 되면
* 복사 - 붙여넣기 후 필요한 만큼 코드를 수정한다.
*/
import type { RouteRecordRaw } from 'vue-router';
export const pointRoutes: RouteRecordRaw[] = [];
pointRoutes.push({
path: '/point/pointDetail',
name: 'pointDetail',
component: () => import('@/pages/point/PointDetail.vue'),
});
그동안은 별 생각이 없었는데, 이걸 수십번을 반복하게 되니 살짝 회의가 들기 시작했다.
복사 - 붙여넣기로 코드 뼈대를 생성하는 작업은 절대 '세련된' 방법이 아니었고, 만약 신규 개발자가 합류하게 되었을 때 '새로운 경로를 생성하려면 기존 route.ts
를 하나 잡아 복사해 쓰시면 됩니다' 라고 소개해야 한다면 차마 고개를 들기 어려울 것 같았다.
그래서 이 문제를 해결하기 위해, GPT의 도움을 받아 스크립트를 생성해 봤다.
"scripts": {
"new:route": "node scripts/new-route.js",
}
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { select, input } from '@inquirer/prompts';
// 현재 파일 경로를 가져오기
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const modulesPath = path.resolve(__dirname, '../src/modules');
const pagesPath = path.resolve(__dirname, '../src/pages');
const routerIndexPath = path.resolve(__dirname, '../src/router/index.ts');
// 모듈 목록을 가져오는 함수
function getModules() {
if (!fs.existsSync(modulesPath)) {
console.error('❌ 모듈 폴더가 존재하지 않습니다.');
process.exit(1);
}
return fs.readdirSync(modulesPath).filter(dir => {
return fs.statSync(path.join(modulesPath, dir)).isDirectory();
});
}
// 페이지 컴포넌트 파일 경로 확인 함수
function doesPageComponentExist(moduleName, routeName) {
const filePath = path.join(pagesPath, moduleName, `${routeName}.vue`);
return fs.existsSync(filePath);
}
// 페이지 컴포넌트 파일 생성 함수
function createPageComponent(moduleName, routeName) {
const componentPath = path.join(pagesPath, moduleName);
const filePath = path.join(componentPath, `${routeName}.vue`);
// 해당 폴더가 없으면 생성
if (!fs.existsSync(componentPath)) {
fs.mkdirSync(componentPath, { recursive: true });
}
// 파일이 존재하지 않으면 생성
if (!fs.existsSync(filePath)) {
const template = `
<template>
<div>새로운 페이지</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
`;
fs.writeFileSync(filePath, template.trim(), 'utf8');
console.log(`✅ 새로운 페이지 컴포넌트가 생성되었습니다: ${filePath}`);
} else {
console.log(`⚠️ 페이지 컴포넌트가 이미 존재합니다: ${filePath}`);
}
}
// route.ts 파일을 생성 또는 수정하는 함수
function createOrUpdateRoute(moduleName, routeName, param) {
const routeFilePath = path.join(modulesPath, moduleName, 'route.ts');
const routeComponentPath = `@/pages/${moduleName}/${routeName}.vue`;
// route.ts 파일이 존재하지 않으면 새로 생성
if (!fs.existsSync(routeFilePath)) {
fs.writeFileSync(
routeFilePath,
`import type { RouteRecordRaw } from "vue-router";\n\nexport const ${moduleName}Routes: Array<RouteRecordRaw> = [];\n`,
'utf8',
);
console.log(`✅ 새로운 route.ts 파일이 생성되었습니다: ${routeFilePath}`);
}
// 경로 추가 구문 작성
const routeConfig = `
${moduleName}Routes.push({
path: "/${moduleName}/${routeName}${param ? `/:${param}` : ''}",
name: "${routeName}",
component: () => import("${routeComponentPath}")}
});\n`;
// route.ts 파일에 추가
fs.appendFileSync(routeFilePath, routeConfig, 'utf8');
console.log(`✅ ${routeName} 경로가 ${moduleName}/route.ts에 추가되었습니다.`);
}
// router/index.ts 파일을 업데이트하는 함수
function updateRouterIndex(moduleName) {
const importStatement = `import { ${moduleName}Routes } from '@/modules/${moduleName}/route';`;
const routesStatement = `...${moduleName}Routes,`;
let routerIndexContent = fs.readFileSync(routerIndexPath, 'utf8');
// 이미 해당 모듈이 import 되어 있는지 확인
if (routerIndexContent.includes(importStatement)) {
console.log(`✅ ${moduleName}Routes는 이미 router/index.ts에 추가되어 있습니다.`);
return;
}
// import 구문 추가
const importPosition = routerIndexContent.indexOf('\n');
routerIndexContent =
routerIndexContent.slice(0, importPosition + 1) +
importStatement +
'\n' +
routerIndexContent.slice(importPosition + 1);
// routes 배열에 추가
const routesPosition = routerIndexContent.lastIndexOf('routes: [');
const closingBracketPosition = routerIndexContent.indexOf(']', routesPosition);
routerIndexContent =
routerIndexContent.slice(0, closingBracketPosition) +
` ${routesStatement}\n` +
routerIndexContent.slice(closingBracketPosition);
// 변경된 내용을 router/index.ts에 저장
fs.writeFileSync(routerIndexPath, routerIndexContent, 'utf8');
console.log(`✅ ${moduleName}Routes가 router/index.ts에 추가되었습니다.`);
}
// 사용자 입력을 받는 함수
async function promptUser() {
const modules = getModules();
try {
// 모듈 선택
const moduleName = await select({
message: '모듈명을 선택하세요',
choices: modules.map(mod => ({ name: mod, value: mod })),
});
let routeName;
// 경로명 입력 (이미 존재하는 경우 재입력 요청)
while (true) {
routeName = await input({
message: '경로명을 입력하세요 (Ex. NewPage)',
validate: input => {
if (input.trim() === '') {
return '경로명을 입력하세요.';
}
if (doesPageComponentExist(moduleName, input.trim())) {
return `⚠️ 해당 경로명이 이미 존재합니다. 다른 경로명을 입력해주세요.`;
}
return true;
},
});
// 중복되지 않은 유효한 경로명을 입력한 경우 루프 종료
if (!doesPageComponentExist(moduleName, routeName.trim())) break;
}
// 동적 파라미터 입력
const param = await input({
message: '동적 파라미터명을 입력하세요 (없으면 Enter)',
default: '',
});
return { moduleName, routeName, param };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
console.log('⚠️ 입력이 취소되었습니다.');
process.exit(0);
}
}
// 실행 함수
async function run() {
const { moduleName, routeName, param } = await promptUser();
// 페이지 컴포넌트 파일 생성
createPageComponent(moduleName, routeName);
// route.ts 파일 생성 또는 수정
createOrUpdateRoute(moduleName, routeName, param);
// router/index.ts 업데이트
updateRouterIndex(moduleName);
}
run();
대략 다음과 같은 질의를 거치고, 선택한 모듈에 빈 페이지를 생성한 다음 해당 페이지를 라우트 목록에 등록까지 해주는 CLI다.
(실제로 사용하는 스크립트에는 인증, 메타데이터 등을 설정하는 코드가 조금 더 있기는 하다)
아무튼 이제 신규 개발자가 합류했을 때 '이건 여기서 복붙해 쓰시면 됩니다 😅' 처럼 민망한 웃음을 동반한 얼레벌레 온보딩 대신, 일관된 스캐폴딩 경험을 제공할 수 있는 스크립트가 생겼다.
예전에는 https://github.com/SBoudrias/Inquirer.js 같은 문서도 한번씩 탐독해야 삽을 뜰 수 있었는데, AI에게 적절한 프롬프트를 넘겨주면 이런 스크립트를 수 분 내에 만들 수 있게 되었다.
앞으로 더 많은 '짜치는' 작업들을 CLI로 개선해보려 한다.
'👨🏻💻 기록' 카테고리의 다른 글
토이 프로젝트 스토리지 결정하기 (2) | 2022.06.11 |
---|