본문으로 건너뛰기
runlot
시작하기로컬 실행

로컬 실행

runlot dev는 내 컴퓨터에서 워커를 runlot 바인딩과 함께 실행합니다. 배포하지 않고 변경을 확인합니다.

runlot dev

runlot dev는 runlot deploy가 올릴 번들을 그대로 만들어 workerd 위에서 실행합니다. 배포된 앱이 도는 바로 그 런타임입니다. env.db, env.storage, env.auth, env.email, env.assets는 로컬 대역으로 연결합니다. 시작할 때 열어볼 주소와 함께 어느 바인딩이 실물이고 어느 것이 대역인지 표로 보여주고, 파일을 저장할 때마다 다시 빌드해 재시작합니다.

  runlot dev — my-app
  http://127.0.0.1:8787

  env.db                   real        real PostgreSQL (postgres://***@localhost:5432/app)
  env.storage              local       real bytes in .runlot/dev/storage
  env.auth                 fake        one dev user, a stub sign-in page, no providers
  env.email                fake        writes .eml into .runlot/dev/mail, sends nothing
  env.assets / env.ASSETS  real        the bundle's assets directory
  fetch()                  widened     dev also allows private and loopback addresses; production allows public only

상태는 .runlot/dev에 둡니다. .gitignore에 .runlot을 넣어 두세요.

옵션

runlot dev [dir] [--port 8787] [--cron "*/5 * * * *"]

--port는 열어볼 주소이고 기본값은 8787입니다. --cron은 워커의 scheduled() 핸들러를 그 5필드 UTC 일정으로 실행합니다. 원하는 때에 한 번만 실행할 수도 있습니다.

curl localhost:8787/__runlot/cron

런타임

runlot dev는 Node 흉내가 아니라 실제 workerd 바이너리 — 배포했을 때 워커를 실행하는 것과 같은 런타임 — 로 워커를 실행합니다. 이것이 중요한 이유가 있습니다. 로컬 실행이 잡아야 할 결함 — 내 compatibilityDate에서는 nodejs_compat이 아직 열어 주지 않는 Node 내장 모듈, 런타임이 싣지 못하는 모듈 그래프, Node의 것이 아닌 Request — 은 Node 위에 아예 존재하지 않습니다. Node에서 초록이 나와도 배포에 대해 알려주는 것이 없습니다.

바이너리는 CLI와 함께 설치됩니다. 설치할 때 선택적 의존성을 건너뛰었다면 프로젝트에 추가하세요.

npm i -D workerd

이미 가진 바이너리가 있다면 RUNLOT_DEV_WORKERD=/path/to/workerd로 가리킵니다.

무엇이 실물이고 무엇이 아닌가

env.db

PostgreSQL 주소를 주면 env.db는 그 데이터베이스입니다. 문장은 실제로 실행되고, 세션은 트랜잭션을 연 채로 유지하며, 실패한 문장은 진짜 SQLSTATE를 error.code에 담아 돌아옵니다. 셀은 프로덕션과 같은 표로 해석하므로, int8은 여기서도 저기서도 문자열입니다.

export RUNLOT_DEV_DATABASE_URL=postgres://localhost:5432/my_app_dev

주소 하나를 팀이 함께 쓴다면 runlot.json에 적어도 됩니다. 환경 변수가 우선합니다.

{ "dev": { "database": "postgres://localhost:5432/my_app_dev" } }

주소가 없으면 env.db는 모든 문장을 받아들이고 0행을 돌려주는 가짜이고, runlot dev가 시작할 때 그렇게 말합니다. 데이터베이스를 언급하는 워커가 기동하기에는 충분하지만, 쿼리가 맞는지 알려주지는 않습니다.

runlot pg migrate는 배포된 프로젝트에 migrations/를 적용합니다. 로컬 데이터베이스에는 psql로 적용하세요.

env.storage

.runlot/dev/storage에 실제 바이트로 저장합니다. put, get, head, delete, list는 프로덕션과 같게 동작합니다. 커서 페이지도, 없는 키를 지우는 것이 성공인 것도 같습니다. presign은 개발 서버 포트로 향하는 서명된 URL을 돌려주고 서명을 실제로 검증합니다. 바이트 수를 묶었다면 그것까지 검증하므로, 길이가 다른 업로드는 여기서도 거절당합니다.

env.auth

사용자 한 명과 /__runlot/auth/sign-in의 임시 로그인 페이지가 전부입니다. 어떤 주소든 받아들이고, 비밀번호를 확인하지 않으며, 어떤 공급자와도 통신하지 않습니다. env.auth.user(request)는 쿠키가 생긴 뒤에는 그 사용자를, 그 전에는 null을 돌려줍니다. 코드가 실제로 의존하는 구분은 그 둘입니다. 페이지를 아예 건너뛰려면 이렇게 합니다.

export RUNLOT_DEV_USER=[email protected]

env.auth.users의 읽기는 동작합니다. 쓰기는 흉내 내지 않습니다.

env.email

env.email.send는 .runlot/dev/mail에 .eml 파일을 쓰고 한 줄을 출력합니다. 아무것도 발송하지 않습니다. 보낸 것이 어떻게 생겼는지는 메일 클라이언트로 그 파일을 열어 보면 됩니다.

env.ai

env.ai는 로그인되어 있으면 로컬에서 동작합니다. 호출은 컨트롤 플레인을 거쳐 Studio의 로컬 런타임이 쓰는 것과 같은 레인으로 중계됩니다 — 같은 카탈로그, 같은 가격, 모든 플랜에 무료 플랜의 상한(출력 4,096 토큰, 분당 30회, 동시 2개) — 그리고 조직의 AI 크레딧에 lane = dev로 과금되므로 대시보드 원장에서 개발이 쓴 금액이 보입니다. runlot.json에 "org"가 있어야 하고(또는 --org), 프로젝트의 AI가 켜져 있어야 합니다(끄지 않았다면 모든 프로젝트가 켜져 있습니다). 스트리밍(stream: true)은 이름을 대고 거절합니다. 중계는 JSON 본문 하나를 답하는데, 코드가 청크를 기대하는 곳에 통째 답이 오면 로컬에서는 통과하고 배포하면 모양이 바뀌기 때문입니다. RUNLOT_DEV_AI=off를 주면 그 실행은 env.ai를 거절합니다. 로그인하지 않았으면 거절하고 바인딩 표에 그 이유를 적습니다.

env.assets

번들의 자산 디렉터리를 디스크에서 서빙합니다. "notFound": "spa"를 지키고, "runWorkerFirst": false는 프로덕션과 똑같이 자산 라우터를 워커 앞에 둡니다.

Durable Objects

대역이 아니라 실물입니다. Durable Objects를 구현하는 런타임이 workerd이므로, runlot dev는 durable_objects와 migrations 블록에서 같은 네임스페이스를 같은 런타임에 띄우고 new_classes / new_sqlite_classes 구분도 그대로 지킵니다. ctx.storage, ctx.storage.sql, transactionSync, alarm, idFromName은 런타임 자신의 것입니다. 여기서 찾은 결함은 프로덕션에서도 나왔을 결함입니다.

상태는 .runlot/dev/objects에 씁니다. 매번 다시 빌드할 때 지워지는 디렉터리 바깥 입니다. 파일을 저장할 때마다 카운터가 0으로 돌아가는 객체는 Durable Object가 아니니까요. 저장소는 프로젝트의 name을 키로 삼습니다. runlot.json에서 프로젝트 이름을 바꾸면 이전 상태는 따라오지 않고 그 자리에 남습니다.

dev에 없는 것은 객체를 둘러싼 플랫폼입니다. 프로세스가 하나라 배치도, locationHint나 jurisdiction도, 노드를 넘는 스텁도, 주차와 깨우기도, 백업도 없습니다. 로컬의 같은 env.db, env.storage, env.auth, env.email, env.ai, 애셋과 시크릿은 기본 export와 각 객체 클래스에 모두 연결됩니다. 그래도 로컬 지연 시간은 프로덕션을 말해 주지 않습니다. 프로덕션에서 객체의 env.db 호출은 프로젝트의 홈까지 네트워크로 이동합니다.

  env.ROOMS         real        class Room, state in .runlot/dev/objects
  durable objects   local only  one process, no placement, no park and wake, no backup — env.db from an object is local here and a network call in production

dev가 집행하는 매니페스트 규칙은 배포의 규칙이고, 보장은 한 방향입니다. 배포가 받아들일 것을 여기서 거절하는 일은 없습니다. 반대는 약속하지 않습니다. 여기서 통과한 매니페스트가 배포에서 거절당할 수 있습니다.

비밀값

이름과 값은 프로젝트 루트의 .env.runlot에서 읽습니다. 한 줄에 NAME=value 하나, #은 주석입니다. 이 파일은 .gitignore에 넣으세요. runlot dev는 읽어들인 이름만 출력하고 값은 절대 출력하지 않습니다. 이름은 배포가 집행하는 규칙과 같은 규칙을 통과해야 하므로, 여기서 되는 이름은 거기서도 됩니다.

값은 디스크의 파일이 아니라 프로세스 환경을 통해 워커에 닿습니다. 플랫폼이 쓰는 것과 같은 분리입니다.

fetch()

워커에서 나가는 fetch는 개발에서 사설 주소와 루프백까지 열어 둡니다. 로컬 앱을 로컬 API에 붙여 보는 것이 로컬에서 돌리는 평범한 이유이기 때문입니다. 배포된 워커는 공개 주소에만 닿습니다. 코드가 http://localhost:…를 부른다면 여기서는 되고 거기서는 실패합니다.

프레임워크

framework: "next"는 변경마다 OpenNext로 빌드해 그 산출물을 서빙합니다. 바인딩과 라우팅, 그리고 배포가 가질 모양을 확인하기에 맞는 자리이지만, 빠른 UI 반복을 위한 자리는 아닙니다. UI는 next dev로, 실제로 나갈 것의 확인은 runlot dev로 하세요. 프레임워크를 참고하세요.

CLI를 다른 API 엔드포인트로 돌리기

CLI는 따로 지정하지 않으면 https://dash.runlot.io와 통신합니다. RUNLOT_API_URL 또는 --api 플래그로 명령 하나, 또는 셸 세션 전체를 다른 곳으로 보낼 수 있습니다.

로그인 자격 증명은 엔드포인트별로 저장되므로, 대상을 바꿔도 한 호스트의 토큰이 다른 호스트로 전송되는 일은 없습니다.

runlot versions --api https://dash.runlot.io

이 페이지의 목차