본문 바로가기
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/