[node.js] npm이 사용하는 폴더 구조

원문: https://docs.npmjs.com/cli/v8/configuring-npm/folders

번역: 양정석 (dasomoli@gmailREMOVETHIS.com)

설명

npm은 여러분의 컴퓨터 상에 다양한 것들을 둡니다. 이게 그게 하는 일이예요.

이 문서는 이게 어디에 무엇을 두는지 설명합니다.

요약

  • 로컬 설치 (기본): 현재 패키지의 루트 디렉토리의 ./node_modules 에 설치
  • 글로벌 설치 (-g 옵션): /usr/local 또는 노드가 설치된 곳에 설치
  • require()를 한다면 로컬에 설치
  • 커맨드 라인에서 실행한다면 글로벌 설치
  • 로컬과 글로벌 둘 다 필요하다면 양쪽 모두에 설치하거나 npm link를 사용

prefix 설정

기본 prefix 설정은 노드가 설치된 곳입니다. 대부분 /usr/local 입니다. 윈도우즈에서는 %AppData%\npm 입니다. {prefix}/node.exe 가 아니라 {prefix}/bin/node에 보통 설치되므로 유닉스 시스템에서는 한 레벨 더 들어갑니다.

global 플래그가 설정되면, npm은 이 prefix 내에 설치합니다. 설정되지 않았다면, 패키지 내에 없으면 현재 패키지 또는 현재 작업 디렉토리의 루트 디렉토리를 사용합니다.

노드 모듈

패키지들은 prefix 아래의 node_modules 폴더 내에 들어갑니다. 로컬에 설치될 때, 이는 main 모듈을 로드하기 위해 여러분이 require("packagename")을 사용하거나 다른 모듈을 사용하기 위해서 require("packagename/lib/path/to/sub/module")을 사용할 수 있다는 뜻입니다.

유닉스 시스템 상에서의 글로벌 설치는 {prefix}/lib/node_modules로 진행됩니다. 윈도우즈 상에서 글로벌 설치는 {prefix}/node_modules로 진행(즉, lib가 없음)됩니다.

범위가 지정된 패키지들은 @ 기호가 붙는 범위 prefix의 이름과 연관된 node_modules 폴더의 하위 폴더 내에 함께 그룹지어지는 것을 빼면 같은 방식으로 설치됩니다. 예를 들어, npm install @myorg/package는 그 패키지를 {prefix}/node_modules/@myorg/package 내에 설치합니다. 더 자세한 사항은 scope을 참고하세요.

패키지를 require()하길 원한다면, 로컬에 설치하세요.

실행 파일

글로벌 모드에서 유닉스 상에서는 {prefix}/bin, 윈도우즈 상에서는 {prefix}에 직접 실행 파일이 링크됩니다. 여러분의 터미널의 PATH 환경 변수에 이를 실행할 수 있도록 이 경로가 들어가 있는지 꼭 확인하세요.

로컬 모드에서 실행파일은 ./node_modules/.bin 내에 링크되기 때문에 npm을 통해 스크립트 실행이 가능합니다. (예를 들어 npm test를 실행할 때 그 경로 내에 테스트 러너가 있을 겁니다)

Man Pages

글로벌 모드에서 man page는 {prefix}/share/man 내로 링크됩니다.

로컬 모드에서는 man page는 설치되지 않습니다.

Man page는 윈도우즈 시스템 상에서 설치되지 않습니다.

캐시

npm cache를 보세요. 캐시 파일들은 Posix 상에서는 ~/.npm 내에, 윈도우즈 상에서는 %AppData%/npm-cache 내에 저장됩니다.

이는 cache 설정 파라미터에 의해 제어됩니다.

임시 파일

임시 파일은 기본 값인 TMPDIR, TMP, TEMP 환경변수, 또는 유닉스 상에서 /tmp나 윈도우즈 상에서 c:\windows\temptmp 설정으로 지정된 폴더 내에 저장됩니다.

임시 파일들은 이 루트 디렉토리 안에 프로그램의 각각의 실행에 대한 유일한 폴더가 지정되고, 성공적으로 끝나면 지워집니다.

더 자세한 정보

로컬 설치 시에 npm은 먼저 적절한 prefix 폴더를 찾으려고 시도합니다. 이는 그래서 npm install foo@1.2.3은 여러분이 다른 폴더로 cd를 이용해서 이동했었다 하더라도 여러분의 패키지의 합리적인 루트 디렉토리에 설치할 겁니다.

$PWD에서 시작해서 npm은 package.json 파일 아니면 node_modules 폴더를 갖는 폴더를 확인하면서 탐색합니다. 그런 것을 찾으면 npm 명령을 실행하는 목적에 맞는 실제적인 “현재 디렉토리”로 취급합니다. (이 동작은 git의 작업 디렉토리 내에서 git 명령을 실행했을 때 .git 폴더를 찾는 로직에서 영감을 받아 비슷합니다).

아무 패키지 루트도 찾지 못했다면, 현재 폴더가 사용됩니다.

npm install foo@1.2.3을 실행하면, 그 패키지는 캐시 내에 로드된 후, ./node_modules/foo내에 풀게 됩니다. 그 후 foo의 디펜던시들은 비슷하게 ./node_modules/foo/node_modules/...에 풀게 됩니다.

./node_modules/.bin에 심볼릭 링크된 바이너리 파일들은 npm 스크립트가 필요할 때 찾을 수 있습니다.

글로벌 설치

global 설정이 true로 설정되면, npm은 “글로벌”로 패키지를 설치합니다.

글로벌 설치에서 패키지들은 위에서 설명된 폴더를 사용해서 거의 같은 방식으로 설치됩니다.

순환, 충돌, 그리고 폴더의 중복 제거

순환은 node_modules 폴더를 찾는 디렉토리를 탐색하는 노드의 모듈 시스템 속성을 사용해서 처리됩니다.모든 단계에서 어느 패키지가 이미 그 상위의 node_modules 폴더에 설치되어 있다면 현재 위치에는 설치하지 않습니다.

foo -> bar -> baz에서 위의 경우를 생각해 봅시다. baz가 bar에 의존한다고 상상해보면, foo -> bar -> baz -> bar -> baz ... 가 될 겁니다. 그러나 폴더 구조가 foo/node_modules/bar/node_modules/baz가 되기 때문에 require("bar")를 부르면, foo/node_modules/bar 내에 설치된 복사본을 얻을 것이므로 bar의 다른 복사본을 .../baz/node_modules 내에 둘 필요가 없습니다.

이 손쉬운 방법은 정확히 같은 버전이 중첩된 node_modules 폴더 내에 설치되었을 때만 사용됩니다. 두 “a” 패키지가 다른 버전이라면 a/node_modules/b/node_modules/a가 되는 것이 여전히 가능합니다. 그러나 정확히 같은 패키지를 여러번 반복하는 일 없이 무한 회귀는 언제나 방지됩니다.

다른 최적화는 아래의 로컬라이즈된 “target” 폴더 아래에 가능한 한 가장 높은 레벨에 의존 모듈들을 설치해서 구성할 수 있습니다.

예제

이런 의존 그래프를 생각해보죠.

foo
+-- blerg@1.2.5
+-- bar@1.2.3
|   +-- blerg@1.x (latest=1.3.7)
|   +-- baz@2.x
|   |   `-- quux@3.x
|   |       `-- bar@1.2.3 (순환)
|   `-- asdf@*
`-- baz@1.2.3
    `-- quux@3.x
        `-- bar

이 경우, 우리는 다음과 같은 폴더 구조를 기대할 수 있을 겁니다.

foo
+-- node_modules
    +-- blerg (1.2.5) <---[A]
    +-- bar (1.2.3) <---[B]
    |   `-- node_modules
    |       +-- baz (2.0.2) <---[C]
    |       |   `-- node_modules
    |       |       `-- quux (3.2.0)
    |       `-- asdf (2.3.4)
    `-- baz (1.2.3) <---[D]
        `-- node_modules
            `-- quux (3.2.0) <---[E]

foo가 bar@1.2.3baz@1.2.3에 직접 의존하므로, 이들은 foo의 node_modules 폴더 내에 설치됩니다.

blerg의 최신본이 1.3.7임에도 불구하고, foo는 1.2.5 버전으로 지정된 의존을 갖습니다. 그래서 이는 [A]에 설치됩니다. blerg의 상위 설치가 bar의 blerg@1.x 의존을 만족하므로, [B] 아래에 다른 복사본을 설치하지 않습니다.

bar [B] 또한 baz와 asdf에 의존을 갖습니다. 그래서 이들은 bar의 node_modules 폴더에 설치됩니다. 이게 baz@2.x에 의존하므로 그 상위의 node_modules 폴더 [D] 에 설치된 baz@1.2.3을 재사용할 수는 없고, 그 자신의 복사본 [C]를 설치해야만 합니다.

bar 아래에 baz -> quux -> bar 의존은 순환을 만듭니다. 그러나 bar가 이미 quux의 조상 [B] 이므로, 그 폴더 내에 bar의 다른 복사본을 풀지 않습니다.

foo -> baz [D] 아래에, quux의 [E] 폴더 트리는 비게 됩니다. bar 상의 그 의존이 [B] 에 설치된 상위 폴더 복사본에 의해 만족되기 때문입니다.

무엇이 어디에 설치되는지의 그래프 분석은 npm ls를 사용하세요.

퍼블리싱

퍼블리싱 상에서 npm은 node_modules 폴더 안을 들여다 봅니다. 항목 들 중 어느 것도 bundledDependencies 배열에 들어있지 않으면, 그들은 패키지 tarball 내에 포함되지 않을 겁니다.

이는 패키지 메인테이너가 그들의 의존 모두를 로컬에 설치할 수 있도록 하지만, 다른 곳에서 찾을 수 없는 이들 항목만 다시 퍼블리싱 합니다. 더 자세한 정보는 package.json을 보세요.

더 읽을 거리

[node.js] node.js 설치는 nvm으로

node.js 설치를 위해서 nodejs.org의 다운로드에서 다운로드 받아 설치할 수도 있지만(이런 방식을 Node installer라고 한다), nvm을 이용하는 것이 좋다.

nvm은 node version manager의 약자로, node 버전을 필요에 따라 바꿔가며 사용할 수 있다. nvm의 설치는 https://github.com/nvm-sh/nvm#installing-and-updating를 참고하자.

nvm을 설치하고 나면 이 nvm을 통해 node.js의 특정 버전을 설치하면 된다. 현재 기준 LTS인 16을 설치하려면 다음과 같이 한다.

$ nvm install 16

그러면 해당 버전의 node.js와 함께 npm이 설치된다. npm은 node package manager의 약자이다.

최신 LTS 버전을 설치하고 싶을 때는 그냥 다음과 같이 하면 된다.

$ nvm install --lts

최신 npm을 다운로드하고 싶을 때는 npm 그 자체를 이용한다. 다음과 같이 하면 된다.

$ npm install --location=global npm

yarn 을 사용하고 싶다면 그냥 다음과 같이 하면 된다.

$ corepack enable

[Mac] Dock에 앱 관련 아이콘 추가

Launchpad 추가

기본으로 Dock에 Launchpad 아이콘은 추가되어 있다. 없앴을 때 복구하고 싶다면 다음과 같이 한다.

  1. “Finder” 실행
  2. 왼쪽 바의 “응용 프로그램”에서 우클릭 후 “Dock에 추가”

“응용 프로그램” 추가

아래처럼 “응용 프로그램” 전체를 볼 수 있는 아이콘을 추가한다.

  1. “Finder” 실행
  2. 왼쪽 바의 “위치”에서 “XXX의 MacBook Pro” / “Macintosh HD”
  3. “응용 프로그램”을 끌어다 Dock에 갖다 놓기

[Mac] Windows manager Rectangle쓰기

기존에 Windows manager로 Veer를 쓰다가 Rectangle이 더 나은 것 같아 갈아탔다. 설치는 Rectangle 사이트(https://rectangleapp.com/)에서 다운로드 받아 설치하면 된다.

내 Rectangle 설정 (32:9 모니터 기준)
내 Rectangle 설정 (32:9 모니터 기준)

나같은 경우는 아래 절반, 위 절반을 사용하지 않기 때문에 둘 모두 없앴다. 대신 높이 최대화를 Ctrl + Option + 위, 가운데 절반을 Ctrl + Option + 아래로 설정했다.

32:9 울트라 와이드 모니터의 경우 Ctrl + Option + D, F, G가 유용할 것 같다. Ctrl + Option + D만 눌러도 1/3씩 토글되면서 움직인다.

왼쪽 위, 오른쪽 위, 왼쪽 아래, 오른쪽 아래도 사용하지 않을 것 같아서 1/4 쪽으로 U, I, J, K도 바꿨다. 역시 Ctrl + Option + U만 눌러도 1/4씩 토글되면서 움직인다.

창 복원은 항상 Ctrl + Option + BackSpace 이다.

[Mac] 초기 마우스/키보드 설정

마우스

마우스 스크롤 방향 바꾸기

  • “시스템 환경설정” / “마우스” / “스크롤 방향: 자연스럽게” 체크 해제

Logitech Options 설치 (Logitech MX Master 2s, MX Vertical, Craft 키보드 사용 시)

키보드

키보드 지연 시간 설정

  1. “시스템 환경 설정” / “키보드” / “키보드”
  2. “키 반복”을 가장 오른쪽 “빠르게”
  3. “반복 지연 시간”을 가장 오른쪽 “짧게”

Karabiner-Element 설치

  1. Karabiner-Element 사이트에서 다운로드 받아 설치
  2. “시스템 환경 설정” / “보안 및 개인 정보 보호” / “개인 정보 보호” / “입력 모니터링”에 karabiner_grabber와 karabiner_observer 둘 모두 체크

Caps Lock으로 한영 전환 시 지연 없애기

  1. “Karabiner Element Preferences” / “Simple Modifications” / “For all devices” 에서 “Add Item”, 왼쪽 “—-” 를 누른 후, “Modifier keys” / “caps_lock”, 오른쪽 “—-” 를 누른 후, “To key”에 “Function keys” / “f19”
  2. “시스템 환경 설정” / “키보드” / “단축키” / “입력 소스”에서 “입력 메뉴에서 다음 소스 선택”의 오른쪽에 있는 단축키를 선택하고 Caps Lock 키를 눌러 “F19″로 설정.

외부 키보드 사용 시 Command 키와 Option 키 바꾸기

  1. “Karabiner Element Preferences” / “Simple Modifications” 에서 외부 키보드, 예를 들면 “REALFORCE_1 (Topre Coperation)” 을 선택
  2. “From key”에 “Modifier keys” / “left_option”, “To key”에 “Modifier keys” / “left_command”
  3. “From key”에 “Modifier keys” / “left_command”, “To key”에 “Modifier keys” / “left_option”

외부 키보드 사용 시 오른쪽 Alt(Option)키를 한영 전환키로 바꾸기

  1. “Karabiner Element Preferences” / “Simple Modifications” 에서 외부 키보드, 예를 들면 “REALFORCE_1 (Topre Coperation)” 을 선택
  2. “From key”에 “Modifier keys” / “right_option”, “To key”에 “Function keys” / “f19”

외부 키보드에 한영키가 있는 경우

  1. “Karabiner Element Preferences” / “Simple Modifications” 에서 외부 키보드, 예를 들면 “REALFORCE_1 (Topre Coperation)” 을 선택
  2. “From key”에 “International keys” / “lang1”, “To key”에 “Function keys” / “f19”

PC-Style Home / End 키 동작

  1. “Karabiner Element Preferences” / “Complex Modifications” / “Add rule”을 선택
  2. “Import more rules from the Internet (Open a web browser)”
  3. “pc style Shortcuts”를 입력
  4. “PC-Style Shortcuts”의 오른쪽 “Import” 버튼을 누른 후 “Import”
  5. 추가된 “PC-Style Shortcuts”의 rule 중 “Home key to the beginning of the line (Control + a)” 옆 “+ Enable”
  6. 추가된 “PC-Style Shortcuts”의 rule 중 “End key to the end of the line (Control + e)” 옆 “+ Enable”
  7. 추가된 “PC-Style Shortcuts”의 rule 중 “Home key to the beginning of the sentence (Command + Left). Doesnt work in terminal” 옆 “+ Enable”
  8. 추가된 “PC-Style Shortcuts”의 rule 중 “End key to the end of the sentence (Command + Right). Doesnt work in terminal” 옆 “+ Enable”
  9. 추가된 “PC-Style Shortcuts”의 rule 중 “PC-Style Home/End” 옆 “+ Enable”
  10. 위의 Enable 순서가 바뀌지 않도록 한다.

[Todoist] 자연어로 마감 날짜 설정

우리가 그냥 쓰는 말로, 예를 들어 “매일”, “평일마다”와 같은 식으로 Todo list를 작성할 수 있다. 반복 관련 해석은 다음과 같다.

https://todoist.com/ko/help/articles/set-a-recurring-due-date

그냥 마감 날짜 설정은 다음과 같다.

https://todoist.com/ko/help/articles/due-dates-and-times

[Excel] timestamp를 날짜/시간으로 바꾸는 방법

열을 하나 더 만들어서 아래 수식을 입력한다. 아래에서 A2가 13자리 timestamp가 적힌 셀이다.

=A2/86400000+DATE(1970,1,1)

셀 서식에서 날짜나 시간으로 바꾼다. 날짜와 시간이 모두 필요한 경우 “사용자 지정”의 “종류”에 다음을 적는다.

yyyy.m.d h:mm:ss

timestamp가 11자리인 경우 86400000 대신 864000을, 16자리인 경우 86400000000을 쓴다.

[Mythril] myth analyze 시 import 에러

Mythril은 ConsenSys의 smart contract의 보함 결함을 점검해주는 툴이다. analyze를 통해 분석을 하려고 할 때 openzeppelin 등을 사용한 컨트랙트의 경우 solc의 import 에러를 겪을 수 있다.

$ myth analyze contracts/dasomoli.sol 
mythril.interfaces.cli [ERROR]: Solc experienced a fatal error.

ParserError: Source "@openzeppelin/contracts/token/ERC20/IERC20.sol" not found: File not found. Searched the following locations: "".
 --> contracts/dasomoli.sol:6:1:
  |
6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

solc를 command line에서 사용할 때는 아래와 같이 remap을 해주면 된다.

solc @openzeppelin/=$(pwd)/node_modules/@openzeppelin/ contracts/dasomoli.sol
Compiler run successful, no output requested.

myth 사용 시에는 이를 json 파일을 만들어 --solc-json 옵션을 통해 줄 수 있다.

  • solc.json
{
   "remappings": [ "@openzeppelin/=/Users/dasomoli/src/smartcontract/node_modules/@openzeppelin/" ]
}
$ myth analyze --solc-json solc.json contracts/dasomoli.sol 
The analysis was completed successfully. No issues were detected.

참고

[Docker] docker in docker의 Permission denied 문제

도커 컨테이너 안에서 도커를 사용하는 방법은 Host의 /var/run/docker.sock를 컨테이너에서 bind mount하면 된다. 쉽게 설명해 docker run 할 때 아래처럼 -v /var/run/docker.sock:/var/run/docker.sock 옵션을 주면 된다는 뜻이다. 물론 컨테이너 안에는 도커가 설치되어 있어야 한다.

docker run -it --name ubuntu -v /var/run/docker.sock:/var/run/docker.sock ubuntu:20.04

이렇게 하면 도커 컨테이너 안에서 /var/run/docker.sock의 소유자가 root:docker 로 생기는데 이때의 그룹 id는 호스트의 그룹 id 값을 따르게 된다. 내 맥에서는 docker 그룹의 gid가 134이고, ubuntu 컨테이너의 docker 그룹의 gid는 1000이다.

$ cat /etc/group | grep docker # Host에서..
docker:x:134:dasomoli

컨테이너 안에서 /var/run/docker.sock를 살펴보면 아래처럼 소유자가 root:134 이다.

root@ce1351052f58:/# ls -l /var/run/docker.sock
srw-rw---- 1 root 134 0 Dec 21 13:19 /var/run/docker.sock

그러나 컨테이너 내에서 도커 설치 후 docker 그룹의 gid는 호스트의 gid인 134가 아닌 컨테이너 내부 환경에서 주어진다. 아래에서는 999이다.

root@ce1351052f58:/# cat /etc/group | grep docker
docker:x:999:

따라서 컨테이너 내부에서 root가 아닌 일반 유저로 도커를 사용하려고 하면 아래처럼 Permission denied 에러가 발생하게 된다.

$ id
uid=1000(dasomoli) gid=1000(dasomoli) groups=1000(dasomoli)

$ docker ps -a
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json?all=1": dial unix /var/run/docker.sock: connect: permission denied

이 문제의 해결 방법으로 사람들이 이야기하는 방법은 chown으로 컨테이너 내부에서 owner를 root:docker 로 바꾸고 사용하는 방법, 또는 chmod로 아예 a+rw 를 주는 방법을 이야기하는데, chown으로 owner를 바꾸면 컨테이너 외부의 호스트 파일의 소유자 정보도 바뀌는 문제가 있다. 위의 docker의 gid가 컨테이너에서는 999, 호스트에서는 134인 경우에 컨테이너에서 chown root:docker를 해보면 호스트에서 gid가 999인 그룹에 속한 파일로 나온다. 즉, 호스트의 도커 실행 환경에 문제가 생긴다.

$ ls -l /var/run/docker.sock
srw-rw---- 1 root systemd-coredump 0 12월 21 22:19 /var/run/docker.sock # docker가 아니고 systemd-coredump 그룹 소유로 바뀌었다!!

$ cat /etc/group | grep systemd-coredump
systemd-coredump:x:999:

따라서 이 방법보다는 둘 모두의 docker 그룹의 gid를 맞춰주는 방법이 더 낫다. 컨테이너 안에서 groupmod로 다음을 실행한다. 아래에서 134는 호스트에서의 docker 그룹의 gid이다.

root@ce1351052f58:/# groupmod -g 134 docker

root@ce1351052f58:/# chown root:docker /var/run/docker.sock # 컨테이너가 실행 중이었다면 owner를 바꾼다.

root@ce1351052f58:/# ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Dec 21 22:19 /var/run/docker.sock

호스트에서도 확인해보자.

$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 12월 21 22:19 /var/run/docker.sock

도커 이미지를 빌드할 때 Dockerfile 내에서 아예 먼저 해준다면, 실행 시에 따로 해주지 않아도 된다.

# Dockerfile 안의 내용
ARG HOST_DOCKER_GID="134"

RUN groupmod -g ${HOST_DOCKER_GID} docker

위의 Dockerfile로 도커 이미지를 빌드할 때는 다음과 같은 방식으로 넘겨도 된다.

$ docker build --build-arg HOST_DOCKER_GID=$(stat -c "%g" /var/run/docker.sock) -t ubuntu20.04

위에서 사용한 호스트에서 docker 그룹의 gid를 얻는 방법은 “[Linux/Mac] group id 얻기” 글을 참조한다. 호스트와 컨테이너 둘 간의 gid 충돌은 유의하자.