개발 환경을 구축하는건 공부와 노력이 꽤 필요한 일이다. 엄청난 삽질이 쌓여 이정도면 괜찮다싶은 세팅이 머릿속에 생겼지만, 자주 만져본 설정 파일들에 익숙해졌고 대부분의 구조는 yarn create vite
로 만들다보니 셋업하는게 비효율적이라는 생각을 못했었다.
최근에 스터디와 테오의 스프린트, 그리고 이런저런 과제와 개인 연습용 프로젝트들을 하면서 단기간에 여러번의 비슷한 작업을 반복했다. 필요한 것들을 하나하나 챙기는 데에 시간이 꽤 걸린다는걸 느꼈고 휴먼 에러도 종종 있었다.
그래서 드디어 보일러 플레이트를 만들었다. 만든김에 터미널 커맨드로만 셋업할 수 있으면 더 좋겠다는 생각이 들었다. 매번 create-vite
사용하면서도 어떻게 굴러가는지 몰랐는데 궁금하기도 해서 만들어봤다.
create <package>
하면 패키지매니저가 알아서 해당 패키지에 맞는 개발 환경을 구성해줄리는 없다. --help 옵션으로 이 커맨드가 뭔지 알아봤다.
create-se-app [main●●] % npm create --help
Create a package.json file
Usage:
npm init <package-spec> (same as `npx <package-spec>)
npm init <@scope> (same as `npx <@scope>/create`)
Options:
[-y|--yes] [-f|--force] [--scope <@scope>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root]
aliases: create, innit
Run "npm help init" for more info
npm init
이랑 같고 package.json을 만든다는 얘기다. 뭐 어떡하라고.. 싶어서 npm help init
해봤더니 더 친절하게 알려줬다.
npm init <initializer> can be used to set up a new or existing npm package.
initializer in this case is 1️⃣ an npm package named create-<initializer>, 2️⃣ which will be installed by npm help npm-exec, 3️⃣ and then have its main bin executed -- presumably creating or updating package.json and running any other initialization-related operations.
그러니까 npm init <initializer>
처럼 쓰이는 경우, 로컬이나 리모트 패키지의 커맨드를 실행시키는 커맨드인 npm-exec에 의해 create-<initializer>
패키지의 main bin이 실행된다. 아래와 같은 규칙으로 변환된다고 한다.
The init command is transformed to a corresponding npm exec operation as follows:
• npm init foo -> npm exec create-foo
• npm init @usr/foo -> npm exec @usr/create-foo
• npm init @usr -> npm exec @usr/create
• npm init @usr@2.0.0 -> npm exec @usr/create@2.0.0
• npm init @usr/foo@2.0.0 -> npm exec @usr/create-foo@2.0.0
번외로 추상화 레벨이 굉장히 높은 yarn의 설명 ...
Usage: yarn [command] [flags]
Creates new projects from any create-* starter kits.
그러면 그 create-<initializer>
패키지는 어떻게 생겼는지 궁금해진다.
레퍼런스들을 읽어보니 이해하는건 어렵지 않았는데, 노드로 작성한 스크립트를 터미널에서 실행시키는거였다. 패키지 커맨드는 아래처럼 매핑되어 있다.
// create-vite
"bin": {
"create-vite": "index.js",
"cva": "index.js"
},
// create-react-app
"bin": {
"create-react-app": "./index.js"
},
https://github.com/vitejs/vite/tree/main/packages/create-vite
https://github.com/facebook/create-react-app/tree/main/packages/create-react-app
그래서 자주 사용하는 create-vite
의 스크립트 소스를 읽으면서 흐름을 파악해봤다. 코드 양이 많아서 필요한 부분만 발췌했다.
우선 prompt 패키지를 사용해서 터미널 입력을 받는다. 이 부분이 길고 복잡해보이지만 중요한건 입력을 받는 동작이다.
let result: prompts.Answers<
'projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant'
>
try {
result = await prompts(
[
{
type: argTargetDir ? null : 'text',
name: 'projectName',
message: reset('Project name:'),
initial: defaultTargetDir,
onState: (state) => {
targetDir = formatTargetDir(state.value) || defaultTargetDir
},
},
...
원래는 아래 사진처럼 프레임워크, 언어와 컴파일러 등의 항목이 있는데 내 템플릿은 스택이 React + TypeScript 고정이라서 프로젝트와 패키지 이름만 남겼다.
개인적인 용도라면 선택지 없이 고정해두는게 편하다. 또는 npm create @usr/foo -> npm exec @usr/create-foo
규칙을 참고해서 네임스페이스와 여러개의 템플릿으로 만드는 것도 좋을 것 같다.
근데 알록달록한 터미널 예뻐보여서.. 기술 스택 늘어난다면 추가도 해보고싶다.
입력이 정상적으로 끝났다면 터미널 입력, 환경변수 등을 사용해 선택된 템플릿, 패키지매니저 정보를 프로세싱한다.
const { framework, overwrite, packageName, variant } = result;
const root = path.join(cwd, targetDir);
let template: string = variant || framework?.name || argTemplate;
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
const pkgManager = pkgInfo ? pkgInfo.name : "npm";
const isYarn1 = pkgManager === "yarn" && pkgInfo?.version.startsWith("1.");
그 정보들로 템플릿 디렉토리의 파일들을 타겟 디렉토리로 복사하고,
const templateDir = path.resolve(fileURLToPath(import.meta.url), "../..", `template-${template}`);
const files = fs.readdirSync(templateDir);
for (const file of files.filter((f) => f !== "package.json")) {
write(file);
}
const pkg = JSON.parse(fs.readFileSync(path.join(templateDir, `package.json`), "utf-8"));
pkg.name = packageName || getProjectName();
write("package.json", JSON.stringify(pkg, null, 2) + "\n");
완료되면 실행 방법을 안내한다.
const cdProjectName = path.relative(cwd, root);
console.log(`\nDone. Now run:\n`);
if (root !== cwd) {
console.log(` cd ${cdProjectName.includes(" ") ? `"${cdProjectName}"` : cdProjectName}`);
}
switch (pkgManager) {
case "yarn":
console.log(" yarn");
console.log(" yarn dev");
break;
default:
console.log(` ${pkgManager} install`);
console.log(` ${pkgManager} run dev`);
break;
}
console.log();
https://github.com/se030/create-se-app/blob/main/src/index.js
이정도 부분만 사용해서 스크립트를 만들었다. 나는 템플릿 디렉토리도 한개이기 때문에 그쪽 파일을 복사하기만 하는 간단한 동작이다.
사실 이것저것 가져온 쪽에 더 가깝지만 ..ㅋㅋ 패키지매니저 관련 고려할 것들이 많은 듯해서 오픈소스를 쓰는게 나아보인다. 작성한 스크립트는 node <스크립트 경로>
로 실행시켜 볼 수 있다.
배포를 위해서는 빌드가 필요하다.
번들링을 통해 외부 패키지 용량도 줄이고, 왠지 써보고 싶은 Rollup 번들러도 사용해 볼 수 있다. Webpack에 비해 가벼운 번들러다.
파일이 하나라서 설정은 복잡하지 않다.
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
export default {
input: "index.js",
output: {
banner: "#!/usr/bin/env node",
file: "dist/index.mjs",
format: "esm",
},
plugins: [commonjs(), resolve()],
};
input, output을 원하는대로 넣어준다. 나는 bin 커맨드를 아래처럼 작성했다.
"bin": {
"create-se-app": "dist/index.mjs"
},
이 때 output에 대한 templateDir 상대 경로가 input 기준의 경로와 다르다면 스크립트 내용에 수정이 필요하다.
실행 환경을 정의하는 라인인 #!/usr/bin/env node
가 번들링 과정에서 사라진다. rollup #235를 참고해 banner로 넣어줬다.
@rollup/plugin-node-resolve
, @rollup/plugin-commonjs
CommonJS를 포함한 의존성 패키지들을 가져온다.
빌드가 끝나면 node dist/index.mjs
로 잘 동작하는지 테스트 해본다.
여기까지 하고나면 배포는 정말 별거 없다.
package.json
파일에 필요한 정보들을 작성하고 npm publish
한 뒤에 이런저런 인증 과정을 거치면 정상적으로 배포된다.
// package.json
{
"name": "create-se-app",
"description": "bin script to create a productive scaffold for a web client project",
"version": "0.0.12",
"type": "module",
"bin": {
"create-se-app": "dist/index.mjs"
},
"main": "src/index.js",
"files": [
"src",
"template",
"dist"
],
배포가 끝나면 원했던대로 명령어만으로 프로젝트 셋업이 가능하다. test 디렉토리에 템플릿이 잘 들어가는 것을 확인할 수 있다.
글로벌로 설치도 한번 해봤다. 이게 맘에 든다면 이렇게 쓸 수도 있다. 개인적으로는 손에 안익고, 템플릿이 업데이트 되었을 때 설치된 커맨드도 수동 업데이트 해야하기 때문에 좋은 방식은 아닌 것 같다.
create-se-app [main●] % npm i -g create-se-app
added 1 package, and audited 2 packages in 1s
found 0 vulnerabilities
create-se-app [main●] % create-se-app test
👷 Scaffolding project in /Users/swym/Desktop/temp/create-se-app/test...
✨ Your test is now ready! Enjoy dev with:
cd test
npm install
npm run dev
이렇게 프로젝트 셋업용 패키지가 만들어졌다. 편하게 사용할 수 있는 구조를 커맨드만으로 만들 수 있는건 생각보다 기분 좋은 일이다!
내 template 디렉토리 구조를 간단하게 살펴보면 아래와 같다. 그동안의 프로젝트에서 이것저것 시도해보면서 찾은 나름의 취향이 반영된 best practice다.
vite + React + TypeScript 앱에 필요한 디렉토리 구조를 만들고 절대경로와 타입 선언 폴더 설정을 작성했다.
컨벤션을 지킬 수 있게 도와주는 린트 설정 파일과 커밋되는 코드를 린팅해주는 husky hook을 설정했다.
github 협업에 필요한 Issue, PR 템플릿과 라벨 자동 생성 Action도 넣었다.
template
├── .github
├── .husky
├── .eslintrc
├── .prettierrc
│
├── index.html
├── package.json
├── public
├── src
│ ├── App.tsx
│ ├── main.tsx
│ ├── @types
│ ├── apis
│ ├── assets
│ ├── components
│ ├── constants
│ ├── hooks
│ ├── pages
│ ├── routes
│ ├── styles
│ └── utils
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.path.json
└── vite.config.ts
내 보일러플레이트도 배포하고 싶다면 레포를 fork / clone 해서 template 디렉토리와 package.json만 바꾸어 배포할 수도 있고, 포스트 내용을 참고해서 직접 만들어도 된다.
블로그 쓰려고 들어가봤는데 누가 이렇게 받아간거 .. 잘 쓰셨나요 ..?