ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Yarn berry] pnp(Plug And Play), Zero Install을 위한 Dependency 문제 해결하기
    개발 블로깅/기타 개념 2022. 9. 11. 22:30

     

     

    이번에 프로젝트 빌드 환경을 개선시키기 위해 Zero install을 적용시키려고 한다.
    yarn berry 환경 세팅까지는 수월했으나, PnP(Plug In Play)을 통해 Zero install을 하려고 하니 Yarn에 대한 개념이 꽤나 필요했다.

    그래서 이번에 Yarn Berry의 핵심 기능인 Plug In Play가 무엇인지 간단히 설명해보고, PnP를 통해 Zero Install을 하기 위해 우리가 해결해야 할 문제가 무엇인지 설명해 보려고 한다.

     

    Zero Install

    Zero Install은 말 그대로 설치를 하지 않고 이용하는 방식을 말한다.

    기존에 node_modules에서 모든 디펜던시를 새로 인스톨하려고 하면, 모든 디펜던시 모듈이 인스톨에만 굉장히 많은 시간이 소모하게 된다.

    그러나 Zero Install을 이용하면 디펜던시 Install에 걸리는 시간을 없앨 수 있기 때문에, CI 단이나 배포 파이프라인 등에서 반복되는 install 시간을 단축 시킬 수 있게 되고 CI 실행시간 및 배포 시간을 대폭 감소 시킬 수 있다.

    그럼 이러한 Zero Install 방식을 어떤 방식으로 적용해 볼 수 있을까?
    Yarn에서는 Yarn Berry 버전을 통해 PnP(Plug And Play)라는 기능을 지원하는데, 이 기능을 이용해서 zero install을 할 수 있다.

     

    PnP(Plug And Play)

    그럼 pnp에 대해서 한번 알아보자.

    우선 pnp를 이용하려면 프로젝트 환경이 yarn Berry 환경이어야 한다.

    // 새로운 프로젝트 세팅 시,
    $ yarn init -2
    
    // 기존의 yarn classic인 프로젝트를 yarn berry로 migration 시
    $ yarn set version berry // berry 활성화
    $ yarn set version stable // yarn 최신버전으로 업데이트

     

    berry 버전이 세팅이 되면 프로젝트 루트 경로에 .yarnrc.yml 파일이 생기는데, 해당 파일에 yarn config 관련 내용들을 설정할 수 있다.

    yarn berry에서는 디펜던시 모듈을 관리할 수 있는 방식이 여러가지가 있는데, 아래와 같이 설정할 수 있다.

    // .yarnrc.yml
    nodeLinker: "pnp" // pnp(default), pnpm, node_modules 중 설정 가능.

     

    Zip Archive File

    pnp 모드로 설정으로 디펜던시 모듈을 설치하게 되면, 기존처럼 node_modules로 디펜던시 모듈들이 설치되지 않고, 대신 필요한 라이브러리 모듈들이 .yarn/.cache 디렉토리에 zip 아카이브 파일로 관리하게 되고, zip으로 된 각 모듈의 의존성 트리 정보들은 프로젝트 루트의 .pnp.cjs 파일로 관리하게 된다.


    node_modules로 설치할때보다 zip으로 압축된 파일로 관리하게 됨으로써 디펜던시 사이즈를 많이 줄일 수 있는 장점을 가져가게 된다.

     

    또한 기존에는 node_modules 내에서 각 의존성 모듈 별로 중복되는 모듈을 상위로 호이스팅 하는 등, 각 의존성 트리가 굉장히 복잡하면서, 직접 의존성 추가를 하지 않은 모듈을 이용할 수 있는 유령 디펜던시 문제가 발생하는 등 의존성 관리에 대한 문제 요소들이 많았다.

    https://toss.tech/article/node-modules-and-yarn-berry

     

    pnp에서는 하나의 의존성 모듈이 하나의 zip 파일로 관리함으로써, 각 의존성 모듈이 서로 얽히지 않고 독립적으로 관리를 할 수 있게 되고 각 디펜던시 트리의 복잡성이 없앨 수 있게 된다.

     

     

    그러나 PnP 적용에 있어서의 문제점

    기존의 프로젝트로 막상 yarn berry 버전에서 pnp를 이용해보면, 예상과는 다르게 정상적으로 동작하지 않고 디펜던시 관련 에러가 발생할 것이다.

     

    각 모듈이 pnp로 사용할 수 있으려면, pnp 방식에 맞게 의존성 관리가 strict하게 세팅이 되어 있어야 한다. 그러나 모든 모듈이 pnp 형태에 맞게 호환이 모두 되어 있는 상태가 아니기 때문에, pnp를 제대로 사용하기에는 아직 많은 문제가 발생할 수 밖에 없다.

    이로 인해 Yarn에서도 완전히 strict한 방식 대신, pnp loose 모드를 통해 명시적으로 요구하는 디펜던시를 요구하지 않도록 할 수 있다.

    // .yarnrc.yml
    
    pnpMode: loose

     

    그러나 이는 pnp가 정상적으로 동작하는 것을 보장하지 않는다. 따라서 왠만하면 pnp 모드를 이용하려면 strict 모드로 이용하는 것이 좋다.

     

    아무튼, 위에서 설명한 것처럼 각 의존성 모듈이 zip 아카이브 파일로 독립적으로 관리가 됨으로써, 의존성 트리가 더욱 strict 하게 관리가 필요하다. 따라서 pnp 형식으로 관리될 때는 이러한 의존성 모듈 관리에 대해 문제되는 부분을 모두 해결해주어야만 정상적으로 pnp 방식으로 모듈을 이용할 수 있게 된다.

    그러면 우리가 이러한 디펜던시 문제를 어떻게 확인하고 어떻게 해결할 수 있을까?

     

    PnP Dependency 문제 해결해보기

    pnp를 통한 yarn install을 하면, 각 디펜던시에 대해 문제되는 부분을 기본적으로 알려준다.
    아래에서 각 예시를 통해 확인해보도록 하자.

     

    # 사용되는 디펜던시의 하위 디펜던시 모듈이 실제로 제공되지 못하는 경우.

    Style Library로 흔히 많이 사용되는 styled-components를 예시로 들어보자.

    $ yarn install
    ➤ YN0000: ┌ Resolution step
    ➤ YN0002: │ ProjectName@workspace:. doesn't provide react-is (p66f35), requested by styled-components
    ➤ YN0000: │ Some peer dependencies are incorrectly met; run yarn explain peer-requirements <hash> for details, where <hash> is the six-letter p-prefixed code
    ➤ YN0000: └ Completed in 0s 333ms
    ➤ YN0000: ┌ Fetch step
    ➤ YN0000: └ Completed in 0s 412ms
    ➤ YN0000: ┌ Link step
    ➤ YN0000: │ ESM support for PnP uses the experimental loader API and is therefore experimental
    ➤ YN0000: └ Completed
    ➤ YN0000: Done with warnings in 1s 75ms

     

    프로젝트 내 디펜던시로 styled-components를 추가한 상태에서 yarn install을 해보면, 3번째 로그에, "ProjectName@workspace:. doesn't provide react-is (p66f35), requested by styled-components" 라고 뜨는 것을 볼 수 있다. (2022.09.10일 기준)

    이것은, ProjectName(현재 프로젝트)에서 사용 중인 styled-components가 하위 의존성 모듈로 react-is를 필요로 하는데, 실제로 styled-components 라이브러리 내에서 하위 디펜던시로 react-is를 명시하지 않은 상태이기 때문에 발생하는 문제이다.

     

    이를 해결하기 위해 우리는 특정 디펜던시의 하위 디펜던시를 확장 설치를 해줄 필요가 있다.

    yarn berry에서는 이와 같이 확장 설치를 할 수 있는 기능을 yarnrc.yml 파일의 "packageExtentions" 기능을 통해 제공한다.

     

    Configuration options

    List of all the configuration option for Yarn (yarnrc files)

    yarnpkg.com

     

    방법은 아래와 같다.

    // .yarnrc.yml
    nodeLinker: pnp
    yarnPath: .yarn/releases/yarn-3.2.3.cjs
    
    ...
    
    # 디펜던시 확장 모듈 설치
    packageExtensions:
      'styled-components@*':
        dependencies:
          'react-is': '^18.2.0'

     

     

    styled-components 하위로 react-is:^18.2.0 를 dependencies로 설치 할 것을 명시하여 확장 설치를 할 수 있다.
    이후 다시 yarn install을 해보면, 해당 로그가 뜨지 않는 것을 확인할 수 있다.

     

    💡styled-components에서 요구하는 react-is 버전이 있을텐데, 해당 버전은 어떻게 확인할 수 있나요?

    yarn install 하면서 나온 디펜던시 문제에 대한 로그를 보면, 중간에 로그 hash 값을 확인할 수 있는데, 해당 hash 값을 이용해서 요구되는 디펜던시 정보를 자세히 확인해볼 수 있다.

    $ yarn explain peer-requirements p66f35      
    ➤ YN0000: ProjectName@workspace:. doesn't provide react-is, breaking the following requirements:
    ➤ YN0000: styled-components@npm:5.3.5 [7f092] → >= 16.8.0 ✘

     

    위 로그는, styled-components@npm:5.3.5에서 요구되는 react-is 버전이 ">= 16.8.0" 이여야 한다는 뜻이다.

     

    # 하위 의존성 모듈은 있으나, 호환에 맞는 버전으로 설치가 되어 있지 않은 경우.

    디펜던시 모듈에서 요구되는 특정 하위 디펜던시 모듈이 있긴 하지만, 제대로 버전에 맞는 하위 디펜던시로 제공을 받지 못하고 있을때 발생하는 문제도 있다.

    "MyProject@workspace:. provides eslint (pca27c) with version 8.23.0, which doesn't satisfy what @eslint-config-library and some of its descendants request"

    위 로그의 경우에는 @eslint-config-library 라는 디펜던시에서 요구하는 eslint가 8.23.0버전대로 제공받고 있지만, 해당 버전을 통해 완전히 안정적으로 호환되고 있지 않은 상태임을 뜻한다. (@eslint-config-library는 실제로 있는 모듈이 아닌, 임의 모듈로 가정함.)

    그러면 @eslint-config-library가 요구하는 eslint 모듈 버전이 무엇인지 확인해보자.

    $ yarn explain peer-requirements pca27c      
    ➤ YN0000: myProject@workspace:. provides eslint@npm:8.23.0 with version 8.23.0, which doesn't satisfy the following requirements:
    ➤ YN0000: @eslint-config-library@npm:1.6.2::__archiveUrl=...(생략) [7f092] → ^7                                         ✘
    ➤ YN0000: @typescript-eslint/eslint-plugin@npm:5.36.1 [bbaa8]   → ^6.0.0 || ^7.0.0 || ^8.0.0                 ✓
    ➤ YN0000: @typescript-eslint/parser@npm:4.33.0 [8fe0d]          → ^5.0.0 || ^6.0.0 || ^7.0.0                 ✘
    ➤ YN0000: @typescript-eslint/parser@npm:5.36.1 [bbaa8]          → ^6.0.0 || ^7.0.0 || ^8.0.0                 ✓
    ➤ YN0000: @typescript-eslint/type-utils@npm:5.36.1 [8c7de]      → *                                          ✓
    ➤ YN0000: @typescript-eslint/utils@npm:5.36.1 [8c7de]           → ^6.0.0 || ^7.0.0 || ^8.0.0                 ✓
    ➤ YN0000: eslint-config-airbnb-base@npm:14.2.1 [bbaa8]          → ^5.16.0 || ^6.8.0 || ^7.2.0                ✘
    ➤ YN0000: eslint-config-next@npm:11.1.4 [bbaa8]                 → ^7.23.0                                    ✘

     

    확인해보니, 뭔가 styled-components과는 다르게, 여기저기서 eslint를 요구하는 다른 디펜던시 모듈들이 많이 나타난다.

    @eslint-config-library 하위의 여러 디펜던시에서 eslint를 하위 디펜던시로 요구하고 있는 것에 대한 로그들이다.

    @typescript-eslint/eslint-plugin를 예시로 봐보자.

    ➤ YN0000: @typescript-eslint/eslint-plugin@npm:5.36.1 [bbaa8]   → ^6.0.0 || ^7.0.0 || ^8.0.0  ✓

    @typescript-eslint/eslint-plugin은 요구하는 eslint 버전이 "^6.0.0 || ^7.0.0 || ^8.0.0"로 되어 있는데, 현재 프로젝트에 명시되어 있는 eslint 버전은 8.23.0이고 이에 충족하기 때문에 정상적으로 체크 표시가 뜨는 것을 확인할 수 있다.

     

    이와 반대로, @typescript-eslint/parser@npm:4.33.0는 아래처럼 되어 있다.

    ➤ YN0000: @typescript-eslint/parser@npm:4.33.0 [8fe0d]     → ^5.0.0 || ^6.0.0 || ^7.0.0   ✘

     

    @typescript-eslint/parser@npm:4.33.0에서 요구하는 eslint 버전은 "^5.0.0 || ^6.0.0 || ^7.0.0"로 되어 있으나, 현재 설치되어 있는 eslint 8 버전대가 조건에 맞지 않아 x 표시가 되어 있는 것을 확인할 수 있다.

     

    이처럼 각 디펜던시 모듈의 하위 모듈이 요구하는 버전을 모두 충족시켜 줄 수 있어야 한다.

    위 @eslint-config-llibrary에서는 하위의 각 모듈이 요구하는 eslint 여러 버전 중, 중복으로 요구되는 eslint 버전이 "^7.0.0" 인 것을 확인할 수 있다. 따라서, 인스톨 할 때, eslint를 요구하는 디펜던시가 있으면 eslint의 "^7.0.0"으로 인스톨 할 수 있도록 peerDependency 아래처럼 명시한다.

    {
      "scripts": {
        ...
      },
      "peerDependencies": {
        "eslint": "^7.0.0"
      }
      
    }

     

    이후 다시 yarn install 실행 시, @eslint-config-library라이브러리의 의존성 문제가 해결되어 더 이상 로그가 뜨지 않는 것을 확인할 수 있다.

     

    그러나 위 경우에는 운이 좋게도 하위 디펜던시들이 요구하는 eslint 버전이 7 버전대로 모두 겹치는 하나의 버전이 있었기 때문에 가능했는데, 만약 요구하는 버전대가 완전히 다른 디펜던시가 존재한다면 어떻게 해야할까?

     

    예를 들어 아래와 같은 경우이다.

    $ yarn explain peer-requirements pca27c      
    ➤ YN0000: myProject@workspace:. provides eslint@npm:8.23.0 with version 8.23.0, which doesn't satisfy the following requirements:
    ➤ YN0000: @eslint-config-library@npm:1.6.2::__archiveUrl=...(생략) [7f092] → ^7                                         ✘
    ➤ YN0000: @typescript-eslint/eslint-plugin@npm:5.36.1 [bbaa8]   → ^6.0.0 || ^7.0.0 || ^8.0.0                 ✓
    ➤ YN0000: @typescript-eslint/parser@npm:4.33.0 [8fe0d]          → ^5.0.0 || ^6.0.0 || ^7.0.0                 ✘
    ➤ YN0000: @typescript-eslint/parser@npm:5.36.1 [bbaa8]          → ^1.0.0 || ^2.0.0 || ^3.0.0                 ✘

     

    하위 디펜던시 모듈 중 위 2개는 중복되는 요구 버전대 ^7.0.0이 있다.
    그러나 3번째의 모듈은 eslint의 "^1.0.0 || ^2.0.0 || ^3.0.0" 버전대를 요구함으로써, 위 두개와는 완전 다른 버전을 요구하고 있는 상태이다.

    우리가 이것을 어떻게 모두 충족시켜 줄 수 있을까?

     

    그냥 쉽게 eslint 버전 명시하는 곳에 여러 버전을 아래처럼 같이 명시해주면 된다.

    {
      "scripts": {
        ...
      },
      "peerDependencies": {
        "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
      }
      
    }

     

    혹은 아래처럼 요구하는 모든 버전의 eslint를 각자 설치할 수 있도록 할 수 있다.

    {
      "scripts": {
        ...
      },
      "peerDependencies": {
        "eslint": "*"
      }
      
    }



    정리하며

    우선 pnp를 위해 내가 직접 겪고 파악해본 요소는 여기까지이다.
    위 내용들은 기존에 운영하던 하나의 서비스인 단일 레포에서 pnp를 적용했을 때 발생했던 문제들과 해결한 방식들이었는데, 만약 Yarn Workspace로 구축한 모노레포 환경에서도 이정도만으로 pnp를 안정적으로 적용할 수 있을지는 한번 봐바야 할 것 같다.

    나중에 모노레포에도 pnp를 적용하면서 새로운 문제를 마주하게 되면 추가로 정리해보려고 한다.

     

     

    Plug'n'Play

    An overview of Plug'n'Play, a powerful and innovative installation strategy for Node.

    yarnpkg.com

     

     

    node_modules로부터 우리를 구원해 줄 Yarn Berry

    토스 프론트엔드 레포지토리 대부분에서 사용하고 있는 패키지 매니저 Yarn Berry. 채택하게 된 배경과 사용하면서 좋았던 점을 공유합니다.

    toss.tech

     

    반응형

    댓글

Designed by Tistory.