마루옵스서버 · 네트워크 · AI 기술 노트

소개문의

[Python 개발 환경 04] 웹 프로젝트 구성하기: src 구조·uv·테스트·배포 환경

키보드·메모장·헤드폰이 놓인 책상

참고 사진 · 키보드·메모장·헤드폰이 놓인 책상 · 사진: Teresa Kluge lum3n / 원본 / CC0 · 목록용 크기 조정

웹 프로젝트에서 환경을 나누는 기준은 폴더 개수보다 실행 단위에 가깝습니다. 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 패키지는 본문의 절차로 별도 준비합니다.

댓글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

닉네임·이메일·댓글 내용·IP 주소·브라우저 정보는 댓글 게시 및 관리에 사용됩니다. 이메일과 접속 정보는 공개 댓글에 표시하지 않습니다. 개인정보 요청은 운영자 이메일로 보내 주세요. 개인정보처리방침