npm run dev는 어떻게 동작하는가?

FGPRJS·2021년 11월 15일
1
post-custom-banner

node의 package.json부문

npm에 대하여

npm docs
npm run에 대해서는 상기 링크에 다음과 같이 기재되어 있다

npm run은 package.json의 scripts객체에서 임의의 명령을 실행한다.

npm run-script < command > [--if-present][--silent] [-- ]
npm run-script < command > [--workspace=]
npm run-script < command > [--workspaces]

run-script에서 -script를 생략하고 run만 기입해도 된다.

how does npm run work?

위 링크에서 언급한 대표적인 동작의 코드는 다음과 같다.
npm/lifecycle.js

  /*
   * part of
   * npm/lifecycle.js
  */
  function runCmd_ (cmd, pkg, env, wd, stage, unsafe, uid, gid, cb_) {
  function cb (er) {
    cb_.apply(null, arguments)
    log.resume()
    process.nextTick(dequeue)
  }

  //conf : 환경변수등을 갖고 있음
  var conf = {
    cwd: wd,
    env: env,
    stdio: [ 0, 1, 2 ]
  }

  if (!unsafe) {
    conf.uid = uid ^ 0
    conf.gid = gid ^ 0
  }
  /* Windows외의 운영체제 :
   * 쉘을 사용하며, 추가적인 커맨드는 -c이다.
   * -c는 현재 프로그램에서 이후 커맨드를 실행하라는 뜻이다.
  */
  var sh = 'sh'
  var shFlag = '-c'

  /* Windows에서의 운영체제 :
   * cmd (명령 프롬프트)를 사용하며, 추가적인 커맨드는 /d /s /c이다.
   * /d : 자동 실행 명령의 실행을 사용하지 않음
   * /s : /c, /k뒤의 문자열 처리를 수정함
   * /c : 문자열에 지정된 명령을 수행하고 중지함
   * 
  */
  if (process.platform === 'win32') {
    sh = process.env.comspec || 'cmd'
    shFlag = '/d /s /c'
    conf.windowsVerbatimArguments = true
  }

  log.verbose('lifecycle', logid(pkg, stage), 'PATH:', env[PATH])
  log.verbose('lifecycle', logid(pkg, stage), 'CWD:', wd)
  log.silly('lifecycle', logid(pkg, stage), 'Args:', [shFlag, cmd])

  var progressEnabled = log.progressEnabled
  if (progressEnabled) log.disableProgress()
  /*
   * spawn 함수
   * node.js에서 사용하는 함수이다.
   *
   * child_process.spawn(command[, args][, options])
   *
   * 명령줄의 argument와 함께 새 프로세스를 생성한다.
  */
  
  var proc = spawn(sh, [shFlag, cmd], conf)

  
  

spawn 과 관련된 node.js공식 문서

위 lifecycle.js 코드를 참조하면 다음과 같다.

  1. runPackageLifeCycle, 혹은 dequeuerunCmd를 호출한다.
    dequeue로 호출할 경우 queue.shift()를 인수(argument)로 받는다. 최종적으로 apply(null, queue.shift())한다. (this는 신경쓰지 않고(null), queue에서 받은것만 인자로서 호출)
  2. runCmd는 현재 무언가 하고 있는지 확인한다. 무언가 하고 있다면(running), 지금 인자의 배열로 받은 Cmd를 다시 갖고 있는 queue(그냥 list이다)의 맨 끝에 다시 넣어버린다. (push) 결국 현재 무언가를 하고 있으면 다른 것은 하지 못한다. (스레드적인 동작을 하지는 않는다.)
  3. 무언가 하고 있지 않다면, runningtrue로 만든 뒤 safe여부를 확인한다. 플랫폼이 win32라면 unsafe하다.
  4. unsafe하다면 uid, gid를 0으로 한다.
    safe하다면 uid, giduidNumber함수를 사용하여 얻어낸다.
    그 후, runCmd_를 실행한다.
  5. 최종적으로, runCmd_는 프로세스를 spawn한다.

단, npm-lifecycle은 npm v7이전에만 유효하며 npm v7이후로는 npmcli/run-script를 사용한다. 현재(2021.11 기준)는 npm v8 이상수준이다.

npm에서의 run-script

run-script는 npm-lifecycle에서 다음을 만족하는 구현이다.

  • RFC 90
    라이프사이클 스크립트 environment 사이즈 줄이기
  • RFC 77
    npm install시 굳이 알 필요 없는 로그는 생략
  • RFC 73
    npm run시 목표를 찾지 못했을 때, 디렉토리의 루트까지 올라가봄
  • 모던한 코딩 테크닉(실제로 화살표 함수나 const, Promise등의 더 모던한 코딩 테크닉을 사용함)과 더 나은 테스트 커버리지로 새로고침된 코드베이스

run-script에서는 다음과 같이 동작한다.

  1. event(install/run), argument(인수)path(실행할 패키지 path), npm_package_from, ..._resolved, ..._integrity를 포함한 environment, 실행할 scriptShell(shell이나 cmd의 경로/디폴트 있음)등을 갖고 있는 객체인 option을 주면서 실행한다.

  2. 0번에서 호출할때 준 optionvalidate체크(타입이 안맞으면 에러를 throw함)하고, 문제 없이 넘어갔다면 options들 중에서 pkgpathconst로 따로 갖고 온다.
    const로 가져오기 때문에 pkgpath 값은 바뀌지 않음을 보장한다.

  3. pkgnull이 아니라면 runScriptPkgoptions를 인자로 가져오며 실행시키고, null이라면 read-package-json-fast라는 패키지에게 path와 'package.json'을 결합한 string을 건네며 비동기식 실행한다(프로젝트 파일을 읽기 위하여 결국 파일 I/O하여야 하기 때문). promise타입이며, 성공했다면 optionpkg를 엮은 객체를 인수로 건네며 runScriptPkg를 수행한다.

    read-package-json-fast
    node_modules트리에서 package.json파일을 읽는데에만 최적화된 작은 패키지이다.

  4. runScriptPkg에서는 받아온 pkg로부터 scriptsgypfile을 받아오며, cmd를 여러 방식으로 결정한다, 그 이후 promiseSpawnoptions로부터 받아오거나, 함수 자체적으로 설정하였던 cmd등을 이용하여 makeSpawnArgs를 호출하고, 그 결과를 인자로서 실행한다.

  5. 최종적으로 수행하는 promiseSpawn은 명령을 수행하고, 프로세스 결과에 따라 resolve/reject하는 프로미스를 반환하는 함수이다.

최종적으로 package.json에 존재하는 scripts에 존재하는 key를 spawn하는 함수로 볼 수 있다.

Parcel-bundler 부문

(여기서는 parcel만 다룬다. 또다른 번들러 webpack의 경우)

package.json에 존재하는 scripts의 key의 예시는 다음과 같다.

  "scripts": {
  "dev": "parcel ui_sample.html",
  "build": "parcel build index.html",
  "test": "jest"
  },

package.jsonscriptsparcel 명령어(parcel-bundler)를 사용한다.
parcel-bundlernpm에서 설치할 수 있는 패키지의 하나이다.
(parcel-bundler 자체는 v1이며, deprecated되었다.)
현재는 v2인 parcel을 사용한다. npm parcel link

위에서 사용하는 parcel cli명령어의 링크는 다음과 같다. parcel document

parcel의 명령은 serve(default) / watch / build로 되어있다.

  • serve 명령은 파일을 변경할 때(edit후 save할 때) 앱을 자동으로 다시 빌드하고 핫 리로딩을 지원하는 개발 서버를 시작한다.
    default이므로 명령 없이 사용할 수 있다.
  parcel src/index.html

명령(serve)을 사용하면 parcel내장 개발 서버가 자동으로 실행된다. 기본적으로 http://localhost:1234에서 서버를 시작한다.

  • cli 옵션 중 -p, --port를 통하여 이 포트를 따로 지정할 수 있다.

    위와 같이 포트를 따로 지정할 수 있다. 실제 serve명령시 localhost:8080에서 진행된다.
  • cli 옵션 중 --open을 통해 수행 즉시 기본 브라우저에서 항목을 자동으로 연다. 브라우저 이름을 전달하여 다른 브라우저를 열 수도 있다.

따라서

  1. npm run dev를 수행하면, npmrun명령에 맞게 packages.json에 존재하는 scriptsdev키를 찾아서 그것을 promiseSpawn(새로운 프로세스를 수행, 프로미스 리턴)한다.

  2. devparcel명령어인 경우, parcelcli 명령어등을 참고하여 port등을 지정하고, (없으면 localhost:1234) 해당 설정에 맞는 개발 서버를 오픈한다.

  3. 따라서 개발 서버는 parcel에 의한 것이다.
    npm에 의한 것도, node.js에 의한 것도 아니다.

    webpack의 경우 webpack-dev-server를 설치하여 사용하는 것으로 사용할 수 있다.

기타등등

  • parcel문서에서 parcel은 잠금파일 (~~.lock) 파일을 기반으로 패키지 관리자를 자동으로 감지하는데, 잠금파일이 없으면 시스템에 설치된 항목 중 우선순위에 따라 패키지 관리자가 선택된다. 그런데 우선순위가 Yarn, Pnpm, npm순이다.
    특이한 점으로, Yarn이 제일 높은것으로 보아 parcel은 Yarn을 우선적으로 작업하는것으로 보인다.
profile
FGPRJS
post-custom-banner

0개의 댓글