Starlette 1.0이 출시됐다. 꽤 중요한 사건이다. Starlette은 브랜드 인지도에 비해 실제 사용량이 가장 많은 Python 프레임워크 중 하나일 것이다. FastAPI의 기반이 바로 Starlette이기 때문인데, FastAPI가 워낙 큰 화제를 모으면서 정작 Starlette 자체는 그 그늘에 가려져 온 셈이다.
Kim Christie가 2018년부터 Starlette 개발을 시작했고, 새로운 세대의 Python ASGI 프레임워크 중에서 금세 내가 가장 좋아하는 프레임워크가 됐다. 내 프로젝트인 Datasette의 기반으로 채택하지 않은 유일한 이유는, 당시 Starlette이 아직 안정성을 보장하지 않았기 때문이다. Datasette 플러그인에 안정적인 API를 제공하겠다는 의지가 확고했던 터라... 물론 나 자신도 26번의 알파 버전을 거치고도 아직 1.0을 출시할 용기를 내지 못하고 있지만!
그러다 2025년 9월, Marcelo Trylesinski가 Starlette과 Uvicorn을 자신의 GitHub 계정으로 이전한다고 발표했다. 수년간의 기여를 공식적으로 인정받고, 두 프로젝트에 대한 후원도 더 수월하게 받을 수 있도록 하기 위해서였다.
1.0 버전에는 0.x 시리즈와 비교해 몇 가지 호환성이 깨지는 변경 사항이 있으며, 2월에 공개된 1.0.0rc1 릴리스 노트에 자세히 설명돼 있다.
가장 눈에 띄는 변경 사항은 시작(startup)과 종료(shutdown) 시 코드 실행 방식이 바뀐 것이다. 기존에는 on_startup과 on_shutdown 파라미터로 처리하던 것을, 이제는 비동기 컨텍스트 매니저(async context manager) 기반의 깔끔한 lifespan 메커니즘으로 대체했다:
@contextlib.asynccontextmanager async def lifespan(app): async with some_async_resource(): print("Run at startup!") yield print("Run on shutdown!") app = Starlette( routes=routes, lifespan=lifespan )
Starlette을 써본 적 없다면, 내 느낌으론 asyncio 네이티브 방식으로 Flask와 Django를 절충한 프레임워크에 가깝다. 창시자인 Kim Christie가 Django REST Framework도 만든 사람이니 어찌 보면 당연한 결과다. 핵심적인 장점은 Flask처럼 대부분의 앱을 Python 파일 하나로 작성할 수 있다는 것이다.
덕분에 LLM이 프롬프트 하나만으로도 동작하는 Starlette 앱을 뚝딱 만들어내기가 무척 쉽다.
그런데 한 가지 문제가 있다. 1.0이 기존 Starlette 코드와의 호환성을 깼다면, 모델이 학습한 코드가 구버전 기준인데 어떻게 1.0에서 동작하는 코드를 생성하게 할 수 있을까?
이걸 Skill로 해결할 수 있을지 직접 시도해봤다.
claude.ai의 일반 Claude 채팅에는 skills 기능이 있고, 기본 제공 스킬 중 하나가 바로 skill-creator skill이다. 즉, Claude는 자기 자신의 스킬을 직접 만드는 방법을 알고 있다.
그래서 채팅 세션을 열고 이렇게 입력했다:
GitHub에서 Starlette을 클론해줘. 방금 1.0이 릴리스됐어. 이 버전에 맞는 스킬 마크다운 문서를 만들어줘. 모든 기능에 대한 코드 예제도 포함해서.
저장소 위치도 알려주지 않았다. Starlette이 충분히 알려진 프레임워크인 만큼 Claude가 알아서 찾을 거라 예상했기 때문이다.
Claude는 git clone https://github.com/encode/starlette.git를 실행했는데, 사실 이건 예전 저장소 이름이다. 하지만 GitHub이 자동으로 리다이렉트를 처리해줘서 문제없이 동작했다.
생성된 스킬 문서는 꽤 꼼꼼하게 작성된 것 같았다. 그런데 상단에 전에는 보지 못했던 "Copy to your skills"라는 버튼이 새로 생긴 게 눈에 띄었다. 눌러봤다:

이제 일반 Claude 채팅에서도 해당 스킬을 바로 사용할 수 있게 됐다!
새 대화를 열고 이렇게 프롬프트를 입력했다:
Starlette로 태스크 관리 앱을 만들어줘. 프로젝트, 태스크, 댓글, 레이블 기능이 있어야 해.
Claude는 곧바로 요구사항대로 구현했다. Starlette 1.0과 SQLite 데이터베이스(aiosqlite 사용), Jinja2 템플릿을 활용한 간단한 GitHub Issues 클론이 만들어졌다.
Claude는 앱을 직접 이런 방식으로 수동 테스트까지 했다:
cd /home/claude/taskflow && timeout 5 python -c "
import asyncio
from database import init_db
asyncio.run(init_db())
print('DB initialized successfully')
" 2>&1
pip install httpx --break-system-packages -q \
&& cd /home/claude/taskflow && \
python -c "
from starlette.testclient import TestClient
from main import app
client = TestClient(app)
r = client.get('/api/stats')
print('Stats:', r.json())
r = client.get('/api/projects')
print('Projects:', len(r.json()), 'found')
r = client.get('/api/tasks')
print('Tasks:', len(r.json()), 'found')
r = client.get('/api/labels')
print('Labels:', len(r.json()), 'found')
r = client.get('/api/tasks/1')
t = r.json()
print(f'Task 1: \"{t[\"title\"]}\" - {len(t[\"comments\"])} comments, {len(t[\"labels\"])} labels')
r = client.post('/api/tasks', json={'title':'Test task','project_id':1,'priority':'high','label_ids':[1,2]})
print('Created task:', r.status_code, r.json()['title'])
r = client.post('/api/comments', json={'task_id':1,'content':'Test comment'})
print('Created comment:', r.status_code)
r = client.get('/')
print('Homepage:', r.status_code, '- length:', len(r.text))
print('\nAll tests passed!')
"Claude Code에 관심이 쏠리다 보니 간과하기 쉽지만, Claude 자체도 이제 엄연한 코딩 에이전트다. 코드를 작성하고 직접 테스트까지 수행할 수 있다.
완성된 앱의 모습은 이렇다. 코드는 내 리서치 저장소에서 확인할 수 있다.

이 글은 블로그의 장문 아티클만 모아둔 피드입니다. 모든 포스트를 받아보려면 /atom/everything/을 구독하거나, 다른 구독 옵션을 확인해보세요.