7가지 Python 툴이 내 개발 워크플로우를 완전히 바꿔버린 이야기 (2025 기준)
Python 코드 하루에도 몇 번씩 쓰다 보면 이런 순간, 다들 한 번씩 겪죠.
“아이디어는 머릿속에 다 그려져 있는데…
환경 세팅이랑 패키지 설치, 이상한 에러 때문에 하루가 그냥 날아간다…”
코드 자체보다 env, 설치 속도, 테스트, 포맷팅 같은 것들 때문에 에너지가 먼저 바닥나는 느낌.
저도 꽤 오래 그 루프 안에서 빙빙 돌았어요.
그러다 어느 순간부터 특정 툴 세트를 꾸준히 쓰기 시작했고, 그때부터 진짜로 달라졌습니다.
이 툴들이 제 대신 앱을 만들어주는 건 아니에요.
하지만 개발 과정에서 계속 발목 잡던 마찰을 하나씩 없애줬습니다.
이 글에서는 2025년에 제가 실제로 쓰면서
Python 워크플로우를 통째로 바꿔버린 7가지 툴을 차근차근 풀어볼게요.
- Cursor – AI-first code editor지만, 코드 주도권은 개발자가 쥐고 있는 에디터
- uv – pip, venv 대신 쓰는 초고속 패키지 & 환경 매니저
- python-dotenv – .env 파일 환경변수 로딩의 사실상 표준
- ConfigCat – 안전하게 기능을 켰다가 껐다가 할 수 있는 feature flag 서비스
- Ruff – 미친 속도의 Python linter & formatter
- pytest – 작은 함수부터 거대한 서비스까지 다 커버하는 테스트 프레임워크
- Docker – “내 컴퓨터에서는 되는데?”를 끝내주는 컨테이너 기반 환경
가능한 한 실제 명령어, 사용 흐름, 어디에 어떻게 쓰면 좋은지를 전부 담았습니다.
“도구 나열”이 아니라, 이 7가지를 한 워크플로우로 엮어서 보여드릴 거예요.

1. Cursor – AI가 도와주지만, 코드는 여전히 내가 주인인 에디터
AI 코딩 얘기 나오면 보통 이런 그림 떠올리죠.
“프롬프트 한 번 쓰면 앱이 뚝딱 만들어지는 세상”
현실은… 그렇게 달콤하지 않습니다.
진짜 프로젝트에서는 **“코드 전체를 알아서 짜주는 블랙박스”**보다
내가 이해하고, 내가 통제할 수 있는 상태가 훨씬 중요해요.
그래서 제가 정착한 게 Cursor입니다.
처음 켜보면 거의 VS Code랑 똑같이 생긴 코드 에디터예요.
사이드바, 탭, 파일 트리 전부 익숙한 그 느낌 그대로고,
그 위에 AI agent와 code edit 기능이 얹혀 있는 형태라고 보면 됩니다.
왜 2025년 기준 메인 에디터가 Cursor인가
지금은 Python 코딩할 때 거의 항상 Cursor를 씁니다.
심지어 Python 말고 다른 언어 할 때도 웬만하면 Cursor로 해요.
제가 좋아하는 지점은 딱 두 가지예요.
- VS Code 감성 그대로
- 기본 개발 루틴이 바뀌지 않습니다.
- “새로운 에디터 적응기” 같은 거 거의 필요 없어요.
- AI가 “도와주는 역할”에 머무른다
- 전부 대신 짜는 게 아니라, 내가 지정한 범위 안에서 일해줍니다.
예를 들어 이런 식이죠.
- Quick Edit
- 함수나 블록 하나 드래그하고
“조금 더 깔끔하게 refactor해줘”, “이 부분 성능 위주로 최적화해줘”
이런 식으로 한 줄 요청하면 바로 제안 코드를 보여줍니다. - 마음에 들면 적용, 아니면 무시. 주도권은 항상 저한테 있어요.
- 함수나 블록 하나 드래그하고
- Agent 모드
- 규모 있는 수정이 필요할 때는 agent 사이드 패널을 열고
check_in_listener.py, 리뷰 관련 모듈 등 여러 파일을 태그한 뒤,
“이 흐름 전체를 ask mode 기반으로 바꿔줘” 같은 식으로 요청합니다. - 그러면 관련 파일들까지 같이 손봐서 제안해줘요.
- 규모 있는 수정이 필요할 때는 agent 사이드 패널을 열고
- 기본 개발 경험 그대로 유지
- Go to Definition, 전체 검색, refactor 흐름 등
VS Code에서 하던 것들을 그대로 쓸 수 있습니다.
- Go to Definition, 전체 검색, refactor 흐름 등
여러 AI code tool에 수천 달러를 써보면서 이것저것 다 써봤지만,
“진짜 프로젝트” 할 때는 결국 Cursor로 다시 돌아오게 되더라고요.
AI와 페어 프로그래밍하는 느낌은 살리되,
코드의 설계와 소유권은 끝까지 내가 가져가고 싶다면 Cursor가 정말 잘 맞습니다.
2. uv – pip와 venv를 조용히 은퇴시켜버린 패키지 & 환경 매니저
솔직히 말해서, pip + 수동 venv 조합…
오래 버텨준 건 맞는데, 2025년의 눈으로 보면 꽤나 불편합니다.
특히 여러 프로젝트를 왔다 갔다 할 때 더 그래요.
그래서 저는 요즘 새 프로젝트 만들면 대부분 uv부터 깝니다.
uv는 한마디로 말하면:
“Python용 초고속 package & environment manager”
uv가 진짜 빠르다
공식 벤치마크에서도 그렇고, 실제로 써봐도
기존 pip 대비 체감상 10~100배 빠른 경우가 많습니다.
패키지 설치 기다리느라 멍하니 터미널만 바라보는 시간,
이제 많이 줄어들어요. 그만큼 집중력이 덜 깨지고, 흐름이 길게 이어집니다.
uv 설치 방법
조금 웃긴데, uv 설치는 pip로 하는 게 제일 간단합니다.
pip install uv
이제 uv라는 CLI를 쓸 수 있어요.
uv로 새 Python 프로젝트 초기화하기
uv의 진짜 매력은 프로젝트 시작 단계에서 드러납니다.
- 새 디렉터리를 하나 만들고,
- 그 안에서 이렇게만 치면 됩니다:
uv init .
이 한 줄로 여러 가지가 자동으로 생성돼요:
- .python-version – 이 프로젝트에서 쓸 Python 버전 (예: 3.13)
- main.py – 간단한 엔트리 스크립트
- pyproject.toml – 옛날 requirements.txt 대신 요즘 표준
- .venv – virtualenv 폴더까지 자동 생성
직접 python -m venv .venv 이런 거 안 해도 되고,
환경 활성화 명령어도 사실상 잊고 살아도 됩니다.
패키지 설치는 uv add
패키지 추가는 그냥 이렇게 합니다.
uv add streamlit
uv add fastapi
순식간에 설치되고,
pyproject.toml에 dependency도 자동 기록됩니다.
uv로 코드 실행하기
uv의 꿀 포인트는 여기서 나옵니다.
가상환경을 ‘활성화’ 할 필요 없이,
그냥 uv run으로 실행하면 알아서 그 프로젝트 env를 씁니다.
예를 들어 main.py가 있다면:
uv run main.py
이 한 줄로:
- 이 디렉터리의 .venv를 찾아서
- 거기 있는 Python 인터프리터로
- 거기 설치된 패키지들을 사용해서 실행합니다.
main.py에 이런 코드가 있다 가정해볼게요.
from fastapi import FastAPI
app = FastAPI()
print("Hello from Python tools!")
이 상태에서:
uv run main.py
하면 별도의 source .venv/bin/activate 같은 절차 없이
그냥 바로 FastAPI가 import 돼서 실행됩니다.
CLI 있는 툴도 uv로 실행
streamlit, pytest, ruff처럼 자체 CLI를 가지는 Python 툴들도
똑같이 uv run 앞에 붙여 쓰면 됩니다.
예를 들어 Streamlit 앱 실행:
uv run streamlit run main.py
- 이 명령은 해당 프로젝트의 venv 안에서
- streamlit 커맨드를 실행하고
- main.py를 Streamlit 앱으로 올려줍니다.
터미널에서 global하게 이것저것 설치할 필요 없이,
프로젝트 단위로 깔끔하게 격리된 상태에서 모든 걸 돌릴 수 있어요.
패키지 제거도 한 줄
필요 없어진 패키지는 이렇게 정리합니다.
uv remove fastapi
pyproject.toml도 알아서 정리해주고,
환경 관리가 훨씬 가볍습니다.
uv가 바꿔준 것들
uv를 쓰기 시작하면서 생긴 변화는:
- pip install 속도 기다리느라 시간 덜 버림
- venv 활성화/비활성화에 쓸데없이 신경 쓰지 않음
- pyproject.toml 기반으로 더 현대적인 프로젝트 구조
- 여러 프로젝트 왔다 갔다 할 때 정신적 부담 감소
프로젝트가 여러 개 있거나, env 꼬이는 걸 질색하는 개발자라면,
uv 하나만 도입해도 스트레스가 꽤 줄어들 겁니다.
3. python-dotenv – .env 쓰는 사람이라면 사실상 필수
어느 정도 규모 있는 앱이라면 환경설정이 항상 필요합니다.
- API 키
- secret token
- DB URL
- DEBUG 모드 여부
- 각종 limit 값…
이걸 코드에 박아넣을 수도 없고,
매번 터미널에 export 치고 시작하는 것도 비효율적이죠.
그래서 대부분 .env 파일을 쓰기 시작합니다.
APP_NAME=My Amazing Python App
DEBUG=true
MAX_CONNECTIONS=100
API_KEY=super-secret-key
문제는…
Python이 이 .env 파일을 자동으로 읽어주지 않는다는 점입니다.
os.getenv()만 쓰면 OS에 실제로 올라간 환경변수만 보이고,
같은 폴더의 .env 내용은 완전히 무시돼요.
python-dotenv 없을 때 생기는 전형적인 상황
예를 들어 이런 코드를 썼다고 해봅시다.
import os
app_name = os.getenv("APP_NAME", "Not set")
debug = os.getenv("DEBUG", "Not set")
max_connections = os.getenv("MAX_CONNECTIONS", "Not set")
api_key = os.getenv("API_KEY", "Not set")
print("App name:", app_name)
print("Debug:", debug)
print("Max connections:", max_connections)
print("API key:", api_key)
그 옆에 .env도 분명 이렇게 잘 만들어놨어요.
APP_NAME=My App
DEBUG=true
MAX_CONNECTIONS=200
API_KEY=abc123
그리고:
uv run main.py
실행했는데 결과가…
App name: Not set
Debug: Not set
Max connections: Not set
API key: Not set
이러면 사람 미칩니다.
“같은 디렉터리에 .env 있는데 왜 못 읽냐…?” 하면서요.
python-dotenv로 깔끔하게 해결
python-dotenv는 이 문제만 해결해주는 아주 단순하지만
그래서 더 자주 쓰이는 패키지입니다.
설치는 uv로:
uv add python-dotenv
또는 pip:
pip install python-dotenv
이제 코드에 딱 두 줄만 추가하면 됩니다.
import os
from dotenv import load_dotenv
load_dotenv() # 현재 디렉터리의 .env 파일 로드
app_name = os.getenv("APP_NAME", "Not set")
...
다시:
uv run main.py
출력이 이제는 .env 내용대로 잘 나옵니다.
여러 환경 파일 – dev / staging / production
실전 앱에서는 환경이 하나로 끝나지 않죠.
- .env – 기본 세팅
- .env.local – 로컬 개발용
- .env.staging – 스테이징
- .env.production – 프로덕션
이렇게 나누는 경우가 많습니다.
python-dotenv는 이런 식으로 여러 파일을 순차적으로 읽도록 만들 수 있어요.
from dotenv import load_dotenv
# 기본값 먼저 로드
load_dotenv(".env")
# 그 위에 production 설정 덮어쓰기
load_dotenv(".env.production", override=True)
이러면:
- .env에 공통값을 넣어두고
- .env.production에서 덮어쓰는 식의 구성이 가능합니다.
.env 기반 설정은 Docker, CI/CD, cloud 환경이랑도 잘 어울려서,
**“설정은 코드 밖으로 빼고, 코드에서는 가져다 쓰기만 한다”**는 패턴을 완성해 줍니다.
왜 거의 모든 프로젝트에 python-dotenv를 넣는가
이유는 단순합니다.
- .env를 쓰는 이상, 그걸 읽어줄 애가 필요하고
- 그 역할을 제일 잘하는 라이브러리가 python-dotenv이기 때문입니다.
환경변수 때문에 “Not set”만 잔뜩 뜨고 헤매 본 적 있다면,
이 패키지는 진짜 멘탈 비용을 많이 줄여줄 거예요.
4. ConfigCat – 릴리즈를 안전하게 지켜주는 feature flag 서비스
진짜 배포를 한 번이라도 제대로 해본 사람이라면 다 아는 감정이 있습니다.
“내 로컬, 내 staging에서는 멀쩡했는데…
실 사용자들이 쓰기 시작하니까 문제 터진다…”
이때 우리가 제일 간절히 원하는 건 딱 하나예요.
“이 기능만 빠르게 꺼버릴 수 있는 스위치”
코드 다시 빌드하고, 배포 기다리고,
롤백하고… 이런 과정 거치기 전에 딱 그 기능만 껐다 켤 수 있으면 정말 좋겠죠.
그 역할을 해주는 게 바로 feature flag이고,
제가 즐겨 쓰는 서비스가 ConfigCat입니다.
ConfigCat이 뭘 해주는가
ConfigCat은 한마디로:
“기능 온/오프를 코드 밖에서 컨트롤하게 해주는
privacy-first feature flag 플랫폼”
대략 이런 것들이 가능합니다.
- 특정 기능을 일부 유저에게만 켜기
- 트래픽의 일정 비율만 새 기능으로 보내보기 (gradual rollout)
- A/B test용으로 두 버전 나눠서 서비스
- 문제가 생겼을 때 kill switch처럼 즉시 끄기
이 모든 걸 깃 푸시 없이,
웹 대시보드 상에서 클릭 몇 번으로 제어할 수 있습니다.
다양한 SDK 지원
ConfigCat은 19개가 넘는 플랫폼용 SDK를 제공합니다.
- Python
- JavaScript
- Ruby
- Java
- 심지어 Unreal Engine까지
즉, 백엔드·프론트엔드·게임 클라이언트까지
** 같은 서비스 하나로 feature flag를 관리**할 수 있다는 의미예요.
설정은 금방, 가격 구조도 깔끔
Python 앱 기준으로 보면,
대략 10분 정도면 기본 연동은 끝낼 수 있을 정도로 단순합니다.
또한:
- 사용자 데이터를 따로 저장하지 않는 구조
- 팀원 수 무제한 seat 지원
- 여러 협업/DevOps 툴과 연동
- OpenFeature 표준 지원으로 vendor lock-in 완화
무료 플랜도 있고,
규모가 커지면 유료 플랜으로 넘어가는 구조라
개인 프로젝트 → 스타트업 → 서비스 확장까지 무난하게 가져갈 수 있습니다.
진짜 중요한 건 “마음의 안전망”
개발하면서 한 번쯤은 이런 상황을 맞게 됩니다.
- 새 기능 배포
- 특정 조건에서만 에러 발생
- 롤백하기엔 이미 다른 변경사항이 섞여 있음
이때 ConfigCat 같은 feature flag가 있으면
코드 롤백 없이, 해당 기능만 비활성화하고 숨을 돌릴 수 있어요.
이게 주는 안정감이 상당합니다.
릴리즈할 때 불안, 초조 대신
“문제 생겨도 최소한 꺼버릴 수 있다”는 든든함이 생깁니다.
프로덕션에 기능을 자주 추가하는 팀이라면
feature flag 도입은 사실 거의 필수에 가깝다고 느껴요.
5. Ruff – 스타일과 품질을 동시에 챙겨주는 초고속 linter & formatter
몇 주 만에 다시 연 코드 파일을 봤는데,
- 띄어쓰기 들쑥날쑥
- 줄 길이 제각각
- 더 이상 안 쓰는 import가 덕지덕지
일단 돌아는 가지만, 보는 것만으로 피곤해지는 코드…
다들 한 번쯤은 봤을 겁니다.
이런 걸 정리해주는 존재가 linter와 formatter이고,
Python 쪽에서 요즘 제일 마음에 드는 도구가 Ruff입니다.
Ruff가 해주는 일
Ruff는:
“정말 빠른 Python linter 겸 formatter”
다음 같은 것들을 잡아줍니다.
- indentation 문제
- 과도하게 긴 line
- PEP 8 스타일 위반
- 사용되지 않는 import
- 중복 변수 이름
- 사용되지 않는 변수
- 기본값이 이상한 parameter 등
속도가 워낙 빨라서
큰 코드베이스에도 부담 없이 돌릴 수 있습니다.
설치 & 기본 사용
uv로 설치:
uv add ruff
이후 전체 프로젝트 체크:
uv run ruff check .
현재 디렉터리 아래의 모든 .py를 훑으면서
파일/라인/에러 코드/설명을 쫙 보여줍니다.
특정 파일만 보고 싶다면:
uv run ruff check messy_code.py
일부러 엉망으로 만든 messy_code.py를 대상으로 돌리면
각종 스타일 에러들이 주르륵 쏟아져 나올 거예요.
Ruff의 자동 고침 기능 (--fix)
Ruff는 단순히 “문제가 있다”고 알려만 주는 게 아니라
그중 자동으로 고칠 수 있는 부분은 실제로 고쳐줍니다.
예를 들어 메시지에:
“Found 9 fixable errors”
라고 나오면, 이렇게 한 번 더 실행하면 됩니다.
uv run ruff check --fix messy_code.py
이제 Ruff가 직접 파일을 수정해 줘요.
그 다음 다시:
uv run ruff check messy_code.py
를 돌리면 전체 에러 수가 줄어든 걸 확인할 수 있습니다.
(예: 23개 → 14개 이런 식으로)
즉, 기계적으로 고칠 수 있는 스타일 문제는 Ruff에게 떠넘기고,
사람이 판단해야 하는 부분만 직접 손보면 되는 구조가 됩니다.
formatter 모드로 쓰기 – ruff format
Ruff는 linter뿐 아니라 formatter 역할도 합니다.
uv run ruff format messy_code.py
이렇게 실행하면:
- indentation 정리
- 공백 정리
- 전체적인 코드 레이아웃을 깔끔하게 재구성
해 줍니다.
따옴표 스타일 같은 취향 문제까지 100% 맞춰주지는 않아도,
읽기 힘든 상태에서 **“최소한 사람이 보기 좋은 모양”**으로 바꿔 줍니다.
Git / CI와 연동하기
Ruff는 pre-commit hook으로 쓰기도 좋고,
GitHub Action으로 올려서 pull request마다 자동으로 돌리기도 좋습니다.
- PR 올릴 때 매번 스타일 지적할 필요 없이
Ruff가 자동으로 잡아주고 - 팀 전체가 동일한 규칙을 따를 수 있게 되죠.
Ruff가 주는 실질적인 이득
- “띄어쓰기, 줄바꿈 같은 사소한 것”에 쓸 에너지를 아끼고
- 그 시간에 설계/아키텍처/도메인 로직에 집중할 수 있습니다.
- 코드 리뷰에서도 “공백 한 칸” 얘기 대신 “로직” 얘기를 하게 됩니다.
한 번 워크플로우에 넣어두면,
이제는 Ruff 없는 프로젝트에서 거꾸로 답답함을 느끼게 됩니다.
6. pytest – 버그를 사용자보다 먼저 발견하게 해주는 테스트 프레임워크
테스트는 개발자에게 **“심리적 안전망”**에 가깝습니다.
테스트가 없으면:
- 작은 수정 하나에도 불안하고
- 리팩터링이 무서워지고
- 배포는 항상 도박처럼 느껴집니다.
반대로, 테스트가 잘 되어 있으면:
- 마음 편하게 코드를 뜯어고칠 수 있고
- 새로운 아이디어도 과감하게 시도할 수 있고
- 배포 직전에도 심장이 덜 뛰어요.
Python에서 그 역할을 맡는 게 바로 pytest입니다.
pytest가 좋은 이유
pytest는 입구는 쉬운데, 깊게 들어가면 굉장히 강력한 프레임워크입니다.
- 단순히 assert 한 줄로 시작할 수 있고
- 나중에는 fixture, mock, parameterization, class 기반 테스트 등
점점 확장해 나갈 수 있어요.
지원하는 것들:
- 함수 단위 unit test
- class / service 단위 테스트
- fixture 기반의 공통 setup/teardown
- mock/patch 활용
- parameterized test 등
실패했을 때 나오는 에러 메세지와 diff가 매우 읽기 좋다는 것도 장점입니다.
실제 프로젝트에서의 pytest 사용 예
제가 운영 중인 프로젝트 중 하나는
멘토링 프로그램용 Discord bot입니다.
코드베이스가 꽤 크고,
각종 서비스/모듈/외부 API와 엮여 있습니다.
여기에 pytest 기반의 테스트가 수십 개 파일로 흩어져 있어요.
(예: 65개 정도의 test 모듈)
- 작은 util 함수부터
- 특정 command handler
- 외부 API mock
- 에러 메시지, 로그까지 포함한 검증
전부 pytest로 커버합니다.
실행은 늘 똑같습니다.
uv run pytest
그러면 pytest가:
- test 파일을 자동으로 탐색하고
- test case를 전부 수집한 뒤
- 순서대로 돌려줍니다.
정상적인 경우에는 이런 메시지가 떠요.
65 passed, 1 warning in X.XXs
이 한 줄만 봐도 배포 전에 마음이 많이 놓이죠.
pytest가 버그를 잡는 순간
예를 들어, 제가 코드 어디를 잘못 수정했다고 해봅시다.
로그 메시지 하나를 바꿨는데 그걸 테스트가 기대하던 값과 다르게 바꿔버렸다든지.
다시:
uv run pytest
를 돌리면:
================================== FAILURES ==================================
...
E AssertionError: Expected "Ready" but got "Not ready" instead
...
=========================== short test summary info ===========================
FAILED tests/test_some_feature.py::test_some_case - AssertionError: ...
이런 식으로 딱 어떤 테스트가, 어떤 기대값 때문에 깨졌는지를 보여줍니다.
테스트 안에서 출력한 로그도 같이 확인할 수 있어요.
- “이 유저는 아직 서버에 없다”
- “조건이 만족되지 않았다”
같은 로그를 보면서 디버깅하면,
버그를 서비스에 태우기 전에 먼저 잡을 수 있습니다.
pytest를 워크플로우에 녹이는 방법
개발 사이클 안에 pytest를 넣으면 이런 흐름이 됩니다.
- 로컬에서 기능 개발
- 관련 테스트 작성 또는 기존 테스트 수정
- uv run pytest
- 테스트 통과 확인 후 commit & push
- CI에서 한 번 더 pytest
- 모두 통과하면 배포
이 루프가 몸에 배면,테스트 없는 프로젝트로 돌아가기 싫어져요.
테스트가 바꾸는 “코드 쓰는 방식”
pytest의 혜택은 버그 리스크를 낮추는 것만이 아닙니다.
테스트를 작성하다 보면 자연스럽게:
- 함수/모듈의 입력과 출력이 명확해지고
- side effect를 최소화하려 하고
- 코드 구조가 테스트 가능하게 바뀝니다.
결국 더 좋은 설계로 이어지고,
미래의 나와 동료들이 코드를 이해하기 훨씬 쉬워집니다.
7. Docker – “어디서 돌리든 똑같이 동작”하게 만드는 기반
어느 순간이 되면
**“내 컴퓨터에서는 잘 되는데…”**라는 말은 더 이상 통하지 않습니다.
- 내 노트북
- 팀 동료의 PC
- staging 서버
- production 서버
이 모든 환경에서 같은 방식으로 앱이 돌아가야 하죠.
OS 버전, system library, Python 버전 조금만 달라도
미묘하게 문제가 터질 수 있고,
이걸 사람 손으로 맞추려고 하면 지옥을 경험하게 됩니다.
그래서 요즘 Python 프로젝트에서 Docker는 거의 기본 도구가 됐습니다.
Docker가 Python 개발자에게 해주는 것
Docker의 핵심은 “컨테이너”입니다.
컨테이너 안에는:
- base OS image
- 필요한 Python 버전
- dependency
- 내 코드
가 모두 들어있어요.
이 모든 걸 Dockerfile 하나에 정의해 두고,
어디서든 동일한 Docker image만 받아서 실행하면 됩니다.
“한 번 잘 돌아간 컨테이너는
어디로 옮겨도 똑같이 동작한다”
는 안정감을 얻게 되는 거죠.
일반적인 Python + Docker 구성
대부분의 프로젝트에서 저는 보통 이렇게 구성합니다.
- 서비스별 Dockerfile
- 로컬 개발용 docker-compose.yml
- staging/production용 docker-compose.production.yml 또는 별도 compose 파일
이 파일들 안에:
- 어떤 image를 쓸지
- 어떤 포트를 열지
- 어떤 환경변수를 주입할지
- 어떤 volume을 마운트할지
전부 코드로 명시해 둡니다.
로컬에서 개발할 때는:
docker compose up
한 번이면:
- backend 서비스
- DB
- Redis나 message queue 같은 기타 서비스
가 한 번에 올라옵니다.
터미널 여러 개 열어서 이거저거 띄울 필요가 줄어들어요.
끝낼 때도:
docker compose down
이면 깔끔하게 정리됩니다.
.env와도 찰떡궁합
Docker는 .env와도 잘 맞습니다.
- .env에 공통 설정을 넣고
- docker-compose에서 해당 값들을 넘겨주고
- Python 코드에서는 python-dotenv로 로딩하거나
os.getenv()로 바로 사용하는 구조
를 만들면,
코드와 설정을 완전히 분리하면서도
환경마다 다른 값을 쉽게 줄 수 있습니다.
Docker가 줄여주는 “머릿속 부담”
애플리케이션이 Docker 컨테이너 안에서 안정적으로 돌아가기 시작하면:
- “저 서버는 Python 몇 버전이었지?” 같은 생각을 안 하게 되고
- 새로운 머신을 받았을 때도 docker compose up 한 번으로 환경을 재현할 수 있고
- 팀에 신규 인원이 들어와도 onboarding이 쉬워집니다.
배포 측면에서도 Docker image 하나를 기준으로
여러 환경에 동일한 아티팩트를 배포할 수 있으니,
“로컬에서만 되는 기묘한 상태”가 현저히 줄어듭니다.
7개 툴, 하나의 워크플로우로 묶기
지금까지 툴은 이렇게 7개였죠.
- Cursor
- uv
- python-dotenv
- ConfigCat
- Ruff
- pytest
- Docker
각각이 하는 역할은 다르지만,
이걸 한 흐름으로 묶으면 꽤 아름다운 그림이 나옵니다.
1) Cursor로 코딩 시작
에디터는 Cursor로 통일합니다.
- VS Code 기반 UI라 적응이 쉽고
- AI quick edit, agent 기능을 붙여서
“대량 리팩터링”과 “부분 수정” 모두 편하게 처리할 수 있습니다. - 코드에 대한 이해와 결정은 여전히 내 몫입니다.
2) uv로 프로젝트 뼈대 세우기
새 디렉터리를 만들고, 그 안에서:
uv init .
한 번이면:
- main.py
- pyproject.toml
- .python-version
- .venv
까지 알아서 만들어줍니다.
필요한 패키지는:
uv add fastapi
uv add streamlit
uv add python-dotenv
uv add ruff
uv add pytest
이렇게 추가하고,
실행은:
uv run main.py
uv run streamlit run main.py
uv run ruff check .
uv run pytest
로 다 해결합니다.
virtualenv 활성화 명령은 더 이상 기억할 필요가 없습니다.
3) python-dotenv로 설정 관리하기
.env 파일을 하나 만들고:
APP_NAME=Modern Python Stack
DEBUG=true
MAX_CONNECTIONS=500
API_KEY=my-secret-key
코드에서는:
from dotenv import load_dotenv
import os
load_dotenv()
app_name = os.getenv("APP_NAME")
이렇게 가져다가 쓰면 됩니다.
나중에 .env.production 같은 파일을 추가해서
환경별로 겹치는 값은 override하는 패턴도 쉽게 적용할 수 있죠.
4) ConfigCat으로 기능 켜고 끄기
새 기능을 만들 때, 바로 모든 사용자에게 배포하지 않고
ConfigCat의 feature flag 뒤에 숨겨둡니다.
- 처음엔 internal only
- 다음엔 5% 유저
- 문제가 없으면 100% rollout
이렇게 단계적으로 늘려가면서
문제 발견 시 대시보드에서 버튼 한 번으로 꺼버립니다.
5) Ruff로 코드 스타일 관리
커밋 전에:
uv run ruff check --fix .
uv run ruff format .
를 돌려서:
- 자동으로 고칠 수 있는 스타일 이슈는 Ruff에 맡기고
- 남은 부분만 사람이 직접 정리합니다.
pre-commit, GitHub Action에 붙여두면
팀 전체 코드 스타일이 자연스럽게 통일됩니다.
6) pytest로 안전망 깔기
기능을 만들면서 pytest 기반 테스트를 같이 작성합니다.
- 작은 함수
- service layer
- external API mock 등
전부 pytest로 묶어두고:
uv run pytest
으로 로컬/CI에서 계속 검증합니다.
테스트가 통과해야만 main 브랜치에 merge되게 만들어두면
배포 후 이슈를 훨씬 덜 겪게 됩니다.
7) Docker로 어디서든 같은 환경 재현
마지막으로 Dockerfile과 docker-compose.yml을 만들어서
서비스 전체를 컨테이너로 올립니다.
로컬에서는:
docker compose up
으로 모든 서비스를 한 번에 띄우고,
끝나면:
docker compose down
으로 정리합니다.
staging/production에서도
같은 Docker image를 기준으로 배포하면
환경 차이에서 오는 문제를 크게 줄일 수 있어요.
마무리: 내 워크플로우도 “디자인 대상”이다
많은 개발자들이 framework, library에는 관심이 많지만,
정작 자기 개발 워크플로우는 깊게 고민하지 않는 경우가 많습니다.
하지만 실제로는:
- 어떤 도구를 쓰는지
- 어떤 순서로 작업하는지
- 어느 지점에서 자동화를 거는지
이 모든 게 생산성과 멘탈 건강에 직접적인 영향을 줍니다.
한 번에 7개 다 바꿀 필요도 없습니다.
- 오늘은 uv랑 Ruff만 도입해도 되고
- 다음 주에는 pytest를 얹어보고
- 나중에 Docker와 ConfigCat까지 하나씩 추가해도 됩니다.
중요한 건,
조금씩 마찰을 줄이고,
조금씩 더 마음 편한 상태로 개발할 수 있는 방향으로
워크플로우를 계속 개선해 나가는 거예요.
이 글이:
- 기술적인 디테일과
- 실제 감정(불안, 자신감, 여유)을 함께 담은
하나의 “레퍼런스 워크플로우”로 도움이 되었으면 좋겠습니다.
창의력과 집중력을 보호해주는 도구들을 쓸 자격이 충분합니다.
이제는 도구가 발목을 잡는 게 아니라,
속도를 따라와 주는 환경을 만들어도 됩니다.