[개발 환경] PLOP을 이용한 코드 제너레이션

Chad Lee·2024년 5월 13일
0

개발 환경

목록 보기
1/2
post-thumbnail

개요

  6명으로 이루어진 팀에서 모노레포를 적용하며 프로젝트 측면에서 어떤 문제들이 있을 수 있고 어떻게 해결 또는 개선 가능할지 생각 하던 중

잠재적 문제

  먼저 문제를 판단하기에 앞서 우리가 모노레포 도입으로 달성하고자 하는 목표를 생각해 보면...

  1. 하나의 프로젝트에서 여러 앱을 구현
  2. 일관적인 앱 구조를 통한 빠른 프로토 타이핑 및 팀원간 빠른 앱 이해
  3. 재사용 가능한 공통 컴포넌트, 유틸리티 코드 공유
  4. 일관적인 프로젝트, 코드 작성 환경
  5. 단일화된 배포 파이프 라인

적어 놓고 생각해 보면 결국 일관적이고, 구현의 효율을 목표로 한다고 이해 할 수 있다.

위 관점에서 여러명이 함께 여러 코드베이스가 섞여있는 프로젝트에서 발생 할 수 있는 문제는 일관성 이다.
최초 프로젝트 생성하고 진행 할 때는 다들 신경 써서 진행 할 수 있으나 시간이 지날 수록 무뎌지기 마련이다. 그렇기 때문에 이를 대비할 필요가 있다.

일관성 있는 코드 베이스 갖추기

 일관적으로 관리한다는 목표를 생각해 보자.
컴포넌트, 코드, 앱의 구조가 동일한 뼈대를 가진 코드 베이스에서 시작되면 최대한 서로 비슷한 경험을 가진 상태에서 시작 할 수 있다. 이렇게 베이스 캠프를 가지고 시작하려면 코드 베이스가 될 코드 목적별 템플릿이 필요 하고 이 템플릿은 나의 사용에 맞게 같편하게 사용 할 수 있으면 좋다.

 템플릿이 이미 존재 한다는 가정 하에 문제를 해결 하기 위한 방법 1은 단순히 생각해서 ctrl + c, v 하면 된다.

  1. 목적 템플릿을 한 폴더에 넣어서 관리한다.
  2. 해당 템플릿을 복사하여 원하는 폴더에 붙여 넣는다.
  3. 코드를 확인하여 이름 등을 수정 한다.

이렇게 해결 하는 것도 방법이지만 2, 3번을 좀 더 실수 없고 편하게 하기 위한 방법으로 code generator를 이용하는 방법이 있다.
여러 라이브러리 들이 있지만 여기서는 plop 라이브러리를 이용 했다.
장점은 사용법이 쉽고 프롬프트(inquirer)와 템플릿엔진(handlebar)을 이용할 수 있다.

PLOP

기본 작동 구조

간단한 사용 구조는 아래의 도식과 같다.

코드로 보면 아래와 같다.

module.exports = function (plop) {
  plop.setGenerator('Application', application);
  plop.setGenerator('Component', component);
}

결국 코드 생성기를 prop에 set 해주면 동작 한다.

코드 생성기 구조


description은 해당 generator의 설명
prompts는 해당 코드 생성시 동작하는 prompt들의 묶음
actions는 실제 코드 생성시 어떻게 동작할 것인지의 정의들의 묶음으로 prompts등에서 입력된 매개변수를 참조 한다.

여기서 prompt는 inquirer의 사용법을 따르고 생성기 설정에는 handlebar의 방식을 따른다.

적용

적용에 앞서 생성할 템플릿이 뭐가 있을지 생각해 보면 현재 필요한 반드시 필요한 템플릿은 두가지다.

  1. application
  2. component

실제 제품화 될 application과 공통 컴포넌트로 사용될 component의 먼저 템플릿을 먼저 만들고 이를 사용자 입력에 따라 원하는 위치에 생성 할 수 있게 한다.

plop의 초기 셋업은 링크를 따라 설정한다.
이후 구성은 아래와 같이 해보았다.

prop-templates에서는 생성할 코드의 template을 정의하고 prop-generators에서는 template를 참조하여 code generator를 정의한다. 이를 propfile에서 setGenerator를 통해 실행 할 수 있게 한다.
개별 generator와 template을 별도로 구분하는편이 향후 확장에 용이 해 보인다.

이제 개별 generator에 prompt와 action을 정의 해보자.
prompt에서 매개변수로 받고 싶은 항목을 정의해 보면...

generator매개변수설명비고
applicationnameapplication 이름필수
pathapplication이 위치할 경로값이 없으면 기본 경로
componentnamecomponent 이름필수
pathcomponent가 위치할 경로값이 없으면 기본 경로

더 복잡한 템플릿과 조건이 들어갈 수 있지만 지금은 이 정도면 될것 같다.
이를 generator에 옮겨 보자.
먼저 prompt를 채워 보자.

// application.cjs
module.exports = {
  description: 'Create new application',
  prompts: [
    {
      type: 'input',
      name: 'name',
      message: 'Insert application name',
      validate: function (value) {
        if (/.+/.test(value)) {
          return true
        }
        return 'name is required'
      }
    },
    {
      type: 'input',
      name: 'path',
      message: 'Path (By default will be added in "applications/"):',
    },
  ], // array of inquirer prompts
  actions: []
}

type "input"을 통해 입력을 받는 두 객체 리터럴을 생성하여 배열에 넣어준다. 그러면 inquirer를 통해 실제 두번의 매개변수를 묻는 프롬프트가 순서대로 실행 된다.
여기서 "name" 프롬프트는 필수 조건 이기 때문에 validate를 통해서 값의 유무를 판단하여 boolean을 반환한다.

이제 actions에 값을 채워보자

module.exports = {
  description: 'Create new application',
  prompts: [ ... ], // array of inquirer prompts
  actions: (data) => {
    let defaultPath = 'applications/';
    if (data.path) {
      defaultPath = `${defaultPath}/${data.path}}/`;
    }
    return ['Wait...',{
      type: 'addMany',
      destination: `${defaultPath}{{name}}/`,
      base: 'plop-templates/application/',
      templateFiles: '**/*',
      globOptions: {
        dot: true
      },
      abortOnFail: true,
    },'Completed!']
  }
}

actions에는 prompt에서 입력 받은 매개변수들을 사용하여 여러 작업을 수행 할 수 있다.
위 코드를 예로 들면...

  1. 입력 받은 path가 있다면 해당 path를 적용
  2. 입력 받은 name으로 폴더 이름을 생성
  3. 설정된 경로 아래의 모든 폴더와 파일을 생성한 경로에 생성

위 액션을 취한다.
여기서 {{name}} 으로 되어 있는 부분은 handlebar 템플릿 엔진의 문법을 따른다.

여기까지는 단순 폴더 이름을 입력 받은 이름으로 하고 정의된 template그대로 코드를 원하는 위치에 생성하는 역할을 한다. 그런데 입력 값을 코드 내부로 삽입 하고 싶을 수 있다. 그때는 .hbs 파일(handlebar)로 템플릿 파일을 생성하여 적용한다. 예를 들어 component 하나를 생성하는데 아래와 같은 파일이 필요하다고 하자.


storybook 파일 하나와 컴포넌트를 구현할 index.vue 파일이 있다. 이를 .hbs파일로 생성하고 입력 하여 변경 되고자 하는 코드 곳곳에 매개변수를 받도록 코드를 수정 해주면 된다.

import type { Meta, StoryObj } from '@storybook/vue3';
import Common{{name}} from './index.vue';

const meta: Meta<typeof Varco{{name}}> = {
  title: 'Common UI/Component/{{name}}',
  component: Common{{name}},
  tags: ['autodocs'],
  render: (args) => ({
    setup() {
      return { args };
    },
    template: `<Common{{name}} v-bind="args" />`,
  }),
};

type Story = StoryObj<typeof Common{{name}}>;

export default meta;

export const Default: Story = {
  name: 'default',
  args: { },
};

이 부분은 plop의 영역이 아니고 템플릿 엔진의 영역이다. (handlebar 참조)

actions에서 분기하여 서로 다른 액션을 실행 시킬 수도 있다.

actions: (data) => {
    let actions = [];
    if (data.action === 'An Action') {
      actions = actions.concat([{
        type: 'add',
        path: `src/components/{{path}}/{{name}}.js`,
        templateFile: 'plop-templates/component/{{name}}.js.hbs',
        abortOnFail: true,
      }]);
    } else {
      actions = actions.concat(
        {
          type: 'add',
          path: `src/reducers/calls/{{path}}/{{name}}Requested.js`,
          templateFile: 'plop-templates/calls/calls.js.hbs',
          abortOnFail: true,
        },
        {
          type: 'add',
          path: `src/sagas/{{path}}/{{name}}.js`,
          templateFile: 'plop-templates/sagas/saga.js.hbs',
          abortOnFail: true,
        }
      );
    }
    return actions;
  }

실행

실행을 위해 package.json > script에 아래와 같이 추가한다.

이후 해당 스크립트를 실행 하면 아래와 같이 사용 할 수 있다.

생성 된 파일

결론

일관된 코드 구조등을 유지하기 위해서 정해진 템플릿을 ctrl + c,v, code snipet, 지금 까지 설명한 code generator를 이용할 수 있다.

만약 단순 화면이나 코드 뭉치로 생성 한다면 snipet으로 생성하는 것도 좋은 방법이다. 하지만 여러 파일과 코드를 생성해야 한다면 code generator가 더 좋은 대안이 될 수 있다.

그리고 같이 알아본 yeoman 도 좋은 툴로 보여지니 확인해 보는 것도 좋을 것 같다.

0개의 댓글