Split View: 패키지 매니저 완전 가이드 — npm, uv, RPM, Homebrew 원리와 소프트웨어 등록법
패키지 매니저 완전 가이드 — npm, uv, RPM, Homebrew 원리와 소프트웨어 등록법
들어가며
소프트웨어 개발에서 패키지 매니저는 인프라의 근간이다. 우리가 매일 사용하는 npm install, pip install, brew install, yum install 같은 명령어 뒤에는 의존성 해결, 버전 관리, 바이너리 배포라는 복잡한 메커니즘이 숨어 있다.
이 글에서는 네 가지 주요 패키지 매니저의 내부 원리를 파헤치고, 각 생태계에 자신의 소프트웨어를 등록하는 방법까지 다룬다.
Part 1: npm 원리 (JavaScript / Node.js)
1-1. npm이란
npm(Node Package Manager)은 JavaScript 생태계의 표준 패키지 매니저다. 세 가지 핵심 구성 요소가 있다.
레지스트리(Registry): 모든 패키지가 저장되는 중앙 저장소다. registry.npmjs.org에 호스팅되며 전 세계에 CDN으로 배포된다. 2026년 기준 300만 개 이상의 패키지가 등록되어 있다.
package.json: 프로젝트의 메타데이터와 의존성을 선언하는 파일이다.
{
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.21"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
node_modules: 의존성이 실제로 설치되는 디렉토리다. npm은 기본적으로 중첩(nested) 구조를 사용하지만, 가능한 한 끌어올림(hoisting)을 통해 디렉토리를 평탄화한다.
1-2. 의존성 해결 메커니즘
npm의 의존성 해결은 크게 네 가지 개념으로 설명된다.
Semver(Semantic Versioning):
npm은 semver 규칙을 따른다. 버전은 MAJOR.MINOR.PATCH 형식이며, 각 범위 지정자의 의미는 다음과 같다.
| 지정자 | 의미 | 예시 |
|---|---|---|
^4.18.0 | MAJOR 고정, MINOR와 PATCH 허용 | 4.18.0 이상 5.0.0 미만 |
~4.17.21 | MAJOR와 MINOR 고정, PATCH만 허용 | 4.17.21 이상 4.18.0 미만 |
4.18.0 | 정확히 해당 버전만 | 4.18.0만 |
>=4.0.0 | 해당 버전 이상 모두 | 4.0.0 이상 |
Lock 파일:
package-lock.json은 설치된 모든 패키지의 정확한 버전, 무결성 해시, 해결된 URL을 기록한다. 이를 통해 팀 전체가 동일한 의존성 트리를 재현할 수 있다.
{
"name": "my-project",
"lockfileVersion": 3,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-abc123..."
}
}
}
Hoisting(끌어올림):
패키지 A가 lodash 4.17.21에 의존하고, 패키지 B도 lodash 4.17.21에 의존한다면, npm은 lodash를 최상위 node_modules에 한 번만 설치한다. 하지만 서로 다른 버전이 필요하면 하위 node_modules에 중복 설치된다.
팬텀 의존성(Phantom Dependencies):
hoisting으로 인해 직접 의존하지 않은 패키지를 코드에서 import할 수 있게 되는 문제다. package.json에 선언하지 않았는데도 우연히 사용 가능한 상태가 되는 것이다. 이는 나중에 의존성 트리가 바뀌면 갑자기 깨지는 원인이 된다.
1-3. npm vs yarn vs pnpm
세 패키지 매니저의 핵심 차이를 비교해보자.
| 항목 | npm | yarn (Berry) | pnpm |
|---|---|---|---|
| 저장 방식 | node_modules (hoisted) | Plug'n'Play (PnP) | content-addressable store + symlinks |
| Lock 파일 | package-lock.json | yarn.lock | pnpm-lock.yaml |
| 팬텀 의존성 방지 | X | O (strict PnP) | O (격리된 node_modules) |
| 디스크 사용량 | 높음 | 낮음 | 매우 낮음 (하드링크) |
| 워크스페이스 | npm workspaces | yarn workspaces | pnpm workspaces |
| 성능 | 보통 | 좋음 | 매우 좋음 |
pnpm의 핵심 아이디어:
pnpm은 글로벌 content-addressable store에 패키지를 한 번만 저장하고, 프로젝트의 node_modules에는 하드링크(hard link)를 만든다. 동일한 버전의 lodash를 10개 프로젝트에서 사용해도 디스크에는 한 번만 저장된다.
~/.pnpm-store/
v3/
files/
ab/cdef1234... # lodash 4.17.21의 실제 파일
project-a/node_modules/.pnpm/
lodash@4.17.21/
node_modules/
lodash/
index.js --> ~/.pnpm-store/v3/files/ab/cdef1234... (하드링크)
1-4. npm에 패키지 등록하기
자신의 패키지를 npm 레지스트리에 배포하는 단계별 과정이다.
1단계: 계정 생성 및 로그인
npm adduser
# 또는 이미 계정이 있다면
npm login
2단계: 패키지 초기화
mkdir my-awesome-lib
cd my-awesome-lib
npm init
3단계: package.json 완성
{
"name": "my-awesome-lib",
"version": "1.0.0",
"description": "A library that does awesome things",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"keywords": ["awesome", "utility"],
"author": "Your Name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourname/my-awesome-lib"
}
}
4단계: 배포
# 빌드 확인
npm run build
# 배포 전 미리보기 (어떤 파일이 포함되는지 확인)
npm pack --dry-run
# 실제 배포
npm publish
# 스코프가 있는 패키지 (공개)
npm publish --access public
5단계: 버전 관리
# 패치 버전 올리기 (1.0.0 -> 1.0.1)
npm version patch
# 마이너 버전 올리기 (1.0.1 -> 1.1.0)
npm version minor
# 메이저 버전 올리기 (1.1.0 -> 2.0.0)
npm version major
# 배포
npm publish
Part 2: uv 원리 (Python)
2-1. uv란
uv는 Astral에서 Rust로 개발한 초고속 Python 패키지 매니저이자 프로젝트 관리 도구다. pip를 대체하면서도 pip의 10~100배 빠른 속도를 보여준다.
uv가 빠른 이유는 여러 가지다.
- Rust로 작성: 네이티브 바이너리로 컴파일되어 Python 인터프리터의 오버헤드가 없다
- 병렬 다운로드: 의존성 해결과 다운로드를 동시에 수행한다
- 글로벌 캐시: 한 번 다운로드한 패키지는 모든 프로젝트에서 재사용한다
- 최적화된 SAT 솔버: 의존성 그래프를 효율적으로 해결한다
# uv 설치
curl -LsSf https://astral.sh/uv/install.sh | sh
# 프로젝트 생성
uv init my-project
cd my-project
# 의존성 추가
uv add requests flask
# 의존성 동기화 (lock 파일 기반)
uv sync
2-2. pip vs uv vs poetry vs conda
| 항목 | pip | uv | poetry | conda |
|---|---|---|---|---|
| 언어 | Python | Rust | Python | Python/C |
| 의존성 해결 | 백트래킹 | SAT 솔버 | SAT 솔버 | SAT 솔버 |
| Lock 파일 | X (수동 freeze) | uv.lock | poetry.lock | environment.yml |
| 가상환경 관리 | X (별도 venv) | O (내장) | O (내장) | O (내장) |
| 속도 (cold install) | 느림 (기준) | 10~100x 빠름 | 2~5x 빠름 | 느림 |
| 빌드 시스템 | setuptools | 자체 해결 | 자체 빌드 | 자체 빌드 |
| 비-Python 패키지 | X | X | X | O (numpy C 라이브러리 등) |
속도 비교 (실제 벤치마크):
# requests + flask + sqlalchemy 설치 (cold cache)
pip install: 12.4s
poetry install: 8.1s
uv sync: 0.8s # 15x 빠름
2-3. uv의 의존성 해결 알고리즘
uv는 PubGrub 알고리즘을 기반으로 한 SAT 솔버를 사용한다. 이 알고리즘이 하는 일을 단계별로 살펴보자.
1. 의존성 그래프 구축:
프로젝트의 직접 의존성에서 시작하여, 각 패키지의 메타데이터를 읽어 전이적(transitive) 의존성 그래프를 구축한다.
2. 제약 조건 전파(Constraint Propagation):
각 패키지의 버전 요구사항을 제약 조건으로 변환하고, 이를 전파하여 가능한 버전 공간을 줄여나간다.
3. 단위 전파(Unit Propagation):
하나의 가능한 값만 남은 변수가 있으면, 그 값을 확정하고 관련 제약을 업데이트한다.
4. 충돌 기반 학습(Conflict-Driven Clause Learning):
충돌이 발생하면 그 원인을 분석하여 "학습된 절(learned clause)"을 추가한다. 이를 통해 같은 실패를 반복하지 않는다.
예시: A>=1.0 requires B>=2.0, but C<1.5 requires B<2.0
-> 충돌 발생
-> 학습: A>=1.0 AND C<1.5 는 동시에 성립 불가
-> 백트래킹 후 다른 버전 시도
2-4. PyPI에 패키지 등록하기
Python 패키지를 PyPI(Python Package Index)에 등록하는 현대적인 방법이다.
1단계: pyproject.toml 작성
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-python-lib"
version = "1.0.0"
description = "A useful Python library"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0",
]
[project.urls]
Homepage = "https://github.com/yourname/my-python-lib"
Documentation = "https://my-python-lib.readthedocs.io"
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
2단계: 빌드
# 빌드 도구 설치
uv add --dev build
# 빌드 실행
python -m build
# dist/ 디렉토리에 .whl과 .tar.gz 파일이 생성됨
ls dist/
# my_python_lib-1.0.0-py3-none-any.whl
# my_python_lib-1.0.0.tar.gz
3단계: TestPyPI에서 테스트
# twine 설치
uv add --dev twine
# TestPyPI에 업로드
python -m twine upload --repository testpypi dist/*
# 테스트 설치
pip install --index-url https://test.pypi.org/simple/ my-python-lib
4단계: 실제 PyPI에 배포
# PyPI에 업로드
python -m twine upload dist/*
# 이제 누구나 설치 가능
pip install my-python-lib
# 또는
uv add my-python-lib
Trusted Publisher 설정 (GitHub Actions):
name: Publish to PyPI
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1
이 방식을 사용하면 API 토큰 없이도 PyPI에 배포할 수 있다. GitHub의 OIDC 토큰을 통해 인증이 이루어진다.
Part 3: RPM (Red Hat / CentOS / Rocky Linux)
3-1. RPM이란
RPM(Red Hat Package Manager)은 Red Hat 계열 리눅스 배포판의 패키지 관리 시스템이다. 핵심 구성 요소는 다음과 같다.
RPM 파일: .rpm 확장자를 가진 바이너리 패키지 파일이다. 컴파일된 프로그램, 설정 파일, 문서, 설치/제거 스크립트를 포함한다.
RPM 데이터베이스: /var/lib/rpm에 위치하며, 설치된 모든 패키지의 정보를 추적한다.
yum / dnf: RPM의 의존성 해결 문제를 해결하는 상위 레벨 도구다. RPM은 단일 패키지만 설치할 수 있지만, dnf는 의존성을 자동으로 해결하고 원격 저장소에서 패키지를 다운로드한다.
# RPM 직접 사용 (의존성 자동 해결 안 됨)
rpm -ivh package-1.0.0-1.el9.x86_64.rpm
# dnf 사용 (의존성 자동 해결)
dnf install nginx
# 패키지 정보 조회
rpm -qi nginx
# 패키지에 포함된 파일 목록
rpm -ql nginx
Spec 파일: RPM 패키지를 빌드하기 위한 레시피 파일이다. 소스 코드를 어떻게 컴파일하고, 어떤 파일을 어디에 설치하며, 어떤 의존성이 필요한지를 정의한다.
3-2. RPM 패키지 만들기
1단계: 빌드 환경 준비
# 빌드 도구 설치
dnf install rpm-build rpmdevtools
# 빌드 디렉토리 구조 생성
rpmdev-setuptree
# 생성되는 구조:
# ~/rpmbuild/
# BUILD/ - 빌드가 수행되는 디렉토리
# RPMS/ - 빌드된 RPM 파일
# SOURCES/ - 소스 타르볼
# SPECS/ - spec 파일
# SRPMS/ - 소스 RPM 파일
2단계: Spec 파일 작성
Name: myapp
Version: 1.0.0
Release: 1%{?dist}
Summary: My awesome application
License: MIT
URL: https://github.com/yourname/myapp
Source0: %{name}-%{version}.tar.gz
BuildRequires: gcc
BuildRequires: make
Requires: openssl-libs
%description
MyApp is an awesome application that does useful things.
It supports multiple platforms and is easy to configure.
%prep
%autosetup
%build
%configure
%make_build
%install
%make_install
%files
%license LICENSE
%doc README.md
%{_bindir}/myapp
%{_mandir}/man1/myapp.1*
%config(noreplace) %{_sysconfdir}/myapp.conf
%changelog
* Sat Apr 12 2026 Your Name <you@example.com> - 1.0.0-1
- Initial package
3단계: 빌드
# 소스 타르볼을 SOURCES에 복사
cp myapp-1.0.0.tar.gz ~/rpmbuild/SOURCES/
# RPM 빌드 (-ba: 바이너리와 소스 RPM 모두)
rpmbuild -ba ~/rpmbuild/SPECS/myapp.spec
# 빌드된 RPM 확인
ls ~/rpmbuild/RPMS/x86_64/
# myapp-1.0.0-1.el9.x86_64.rpm
4단계: 로컬 레포지토리 만들기
# createrepo 설치
dnf install createrepo_c
# 레포 디렉토리 생성
mkdir -p /var/www/html/myrepo/
# RPM 복사
cp ~/rpmbuild/RPMS/x86_64/myapp-*.rpm /var/www/html/myrepo/
# 레포 메타데이터 생성
createrepo /var/www/html/myrepo/
# /etc/yum.repos.d/myrepo.repo
[myrepo]
name=My Custom Repository
baseurl=http://myserver.example.com/myrepo/
enabled=1
gpgcheck=0
3-3. DEB vs RPM 비교
| 항목 | RPM (Red Hat 계열) | DEB (Debian 계열) |
|---|---|---|
| 배포판 | RHEL, CentOS, Rocky, Fedora | Debian, Ubuntu, Mint |
| 패키지 형식 | .rpm | .deb |
| 저수준 도구 | rpm | dpkg |
| 고수준 도구 | yum / dnf | apt / apt-get |
| 패키지 정의 | spec 파일 | debian/ 디렉토리 (control, rules 등) |
| 빌드 도구 | rpmbuild | dpkg-buildpackage |
| 레포 생성 | createrepo | apt-ftparchive / reprepro |
| 스크립트 단계 | pre/post install/uninstall | preinst/postinst/prerm/postrm |
| 서명 | GPG | GPG (apt-key) |
핵심 차이는 설계 철학에 있다. RPM의 spec 파일은 모든 것을 하나의 파일에 담는 반면, DEB의 debian/ 디렉토리는 역할별로 파일을 분리한다.
Part 4: Homebrew (macOS / Linux)
4-1. Homebrew 원리
Homebrew는 macOS(그리고 Linux)의 비공식 패키지 매니저다. 핵심 개념들을 살펴보자.
Formula: 패키지 설치 방법을 정의하는 Ruby 스크립트다. 소스 URL, 빌드 옵션, 의존성, 설치 과정을 포함한다.
Tap: Formula를 모아놓은 Git 저장소다. 기본 Tap은 homebrew-core이며, 누구나 자신만의 Tap을 만들 수 있다.
Cellar: 패키지가 실제로 설치되는 위치다. macOS에서는 /opt/homebrew/Cellar/ (Apple Silicon) 또는 /usr/local/Cellar/ (Intel)에 위치한다.
Keg-only: Cellar에 설치되지만 PATH에 심볼릭 링크가 생성되지 않는 패키지다. 시스템에 이미 같은 프로그램이 있을 때 충돌을 방지한다. 대표적인 예가 openssl이다.
# 패키지 설치
brew install wget
# 설치 경로 확인
brew --prefix wget
# /opt/homebrew/opt/wget
# Cellar 내부 구조
ls /opt/homebrew/Cellar/wget/1.21.4/
# bin/ etc/ share/
# keg-only 패키지 강제 링크
brew link --force openssl@3
Bottle: 미리 컴파일된 바이너리 패키지다. 소스에서 빌드하는 대신 Bottle을 다운로드하면 설치 속도가 크게 빨라진다. 대부분의 공식 Formula에는 macOS와 Linux용 Bottle이 제공된다.
4-2. Homebrew에 소프트웨어 등록하기
Homebrew에 소프트웨어를 등록하는 세 가지 방법이 있다.
방법 1: 개인 Tap 만들기
가장 간단하고 제약이 적은 방법이다.
1단계: GitHub 리포지토리 생성
homebrew-mytap이라는 이름으로 GitHub 리포지토리를 만든다. Homebrew는 homebrew- 접두사를 Tap 이름으로 인식한다.
2단계: Formula 작성
# Formula/myapp.rb
class Myapp < Formula
desc "My awesome command-line application"
homepage "https://github.com/yourname/myapp"
url "https://github.com/yourname/myapp/archive/refs/tags/v1.0.0.tar.gz"
sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
license "MIT"
depends_on "go" => :build
def install
system "go", "build", *std_go_args(ldflags: "-s -w -X main.version=#{version}")
end
test do
assert_match "myapp version #{version}", shell_output("#{bin}/myapp --version")
end
end
3단계: 사용
# Tap 추가
brew tap yourname/mytap
# 설치
brew install yourname/mytap/myapp
# 또는 Tap 추가 후 직접 설치
brew install myapp
방법 2: homebrew-core에 PR 제출
공식 Homebrew에 포함되려면 엄격한 기준을 충족해야 한다.
필수 조건:
- GitHub에서 30개 이상의 star (또는 충분한 사용자 기반)
- 안정적인 릴리스 태그
- 오픈소스 라이선스
- CI/CD를 통한 자동 빌드
- macOS와 Linux 모두에서 빌드 가능
# homebrew-core 클론 및 Formula 추가
brew tap --force homebrew/core
cd $(brew --repository homebrew/core)
# Formula 생성 헬퍼
brew create https://github.com/yourname/myapp/archive/refs/tags/v1.0.0.tar.gz
# Formula 검증
brew audit --new myapp
brew test myapp
# PR 제출 (GitHub CLI)
gh pr create --title "myapp 1.0.0 (new formula)" --body "Description of the tool..."
방법 3: Cask (GUI 앱) 등록
.app, .dmg, .pkg 형태의 macOS GUI 애플리케이션을 배포할 때 사용한다.
# Casks/myguiapp.rb
cask "myguiapp" do
version "2.1.0"
sha256 "abc123def456..."
url "https://github.com/yourname/myguiapp/releases/download/v#{version}/MyGuiApp-#{version}.dmg"
name "MyGuiApp"
desc "A beautiful GUI application"
homepage "https://myguiapp.example.com"
app "MyGuiApp.app"
zap trash: [
"~/Library/Application Support/MyGuiApp",
"~/Library/Preferences/com.yourname.myguiapp.plist",
]
end
# Cask 설치
brew install --cask myguiapp
4-3. Formula 작성법 상세
Homebrew Formula는 Ruby DSL로 작성된다. 주요 구성 요소를 살펴보자.
class ComplexApp < Formula
desc "A complex application with many build options"
homepage "https://complexapp.dev"
# 안정 버전 소스
url "https://github.com/yourname/complexapp/archive/refs/tags/v2.0.0.tar.gz"
sha256 "deadbeef..."
# HEAD 버전 (개발 중)
head "https://github.com/yourname/complexapp.git", branch: "main"
license "Apache-2.0"
# 빌드 의존성
depends_on "cmake" => :build
depends_on "pkg-config" => :build
# 런타임 의존성
depends_on "openssl@3"
depends_on "sqlite"
# 플랫폼 제한
depends_on :macos
# Python 바인딩 (선택)
option "with-python", "Build Python bindings"
depends_on "python@3.12" if build.with?("python")
def install
args = %W[
--prefix=#{prefix}
--with-openssl=#{Formula["openssl@3"].opt_prefix}
--with-sqlite=#{Formula["sqlite"].opt_prefix}
]
args << "--with-python" if build.with?("python")
system "./configure", *args
system "make", "install"
# 쉘 완성 스크립트 설치
bash_completion.install "completions/complexapp.bash"
zsh_completion.install "completions/_complexapp"
fish_completion.install "completions/complexapp.fish"
end
# 설치 후 안내 메시지
def caveats
<<~EOS
To start complexapp as a service:
brew services start complexapp
EOS
end
# 설치 검증 테스트
test do
assert_match version.to_s, shell_output("#{bin}/complexapp --version")
system "#{bin}/complexapp", "check"
end
end
Formula의 주요 DSL 메서드:
| 메서드 | 용도 | 예시 |
|---|---|---|
url | 소스 다운로드 URL | url "https://..." |
sha256 | 무결성 검증 해시 | sha256 "abc..." |
depends_on | 의존성 선언 | depends_on "openssl@3" |
install | 빌드 및 설치 과정 | system "make", "install" |
test | 설치 검증 | assert_match ... |
prefix | 설치 기본 경로 | /opt/homebrew/Cellar/app/1.0 |
bin | 실행파일 경로 | prefix/"bin" |
etc | 설정파일 경로 | prefix/"etc" |
share | 공유 데이터 경로 | prefix/"share" |
Part 5: 패키지 매니저 대비교
5-1. 종합 비교 매트릭스
| 항목 | npm | PyPI(uv/pip) | RPM(dnf) | Homebrew | APT(deb) | snap | flatpak |
|---|---|---|---|---|---|---|---|
| 대상 | Node.js 라이브러리 | Python 라이브러리 | 시스템 패키지 | CLI/GUI 앱 | 시스템 패키지 | 데스크톱 앱 | 데스크톱 앱 |
| 플랫폼 | 크로스 플랫폼 | 크로스 플랫폼 | RHEL 계열 | macOS/Linux | Debian 계열 | 리눅스 | 리눅스 |
| 레지스트리 | npmjs.com | pypi.org | 벤더 레포 | homebrew-core | 벤더 레포 | snapcraft.io | flathub.org |
| 격리 방식 | node_modules | virtualenv | 없음 (시스템 전역) | Cellar+symlinks | 없음 (시스템 전역) | 샌드박스 | 샌드박스 |
| 의존성 해결 | semver 범위 | SAT 솔버 | libsolv (SAT) | 자체 | APT solver | snap 자체 관리 | 런타임 공유 |
| 자동 업데이트 | X | X | dnf-automatic | brew upgrade | unattended-upgrades | snapd (자동) | X |
| 보안 서명 | npm 서명 | GPG/Sigstore | GPG | 코드서명 (Cask) | GPG | Snap Store 서명 | Flathub 서명 |
| 크기 제한 | 없음 | 없음 | 없음 | 없음 | 없음 | 큼 (번들) | 큼 (런타임) |
5-2. 어떤 패키지 매니저를 사용해야 할까?
JavaScript/TypeScript 라이브러리를 배포하고 싶다면: npm 또는 GitHub Packages를 사용한다.
Python 라이브러리를 배포하고 싶다면: PyPI에 등록하고 uv/pip으로 설치하게 한다.
리눅스 서버에 시스템 레벨 패키지를 배포하고 싶다면: RPM(RHEL 계열) 또는 DEB(Debian 계열) 패키지를 만든다.
macOS용 CLI 도구를 배포하고 싶다면: Homebrew Formula를 작성하여 개인 Tap에 올리거나 homebrew-core에 PR을 제출한다.
크로스 플랫폼 데스크톱 앱을 배포하고 싶다면: snap 또는 flatpak을 고려한다. snap은 자동 업데이트가 내장되어 있고, flatpak은 더 개방적인 생태계를 가진다.
마치며
패키지 매니저는 단순한 설치 도구가 아니다. 의존성 해결이라는 NP-완전 문제를 실용적으로 풀어내고, 수백만 개의 패키지를 안전하게 배포하며, 개발자 생태계의 혈관 역할을 한다.
각 패키지 매니저의 내부 원리를 이해하면 의존성 충돌을 더 빠르게 해결하고, 캐시를 효율적으로 관리하며, 자신의 소프트웨어를 세상에 배포하는 과정이 훨씬 수월해진다.
자신의 프로젝트를 npm, PyPI, Homebrew, RPM 어디에든 등록해보자. 패키지 매니저의 작동 원리를 체감하는 가장 좋은 방법은 직접 패키지를 만들어보는 것이다.
참고 자료
- npm 공식 문서: https://docs.npmjs.com/
- uv 공식 문서: https://docs.astral.sh/uv/
- RPM Packaging Guide: https://rpm-packaging-guide.github.io/
- Homebrew Formula 쿡북: https://docs.brew.sh/Formula-Cookbook
- PyPI 배포 가이드: https://packaging.python.org/
- PubGrub 알고리즘: https://nex3.medium.com/pubgrub-2fb6470504f
- pnpm 공식 문서: https://pnpm.io/
The Complete Guide to Package Managers — How npm, uv, RPM, and Homebrew Work and How to Publish Software
Introduction
Package managers are the backbone of software development infrastructure. Behind the everyday commands we use, like npm install, pip install, brew install, and yum install, lie complex mechanisms for dependency resolution, version management, and binary distribution.
In this post, we dissect the internals of four major package managers and walk through how to register your own software in each ecosystem.
Part 1: npm Internals (JavaScript / Node.js)
1-1. What Is npm?
npm (Node Package Manager) is the standard package manager for the JavaScript ecosystem. It has three core components.
Registry: The central repository where all packages are stored. Hosted at registry.npmjs.org and distributed worldwide via CDN. As of 2026, over 3 million packages are registered.
package.json: The file that declares project metadata and dependencies.
{
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.21"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
node_modules: The directory where dependencies are actually installed. npm uses a nested structure by default but flattens it as much as possible through hoisting.
1-2. Dependency Resolution
npm's dependency resolution can be explained through four key concepts.
Semver (Semantic Versioning):
npm follows semver rules. Versions use the MAJOR.MINOR.PATCH format, and each range specifier has a specific meaning.
| Specifier | Meaning | Example |
|---|---|---|
^4.18.0 | Fixed MAJOR, allow MINOR and PATCH | 4.18.0 up to but not 5.0.0 |
~4.17.21 | Fixed MAJOR and MINOR, allow PATCH only | 4.17.21 up to but not 4.18.0 |
4.18.0 | Exactly that version | 4.18.0 only |
>=4.0.0 | That version and above | 4.0.0 and higher |
Lock Files:
package-lock.json records the exact version, integrity hash, and resolved URL of every installed package. This ensures the entire team can reproduce identical dependency trees.
{
"name": "my-project",
"lockfileVersion": 3,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-abc123..."
}
}
}
Hoisting:
If package A depends on lodash 4.17.21 and package B also depends on lodash 4.17.21, npm installs lodash only once in the top-level node_modules. However, if different versions are required, duplicates are installed in nested node_modules directories.
Phantom Dependencies:
Because of hoisting, you can import packages that your project does not directly depend on. A package may be usable simply because it was hoisted by another dependency, even though it is not declared in your package.json. This leads to breakages when the dependency tree changes.
1-3. npm vs yarn vs pnpm
Here is a comparison of the three main package managers.
| Feature | npm | yarn (Berry) | pnpm |
|---|---|---|---|
| Storage | node_modules (hoisted) | Plug'n'Play (PnP) | content-addressable store + symlinks |
| Lock file | package-lock.json | yarn.lock | pnpm-lock.yaml |
| Phantom dep prevention | No | Yes (strict PnP) | Yes (isolated node_modules) |
| Disk usage | High | Low | Very low (hard links) |
| Workspaces | npm workspaces | yarn workspaces | pnpm workspaces |
| Performance | Average | Good | Very good |
The Core Idea of pnpm:
pnpm stores packages once in a global content-addressable store and creates hard links in each project's node_modules. Even if 10 projects use the same version of lodash, it is stored only once on disk.
~/.pnpm-store/
v3/
files/
ab/cdef1234... # actual files of lodash 4.17.21
project-a/node_modules/.pnpm/
lodash@4.17.21/
node_modules/
lodash/
index.js --> ~/.pnpm-store/v3/files/ab/cdef1234... (hard link)
1-4. Publishing a Package to npm
Here is the step-by-step process for publishing your package to the npm registry.
Step 1: Create an Account and Log In
npm adduser
# or if you already have an account
npm login
Step 2: Initialize the Package
mkdir my-awesome-lib
cd my-awesome-lib
npm init
Step 3: Complete package.json
{
"name": "my-awesome-lib",
"version": "1.0.0",
"description": "A library that does awesome things",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"keywords": ["awesome", "utility"],
"author": "Your Name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourname/my-awesome-lib"
}
}
Step 4: Publish
# Verify the build
npm run build
# Preview before publishing (check which files are included)
npm pack --dry-run
# Actually publish
npm publish
# For scoped packages (public)
npm publish --access public
Step 5: Version Management
# Bump patch version (1.0.0 -> 1.0.1)
npm version patch
# Bump minor version (1.0.1 -> 1.1.0)
npm version minor
# Bump major version (1.1.0 -> 2.0.0)
npm version major
# Publish
npm publish
Part 2: uv Internals (Python)
2-1. What Is uv?
uv is an ultra-fast Python package manager and project management tool built in Rust by Astral. It replaces pip while delivering 10x to 100x faster performance.
The reasons uv is fast include:
- Written in Rust: Compiled to a native binary with no Python interpreter overhead
- Parallel downloads: Dependency resolution and downloads happen concurrently
- Global cache: Packages downloaded once are reused across all projects
- Optimized SAT solver: Efficiently resolves the dependency graph
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create a project
uv init my-project
cd my-project
# Add dependencies
uv add requests flask
# Sync dependencies (based on lock file)
uv sync
2-2. pip vs uv vs poetry vs conda
| Feature | pip | uv | poetry | conda |
|---|---|---|---|---|
| Language | Python | Rust | Python | Python/C |
| Dependency resolution | Backtracking | SAT solver | SAT solver | SAT solver |
| Lock file | None (manual freeze) | uv.lock | poetry.lock | environment.yml |
| Virtual env management | No (separate venv) | Yes (built-in) | Yes (built-in) | Yes (built-in) |
| Speed (cold install) | Slow (baseline) | 10-100x faster | 2-5x faster | Slow |
| Build system | setuptools | Self-resolving | Self-building | Self-building |
| Non-Python packages | No | No | No | Yes (numpy C libs, etc.) |
Speed comparison (real benchmark):
# Installing requests + flask + sqlalchemy (cold cache)
pip install: 12.4s
poetry install: 8.1s
uv sync: 0.8s # 15x faster
2-3. How uv Resolves Dependencies
uv uses a SAT solver based on the PubGrub algorithm. Here is the step-by-step process.
1. Build the Dependency Graph:
Starting from the project's direct dependencies, uv reads each package's metadata to construct the full transitive dependency graph.
2. Constraint Propagation:
Each package's version requirements are converted into constraints, which are propagated to narrow down the possible version space.
3. Unit Propagation:
When a variable has only one possible value remaining, that value is fixed and related constraints are updated.
4. Conflict-Driven Clause Learning (CDCL):
When a conflict occurs, uv analyzes its root cause and adds a "learned clause" to avoid repeating the same failure.
Example: A>=1.0 requires B>=2.0, but C<1.5 requires B<2.0
-> Conflict detected
-> Learned: A>=1.0 AND C<1.5 cannot hold simultaneously
-> Backtrack and try different versions
2-4. Publishing a Package to PyPI
Here is the modern approach to registering a Python package on PyPI (Python Package Index).
Step 1: Write pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-python-lib"
version = "1.0.0"
description = "A useful Python library"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0",
]
[project.urls]
Homepage = "https://github.com/yourname/my-python-lib"
Documentation = "https://my-python-lib.readthedocs.io"
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
Step 2: Build
# Install build tools
uv add --dev build
# Run the build
python -m build
# .whl and .tar.gz files are created in the dist/ directory
ls dist/
# my_python_lib-1.0.0-py3-none-any.whl
# my_python_lib-1.0.0.tar.gz
Step 3: Test on TestPyPI
# Install twine
uv add --dev twine
# Upload to TestPyPI
python -m twine upload --repository testpypi dist/*
# Test installation
pip install --index-url https://test.pypi.org/simple/ my-python-lib
Step 4: Publish to PyPI
# Upload to PyPI
python -m twine upload dist/*
# Now anyone can install it
pip install my-python-lib
# or
uv add my-python-lib
Trusted Publisher Setup (GitHub Actions):
name: Publish to PyPI
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1
This approach lets you publish to PyPI without API tokens, using GitHub OIDC tokens for authentication.
Part 3: RPM (Red Hat / CentOS / Rocky Linux)
3-1. What Is RPM?
RPM (Red Hat Package Manager) is the package management system for Red Hat-based Linux distributions. Its core components are as follows.
RPM Files: Binary package files with the .rpm extension. They contain compiled programs, configuration files, documentation, and install/uninstall scripts.
RPM Database: Located at /var/lib/rpm, it tracks information about all installed packages.
yum / dnf: Higher-level tools that solve RPM's dependency resolution problem. RPM can only install individual packages, but dnf automatically resolves dependencies and downloads packages from remote repositories.
# Direct RPM usage (no automatic dependency resolution)
rpm -ivh package-1.0.0-1.el9.x86_64.rpm
# Using dnf (automatic dependency resolution)
dnf install nginx
# Query package info
rpm -qi nginx
# List files in a package
rpm -ql nginx
Spec Files: Recipe files for building RPM packages. They define how to compile source code, which files to install where, and what dependencies are needed.
3-2. Building an RPM Package
Step 1: Prepare the Build Environment
# Install build tools
dnf install rpm-build rpmdevtools
# Create the build directory structure
rpmdev-setuptree
# The resulting structure:
# ~/rpmbuild/
# BUILD/ - where builds are performed
# RPMS/ - built RPM files
# SOURCES/ - source tarballs
# SPECS/ - spec files
# SRPMS/ - source RPM files
Step 2: Write a Spec File
Name: myapp
Version: 1.0.0
Release: 1%{?dist}
Summary: My awesome application
License: MIT
URL: https://github.com/yourname/myapp
Source0: %{name}-%{version}.tar.gz
BuildRequires: gcc
BuildRequires: make
Requires: openssl-libs
%description
MyApp is an awesome application that does useful things.
It supports multiple platforms and is easy to configure.
%prep
%autosetup
%build
%configure
%make_build
%install
%make_install
%files
%license LICENSE
%doc README.md
%{_bindir}/myapp
%{_mandir}/man1/myapp.1*
%config(noreplace) %{_sysconfdir}/myapp.conf
%changelog
* Sat Apr 12 2026 Your Name <you@example.com> - 1.0.0-1
- Initial package
Step 3: Build
# Copy source tarball to SOURCES
cp myapp-1.0.0.tar.gz ~/rpmbuild/SOURCES/
# Build RPM (-ba: both binary and source RPM)
rpmbuild -ba ~/rpmbuild/SPECS/myapp.spec
# Check the built RPM
ls ~/rpmbuild/RPMS/x86_64/
# myapp-1.0.0-1.el9.x86_64.rpm
Step 4: Create a Local Repository
# Install createrepo
dnf install createrepo_c
# Create the repo directory
mkdir -p /var/www/html/myrepo/
# Copy RPMs
cp ~/rpmbuild/RPMS/x86_64/myapp-*.rpm /var/www/html/myrepo/
# Generate repo metadata
createrepo /var/www/html/myrepo/
# /etc/yum.repos.d/myrepo.repo
[myrepo]
name=My Custom Repository
baseurl=http://myserver.example.com/myrepo/
enabled=1
gpgcheck=0
3-3. DEB vs RPM Comparison
| Feature | RPM (Red Hat family) | DEB (Debian family) |
|---|---|---|
| Distributions | RHEL, CentOS, Rocky, Fedora | Debian, Ubuntu, Mint |
| Package format | .rpm | .deb |
| Low-level tool | rpm | dpkg |
| High-level tool | yum / dnf | apt / apt-get |
| Package definition | spec file | debian/ directory (control, rules, etc.) |
| Build tool | rpmbuild | dpkg-buildpackage |
| Repo creation | createrepo | apt-ftparchive / reprepro |
| Script stages | pre/post install/uninstall | preinst/postinst/prerm/postrm |
| Signing | GPG | GPG (apt-key) |
The key difference lies in design philosophy. RPM's spec file puts everything in one file, while DEB's debian/ directory separates concerns into different files by role.
Part 4: Homebrew (macOS / Linux)
4-1. How Homebrew Works
Homebrew is the unofficial package manager for macOS (and Linux). Here are the core concepts.
Formula: A Ruby script that defines how to install a package. It includes the source URL, build options, dependencies, and installation steps.
Tap: A Git repository that contains a collection of Formulae. The default Tap is homebrew-core, and anyone can create their own Tap.
Cellar: The location where packages are actually installed. On macOS, this is /opt/homebrew/Cellar/ (Apple Silicon) or /usr/local/Cellar/ (Intel).
Keg-only: A package installed in the Cellar but not symlinked into PATH. This prevents conflicts when the system already has the same program. A classic example is openssl.
# Install a package
brew install wget
# Check the install path
brew --prefix wget
# /opt/homebrew/opt/wget
# Cellar internal structure
ls /opt/homebrew/Cellar/wget/1.21.4/
# bin/ etc/ share/
# Force-link a keg-only package
brew link --force openssl@3
Bottle: A pre-compiled binary package. Downloading a Bottle instead of building from source dramatically speeds up installation. Most official Formulae provide Bottles for both macOS and Linux.
4-2. Registering Software with Homebrew
There are three ways to register software with Homebrew.
Method 1: Create a Personal Tap
The simplest method with the fewest restrictions.
Step 1: Create a GitHub Repository
Create a repository named homebrew-mytap. Homebrew recognizes the homebrew- prefix as a Tap name.
Step 2: Write a Formula
# Formula/myapp.rb
class Myapp < Formula
desc "My awesome command-line application"
homepage "https://github.com/yourname/myapp"
url "https://github.com/yourname/myapp/archive/refs/tags/v1.0.0.tar.gz"
sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
license "MIT"
depends_on "go" => :build
def install
system "go", "build", *std_go_args(ldflags: "-s -w -X main.version=#{version}")
end
test do
assert_match "myapp version #{version}", shell_output("#{bin}/myapp --version")
end
end
Step 3: Use It
# Add the Tap
brew tap yourname/mytap
# Install
brew install yourname/mytap/myapp
# Or, after adding the Tap, install directly
brew install myapp
Method 2: Submit a PR to homebrew-core
Getting included in the official Homebrew requires meeting strict criteria.
Requirements:
- At least 30 GitHub stars (or a substantial user base)
- Stable release tags
- An open-source license
- Automated builds via CI/CD
- Must build on both macOS and Linux
# Clone homebrew-core and add a Formula
brew tap --force homebrew/core
cd $(brew --repository homebrew/core)
# Formula creation helper
brew create https://github.com/yourname/myapp/archive/refs/tags/v1.0.0.tar.gz
# Validate the Formula
brew audit --new myapp
brew test myapp
# Submit PR (GitHub CLI)
gh pr create --title "myapp 1.0.0 (new formula)" --body "Description of the tool..."
Method 3: Cask (GUI Applications)
Used for distributing macOS GUI applications in .app, .dmg, or .pkg format.
# Casks/myguiapp.rb
cask "myguiapp" do
version "2.1.0"
sha256 "abc123def456..."
url "https://github.com/yourname/myguiapp/releases/download/v#{version}/MyGuiApp-#{version}.dmg"
name "MyGuiApp"
desc "A beautiful GUI application"
homepage "https://myguiapp.example.com"
app "MyGuiApp.app"
zap trash: [
"~/Library/Application Support/MyGuiApp",
"~/Library/Preferences/com.yourname.myguiapp.plist",
]
end
# Install a Cask
brew install --cask myguiapp
4-3. Formula Authoring in Detail
Homebrew Formulae are written in a Ruby DSL. Here are the key components.
class ComplexApp < Formula
desc "A complex application with many build options"
homepage "https://complexapp.dev"
# Stable version source
url "https://github.com/yourname/complexapp/archive/refs/tags/v2.0.0.tar.gz"
sha256 "deadbeef..."
# HEAD version (in development)
head "https://github.com/yourname/complexapp.git", branch: "main"
license "Apache-2.0"
# Build dependencies
depends_on "cmake" => :build
depends_on "pkg-config" => :build
# Runtime dependencies
depends_on "openssl@3"
depends_on "sqlite"
# Platform restriction
depends_on :macos
def install
args = %W[
--prefix=#{prefix}
--with-openssl=#{Formula["openssl@3"].opt_prefix}
--with-sqlite=#{Formula["sqlite"].opt_prefix}
]
system "./configure", *args
system "make", "install"
# Install shell completions
bash_completion.install "completions/complexapp.bash"
zsh_completion.install "completions/_complexapp"
fish_completion.install "completions/complexapp.fish"
end
# Post-install message
def caveats
<<~EOS
To start complexapp as a service:
brew services start complexapp
EOS
end
# Installation verification test
test do
assert_match version.to_s, shell_output("#{bin}/complexapp --version")
system "#{bin}/complexapp", "check"
end
end
Key Formula DSL Methods:
| Method | Purpose | Example |
|---|---|---|
url | Source download URL | url "https://..." |
sha256 | Integrity verification hash | sha256 "abc..." |
depends_on | Dependency declaration | depends_on "openssl@3" |
install | Build and install steps | system "make", "install" |
test | Installation verification | assert_match ... |
prefix | Base install path | /opt/homebrew/Cellar/app/1.0 |
bin | Executable path | prefix/"bin" |
etc | Config file path | prefix/"etc" |
share | Shared data path | prefix/"share" |
Part 5: The Grand Comparison
5-1. Comprehensive Comparison Matrix
| Feature | npm | PyPI (uv/pip) | RPM (dnf) | Homebrew | APT (deb) | snap | flatpak |
|---|---|---|---|---|---|---|---|
| Target | Node.js libraries | Python libraries | System packages | CLI/GUI apps | System packages | Desktop apps | Desktop apps |
| Platform | Cross-platform | Cross-platform | RHEL family | macOS/Linux | Debian family | Linux | Linux |
| Registry | npmjs.com | pypi.org | Vendor repos | homebrew-core | Vendor repos | snapcraft.io | flathub.org |
| Isolation | node_modules | virtualenv | None (system-wide) | Cellar + symlinks | None (system-wide) | Sandbox | Sandbox |
| Dep resolution | semver ranges | SAT solver | libsolv (SAT) | Built-in | APT solver | Self-managed | Runtime sharing |
| Auto-update | No | No | dnf-automatic | brew upgrade | unattended-upgrades | snapd (auto) | No |
| Security signing | npm signatures | GPG/Sigstore | GPG | Code signing (Cask) | GPG | Snap Store signing | Flathub signing |
| Size concern | None | None | None | None | None | Large (bundled) | Large (runtime) |
5-2. Which Package Manager Should You Use?
To distribute a JavaScript/TypeScript library: Use npm or GitHub Packages.
To distribute a Python library: Register on PyPI and let users install with uv or pip.
To distribute system-level packages for Linux servers: Build RPM packages (for RHEL family) or DEB packages (for Debian family).
To distribute a CLI tool for macOS: Write a Homebrew Formula and publish it in a personal Tap or submit a PR to homebrew-core.
To distribute a cross-platform desktop application: Consider snap or flatpak. snap has built-in auto-updates, while flatpak offers a more open ecosystem.
Conclusion
Package managers are far more than simple installation tools. They solve the NP-complete problem of dependency resolution in practical ways, safely distribute millions of packages, and serve as the circulatory system of the developer ecosystem.
Understanding the internals of each package manager helps you resolve dependency conflicts faster, manage caches more efficiently, and makes the process of distributing your own software much smoother.
Try registering your project on npm, PyPI, Homebrew, or RPM. The best way to experience how a package manager works is to build and publish a package yourself.
References
- npm Official Docs: https://docs.npmjs.com/
- uv Official Docs: https://docs.astral.sh/uv/
- RPM Packaging Guide: https://rpm-packaging-guide.github.io/
- Homebrew Formula Cookbook: https://docs.brew.sh/Formula-Cookbook
- PyPI Publishing Guide: https://packaging.python.org/
- PubGrub Algorithm: https://nex3.medium.com/pubgrub-2fb6470504f
- pnpm Official Docs: https://pnpm.io/