웹 프로젝트에서 환경을 나누는 기준은 폴더 개수보다 실행 단위에 가깝습니다. API 서버, 프런트엔드 빌드, DB, 백그라운드 작업자는 설치할 도구와 배포 주기가 다를 수 있어요. 처음에는 작은 API 하나로 시작하되, 나중에 이 경계를 분리할 수 있게 구조를 잡아 보겠습니다.
Python 개발 환경 · 01 선택과 venv · 02 uv · 03 conda · 04 웹 개발 · 05 데이터·AI · 06 OCR·서비스 분리
이 글의 목차
웹 제작에서 Python 환경이 담당할 범위
| 구성 요소 | 환경 관리의 예 |
|---|---|
| Python 웹 서버 | Django·Flask·FastAPI 등 선택한 프레임워크와 DB 드라이버를 uv 또는 venv로 관리 |
| 브라우저용 프런트엔드 | Node.js와 package.json·해당 생태계의 잠금 파일을 별도로 관리 |
| DB·Redis 같은 서비스 | 별도 서비스나 컨테이너로 관리. Python DB 드라이버 설치가 DB 서버 설치를 대신하지 않음 |
| 무거운 작업자 | OCR·모델 추론 등은 API와 다른 환경·프로세스로 분리할지 판단 |
관리 화면·인증·ORM까지 한 프레임워크 안에서 구성할지, API와 프런트엔드를 분리할지에 따라 선택은 달라집니다. 아래 FastAPI는 개발 환경의 구조를 설명하기 위한 작은 예제이며, 웹 프레임워크 전체를 비교하는 튜토리얼은 아닙니다. API 사용 방식은 FastAPI 공식 문서를 참고했습니다.
1. 소스와 실행 환경을 분리한다
web-demo/
├── pyproject.toml
├── uv.lock
├── .python-version
├── .env.example
├── .gitignore
├── src/
│ └── web_demo/
│ ├── __init__.py
│ └── main.py
└── tests/
└── test_health.py
src/ 구조는 저장소 루트에 우연히 있는 모듈을 import하는 문제를 줄이고, 설치한 패키지를 기준으로 테스트하기 좋습니다. 대신 프로젝트를 환경에 설치하는 과정이 필요해요. 여기서는 build-system을 선언하고 uv sync가 프로젝트를 editable 형태로 설치하도록 합니다. PyPA의 src·flat 레이아웃 비교를 참고할 수 있습니다.
2. pyproject.toml을 작성한다
[project]
name = "web-demo"
version = "0.1.0"
requires-python = ">=3.12,<3.13"
dependencies = ["fastapi>=0.115,<1", "uvicorn>=0.30,<1"]
[dependency-groups]
dev = ["pytest>=8,<10", "httpx>=0.27,<1", "ruff>=0.11,<1"]
[build-system]
requires = ["hatchling>=1.27,<2"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
testpaths = ["tests"]
실행에 필요한 FastAPI·Uvicorn은 project.dependencies에, 테스트와 린트 도구는 dev 그룹에 둡니다. Hatchling은 이 예제를 Python 패키지로 설치하기 위한 빌드 백엔드입니다. 복잡한 사내 배포가 없어도 이 경계를 명시하면 src 경로를 임시로 PYTHONPATH에 덧붙이는 일을 줄일 수 있어요.
3. API와 테스트 파일을 저장한다
src/web_demo/__init__.py는 빈 파일로 만들고, src/web_demo/main.py에는 다음 내용을 넣습니다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}
tests/test_health.py에는 API를 호출하는 테스트를 저장합니다.
from fastapi.testclient import TestClient
from web_demo.main import app
def test_health():
with TestClient(app) as client:
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
uv python pin 3.12
uv lock
uv sync --locked
uv run --locked python -m pytest
uv run --locked ruff check src tests
uv run --locked python -c 'import sys, web_demo; print(sys.executable); print(web_demo.__file__)'
import 경로가 현재 프로젝트의 src/web_demo를 가리키는지 확인합니다. 프로젝트 배포 이름은 web-demo이고 Python에서 import하는 이름은 web_demo입니다. 하이픈과 밑줄을 혼동하면 패키지는 설치됐는데 import가 안 되는 상황이 생길 수 있습니다.
4. 개발 서버를 실행한다
uv run --locked uvicorn web_demo.main:app --reload --host 127.0.0.1 --port 8000
같은 서버의 다른 터미널에서 curl http://127.0.0.1:8000/health를 실행하면 {"status":"ok"} 응답을 확인할 수 있습니다. 원격 VM에서 실행했다면 PC에서 ssh -N -L 18000:127.0.0.1:8000 labadmin@서버주소로 전달해 접속합니다. 여기서는 서버 방화벽을 끄거나 개발 서버를 공인망에 바로 노출하지 않습니다.
--reload는 소스 변경을 감시하는 개발용 설정입니다. 운영 환경에서는 제거하고 프로세스 관리·HTTPS·리버스 프록시·로그·DB 연결 설정을 갖춥니다. 개발 서버 한 줄을 실행한 것과 운영 배포를 완료한 것은 구분해야 합니다.
5. IDE의 Python도 같은 환경으로 맞춘다
- VS Code 등 편집기의 Python 인터프리터를 프로젝트의
.venv/bin/python으로 선택합니다. - 디버거와 테스트 실행 설정이 같은 인터프리터를 사용하는지 확인합니다.
- 터미널에서는
uv run python -c "import sys; print(sys.executable)"로 비교합니다. - 설정 파일은
.env.example만 형식 예제로 저장하고, 실제.env와 자격 증명은 Git에서 제외합니다.
6. 운영 환경은 개발 도구 없이 새로 만든다
# 운영용 새 체크아웃의 프로젝트 루트
uv sync --locked --no-default-groups
.venv/bin/python -c 'from web_demo.main import app; print(app.title)'
이후 서비스를 실행할 때는 명시적인 실행 파일 경로를 쓰면 셸의 활성화 상태에 덜 의존합니다. 다음 systemd 예시는 webapp 계정·프로젝트 디렉터리·환경 파일을 미리 준비한 서버에 맞춰 수정할 템플릿입니다. 이 글의 확인용 컨테이너에서 systemd 서비스 배포까지 실행한 예시는 아닙니다.
[Unit]
Description=Python web demo
After=network.target
[Service]
User=webapp
WorkingDirectory=/srv/apps/web-demo
EnvironmentFile=/etc/web-demo.env
ExecStart=/srv/apps/web-demo/.venv/bin/uvicorn web_demo.main:app --host 127.0.0.1 --port 8000
Restart=on-failure
[Install]
WantedBy=multi-user.target
systemd가 대화형 셸처럼 source .venv/bin/activate를 알아서 실행하지는 않습니다. 서비스의 실행 경로·작업 디렉터리·사용자 권한을 명시하세요. 운영 환경에서 평소 옵션의 uv run을 다시 실행하면 기본 dev 그룹이 포함될 수 있으므로, 운영용 동기화 정책과 실행 정책을 같이 정합니다.
프런트엔드와 작업자가 늘어날 때
브라우저 프런트엔드가 추가되면 frontend/에 Node 환경과 잠금 파일을 따로 둡니다. OCR이나 GPU 모델 작업이 API와 독립된 배포 단위가 된다면 별도 Python 명세와 잠금 파일을 둘 수 있어요. 단순히 파일을 다른 폴더에 두는 것과 프로세스·의존성 환경을 분리하는 것은 다릅니다. 다음 05 데이터·AI과 06 OCR·서비스 분리에서 이 기준을 구체화합니다.
연재 예제 파일 내려받기 · venv·conda 명세와 uv·웹·데이터·OCR 예제 소스, 확인한 uv 잠금 파일을 묶었습니다. 각 프로젝트 디렉터리에서 실행하며 OS 패키지는 본문의 절차로 별도 준비합니다.


![[Python 개발 환경 06] OCR 프로젝트 구성: 실행 파일·언어팩·API와 워커 분리](https://blog.maruops.com/wp-content/uploads/2026/10/maruops-python-env-6-768x618.webp)
![[Python 개발 환경 05] 데이터 처리·AI 환경 설계: pandas·Jupyter·CPU와 GPU](https://blog.maruops.com/wp-content/uploads/2026/10/maruops-python-env-5-768x960.webp)
답글 남기기