사이드 프로젝트나 기업 과제를 수행할 때, 매번 새롭게 프로젝트 환경을 세팅해줘야 했는데, 프로젝트 기획에 맞춰서 달라지는 부분도 있지만, 거의 비슷하게 가져가는 스택은 그야말로 boilerplate였다. 그런 단순 반복 작업을 피하고자 이번에 날잡고 내 입맛대로 세팅한 '나만의 boilerplate'를 만들어보았다. 이 글은 제작 과정과 트러블슈팅에 대한 내용을 담고있다.
workflows
- bolierplate 제작
1. 내 입맛대로 세팅한 boilerplate 제작
2. boilerplate github push - yarn package 배포
1. 배포용 package 제작
2. yarn publish --access public - 설치 테스트
1. 빈 폴더 생성
2. npx create-tangjin-app .
1. boilerplate 만들기
- boilerplate repository + 로컬 폴더 생성
먼저 boilerplate를 위한 github repository를 생성한다. 그리고 로컬 폴더를 만들고 해당 저장소와 연결한다.
# boilerplate용 폴더 만들기
mkdir create-tangjin-app
cd create-tangjin-app
echo "# boilerplate" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/tangjinlog/boilerplate.git
git push -u origin main
- 환경 별로 boilerplate 로직 구현
내가 처음 의도한 기획은 한 가지의 고정된 boilerplate가 아니라 필요한 스택에 따라 세팅된 여러개의 boilerplate를 사용자가 선택해서 설치할 수 있도록 하는 것이였다. 그래서 우선 테스팅을 위한 2개의 boilerplate를 만들었는데, 다음과 같다.
- [Atomic] Next.js + Emotion + Jest
- [Atomic] Next.js + TailwindCSS + Jest
각각 atomic-nextjs-emotion-jest와 atomic-nextjs-tailwindcss-jest란 이름으로 branch를 만들어 작업하고 github에 push했다.
네이밍 컨벤션으로 맨 앞에는 Design 패턴을 명시하고, 그 뒤 부터는 스택 조합을 표기했다.
폴더구조는 Atomic Design 패턴이 주는 이점이 많은 것 같아, 우선 둘 다 기본적으로 선택했고 Next.js도 마찬가지, 테스팅 툴로 Jest를 기본으로 가져갔다. 차이점으로 스타일링 툴을 뒀는데 CSS-in-JS 방식으로 개발할때는 Emotion을 많이 사용했으므로 해당 세팅과, 요즘들어 연습하고있는 TailwindCSS를 위한 세팅으로 나누었다.
이렇게 재료는 완성됐다.
그래서, 어떻게 적용할 건데?
열심히 만든 boilerplate를 적용할 방법을 어떻게 가져다 쓸것인가에 대해 고민해볼 시간이다.
가장먼저 떠오르는 방법은 boilerplate를 git clone 하는 방법이다.
이 방식으로 진행하면 다음과 같은 flow를 따라야 한다.
- git clone 방식
1. git repositry 접속 후 clone 주소 확인
2. 로컬 폴더에서 git clone
3. yarn install로 의존성 설치
4. 기존 boilerplate의 저장소 연결을 끊고, 진행할 프로젝트의 저장소로 새롭게 연결
3번까지 진행하면 기존의 git clone을 하는 방식이다. 익숙해져도 여간 귀찮은 방식이 아닐 수 없는데.. 여기에 추가로 git remote remove origin + git remote add origin [주소] 까지 해야한다! 물론 boilerplate를 만들지않고 처음부터 세팅하는 것보다야 낫겠지만, 그래도 너무 번거로운 방식이라도 생각한다.
그래서 알아보다, boilerplate를 clone하는 yarn package를 만들어서 사용하는 방식을 발견했다.
이 방식을 사용하면, 기존 git clone 과정을 다 없애고 커맨드 한줄로 boilerplate를 설치할 수 있었다! 그래서 당연하게도 이 방식으로 진행했다.
- yarn package 배포 후 설치 방식
1. package를 개발할 폴더 생성
2. boilerplate를 clone하는 로직의 'generate-app.js' 생성
3. yarn package 배포
4. 새로운 프로젝트를 진행할 폴더 생성
5. 내가 만든 yarn package로 boilerplate 설치
boilerplate용 yarn package를 만드는 기회 비용이 있지만 한 번 만들어두기만 하면, CLI 명령어 한줄로 CRA나 CNA처럼 간편하게 설치할 수 있기때문에 장기적으로 봤을 때, 엄청난 이득이라고 생각한다.
2. yarn package 만들기
- package 배포용 로컬 폴더 생성
이제 package 배포를 위해 로컬에서 새로운 폴더를 하나 만들어준다.
mkdir create-tangjin-app
cd create-tangjin-app
- generate-app.js 생성
내가 만들 package의 주 역할은 'boilerplate를 git clone 하는 것' 이다. 따라서 이 행위를 할 실행 파일. 즉, 'generate-app.js'를 만든다. 내가 만든 패키지가 호출됐을 때 generate-app.js를 실행하려면, package.json에 'bin' 속성으로 명시해줘야 한다.
여기서 package를 설치하고 package가 실행되는 순간에 대해 좀 더 자세히 알아보면,
1. npx my-boilerplate my-app입력
2. node_modules/my-boilerplate에 패키지 설치
3. npm이 my-boilerplate 패키지의 실행파일을 binary 파일로 컴파일.
4. 컴파일 된 binary 파일을 node_modules/.bin에 복사
위 과정 중에서 'my-boilerplate의 실행파일'은 우리가 만들려고 하는 'generate-app.js' 이고, 그 generate-app.js를 binary형식으로 컴파일해서 node_modules/.bin에 복사되는 것이다. 그럼 추후에 패키지가 실행될 때, 복사된 node_modules/.bin 안에있는 binary 파일을 호출해서 사용하게 된다. 여기서는 npx 명령어로 패키지를 설치 & 실행까지 한 번에 하고있기 때문에, binary 컴파일 후 컴파일된 binary파일이 실행된다.
그래서 정리하자면, package를 실행하기 위해선 실행파일이 필요한데, 그 실행파일을 package.json에 bin 속성으로 명시해 줘야하고, 명시된 파일은 npm이 package를 설치할 때, 알아서 binary 파일로 컴파일해주고 package가 실행될 때 그 binary파일을 사용한다.
- package.json
// create-tangjin-app/package.json
{
"name": "create-tangjin-app",
"version": "0.1.0",
"description": "tangjin's boilerplate",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
// name과 일치해야 함
"create-tangjin-app": "./bin/generate-app.js"
},
"repository": {
"type": "git",
"url": "https://github.com/tangjinlog/boilerplate.git"
},
"bugs": {
"url": "https://github.com/tangjinlog/boilerplate/issues"
},
// npm search로 검색될 키워드
"keywords": [
"nextjs",
"boilerplate",
"typescript",
"starter"
],
"author": "tangjinlog",
"license": "ISC",
}
이제 경로대로 generate-app.js 파일을 만들어준다.
mkdir bin
cd bin
touch generate-app.js
- generate-app.js
#! /usr/bin/env node
import { execSync } from 'child_process'
import path from 'path'
import fs from 'fs'
if (process.argv.length < 3) {
console.log('you have to provide a name to your app.');
console.log('For example : ');
console.log(' npx create-my-boilerplate my-app');
process.exit(1);
}
const projectName = process.argv[2];
const currentPath = process.cwd();
const projectPath = path.join(currentPath, projectName);
const GIT_REPO = 'https://github.com/tangjinlog/boilerPlate.git';
if (projectName !== '.') {
try {
fs.mkdirSync(projectPath);
} catch (error) {
if (error.code === 'EEXIST') {
console.log(projectName);
console.log(
`The file ${projectName} already exist in the current directory, please give it another name.`,
);
} else {
console.log(error);
}
process.exit(1);
}
}
async function main() {
try {
console.log('Downloading files...');
execSync(`git clone --depth 1 ${GIT_REPO} ${projectPath}`);
if (projectName !== '.') {
process.chdir(projectPath);
}
console.log('Installing dependencies...');
execSync('yarn install');
console.log('Removing useless files');
execSync('npx rimraf ./.git');
console.log('The installation is done, this is ready to use !');
} catch (error) {
console.log(error);
}
}
main();
작성된 내용을 살펴보면 크게, 최상단에 어떤 주석, 2개의 if문, main함수로 이루어져있는 것을 볼 수 있다. 하나씩 뜯어본다.
1. SheBang (#!)
#! /usr/bin/env node
SheBang(셔뱅)은 '#!'로 표기하는 하나의 기호이며, 이 파일이 어떤 해석기로 명령어를 해석할 것인지 시스템에게 알려주는 역할을 한다. 기본 구문은 '#! + 해석기 프로그램이 설치된 절대 경로' 로 구성되며, 위 처럼 '/usr/bin/env 언어'로 작성하면 해석기의 절대경로 위치에 상관없이 해석기 위치를 알아서 찾고, 실행해준다.
// 예시
#!/bin/bash
#!/usr/bin/env python3
2. import module
import { execSync } from 'child_process'
import path from 'path'
import fs from 'fs'
child_process의 execSync는 외부 프로세스를 생성하고 제어하는 기능을 담당하는데, shell을 생성하고 명령어를 동기적으로 실행하는 메서드이다.
path는 파일의 경로와 관련된 정보를 다루는 모듈이고, fs는 fileSystem으로 파일을 읽고 쓰는 기능을 가진 모듈이다.
3. 사용자 입력값 검사 - argv.length
if (process.argv.length < 3) {
console.log('you have to provide a name to your app.');
console.log('For example : ');
console.log(' npx create-my-boilerplate my-app');
process.exit(1);
}
process.argv에는 사용자 입력값이 배열로 담긴다. 만약 사용자가 'npx create-tangjin-app my-app'를 입력했다면, 결과값은
['npx', 'create-tangjin-app', 'my-app']이 된다. 그래서 ['패키지 실행 명령어', '패키지', '폴더 이름'] 어느 것 하나 빠지면 안되기 때문에, process.argv.length가 3 보다 작으면 오류 메세지를 보여주고 process를 종료시킨다.
4. 사용자 입력값 검사 - 폴더 이름 중복
const projectName = process.argv[2];
const currentPath = process.cwd();
const projectPath = path.join(currentPath, projectName);
const GIT_REPO = 'https://github.com/tangjinlog/boilerPlate.git';
if (projectName !== '.') {
try {
fs.mkdirSync(projectPath);
} catch (error) {
if (error.code === 'EEXIST') {
console.log(projectName);
console.log(
`The file ${projectName} already exist in the current directory, please give it another name.`,
);
} else {
console.log(error);
}
process.exit(1);
}
}
사용자에게 입력받은 생성될 폴더 이름을 projcetName 변수에 담고, process.cwd()로 현재 경로를 currentPath에 담는다.
그리고 두개를 합쳐서 최종으로 생성될 폴더의 전체 경로를 projectPath 변수에 할당하고, fs.mkdirSync 메서드로 해당 경로에 폴더를 생성한다. 만약 동일한 이름의 폴더가 이미 존재한다면, 에러 메세지를 보여주고 프로세스를 종료시킨다.
5. boilerplate clone & 의존성 설치 & 저장소 연결 끊기
async function main() {
try {
console.log('Downloading files...');
execSync(`git clone --depth 1 ${GIT_REPO} ${projectPath}`);
if (projectName !== '.') {
process.chdir(projectPath);
}
console.log('Installing dependencies...');
execSync('yarn install');
console.log('Removing useless files');
execSync('npx rimraf ./.git');
console.log('The installation is done, this is ready to use !');
} catch (error) {
console.log(error);
}
}
main();
앞서 설명한 execSync 메서드로 명령어들을 동기적으로 실행하는 메인 함수이다.
execSync(`git clone --depth 1 ${GIT_REPO} ${projectPath}`);
clone option --depth 1 가장 최근 커밋 한 개만 가져옴으로써 최신 코드를 효율적으로 가져올 수 있다.
if (projectName !== '.') {
process.chdir(projectPath);
}
현재 폴더에 설치하는게 아니라면, process.chdir(projectPath)로 작업 디렉토리를 변경해서 후에 이 경로를 기준으로 작업을 진행하도록 한다.
console.log('Installing dependencies...');
execSync('yarn install');
console.log('Removing useless files');
execSync('npx rimraf ./.git');
execSync('yarn install')로 clone 받은 boilerplate의 패키지 의존성을 설치하고,
execSync('npx rimraf ./.git')으로 기존 clone받으면서 연결했던 boilerplate의 저장소 관련 정보를 삭제한다. 그 때, rimraf 모듈을 사용해 파일을 재귀적으로 안전하게 삭제한다. 이로써 사용자가 boilerplate를 설치했을 때, 번거롭게 remote 연결을 해지할 필요가 없어진다.
- generate-app.js (with inquirer)
위처럼만 작성해도 'npx create-tangjin-app my-app' 입력 한줄로 boilerplate 설치가 가능하다. 하지만, 나는 CRA나 CNA처럼 사용자가 방향키로 원하는 옵션을 선택할 수 있도록 기획했으므로 해당 기능을 위해 inquirer 라는 라이브러리를 사용했다.
주간 다운로드 수가 엄청나다. 해당 라이브러리를 사용하면 CLI 환경에서 커스텀 옵션을 설정하고 사용자가 방향키로 옵션 선택이 가능하다. 지원하는 기능은 일반 List부터 checkbox등 다양한데, 지금은 간단하게 List 방식으로 구현해 봤다.
- inquirer 설치
// 프로젝트 루트에서 설치
yarn add inquirer @types/inquirer
- selectOptions
import inquirer from 'inquirer'
async function selectOptions() {
const questions = [
{
type: 'list',
name: 'project options',
message: 'Choose an option: ',
choices: [
'1) [Atomic] Next.js + Emotion + Jest',
'2) [Atomic] Next.js + TailwindCSS + Jest',
]
}
]
const answer = await inquirer.prompt(questions)
return [answer[questions[0].name],(answer[questions[0].name][0] - 1)]
}
selectOptions 함수는 inquirer 라이브러리를 사용해 List 형식의 옵션을 사용자에게 보여주는 내용을 담고있다. 리턴 값으로 사용자가 선택한 옵션과 옵션의 번호 -1의 값을 배열에 담아 리턴한다. 이 값들은 main() 함수 안에서 사용된다. 아래를 같이 보자.
// generate-app.js
// github branch name
const BRANCH_LIST = ['atomic-nextjs-emotion-jest','atomic-nextjs-tailwindcss-jest']
...
async function main() {
...
// type : 선택한 옵션 값,
// number : 선택한 옵션 번호의 index
const [type, number] = await selectOptions();
console.log(`You selected : ${type}`)
console.log('Downloading files...');
execSync(`git clone --depth 1 -b ${BRANCH_LIST[number]} --single-branch ${GIT_REPO} ${projectPath}`);
...
}
selectOptions 함수에서 리턴한 값으로 사용자에게 선택값을 알려주고, 해당 옵션의 번호 -1 값으로 index 값을 만들어 위에서 정의한 BRANCH_LIST에 접근해, 해당 브랜치 하나를 git clone 한다. boilerplate를 담고있는 github에 옵션별로 branch를 만들어 해당 브랜치를 clone하는 방식이다.
- 최종 generate-app.js
// inquirer 라이브러리 적용 버전 generate-app.js
#! /usr/bin/env node
import inquirer from "inquirer";
import { execSync} from 'child_process'
import path from 'path'
import fs from 'fs'
if (process.argv.length < 3) {
console.log('you have to provide a name to your app.');
console.log('For example : ');
console.log(' npx create-my-boilerplate my-app');
process.exit(1);
}
const projectName = process.argv[2];
const currentPath = process.cwd();
const projectPath = path.join(currentPath, projectName);
const GIT_REPO = 'https://github.com/tangjinlog/boilerPlate.git';
const BRANCH_LIST = ['atomic-nextjs-emotion-jest','atomic-nextjs-tailwindcss-jest']
if (projectName !== '.') {
try {
fs.mkdirSync(projectPath);
} catch (error) {
if (error.code === 'EEXIST') {
console.log(projectName);
console.log(
`The file ${projectName} already exist in the current directory, please give it another name.`,
);
} else {
console.log(error);
}
process.exit(1);
}
}
async function selectOptions() {
const questions = [
{
type: 'list',
name: 'project options',
message: 'Choose an option: ',
choices: [
'1) [Atomic] Next.js + Emotion + Jest',
'2) [Atomic] Next.js + TailwindCSS + Jest',
]
}
]
const answer = await inquirer.prompt(questions)
return [answer[questions[0].name],(answer[questions[0].name][0] - 1)]
}
async function main() {
try {
const [type, number] = await selectOptions();
console.log(`You selected : ${type}`)
console.log('Downloading files...');
execSync(`git clone --depth 1 -b ${BRANCH_LIST[number]} --single-branch ${GIT_REPO} ${projectPath}`);
if (projectName !== '.') {
process.chdir(projectPath);
}
console.log('Installing dependencies...');
execSync('yarn install');
console.log('Removing useless files');
execSync('npx rimraf ./.git');
console.log('The installation is done, this is ready to use !');
} catch (error) {
console.log(error);
}
}
main();
- yarn package 배포
배포에 필요한 파일들의 작성이 끝났고, 드디어 배포만 남았다.
배포는 아래 단계로 진행한다.
- yarn login
npm username, npm email 입력 - yarn publish --access public
package 배포를 위해선 우선 login을 해야한다. 계정이 없다면 npm 계정을 만들어준다. https://www.npmjs.com/signup
성공적으로 로그인했다면 yarn publish [option] 으로 배포하면 되는데, --access public 옵션으로 공개 패키지로 배포한다.
두 번째 배포 부터는 yarn publish만 입력해도 된다.
커맨드로 나만의 boilerplate 설치!
- boilerplate를 설치할 새 폴더를 만든다
- 터미널을 열고, npx [내 패키지] [설치 폴더 이름] 입력
- 원하는 옵션 선택
- 설치 완료!
정말 간편하게 방향키로 옵션을 선택해서, 원하는 스택의 boilerplate를 설치했다. boilerplate 제작부터, yarn package 배포까지!
쉽진 않았지만 나만의 boilerplate를 만들었다는 점이 정말 뿌듯하다! 새로운 프로젝트 생성이 더이상 부담스럽지 않다! 오히려 빨리 새로운 사이드 프로젝트를 진행하고 싶을 정도다..!
아래는 yarn package를 배포하면서 겪은 트러블슈팅이다.
트러블슈팅 1 - SyntaxError: Cannot use import statement outside a module
inquirer 라이브러리를 설치하고, 테스트 겸 node generate-app.js를 하는 순간 발생했다.
package.json에 type 속성을 명시하지 않으면 기본적으로 모듈을 commonjs 방식으로 불러온다고 한다. 그래서 esm 방식인 import 문에서 에러가 난 것. 그래서 2가지를 수정하면서 해결했다.
- generate-app.js -> generate-app.mjs로 esm 형식의 파일임을 명시
- package.json에 type속성을 module로 명시
트러블슈팅 2 - Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'inquirer'
yarn package를 배포하고 새로운 프로젝트 폴더를 만들어서 'npx create-tangjin-app .' 했을 때 발생했다.
왜 inquirer 라이브러리를 찾지 못할까 곰곰히 생각하다, devDependency로 패키지를 설치한게 생각났다.
devDependency 로 설치하면, 배포 시에 담지 못하므로, yarn add inquirer로 재설치 하면서 문제를 해결했다.
트러블슈팅 3 - Error [ERR_REQUIRE_ESM]: require() of ES Module ~ node_modules\cliui\build\index.cjs to a dynamic import() which is available in all CommonJS modules.
해당 에러는 boilerplate를 설치한 후 'yarn test'로 Jest를 실행했을 때 발생했다.
node_modules에 있는 cliui라는 패키지에서 에러가 발생했는데, CLI 환경에서 명령줄을 이쁘게 보여주는 패키지라고 하고, 여기서도 ESM 에러가 발생한 것이다. 원인을 몰라서 계속해서 구글링하다가 실마리가 되는 issue를 발견했다.https://github.com/storybookjs/storybook/issues/22431
yarn 버전 문제일 수 있다는 댓글이였고, 실제로 yarn -v로 확인해본 내 버전은 1.xx 버전인 1.22.21 버전이였다!
그래서 yarn set version stable 으로 버전업하니, Jest가 정상적으로 실행됐다. 한 가지 참고할 점은, yarn을 global로 높은 버전으로 설치할 수 없고, 개별 프로젝트 별로 set version 해야 한다는 점이다.
- 참조
shebang https://bcp0109.tistory.com/343
'FRONTEND > 기타' 카테고리의 다른 글
[CI/CD] Docker-compose + github-actions + EC2 적용기 (0) | 2024.01.31 |
---|---|
Linux 폴더 권한 확인 + 권한 설정 (0) | 2024.01.30 |
테스팅 그게 뭐죠? (feat. Jest, react-testing-library) (0) | 2022.12.02 |