React + TS boilerplate 제작기 2 - 설치 패키지 & npx

DD·2021년 6월 13일
12
post-thumbnail

1편 : React + TS boilerplate 제작기 - 환경 구성

이전 글에서 작성한 보일러 플레이트를 사용하기 위한 가장 쉬운 방법(?)은 github에 올리고 사용할 때 마다 clone을 받는 것입니다. 하지만 CRA처럼 npx 명령을 통해 내 보일러플레이트를 설치하는 방법을 알아보겠습니다!

왜 굳이 npx로 설치하려 하냐구요? 멋있잖아요 ..

는 농담이고, npx를 사용하는 이유를 간단하게 알아보겠습니다.


😵 보일러플레이트를 가져다 쓰려면...

먼저 내 보일러플레이트를 설치하는 입장에서 어떤 동작들을 해야하는지 정리해보겠습니다.

  • git clone으로 필요한 파일을 다운받는다.
    • 이 때 항상 보일러 플레이트의 git repo 주소를 입력해야한다.
  • npm install 명령어로 지정된 의존성을 설치한다.
  • clone을 받았기 때문에 자동으로 git remote origin이 보일러플레이트 git repo 주소를 가르키게되며, 이 연결을 끊어야한다.

크게 이 3가지인데, git repo 주소도 외우거나 어디 기록해놔야하고, 필요에 따라 추가, 삭제하는 동작 등 수동으로 처리하는 일이 귀찮습니다

그렇기에 우리는 generate-app.js라는 스크립트 파일을 만들어서 이 파일을 실행하기만 하면 각종 명령을 자동으로 실행, 처리하는 작업을 진행할 예정입니다! 즉 보일러 플레이트를 설치하는 프로젝트를 새로 하나 만들거에요!


🔥 npm vs npx

❓ 근데 generate-app.js 파일도 clone으로 받아와야하는거 아니야?

혹시 이런 생각을 하셨나요? (아님 말구..) 물론 그럴 수 있겠지만 그래선 의미가 없죠. 그렇기에 우리는 보일러 플레이트를 설치하는 프로젝트를 npm에 배포해서 사용자가 npm을 통해 접근하도록 만들겁니다! 🎉🎉🎉

❓ 그럼 보일러플레이트도 git 대신 npm으로 받으면 되잖아..(무한 반복..?)

  • 이 지점에서 이제 npm vs npx를 고민해볼 필요가 있습니다.

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입니다. npx는 npm@5.2.0이상 버전부터 사용할 수 있는 커맨드입니다.

npx로는 대략 이런 것들을 할 수 있습니다

  • 로컬에 설치된 도구들을 npm run scripts 없이 사용
  • 한 번만 사용할 커맨드를 실행
  • 다른 Node.js 버전으로 커맨드를 실행
  • 인터렉티브한 npm run scripts를 개발할 때
  • gist에 기반한 스크립트를 공유할 때 등....

출처 : npx create-react-app ... 그래서 npx가 뭐길래?


우리는 npx의 위에서 언급한 여러 장점 중
1. npm run scripts 없이 사용
2. 한 번만 사용할 커맨드 실행

이 두 가지를 눈여겨보겠습니다.

npx 명령문 한 줄로 필요한 동작을 모두 수행할 것이고, 자연스레 불필요한 파일은 로컬에 남지 않거나, 직접 지우는 처리를 할겁니다.

그럼 서론이 길었으니 이제 직접 해보도록하죠!


📚 github에 push

먼저 이전 글에서 제작했던 보일러 플레이트를 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

이건 뭐 다들 아실거라 믿고 간단하게 넘어가겠습니다


🚀 boilerplate 설치 패키지

자, 이제 본격적으로 보일러 플레이트를 설치시키는 패키지를 작성하겠습니다.

먼저 새로운 프로젝트 폴더를 생성해주세요

$ mkdir create-dd-app // 프로젝트 이름은 마음대로 만들어주세요
$ cd create-dd-app
$ npm init -y


📃 package.json

{
  "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_modules/webpack에 설치한다.
  • 해당 모듈을 binary로 컴파일한다
  • 컴파일한 binary 파일을 node_modules/.bin에 복사한다

그 이후, 우리는 두 가지 방법으로 해당 모듈을 실행할 수 있습니다.


$ node node_modules/webpack/bin/webpack.js

  • 첫 번째로 실행 파일이 존재하는 경로에 접근해서 직접 실행하는 방법입니다.
  • js 파일을 실행하기 위해서는 앞에 node처럼 어떤 실행기로 이 파일을 읽을건지 지정해주어야 합니다.

  • 이녀석을 실행시키는겁니다.

$ npm start

  • 두 번째로 npm의 scripts를 사용하는 방법입니다.
//package.json
{
  ...
  "scripts" : {
      "start" : "webpack -w"
    }
{
  • 이 경우에는 node_modules/.bin에 있는 binary 실행 파일을 실행하는 것입니다.
  • binary 파일은 별다른 실행기를 필요로 하지 않습니다. 즉 node가 필요하지 않습니다.

❓ 그게 무슨 차이죠?

  • 직접 실행 파일 경로를 지정해서 node로 실행해도 괜찮다면 binary 파일을 굳이 생성할 필요는 없을지 모릅니다.
  • 하지만 우리는 scripts를 사용해서 모듈을 좀 더 편리하게 실행하고자 하기 때문에 .bin 폴더에 binary 파일을 생성해 두는 것입니다. 그리고 이후 설명할 npx로 패키지를 실행하기 위해서 binary 파일이 필요하기도 합니다.

❓ 다시 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에 복사해둬라는 의미입니다.



📃 generate.app.js

이제 진짜루 설치 스크립트를 작성해보겠습니다.

$ mkdir bin
$ cd bin
$ touch generate-app.js


shebang

#! /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에 내장되어 있기 때문에 따로 설치할 필요 없습니다.

  • child_process.execSync : 입력된 명령어를 동기적으로 실행하는 함수입니다. 아래에서 더 자세히 다루겠습니다
  • path : 경로와 관련된 처리를 하는 모듈입니다. join, resolve를 사용하여 경로를 합치는데 주로 사용합니다.
  • fs : fileSystem의 약자로 파일 처리와 관련된 모듈입니다. (파일 읽기, 쓰기 등)

사용자 입력 검사

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;
  • process.cwd는 현재 working directory 경로를 반환합니다.
    • 여기서 working directiory는 사용자가 npx 명령을 실행한 경로가 될 것입니다.
  • path.join을 사용해서 현재 경로와 사용자가 지정한 app 이름을 합친 프로젝트 root폴더 경로를 생성합니다.
  • 이후 위에서 push해둔 우리의 보일러 플레이트 git repo 주소를 변수에 저장해둡니다.

프로젝트 root 폴더 생성


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);
  }
}
  • 위에서 만든 프로젝트 root 폴더 경로에 해당하는 폴더를 실제로 생성합니다. 이때, 이미 기존에 존재하는 폴더명이라면 에러가 발생하기에 catch해주고 관련 안내를 고지한 후 process를 종료합니다.
  • 현재 디렉토리(.)에 설치하는 경우에는 무시됩니다.

main 함수 작성 및 실행


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" 명령어 한줄만 입력하면

  • 보일러 플레이트를 설치하기 위한 패키지를 가져와서
  • generate-app.js 스크립트를 실행하고
  • 입력에 오류가 없는지 검사한 후
  • 입력값을 기반으로 git clone / npm install / .git 제거

를 한 번에 할 수 있게 되었습니다!
물론 아직 끝난건 아니에요. 이제 배포해서 실제 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까지 날려가며 해결했던 고군분투기를 작성해보겠습니다.

  • 이것 저것 얘기하느라 글이 다소 난잡함에도 끝까지 읽어주셔서 감사합니다!

위 내용에서 오류를 발견하시면 댓글 부탁드립니다!

참고한 아티클

profile
기억보단 기록을 / TIL 전용 => https://velog.io/@jjuny546

6개의 댓글

comment-user-thumbnail
2021년 6월 13일

와 디디 너무 자세하고 알기 쉽게 써주셨네요 선댓글 남기고 시간 들여서 이해하며 읽어볼게요!! 좋은 글 감사합니다~ 👍

1개의 답글
comment-user-thumbnail
2021년 6월 20일

좋은 글 유익한 글 감사합니다!

1개의 답글
comment-user-thumbnail
2022년 11월 11일

제가 원하던 글이에요! 고맙습니다~ 덕분에 배포할 수 있었어요 :)

답글 달기
comment-user-thumbnail
2024년 7월 24일

감사합니다.... 관련 내용을 찾아보았는데 가뭄에 단비네요... 다시 한번 고맙습니다.

답글 달기