PNPM에 대해(with Docker)

이희제·2024년 5월 27일
post-thumbnail

기존 npm, yarn의 문제점

성능적으로 느리고 의존성 중복 저장문제를 호이스팅(hoisting)을 통해 해결한다. 이에 따라 유령 의존성 현상이 발생할 수 있다.

즉, 현재 프로젝트에 직접적으로 필요 없는 패키지에 접근을 할 수 있는 문제가 발생할 수도 있다.


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

npm의 문제점

  1. 느린 설치 속도:
    • npm은 하나의 패키지를 설치하면 의존하는 모든 패키지를 node_modules 폴더에 다운로드하여 관리한다. 따라서 각 프로젝트 별로 패키지를 많이 설치하면 node_modules의 크기가 커지고 빌드 시간이 오래걸린다. 특히 대규모 프로젝트에서는 시간이 많이 걸릴 것이다.
  1. 디스크 사용 비효율성:

    • npm은 패키지를 설치할 때 모든 패키지를 프로젝트의 node_modules 폴더에 복사한다. 이로 인해 중복된 패키지가 여러 번 저장되어 디스크 사용량이 증가한다.
  2. 의존성 충돌 문제:

    • npm은 플랫하게 node_modules 폴더를 관리하지 않아, 특정 버전의 패키지가 요구되는 경우 의존성 충돌이 발생할 수 있다. 이러한 충돌은 디버깅을 어렵게 만든다.

Yarn Classic의 문제점

  1. 디스크 사용 비효율성:

    • Yarn도 기본적으로 npm과 유사하게 패키지를 복사하여 설치하므로, 디스크 사용량이 효율적이지 않다.
  2. 속도 문제:

    • Yarn은 npm보다 빠르지만, 여전히 대규모 프로젝트에서 설치 시간이 많이 걸리는 경우가 있다. 특히 패키지 업데이트 시 속도가 느려질 수 있다.
  3. 의존성 문제:

    • Yarn도 의존성 충돌 문제에서 완전히 자유롭지 않다. 특히, 여러 프로젝트에서 동일한 패키지를 사용하는 경우에 관리가 어렵다.

node_modules의 무거움을 표현한 그림


PNPM(Performant NPM)의 특징

PNPM은 node_modules를 직접 설치하는 대신에 Symbolic LinkHard Link를 사용해서 모듈을 관리한다.

Symbolic Link

  • 특정 파일이나 디렉토리를 가리키는 파일 (하드링크는 파일만 참조 가능)
  • 원본 파일이 삭제 되면 링크가 깨지기 때문에 접근 불가

pnpm을 사용하면 프로젝트 내에 node_modules에 가상 저장소를 가리키는 심볼릭 링크로 구성한다.

가상 저장소는 ./node_modules/.pnpm에 위치한다.

Hard Link

  • 원본 파일과 동일한 inode를 가지는 링크
    • inode - 시스템의 파티션에 있는 각 파일에 대한 정보를 기억하는 120바이트의 고정된 크기의 구조체
  • 파일 시스템 내 동일한 파일 내용을 가리키는 두 개 이상의 파일 이름
  • 하나의 하드 링크를 삭제해도 원본 데이터에 접근이 가능하다.
    • inode를 통해서 원본 데이터에 접근하기 때문에 원본 데이터만 사라지지 않는다면 접근 가능한 것이다.

pnpm store에 저장된 패키지나, node_modules/.pnpm에 저장된 패키지나 동일한 데이터(동일한 inode)를 참조하고 있다. (하드링크를 통해)

pnpm store path를 통해 스토어의 위치를 확인할 수 있다.

결론적으로, pnpm은 디스크의 세 위치를 사용하면서 용량을 줄인다.

  • 저장소(Content-addressable store): 패키지 파일 원본
  • 가상 저장소(virtual store): 저장소의 하드 링크
  • node_modules: 가상 저장소의 심볼릭 링크

저장소와 가상 저장소는 같은 inode를 바라 보고 있는 점 다시 한번 기억하자.


Docker 환경에서의 PNPM 적용 방법 (with 캐시)

컨테이너와 이미지에 대한 개념을 그림를 통해 참고하자.

pnpm 공식 문서를 보면 다음과 같이 도커 파일을 구성해두고 있는 예시가 있다.

FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . /app
WORKDIR /app

FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile

FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build

FROM base
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
EXPOSE 8000
CMD [ "pnpm", "start" ]

몰랐던 각 옵션을 살펴보자.

  • corepack enable
    • corepack - coreapck 은 항상 바이너리 파일을 사용해서 사용자와의 인터렉션 없이 패키지 매니저의 버전을 관리해주는 도구다.
  • -mount=type=cache, target=/pnpm/store
    • BuildKit cache mounts
    • 해당 mount를 통해 BuildKit이 관리하는 캐시 디렉토리가 생성되고 이미지 빌드마다 참조할 수 있다.
    • 공식 문서의 내용 - 컨테이너 내에서 target path에 캐시를 마운트 한다.
    • 빌드 간에 참조할 수 있는 캐시 스토어가 생기는 것이고 특정 layer가 재빌드될 때 필요하면 참조해서 사용한다.
    • 컴파일러나 패키지 매니저들을 위한 캐시 디렉토리를 임의의 디렉토리(target)에 연결
    • 이미지를 빌드할 때 컴파일러나 패키지 매니저는 캐시 디렉토리를 사용
    • layer를 재빌드할 때 새로운 것만 다운하도록 할 수 있음
    • best to think of --mount=type=cache as being like a named volume in docker, managed by BuildKit
  • mount=type=bind
    • bind mount는 container가 특정 파일들을 host로부터 직접 사용할 수 있게 해준다. 이를 적용하면 추가적인 COPY 지시어들을 사용하지 않아도 된다. (빌드마다 복사하는 시간을 줄일 수 있을 것으로 예상됨)
  • -prod
    • 패키지 내 devDependencies를 설치하지 않는 옵션
  • -frozen-lockfile (참고)
    • pnpm-lock.yaml를 업데이트하지 않는 설정
    • 현재 package.json 파일과 pnpm-lock.yaml 파일 간에 불일치가 발견되면 오류를 발생시킨다.
    • 일치한다면 pnpm-lock.yaml 파일 기반으로 의존성을 설치한다.

BuildKit 캐시가 어떤 구조로 동작하는 지는 아래 이미지를 참고하자.

BuildKit은 도커 18.09 버전부터 지원이 되고 도커의 빌더로서 도커 데스크탑 또는 23 버전부터 기본 빌더로 내장되어 있다. (참고)

그 이하 버전을 사용한다면 이미지 빌드 시에 다음과 같이 DOCKER_BUILDKIT=1를 추가해서 빌드해주자.

DOCKER_BUILDKIT=1 docker build -t my-image:tag .
profile
그냥 하자

0개의 댓글