지난 포스팅의 끝부분에 uv에 대해서 잠깐 언급했었다. 그 글 이후로 새벽에 프로젝트에 대해서 파트너와 이야기를 좀 나누었다. 내가 uv를 한 번 도입해보자고 제안했고, 내 파트너도 이럴때 아니면 언제 써보겠는가라며 흔쾌히 수락했다. 그래서 우리는 지금 uv를 도입한 uv 브랜치를 따로 만들어 배포가 잘 되는지, 개발 서버가 잘 실행되는지 테스트해보고 있다. 아마 테스트가 성공적으로 마무리되면 uv를 도입하게 될 것 같다.

uv는 2024년 초에 Astral에 의해 개발된 패키지 및 프로젝트 관리자이다. 위 사진은 구글에서 검색어에 대한 통계를 검색해본 결과로, 작년 8월부터 전세계적으로 관심도가 천천히 증가하고 있는 모습이다.

한국에서는 올해 3월쯤부터 uv가 국내에서 점점 입소문을 타기 시작해 점차 관심도가 높아지고 있는 듯하다. 아직 국내 정보가 그렇게 많지 않고, 특히 uv를 프로젝트 관리자로 이용하는 Django 프로젝트에 대해서는 정보량이 거의 가뭄 수준이다. 그래서 uv를 Django 프로젝트에 적용시키기 위해 테스트해본 과정을 글로 남겨 한 조각의 정보를 추가해보고자 한다.
물론, 기술이 핫하다고 무작정 그걸 도입해보는 건 좋은 생각은 아니다. 그렇다면 나는 왜 프로젝트에 uv를 도입해보고 싶다고 생각하게 되었을까?
일단 패키지 설치 속도가 pip와 비교했을 때 어마무시하게 빠르다. 이는 파이썬으로 쓰여진 pip와는 달리 uv는 Rust로 쓰여있기 때문이다.

(출처: Data Science at scale: comparing Rust vs Python and R performance)
이 그래프는 파이썬, R, Rust의 성능을 비교하기 위해 행렬 곱셈을 수행해 본 결과로, Rust가 파이썬보다 빠른 것을 볼 수 있다.
이 그래프 말고도, 다양한 실험과 그래프가 Rust가 파이썬에 비해 빠르다는 것을 보여주고 있다.

(출처: 깃허브의 uv 레포지토리 내 벤치마크 문서)
이 그래프는 uv, PDM, Poetry, pip-sync에서 uv sync 계열의 명령어를 실행한 후 속도를 비교한 그래프인데, uv의 속도가 pip의 확장 패키지 pip-sync보다 훨씬 빠른 것을 볼 수 있다.
이처럼 uv는 pip에 비해 패키지 속도가 빠르기 때문에, uv의 모든 기능을 이용하지 않고 패키지 설치/제거 용도로 도입해서 사용하는 경우도 있다.
기존에 pip만을 이용할 때에는 프로젝트를 할 경우 직접 가상 환경을 만들어서 가상 환경을 활성화 시켜주는 매우 귀찮은 작업이 필요했다.
$ python -m venv myvenv
$ source myvenv/Scripts/activate
(myvenv)
$ 파이썬을 이용한 명령
파이썬의 venv 모듈로 가상 환경을 만들고, 그 가상 환경의 스크립트 폴더에 있는 activate를 실행시키는 성가신 일이다.
하지만, uv는 프로젝트마다 별도의 가상 환경을 자동으로 만들어주므로 가상 환경을 키고 끄는 귀찮은 일을 더 이상 하지 않아도 되게 된다.
uv를 처음 보았을 때, npm과 pip를 합쳐 둔 느낌을 받았다. 실제로 uv를 소개하는 영상의 댓글을 보니 그런 의견을 제시한 사람들이 종종 있기도 했다.
프론트를 찍먹하며 며칠 간 npm을 접했을 때, 상당히 편리함을 느꼈다. 패키지 관리, 의존성 관리를 해준다. 또, 프로젝트별로 독립된 패키지 환경을 제공하며 사용자 정의 스크립트도 제공해줘서 긴 내용의 명령어를 쓰는 수고도 줄여준다.
그래서 uv를 사용해보면, npm을 사용할 때 느낄 수 있었던 생산성 향상을 파이썬에서도 경험할 수 있지 않을까라고 생각했다.
아직 충분히 숙달되지 않은 상태임에도 실전에 도입해보고 싶다는 생각이 든 이유는 몇 가지 주요 기능만 알아두고 나머지는 쓰면서 알아봐도 큰 리스크가 없을 것이라고 생각했기 때문이다. 아직 프로젝트가 초기 설정 단계에 있어서, 기존 설정과 큰 충돌도 발생하지 않을 것이라 생각했다.
그렇지만, 아무리 매력적이어도 실전에 바로 투입할 순 없으니 빈 Django 프로젝트를 만들어서 몇 가지 작업을 진행해 보았다.
$ pip install pipx
$ pipx install uv
uv는 여러 가지 방법으로 설치할 수 있는데, 나는 pip를 이용해 설치하는 방법을 선택했다. 공식 문서에서는 pipx에 uv를 설치하는 방법을 추천하고 있어서, 해당 방법으로 진행했다. pipx는 각 패키지마다 독립된 가상 환경을 만들어서 전역 파이썬 환경이 오염되지 않게끔 해주는 패키지이다.
$ uvx pycowsay moooooo
-------
< moooooo >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
pipx, uv 등에서 Hello World 느낌으로 쓰이고 있는 pycowsay를 실행해 보았다. 소가 귀엽게 생겼다. uvx는 uv tool run의 alias로 임시 가상 환경을 만들어서 그 환경에 패키지를 설치한 후 실행한다. pipx run 쓰듯이 쓰면 된다.
$ uv init 프로젝트명
npm에서 프로젝트 만드는 것처럼, uv init으로 프로젝트를 만든다.
├── .gitignore
├── .python-version
├── README.md
├── main.py
└── pyproject.toml
생성한 프로젝트는 위와 같은 구조를 가지게 된다.
.gitignore: 깃에서 쓰는 그거 맞다. Django 프로젝트용 gitignore의 내용으로 교체하면 된다..python-version: 어떤 파이썬 버전을 이용할 것인지 적혀 있다.README.md: 생략main.py: 파이썬으로 이 파일을 실행시켜 보면 Hello from 프로젝트명!이 출력된다. 기념(?)으로 어디다가 남겨놔도 되고, 지워도 된다. 나는 지웠다.pyproject.toml: 이 프로젝트의 정보가 담겨 있는 파일이다. 프로젝트의 이름부터 요구 파이썬 버전, 의존성 등이 기록된다.생성한 uv 프로젝트 폴더에서 Django 프로젝트를 만들어 준다. 일단 Django를 설치하자.
$ uv add django
이렇게 하면 uv가 가상 환경을 만들어서 거기에 Django를 설치하고, 동시에 pyproject.toml 파일의 의존성 정보에 django를 추가해준다.
[project]
name = "hellloworld"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"django>=5.2.5",
]
django를 설치한 후의 pyproject.toml의 내용이다. 패키지명과 필요한 버전 정보가 같이 기록되어 있는 것을 볼 수 있다.
$ uv run django-admin startproject 프로젝트명 .
명령어를 실행하려면 uv run을 사용하면 된다. Django로 프로젝트를 만들 때, 프로젝트명 다음에 기준 디렉토리(destination)를 지정할 수 있다. 그러면 그 디렉토리에 manage.py가 생성된다. 디렉토리로 .을 주면, 현재 작업중인 폴더를 기준으로 프로젝트를 만들어 준다. 다시 말해, 현재 작업중인 폴더에 manage.py가 생긴다.
$ uv run manage.py runserver
uv run을 사용할 때 .py로 끝나는 파일명을 주면 해당 파이썬 파일은 스크립트로 취급되어 uv run python manage.py runserver를 사용한 것과 같아진다.
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
August 07, 2025 - 22:12:57
Django version 5.2.5, using settings 'testproject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead.
For more information on production servers see: https://docs.djangoproject.com/en/5.2/howto/deployment/

개발용 서버가 잘 실행되고 있음을 볼 수 있다.
uv를 사용할 때, Django의 개발용 서버를 실행시키려면
$ uv run manage.py runserver
라고 입력해야 한다. 뭔가 좀 길지 않은가?
npm의 경우 이 부분에 대해 커스텀 스크립트를 지원하고 있다.
{
"name": "my-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.7.1"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"vite": "^7.0.4"
}
}
npm에서는 package.json의 scripts 부분에 커스텀 스크립트를 정의할 수 있다. 예를 들어, package.json의 내용이 위와 같은 npm 프로젝트에서 npm run dev를 입력하면 npm run vite가 실행되는 방식이다.
하지만, 찾아본 결과 uv는 아쉽게도 이것을 지원하지 않는다. pyproject.toml에 project.scripts가 있고 uv가 이것을 지원하고 있긴 한데, 이것은 커스텀 스크립트의 개념보다는 패키지의 엔트리 포인트의 개념이다. 내가 생각하던 것과는 좀 달라 보인다. Using uv run as a task runner #5903 이슈에서 볼 수 있듯, 커스텀 스크립트에 대해서는 아직 구현 방법에 대한 논의가 진행되고 있다.
대신, 추가적인 패키지를 설치하면 커스텀 스크립트를 사용할 수 있다. 위 이슈에서 작성자가 예시로 든 Poe the Poet과 taskipy를 이용해 커스텀 스크립트를 만들어 보자.
패키지 및 의존성 관리자로 사용되는 poetry라는 패키지가 있는데, poetry는 시를 의미한다. 여기에서 영감을 얻은 네이밍인 것 같다. 참고로 이 패키지의 공식 문서에는 사람 얼굴이 그려져 있는데, 에드거 앨런 포(Edgar Allan Poe)라고 하는 시인 및 작가의 것인 듯하다.
$ uv add poethepoet
[tool.poe]
executor.type = "uv"
[tool.poe.tasks] # 커스텀 task 정의
dev = "python manage.py runserver"
poethepoet을 설치하고 pyproject.toml에 위 내용을 추가해준다.
$ uv run poe dev
Poe => python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
August 07, 2025 - 22:44:18
Django version 5.2.5, using settings 'testproject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead.
For more information on production servers see: https://docs.djangoproject.com/en/5.2/howto/deployment/
작업을 의미하는 task, 만들다를 의미하는 접미사 -ify, 언어 python를 합친 네이밍인 것 같다.
$ uv add taskipy
[tool.taskipy.tasks] # 커스텀 task 정의
dev = "python manage.py runserver"
Poe the Poet처럼 pyproject.toml에 위 내용을 추가해준다.
$ uv run task dev
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
August 07, 2025 - 22:44:59
Django version 5.2.5, using settings 'testproject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead.
For more information on production servers see: https://docs.djangoproject.com/en/5.2/howto/deployment/
두 패키지 모두 커스텀 스크립트라는 목적을 잘 이뤄준다. 어떤 차이가 있는 것인지는 잘 모르겠지만.. 사실 Poe the Poet이랑 taskipy를 굳이 비교해야 하나 싶기도 하고.. 그냥 마음에 드는 것 하나 골라서 쓰면 될 것 같다. poe보단 task라는 이름이 좀 더 직관적이라 마음에 든다든가..
오늘은 이렇게 실전에 uv를 도입하기 전에 테스트용 Django 프로젝트를 만들어서 거기에 uv를 사용해보며 uv의 기능을 테스트해보고, Poe the Poet, taskipy를 이용해서 명령어도 단축시켜 보았다.
이 글을 쓰는 동안, 파트너가 배포 작업을 진행해봤는데 배포가 잘 된 것으로 보인다. uv 도입을 위한 과정이 순조롭게 진행되고 있다. 이제 몇 가지 사항에 대한 검토를 마치면 검토하면 사용이 가능하지 않을까 싶다. 아마 다음 TIL은 그 사항들에 대한 검토 내용을 기록하는 방식이 되지 않을까 싶다.