2026.03.18 기준 / uv 0.6.x 기준
Poetry uv 마이그레이션, 직접 해봤더니 여기서 막혔습니다
Poetry에서 uv로 마이그레이션하면 속도 문제가 단번에 해결된다고 알려져 있습니다. 실제로 uv는 pip 대비 최대 115배 빠르다는 공식 벤치마크가 있고, 대부분의 경우 명령어 한 줄로 전환됩니다. 그런데 막상 해보면, 세 가지 지점에서 생각지도 못한 문제가 터집니다. 기존 블로그에서는 잘 다루지 않는 버전 드리프트, 캐시 폭탄, uvx 함정을 공식 문서와 실측 데이터로 같이 짚어보겠습니다.
uv 0.6.x
Poetry → uv
2026.03 확인
Poetry와 uv, 왜 pyproject.toml이 충돌할까요
결론부터 말씀드리면, 두 도구는 pyproject.toml을 쓰는 방식이 근본적으로 다릅니다. Poetry는 [tool.poetry.dependencies]라는 독자 섹션에 의존성을 선언하는 반면, uv는 Python 표준 명세인 PEP 621의 [project] 섹션을 씁니다. 단순히 형식이 다른 문제가 아닙니다. 빌드 시스템(build-system)도 Poetry는 poetry-core를 쓰지만, uv는 hatchling 등 표준 호환 백엔드로 교체해야 합니다. (출처: Python PEP 621 공식 문서)
이게 왜 문제냐면, 파일을 손으로 옮기다가 섹션 구조가 한 줄만 잘못 들어가도 uv sync가 전혀 다른 오류 메시지를 뱉습니다. uv의 resolver 자체는 훌륭한 오류 메시지를 제공하지만, 설정 파일 형식 불일치에서 비롯된 오류는 처음 보는 분들에게 꽤 당황스럽게 보입니다. 다행히 이 부분은 자동화 도구로 거의 해결됩니다 — 다음 섹션에서 바로 다룹니다.
💡 공식 발표문과 실제 마이그레이션 흐름을 같이 놓고 보니 이런 차이가 보였습니다 — uv가 PEP 621을 따른다는 점은 홍보 포인트로만 언급되지만, 실제로는 이게 기존 Poetry 프로젝트를 그냥 실행할 수 없는 직접적인 이유입니다. 표준 준수가 곧 기존 도구와의 단절을 의미합니다.
명령어 한 줄로 되는 이야기, 사실은 이렇게 됩니다
마이그레이션은 크게 두 갈래입니다. 첫 번째는 커뮤니티에서 만든 migrate-to-uv 도구를 쓰는 방법입니다. uv가 이미 설치돼 있다면 별도 설치 없이 바로 실행됩니다. (출처: uv 공식 문서 — Projects)
# 1단계: Poetry 설정을 uv 표준 형식으로 자동 변환 uvx migrate-to-uv # 2단계: uv.lock 생성 (poetry.lock은 이 시점에 삭제됨) uv lock # 3단계: 가상환경 동기화 uv sync
두 번째는 pdm import를 이용한 방법입니다. 자동화 도구가 실패하거나 섬세하게 제어해야 하는 상황에서 씁니다. uvx pdm import pyproject.toml을 실행한 뒤, 파일 안에 남아 있는 [tool.poetry...] 섹션을 직접 지우고 [tool.pdm.dev-dependencies]를 [dependency-groups]로 바꿔줘야 합니다. (출처: mrlatte.net — Poetry to uv 마이그레이션 가이드, 2026.01)
여기까지만 보면 정말 간단해 보입니다. 그런데 이 명령어들이 끝난 직후 조용히 일어나는 일이 있습니다. 다음 섹션이 핵심입니다.
버전이 바뀌어도 모릅니다 — Version Drift 함정
migrate-to-uv 실행 직후, 패키지 버전이 달라져 있을 수 있습니다
이게 실제로 가장 많은 사람이 놓치는 부분입니다. uv와 Poetry는 의존성 해결 알고리즘이 다릅니다. migrate-to-uv를 실행하면 poetry.lock이 삭제되고, uv가 pyproject.toml의 범위 조건을 기준으로 새로 의존성을 해결합니다. 즉, requests^2.28.0이라고 선언돼 있으면 uv는 현시점 최신 호환 버전을 새로 고릅니다. 기존에 requests==2.28.2로 고정돼 있던 것이 갑자기 requests==2.32.3으로 바뀔 수 있다는 뜻입니다. (출처: mrlatte.net — Poetry to uv 마이그레이션 가이드)
💡 이 내용이 의미하는 바는 단순합니다 — 스테이징에서는 잘 됐는데 프로덕션에서 터지는 상황이 버전을 바꾸지 않았는데도 발생할 수 있습니다. 마이그레이션 자체가 배포 리스크입니다.
마이그레이션 전에 버전을 먼저 고정하는 방법
해결책은 마이그레이션 전에 poetry.lock에서 정확한 버전을 읽어 pyproject.toml에 하드코딩하는 것입니다. 아래 스크립트를 실행하면 poetry.lock 기준으로 각 패키지를 정확한 버전으로 고정해 줍니다.
# 실행 전: pip install toml
from pathlib import Path
import toml
lock_data = toml.load(Path("poetry.lock"))
proj_data = toml.load(Path("pyproject.toml"))
locked = {p["name"]: p["version"] for p in lock_data["package"]}
def pin(deps):
for name in list(deps):
if name in locked:
deps[name] = locked[name]
poetry = proj_data["tool"]["poetry"]
if "dependencies" in poetry:
pin(poetry["dependencies"])
if "group" in poetry and "dev" in poetry["group"]:
pin(poetry["group"]["dev"]["dependencies"])
with open("pyproject.toml", "w") as f:
toml.dump(proj_data, f)
print("버전 고정 완료")
순서는 ①위 스크립트 실행 → ②uvx migrate-to-uv → ③uv lock → ④필요하면 범위 조건 복구입니다. 이 순서를 건너뛰면 버전이 조용히 바뀌어도 알 방법이 없습니다.
캐시가 20GB를 넘어가면 벌어지는 일
uv는 패키지를 전역 캐시에 저장하고 하드링크로 각 프로젝트에 연결합니다. 이 덕분에 동일 패키지를 여러 프로젝트가 공유해도 디스크를 한 번만 씁니다. 처음엔 혜택처럼 느껴지는데, 1년 이상 사용하면 캐시가 20GB 이상까지 쌓입니다. 실제로 bitecode.dev가 1년 사용 후 측정한 결과가 20GB를 초과했습니다. (출처: Bite code! — A year of uv: pros, cons, and should you migrate, 2025.02)
이 수치가 의미하는 건, SSD 용량이 빠듯한 개발 환경(256GB 노트북 등)에서 uv 캐시가 눈에 띄는 용량 압박 요인이 된다는 것입니다. 문제는 uv cache clean으로 지우면 재설치 시 속도 이점이 사라진다는 점입니다. 캐시를 지우면 빠른 uv, 캐시를 남기면 무거운 uv라는 딜레마가 생깁니다.
| 구분 | pip + venv (기존) | uv (전환 후) |
|---|---|---|
| 패키지 설치 속도 (warm cache) | 기준 (1x) | 80~115배 빠름 |
| venv 생성 속도 | ~1.54초 | ~0.018초 (약 80배) |
| 장기 캐시 용량 | venv 수 × 패키지 크기 | 1년 후 20GB+ 가능 |
| 레거시 의존성 호환 | 넓음 (lenient resolver) | 엄격 (strict resolver) |
(출처: Astral 공식 블로그 벤치마크 — astral.sh/blog/uv, bitecode.dev 1년 사용기)
uvx로 dev tool 설치했다가 mypy가 죽는 이유
속도가 빠르다는 이유만으로 uvx를 쓰면 안 되는 상황이 있습니다
uv에는 uvx라는 명령어가 있습니다. Node.js의 npx처럼 별도 설치 없이 패키지를 바로 실행할 수 있고, uv tool install로 전역 dev tool을 설치할 수도 있습니다. 문제는 이 방식이 mypy, pylint 같이 현재 프로젝트의 Python 버전에 의존하는 도구에는 함정이 된다는 점입니다. (출처: Bite code! — A year of uv)
uvx mypy로 설치하면 mypy가 특정 Python 버전의 격리된 환경에 들어갑니다. 그런데 현재 프로젝트가 다른 Python 버전을 쓰고 있으면, mypy가 패키지 타입을 제대로 인식하지 못하거나 Import “requests” could not be resolved 류의 오류를 뱉습니다. yt-dlp나 httpie처럼 독립형 CLI 도구엔 uvx가 완벽하지만, 코드 품질 도구는 프로젝트 환경 안에 직접 넣는 게 맞습니다.
✅ uvx 써도 되는 도구
yt-dlp, httpie, migrate-to-uv, pdm (형식 변환용) — Python 버전 독립적인 독립형 CLI
⚠️ 프로젝트 내부에 설치해야 하는 도구
mypy, pylint, black, ruff, pytest — 프로젝트 Python 버전과 패키지에 의존하는 dev tool
# 잘못된 방식 — mypy가 프로젝트 패키지를 못 찾을 수 있음 uvx mypy src/ # 올바른 방식 — 프로젝트 환경 안에 직접 설치 uv add --dev mypy uv run mypy src/
CI/CD에서 poetry install을 uv sync로 바꾸면 달라지는 것
로컬 마이그레이션이 끝났어도 CI/CD 파이프라인에서 poetry install을 그대로 두면 의미가 없습니다. GitHub Actions 기준으로 바꿔야 할 지점은 세 가지입니다. (출처: mrlatte.net 마이그레이션 가이드)
# Before (Poetry 기반 GitHub Actions) - name: Install dependencies run: poetry install --no-root # After (uv 기반) - name: Install uv uses: astral-sh/setup-uv@v5 - name: Install dependencies run: uv sync --frozen
여기서 --frozen 플래그가 중요합니다. 이 플래그 없이 uv sync를 실행하면 uv.lock을 자동으로 업데이트하려는 시도가 생길 수 있습니다. CI 환경에서 락파일이 커밋과 달라지면 빌드가 불안정해집니다. Docker 빌드에서는 uv pip install을 레이어 분리와 함께 쓰면 캐싱 효율이 올라갑니다. (출처: uv 공식 문서 — Projects)
마지막으로 한 가지 더 짚을 점이 있습니다. uv의 uv.lock은 크로스 플랫폼 락파일입니다. Mac에서 개발하고 Linux에서 배포해도 같은 락파일을 사용할 수 있습니다. 이 부분은 Poetry의 poetry.lock과 달리 플랫폼별 별도 관리가 필요 없다는 실질적인 이점입니다. (출처: uv 공식 문서 — Universal lockfile)
자주 묻는 것들
마치며 — 마이그레이션, 해야 할까요
솔직히 말하면, 신규 프로젝트라면 지금 바로 uv를 쓰는 게 맞습니다. 속도 이점이 크고, 표준을 따르며, 락파일이 크로스 플랫폼이라는 점은 장기적으로 유지보수를 편하게 만듭니다. 기존 Poetry 프로젝트를 전환할 때는, 버전 드리프트와 레거시 의존성 충돌이라는 두 가지 리스크를 감안해야 합니다. 둘 다 대응 방법은 있지만 무시하면 조용히 문제가 생깁니다.
이 부분이 좀 아쉬웠습니다 — uv 관련 글 대부분이 “그냥 uvx migrate-to-uv 하면 됩니다”로 끝납니다. 그 한 줄 이후에 무슨 일이 일어나는지를 미리 알고 진행하는 것과 모르고 진행하는 것은 결과가 다릅니다. 특히 운영 중인 서비스라면요.
생각보다 간단합니다 — 버전 고정 스크립트 먼저, migrate-to-uv 그다음, CI에서 –frozen 플래그. 이 세 가지만 챙기면 대부분의 프로젝트에서 문제없이 전환됩니다.
📚 본 포스팅 참고 자료
본 포스팅 작성 이후 uv 서비스 정책·CLI 명령어·기능이 변경될 수 있습니다. 최신 내용은 uv 공식 문서에서 확인하시기 바랍니다. 본 포스팅은 2026.03.18 기준 / uv 0.6.x 기준으로 작성되었습니다.

댓글 남기기