1편 : React + TS boilerplate 제작기 - 환경 구성
이전 글에서 작성한 보일러 플레이트를 사용하기 위한 가장 쉬운 방법(?)은 github에 올리고 사용할 때 마다 clone을 받는 것입니다. 하지만 CRA처럼 npx 명령을 통해 내 보일러플레이트를 설치하는 방법을 알아보겠습니다!
왜 굳이 npx로 설치하려 하냐구요? 멋있잖아요 ..
는 농담이고, npx를 사용하는 이유를 간단하게 알아보겠습니다.
먼저 내 보일러플레이트를 설치하는 입장에서 어떤 동작들을 해야하는지 정리해보겠습니다.
크게 이 3가지인데, git repo 주소도 외우거나 어디 기록해놔야하고, 필요에 따라 추가, 삭제하는 동작 등 수동으로 처리하는 일이 귀찮습니다
그렇기에 우리는 generate-app.js라는 스크립트 파일을 만들어서 이 파일을 실행하기만 하면 각종 명령을 자동으로 실행, 처리하는 작업을 진행할 예정입니다! 즉 보일러 플레이트를 설치하는 프로젝트를 새로 하나 만들거에요!
❓ 근데 generate-app.js 파일도 clone으로 받아와야하는거 아니야?
혹시 이런 생각을 하셨나요? (아님 말구..) 물론 그럴 수 있겠지만 그래선 의미가 없죠. 그렇기에 우리는 보일러 플레이트를 설치하는 프로젝트를 npm에 배포해서 사용자가 npm을 통해 접근하도록 만들겁니다! 🎉🎉🎉
❓ 그럼 보일러플레이트도 git 대신 npm으로 받으면 되잖아..(무한 반복..?)
우리가 npm으로 설치한 의존성은 해당 프로젝트의 node_modules 폴더에 들어오게 됩니다. -g 옵션으로 global하게 설치하면 global node_modules 폴더에 담기겠죠. 먼저 npm으로 해당 프로젝트를 설치하고, package.json을 아래와 같이 설정해둔다면
// package.json
{
...
"install" : "node generate-app.js"
}
npm으로 다운받고, npm run install
명령을 실행해서 보일러플레이트를 설치할 수 있을겁니다. 하지만 이것도 scripts를 실행해야하는 번거로움이 있고, 무엇보다 보일러 플레이트를 설치하는 프로젝트가 로컬에 남게 되네요.
그렇기에 필요한 것이 바로 npx입니다. npx는 npm@5.2.0
이상 버전부터 사용할 수 있는 커맨드입니다.
npx로는 대략 이런 것들을 할 수 있습니다
출처 : npx create-react-app ... 그래서 npx가 뭐길래?
우리는 npx의 위에서 언급한 여러 장점 중
1. npm run scripts 없이 사용
2. 한 번만 사용할 커맨드 실행
이 두 가지를 눈여겨보겠습니다.
npx 명령문 한 줄로 필요한 동작을 모두 수행할 것이고, 자연스레 불필요한 파일은 로컬에 남지 않거나, 직접 지우는 처리를 할겁니다.
그럼 서론이 길었으니 이제 직접 해보도록하죠!
먼저 이전 글에서 제작했던 보일러 플레이트를 github에 푸시해주세요.
repository 생성
$ git init
$ git branch -M main //기본 브랜치를 master에서 main으로 변경
$ git remote add origin {repository_url} // 생성한 repository 주소로 origin 등록
$ git add .
$ git commit -m "commit message"
$ git push origin main
이건 뭐 다들 아실거라 믿고 간단하게 넘어가겠습니다
자, 이제 본격적으로 보일러 플레이트를 설치시키는 패키지를 작성하겠습니다.
먼저 새로운 프로젝트 폴더를 생성해주세요
$ mkdir create-dd-app // 프로젝트 이름은 마음대로 만들어주세요
$ cd create-dd-app
$ npm init -y
{
"name": "create-dd-app", // 복붙 하더라도 이 부분은 신경써서 중복을 피해주세요
"version": "0.1.0",
"description": "dd`s TS+React boilerplate",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"create-dd-app": "./bin/generate-app.js"
}, // ❓ "name"과 해당 속성값이 같아야 합니다.
"keywords": [
"react",
"boilerplate",
"typescript",
"starter"
],
"author": "",
"repository": {
"type": "git",
"url": "YOUR_REPO_URL"
},
"license": "ISC"
... //불필요한 속성은 지웠습니다.
}
❓ bin 속성은 뭐지?
bin은 binary 약자로 사실 package.json의 bin보다 먼저 설명해야할 부분이 있습니다.
바로 node_moduels를 열어보시면 가장 최상단에 존재하는 .bin이라는 폴더입니다.
이 폴더는 binary 파일(0과 1로만 이루어진 파일)들이 저장되는 곳으로, 이 파일들은 실행 파일입니다.
❓ binary 파일이 머죠?
npm install webpack
우리가 이렇게 webpack이라는 모듈을 설치할 때, 내부적으로 아래와 같은 일이 벌어집니다.
그 이후, 우리는 두 가지 방법으로 해당 모듈을 실행할 수 있습니다.
$ node node_modules/webpack/bin/webpack.js
$ npm start
//package.json
{
...
"scripts" : {
"start" : "webpack -w"
}
{
❓ 그게 무슨 차이죠?
❓ 다시 bin 속성의 존재 이유
다시 package.json의 bin 속성 이야기로 돌아오겠습니다. 먼저 모든 패키지가 실행할 필요가 있는건 아닙니다. 단적인 예로 styled-components를 설치했다고 해서 그걸 '실행'하지는 않지요.
( webpack은 bin 폴더와 실행 파일 webpack.js가 있다 )
( styled-components는 bin 폴더, 실행 파일이 없다 )
"실행할 필요가 있는 모듈"은 결국 실행 파일을 필요로 합니다.
우리가 만들고 있는 건 "보일러 플레이트를 설치 시키는 패키지(모듈)"이기 때문에 실행 파일이 필요하고, 때문에 bin 속성에 그 정보를 담아줘야합니다. 실행 파일로 컴파일 해주는건 npm이 알아서 해줍니다!
만약 하나의 실행 파일만 존재한다면 "실행 파일 이름 === 패키지 이름"이며 아래와 같이 선언해두면 됩니다.
"bin": { "create-dd-app": "./bin/generate-app.js" }
즉, 이 부분은 "create-dd-app"의 실행 요청이 들어오면 "./bin/generate-app.js"를 실행시키는 바이너리 실행 파일을 만들어서 node_modules/.bin에 복사해둬라는 의미입니다.
이제 진짜루 설치 스크립트를 작성해보겠습니다.
$ mkdir bin
$ cd bin
$ touch generate-app.js
#! /usr/bin/env node
먼저 스크립트 파일 최상단에 이렇게 작성해줍니다. 제가 이 프로젝트를 진행하면서 Mac과 Window의 OS 차이를 체감하고 있습니다.. 킹갓맥..
shebang이란 유닉스 계열 OS에서 스크립트 코드 최상단에 이 파일을 어떤 인터프리터로 해석할 것인가 절대 경로를 지정합니다.
html의 <!DOCTYPE>과 비슷한 개념입니다. 이로 인해 킹갓맥에서는 이 한줄로 스크립트 파일을 읽는 방법을 처리할 수 있습니다.
하지만 윈도우는 이 구문을 무시하기 때문에 그냥 실행하면 에러가 발생합니다. 물론 우리는 binary 파일로 컴파일했으니 문제 없습니다!
여담이지만 이 구조는 그렇게 권장되는 패턴은 아니라고 합니다. node로 읽어야한다면 "node로 읽어라" 지정해주는게 어떤 환경의 사용자가 사용하든 동일한 결과를 보장할 수 있습니다.
실제 facebook의 craet-react-app 프로젝트를 뜯어보면 node 명령으로 스크립트를 실행하도록 작성되어 있습니다. 가능하다면 이후 이런 패턴으로 수정하면 좋을 것 같습니다
const { execSync } = require("child_process");
const path = require("path");
const fs = require("fs");
위 3개의 모듈은 모두 Node에 내장되어 있기 때문에 따로 설치할 필요 없습니다.
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는 cli에 입력된 내용을 배열로 담고 있습니다. 예를 들어 "npx create-dd-app my-app"이라는 명령어를 실행했다면
[node.exe 설치 경로, 실행 파일 경로, 'my-app']
이렇게 3개 요소가 담긴 배열이 precess.argv에 담겨있을겁니다.
0, 1 번째 요소는 기본값이라서 2번째 요소를 입력하지 않았다면(즉, 배열 길이가 3보다 작다면) 보일러플레이트를 설치할 경로를 입력하지 않은 것으로 간주해서 관련 안내와 함께 process.exit를 실행하는 것입니다.
const projectName = process.argv[2];
const currentPath = process.cwd();
const projectPath = path.join(currentPath, projectName);
const GIT_REPO = YOUR_GIT_REPO_URL;
if (projectName !== ".") {
try {
fs.mkdirSync(projectPath);
} catch (err) {
if (err.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}`); // 우리의 보일러 플레이트를 clone!
if (projectName !== ".") {
process.chdir(projectPath); // cd입니다 clone을 마친 후 projectPath로 진입
}
console.log("Installing dependencies...");
execSync("npm install"); // package.json에 있는 의존성 설치
console.log("Removing useless files");
execSync("npx rimraf ./.git"); // 이제 보일러플레이트 git과 관련된 내용 제거
console.log("The installation is done, this is ready to use !");
} catch (error) {
console.log(error);
}
}
main();
execSync는 이벤트 루프를 차단해서 명령문이 동기적으로 동작하도록 합니다. 비동기로 실행되는 exec의 동기버전이라고 할 수 있습니다.
execSync는 에러가 발생하면 throw하고 process를 exit합니다.
우리가 위에서 언급한 git clone 후 npm install로 의존성 설치, 불필요한 파일 삭제(.git)의 동작을 진행합니다.
즉, 우리는 "npx create-dd-app my-app" 명령어 한줄만 입력하면
를 한 번에 할 수 있게 되었습니다!
물론 아직 끝난건 아니에요. 이제 배포해서 실제 npx가 동작하게 해야죠.
$ npm login // npm 로그인. (아이디, 비밀번호, 이메일 입력)
$ npm version major/minor/patch 를 통해 버전업
$ npm publish --access public 으로 배포, 최초에만 --access public을 하고 이후에는 그냥 npm public만해도 된다
├── bin
| └── generate-app.js
├── package.json
└── README.md
사실 보일러 플레이트와 설치 패키지를 따로 두는건 애매한 방식임을 느끼고 있습니다. 제가 참고한 블로그도 하나로 묶어놨고, Facebook cra 팀도 하나로 관리하고 있는것 같으니... git clone으로 가져오는건 좀 더 쉽게 구현하기 위함이고 사실 clone없이, 하나의 패키지에서 모든 처리가 가능하긴 합니다.
그럼에도 불구하고 두개로 나눈건 하나로 합쳤을 때 package.json이 보일러플레이트/설치 패키지 두 개가 섞인 형태가 되었기 때문입니다. 이를 해결하려면 package.json을 동적으로 생성해야하는데 시간이 걸릴 것 같아 나중에 도전해보기로 했습니다.
npx로 설치하기는 사실 생각보다 간단한 문제였는데, 저는 하루 이상을 꼬박 날렸습니다. 그 이유는 여러 바보짓으로 온갖 오류를 만났기 때문이죠. 다음 3편은 마지막으로 제가 이 프로젝트를 진행하면서 겪은 각종 오류와 이를 해결하기 위해 참고한 git repo에 issue까지 날려가며 해결했던 고군분투기를 작성해보겠습니다.
이것 저것 얘기하느라 글이 다소 난잡함에도 끝까지 읽어주셔서 감사합니다!
위 내용에서 오류를 발견하시면 댓글 부탁드립니다!
참고한 아티클
와 디디 너무 자세하고 알기 쉽게 써주셨네요 선댓글 남기고 시간 들여서 이해하며 읽어볼게요!! 좋은 글 감사합니다~ 👍