본문 바로가기
Engineering

node_modules 이해하기

node_modules가 의존성을 관리하는 방식 — 중첩·호이스팅 구조와 패키지 매니저의 동작 원리를 파헤쳤습니다.

모노레포 공부를 하다가 이 node_modules라는 놈이 여러가지 패키지들(app, common, etc.)의 의존성을 어떻게 관리해주는가..

또, 약간 논외인 이야기일지도 모르지만 왜 항상 .gitignore에 들어가는지, 왜 무거운파일인지 궁금해져서 쓰는 글.

일단 나는 yarn workspace를 사용해서 모노레포를 공부하고 있음(v1). 왜 굳이 yarn 쓰냐면 나한테 모노레포 자체가 좀 어려워서 익숙한걸 사용해서 최대한 진입장벽을 낮추기 위함이라는 다소 비논리적인 이유로.

이게 궁금해진 이유

: 모노레포 안의 리액트 앱에서 styled-components를 설치했다.

예상) 그럼 리액트 앱의 node_modules에만 스타일드 컴포넌트가 있지않을까?

결과) root의 node_modules에 styled-components가 설치되고 리액트 앱 안의 node_modules에서는 발견되지 않음.

호이스팅

호이스팅.. 이놈의 호이스팅..

한 번이라도 자바스크립트 이론에 대해 공부해봤던 사람에게는 익숙한 단어인 호이스팅.. 패키지 매니저에서도 보게될 줄은 몰랐다.

JavaScript 호이스팅은 인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 또는 임포트(import)의 선언문을 해당 범위의 맨 위로 이동시키는 과정을 말합니다.

자바스크립트의 호이스팅에 대한 정의는 위와 같다.

그렇다면 맥락상 yarn의 호이스팅도 비슷하겠지 ? 머대충 끌어올려준다. 이런거일듯.

node_modules 의존성 구조 다이어그램

구글에 얀베리 뭐 모노레포 어쩌구를 치면 정말 매번 나오는 그림이다.(이 그림은 Single Project 기준)

그림만 보고 이해하자면 뭐 중복 설치돼있는 패키지들을 호이스팅해줌(맨 위로 끌어올려줌)으로써 중복을 제거하고(겹치는놈들 다위로올렷!) 좀더 효율적으로 패키지를 매니징 할 수 있도록 해주는것같은데

모노레포에서는 이렇게 동작한다.

node_modules 의존성 구조 다이어그램

둘다 B 패키지(1.0)을 사용한다는 가정 하에 B(1.0)을 맨위로 끌어올리고 B(2.0)을 쓰는 package-1에만 이를 설치해줌.

하여튼 호이스팅이 됐을때 여전히 하위 패키지가 이를 찾아서 사용할 수 있는 이유는,

해당 패키지의 node_modules에서 모듈을 못찾으면, 계속 상위폴더의 node_modules를 체킹하며 찾아와주기 때문이다. (근데 심지어 이거 중간에 실패하기도 함. 그리고 구조가 복잡할수록 맨위에있으면 시간이 very very 오래걸리겠지.)

(아니 근데 왜 나는 리액트 앱에서만 쓰는 styled-components를 해당 앱에서만 설치했는데 맨위로 올라갔냐니까? 왜 한군데서만 쓰는데 맨위까지 찾아 올라가야되는거냐고 ㅠㅠ)

그래서 yarn 한테 물어봄

> yarn why styled-components                                               main 
yarn why v1.22.19
[1/4] 🤔  Why do we have the module "styled-components"...?
[2/4] 🚚  Initialising dependency graph...
[3/4] 🔍  Finding dependency...
[4/4] 🚡  Calculating file sizes...
=> Found "styled-components@6.1.0"
info Reasons this module exists
   - "_project_#@monoprac#react-swc" depends on it
   - Hoisted from "_project_#@monoprac#react-swc#styled-components"
info Disk size without dependencies: "2.13MB"
info Disk size with unique dependencies: "4.41MB"
info Disk size with transitive dependencies: "4.95MB"
info Number of shared dependencies: 10
✨  Done in 0.15s.

yarn why <패키지명>을 사용하면 이친구가 어디서 호이스팅되어 왔는지 알 수 있다.(근데 사실 내 의문을 풀기보다는 root의 node_modules에 의도하지 않은 친구가 있을때 호구조사용(ㅋㅋ)으로 써야할듯)

⇒ 이유 from yarn 공식문서

efficiency (hoisting as much as possible)

어..어어 as much as possible 이러네… ㅋㅋ 그니까 이래서 “효율성”을 챙기겠다는 얘기인거같은데 그럼 저걸 딱히 사용하지 않는 package-2도 A(1.0)이나 C(1.0)을 임포트 할 수 있게된다. 굳이?라고 생각하는 이유는 필요한 곳에서만 설치돼있으면 효율적이고 찾는데 시간이 덜 걸리지 않냐.. 혹시 이것도 “확장성”을 고려해야하기 때문에 이게 더 효율적이라고하는건가.. 하튼 이걸 유령 종속성(ghost dependency: 직접 설치하지 않은 패키지이나, 설치한 패키지가 의존하는 패키지여서 같이 설치된 패키지)라고 부르는데 이건나중에알아보자.(꼭)

확실하지 않아서 못참고 찾아본 유령 의존성(Phantom/Ghost Dependency) : 호이스팅의 결과에 따라 직접 의존하고 있지 않은 라이브러리를 require()할 수 있음. : 따라서,package.json에 명시하지 않은 라이브러리를 몰래(?) 사용하거나 어떤 다른 의존성을 삭제할때 같이 없어져버리기도 함. : 이해한 바 => 그니까 나는 설치한적이 없는데, 어떤 라이브러리를 설치하면서 호이스팅된 의존성들이 쭊쭊 위로 올라오면서 내가 설치한건 라이브러리 하나인데, node_modules에는 여러개의 내가 설치한적도 없는것들이 우다다 생기는거임. 근데나는 이걸 또 개별적으로 가져와서 쓸 수 있는데 내가 설치했던 라이브러리 제거하면 없어져버려서 오류 대파티나는 문제가 생길 수 있다는거임.

아모턴,, 이건 개발자(나?)가 의도한 의존성 설치가 아니다. 나는 고집있고 뚝심있게 리액트 앱 내에만 styled-components를 설치하고싶고 호이스팅이 되는꼴을 두 눈 뜨고 지켜볼 수 없다. 전체적인 모노레포 설계를 할 수 있는 능력치가 안되는 나는 그냥 괜히 지는기분이라 나의 이 마음을 꺾을수가없다. 그런 나를 위해(?) yarn에서는 nohoist라는 설정할 수 있도록 해준다.

nohoist

root의 package.json에서 nohoist 설정을 해주면, 해당 의존성은 호이스팅 하지맛! 이라고 알려주는거랑 같아진다. 말그대로 no hoist. 호이스트, 하지 않습니다. no japan.. 읍읍

예시)

node_modules 의존성 구조 다이어그램

이렇게 생긴 모노레포가 있다고 치자.

nohoist 설정법

// monorepo's package.json
  ...
  "name": "monorepo",
  "private": true,
  "workspaces": {
    "packages": ["packages/*"],
    "nohoist": ["**/react-native", "**/react-native/**"]
  }
  ...

생각보다 간단하지만 예시를 통해 뜯어보자

"**/react-native" (얕은) : react-native 자체를 호이스팅 하지마 ! 라는 뜻. 그래서 “react-native/metro” 는 끌어올려짐

"**/react-native/**" (깊은) : react-native 뿐만아니라 그 종속 라이브러리까지도 호이스팅하지마 ! 라는 뜻.

호이스팅 후 구조

node_modules 의존성 구조 다이어그램

A, B의 react-native, react-native/metro는 끌어올려지지 않았지만, 해당 경로와 일치하지 않는 X, Y는 끌어올려진것을 볼 수 있다.

케이스 몇가지 더

  1. 패키지 A에만 노호이스팅을 적용하고 싶을 때
"nohoist": ["A/react-native", "A/react-native/**"]
  1. 패키지 A에도 패키지 C가 포함되어야 할 때
"nohoist": ["A/react-native", "A/react-native/**", "A/C"]

적용~~(아무도안궁금할수도있겠지만..)~~

이론만 알고 적용을 안해보면,, 안굴러가는 내용의 블로그글이 된다는 걸 익히 알고있고 당해왔다… 따라서 nohoist를 적용한 결과 모노레포의 리액트 앱에만 설치하기 성공함 !! 뿌듯함 + 1 그리고 위에서 설명한 유령의존성을 조금 이해할 수 있었던 게, 스타일드 컴포넌트 하나 설치하는데 노드모듈스에 엄청 많이 뭐가 생김

node_modules 의존성 구조 다이어그램

styled-components 하나 깔았을 뿐인데..( "nohoist": ["**/styled-components","**/styled-components/**"] 로 경로지정을 해줬기때문에 styled-components의 하위 의존성들도 전부 리액트 앱 내에만 깔리게된것이에요)

깨달은점

모노레포 할거면 의존성 관리도 머리 잘써서 해야겠다는 생각이 들었다. 아 넘무 넘무 어렵다 ! 특히 뭐 한가지만 업데이트해도 전부 뻑나버리는 상황이 발생하기 쉬울 것 같다는 생각이 들었다.근데 아무리 생각해도 더 효율적이고 복잡하지 않은 방식이 있을 것 같다.(매번 nohoist에 개별적으로 사용할 의존성을 추가해줘야하는게 맞나? 싶은) 찾아보다 yarn berry 도 궁금해졌는데, 이건 따로 적고싶음 ㅎㅎ 사실 yarn v1(classic)은 이제는 (내가가고싶은 회사 중에서는) 잘 안쓰는것같다..ㅋㅋㅋㅋㅋㅋㅋ 얀베리.. 딱대.. 조만간패준다..

참고자료

https://classic.yarnpkg.com/blog/2018/02/15/nohoist/ https://toss.tech/article/node-modules-and-yarn-berry https://ondemand.tistory.com/240 https://www.nextree.io/monorepo-yarn-berry/