(여담) uv 패키지 관리자

Pt J·2026년 4월 9일
post-thumbnail

pip에서 uv로 갈아타려고 찾아보다가 이 게시물을 발견했다면
맨 아래 여담 부분 확인 바란다.

uv 패키지 관리자

지금까지 우리는 pip 패키지 관리자를 사용하여 프로젝트를 관리했다.
그런데 Gemini 녀석이 프로젝트 관리와 관련해서
더 알아보아야 할 게 있다면 설명해 달라고 하니까 그제서야
다음과 같은 이야기를 한다.

  1. 현대적인 의존성 관리 (uv & Cargo)

파이썬 생태계에서는 최근 uv라는 도구가 표준으로 자리 잡고 있습니다. [cite: 2026-03-20]

현대적 방식: pip 대신 Rust로 작성된 초고속 패키지 매니저인 uv를 사용하여 pyproject.toml로 의존성을 관리하세요. [cite: 2026-03-20]

이점: Rust의 Cargo와 철학이 비슷하여 패키지 설치 속도가 압도적이며, 프로젝트별 가상 환경 관리가 매우 견고합니다. [cite: 2026-03-20]

처음 듣는 녀석인데, 하고 살펴보니 2024년에 출시되어
Python 생태계의 새로운 사실상 표준으로서 자리 잡아가고 있는 녀석이라고 한다.

Rust로 작성되어 매우 빠르고, (역시 Rust!)
기존에 여러 도구로 분산되어 있던 핵심 기능들을 단일 CLI로 통합하여 제공하고,
여러 가지 현대적인 기법과 최적화 알고리즘이 적용되어 있다고...

Gemini 녀석, 그걸 왜 이제 알려주는 거람.
오래된 버전 제시해 주는 거라거나 내가 인지한 이슈에 대해서는
보완해서 예제를 수정해 달라고 요청하며 학습하고 있었는데
uv는 내가 이 분야를 완전히 등지고 있던 사이에 출시된 녀석이라
전혀 인지하지 못하고 있었다.

하여간 이런 녀석을 인지한 순간 굳이 pip를 사용할 이유는 없지.
작업공간 생성 프로세스를 수정해 보자.

uv 명령어

먼저 uv를 설치해야 한다.
명령어 한 줄이면 금방 설치된다.

~$ curl -LsSf https://astral.sh/uv/install.sh | sh
downloading uv 0.11.5 aarch64-apple-darwin
installing to /Users/edenjint3927/.local/bin
  uv
  uvx
everything's installed!

To add $HOME/.local/bin to your PATH, either restart your shell or run:

    source $HOME/.local/bin/env (sh, bash, zsh)
    source $HOME/.local/bin/env.fish (fish)

(우리는 uv init 를 사용하지 않을 거지만)
uv init 명령으로 프로젝트를 초기화한 뒤
uv add 로 필요한 패키지를 설치하면
자동으로 가상환경을 위한 .venv 디렉토리가 생성되며
이 프로젝트에서 사용 가능하게 로컬로 설치된다.

런타임에서 필요한 패키지는 그냥 uv add 로 설치하면 되고
빌드 타임 도구는 런타임까지 남겨 놓으면 프로젝트가 불필요하게 커지므로
uv add --dev 로 설치하여 빌드 타임에만 필요함을 명시한다.

기존에 추가한 패키지를 제거하고 싶으면 uv remove 를 사용하면 된다.

uv add 로 설치한 패키지들의 정보는 pyproject.toml 파일에 기록되며
uv sync 를 통해 현재 설치된 버전과 기록된 버전이 일치하는지 검증하고
로컬에 설치되지 않은 패키지를 설치할 수 있다.
나중에 프로젝트를 템플릿으로 복제해서 사용할 때는
uv add 과정을 생략하고 uv sync 를 통해
pyproject.toml 파일에 기록된 패키지를 설치하면 된다.

프로젝트를 처음 초기화하면 다음과 같은 구조가 자동 생성된다.

~/project$ ls -a
.		..		.git		.gitignore	.python-version	main.py		pyproject.toml	README.md

uv 환경에서는 source .venv/bin/activate 와 같은 명령어를 통해
가상환경을 실행하여 직접 들어갈 필요 없이
uv run 명령어로 가상환경에서 Python을 실행할 수 있다.

uv run main.py 로 Python 서버를 실행할 수 있다지만
우리는 바닐라 Python 서버를 사용할 게 아니므로 그 실습은 패스한다.
언젠가 Python 테스트 코드를 작성하고 나서야
uv run test.py 라는 식으로 스크립트를 실행하게 되지 않을까.

Maturin 빌드는 마찬가지로 uv run 을 붙여
uv run maturin develop 로 실행하며
uvicorn 실행도 uv run uvicorn app.main:app 로 실행한다.

파일 상단에 #! /usr/bin/env uv run 같은
Shebang을 넣어서 실행 권한만 주면 uv run 을 생략할 수도 있다.

IDE 환경에서는 에디터 우측 하단의 파이썬 인터프리터 설정에서
프로젝트 폴더 내의 .venv/bin/python 을 지정해 주어야 한다고 하는데
프로젝터 규모가 커지기 전까지는 zsh+vim 환경에서 실습할 것이므로
그냥 알아만 두도록 하자.

앞서 우리는 uv init 을 사용하지 않을 거라고 했는데,
uv init 으로 프로젝트의 뼈대를 잡은 후에 maturin init 을 하고자 하면
이미 생성된 프로젝트에는 maturin init 을 못 한다고 오류가 날 테니
uvx maturin init 으로 프로젝트 구조를 잡고 시작한다.
uvx 는 아직 uv init 하지 않은 프로젝트에서
특정 패키지를 임시로 사용하고 보내준다.
uvx maturin init 을 해주었으면 uv init 는 생략한다.
둘 다 pyproject.toml 를 생성하여 설계도를 만드는 역할이라
(그러면서 부수적인 파일들도 생성하지만)
중복해서 사용할 경우 충돌이 일어난다.

작업을 하다 환경이 꼬일 경우
rm -rf target .venv 실행 후 uv sync 하여
패키지를 다시 설치할 수 있다.

작업공간 생성 및 구조 확인

데이터베이스를 사용하는 프로젝트의 템플릿 만들었던 것부터
uv 활용으로 바꾸면 좋을 것 같다.
그 전의 예제들은 어차피 지난 예제니까 그대로 두고
지금부터 uv를 사용하면 되는 거니까.

~/workspace$ mkdir db-template & cd db-template
~/workspace/db-template$ uvx maturin init # 프로젝트 초기화
~/workspace/db-template$ # 선택지 중 기본값인 PyO3 선택
~/workspace/db-template$ uv add fastapi uvicorn orjson # 실행 의존성 추가
~/workspace/db-template$ uv add --dev maturin # 개발 의존성 추가
~/workspace/db-template$ uv sync # 의존성 검증 및 누락된 패키지 설치
~/workspace/db-template$ mkdir app && touch app/main.py # Python 파일 직접 생성
~/workspace/db-template$ tree -a -I .venv -I target
.
├── .github
│   └── workflows
│       └── CI.yml
├── .gitignore
├── app
│   └── main.py
├── Cargo.lock
├── Cargo.toml
├── pyproject.toml
├── src
│   └── lib.rs
└── uv.lock

사실 여기서는 기존에 작성한 코드를 가져올 것이기 때문에
app/main.py 를 생성하지 않아도 무방하지만
전체적인 작업 공간 확인을 위해 생성했다.

uv adduv add --dev 를 통해 의존성 패키지를 설치하면
다음과 같이 pyproject.toml 파일의 적절한 위치에 해당 내용이 자동으로 기록된다.

pyproject.toml

[build-system]
requires = ["maturin>=1.12,<2.0"]
build-backend = "maturin"

[project]
name = "db-template"
requires-python = ">=3.8"
classifiers = [
    "Programming Language :: Rust",
    "Programming Language :: Python :: Implementation :: CPython",
    "Programming Language :: Python :: Implementation :: PyPy",
]
dynamic = ["version"]
dependencies = [
    "fastapi>=0.124.4",
    "orjson>=3.10.15",
    "uvicorn>=0.33.0",
]

[dependency-groups]
dev = [
    "maturin>=1.12.6",
]

코드 이전

Cargo.toml

[lib]name 을 늘 하던 대로 "rust_engine" 으로 수정하고
기존 프로젝트의 [dev] 부분을 가져온다.

Cargo.toml

[package]
name = "db-template"
version = "0.1.0"
edition = "2024"

[lib]
name = "rust_engine"
crate-type = ["cdylib"]

[dependencies]
pyo3 = "0.28.0"
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "macros"] }
tokio = { version = "1.43", features = ["full"] }
tracing-appender = "0.2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
dotenvy = "0.15"

Rust 코드 및 Python 코드

기존 프로젝트에 있던 것을 긁어오면 된다.

~/workspace/db-template$ cp ../db-connection/src/* src
~/workspace/db-template$ cp ../db-connection/app/* app

Docker와 관련된 파일도 추가로 긁어오도록 하자.

~/workspace/db-template$ cp ../db-connection/.env .
~/workspace/db-template$ cp ../db-connection/docker-compose.yml .

빌드 및 실행

~/workspace/db-template$ docker compose up -d # PostgreSQL docker 서비스 실행
~/workspace/db-template$ uv run maturin develop # uv 가상환경에서 Rust 컴파일
~/workspace/db-template$ uv run uvicorn app.main:app --reload # uv 가상환경에서 서버 실행
INFO:     Started reloader process [6247] using StatReload
INFO:     Started server process [6249]
INFO:     Waiting for application startup.
INFO:     Rust 엔진 초기화 중...
2026-04-09T00:53:24.617492Z  INFO ThreadId(17) connect: rust_engine::db: PostgreSQL 18 연결 풀 초기화 완료
INFO:     엔진 초기화 및 DB 연결 성공
INFO:     Application startup complete.
^CINFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     서버 종료 감지: 자원 정리 중...
2026-04-09T00:53:38.260590Z  INFO ThreadId(17) rust_engine: 엔진 종료 절차 시작
2026-04-09T00:53:38.260906Z  INFO ThreadId(17) close: rust_engine::db: PostgreSQL 18 연결 안전하게 종료
INFO:     모든 연결 안전하게 종료
INFO:     Application shutdown complete.
INFO:     Finished server process [6249]
INFO:     Stopping reloader process [6247]

기존 실습에서처럼 잘 작동하는 것을 확인할 수 있다.

여담

사실 이렇게 프로젝트를 새로 만들어서 복사해 올 거 없이
기존 프로젝트에서 uv adduv add --dev 명령어로
pyproject.toml 파일에 의존성을 명시하고
바로 uv run uvicorn app.main:app --reload 해도
정상적으로 작동한다.

하지만 uvx maturin init 부터 시작하여 작업 환경을 구축하는 것도
필요한 과정이니 여기서는 처음부터 프로젝트를 다시 구축하는 걸로 진행했다.

만약 기존 프로젝트가 존재하고 pip에서 uv로 갈아타려고 하는 거라면
번거롭게 새로 만들 필요 없이 uv adduv run 을 사용하자.
(uv run 은 내부적으로 uv sync 를 먼저 실행한다.)

profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다

0개의 댓글