작동 방식
C# 스니펫 하나를 작성하세요. 스니펫이 반환하는 것은 몇 초 만에 HTTPS 공개 주소에 호스팅돼요. 필요한 내용을 모두, 실제로 만나게 될 순서대로 정리했어요.
람다란?
람다는 GenHTTP 핸들러를 반환하는 스니펫이에요. 플랫폼이 스니펫을 컴파일하고 불러와서, 반환된 것을 내 주소 아래에 마운트해요. 프로젝트도, 빌드 파일도, using 문도 필요 없어요. GenHTTP 모듈은 이미 모두 임포트되어 있어요.
return Content.From(Resource.FromString("hello"));
이게 완전한 람다예요. /lambda/your-key/에 배포하면 모든 요청에 hello라고 답해요.
스니펫은 클래스가 아니라 문(statement)의 나열이에요. 마지막에는 요청을 처리할 수 있는 것, 즉 핸들러나 핸들러 빌더를 반환해요.
첫 람다 만들기
- 1람다 만들기를 누르세요. 공개 주소와 에디터 키가 나와요. 키는 다시 들어올 수 있는 유일한 방법이니 꼭 보관하세요. 누구도 대신 찾아 줄 수 없어요.
- 2곧바로 람다의 관리 화면으로 들어가요. 첫 버전으로 작은 REST 서비스가 이미 작성되어 있어요. 시작점일 뿐이에요.
- 3에디터 키를 에이전트에게 주고 무엇을 만들지 말하세요. 에이전트는 MCP로 새 버전을 써요. 아니면 코드를 열고 직접 작성하세요. 검사를 누르면 아무것도 저장하지 않고 컴파일해서, 컴파일러가 뭐라고 하는지 파일과 줄 단위로 알려 줘요.
- 4배포를 누르세요. 이제 온라인이에요. 배포하기 전에는 아무도 접속할 수 없어요. 다시 배포하면 온라인으로 유지되는 기간이 늘어나요.
관리 화면
에디터 링크를 열면 텍스트 상자가 아니라 관리 화면이 나와요. 여기 코드는 대부분 에이전트가 쓰기 때문에, 화면에서 가장 먼저 보이는 건 람다의 상태예요. 사이드바에는 람다 정보(온라인 여부, 주소, 온라인에 올라가길 기다리는 새 버전이 있을 때 나타나는 버튼)와 섹션들이 있어요. 주소 변경이나 삭제처럼 가끔 하는 일은 사이드바의 ⋯ 메뉴 안에 있어요.
- 개요
- 온라인 여부, 오늘 받은 요청 수와 그중 실패한 수, 최근 변경, 남은 공간.
- 수정 요청
- 무엇을 바꿀지 말하면 이 서버의 에이전트가 눈앞에서 해 줘요. 초안에서 작업하고, 거기서 써 보고, 잘 되면 확정해서 다음 버전으로 만들어요. 먼저 직접 초안을 써 보고 싶다면 끝나면 온라인에 올리기 스위치를 끄세요.
- 초안
- 람다와 따로 작업하는 수정이에요. 초안마다 전용 주소에서 써 보고, 제대로 되면 확정해서 다음 버전으로 만들어요. 초안을 열면 초안만의 코드, 데이터, 로그가 있어요.
- 파일
- 버전의 파일, 즉 코드와 에셋으로 된 프로그램 그 자체. 자물쇠나 지구본 아이콘으로 공개 여부를 알려 줘요.
- 데이터
- 람다가 실행 중에 보관하는 것으로, 모든 버전이 함께 쓰는 워크스페이스예요. 안을 살펴보고, 파일을 업로드하거나 삭제하고, 끌 수도 있어요.
- 버전
- 버전마다 무엇이 바뀌었고 무엇을 요청했는지, 그리고 이전 버전과의 차이. 여기서 배포하거나 롤백하고, 어느 버전에서든 초안을 만들 수 있어요.
- 배포 기록
- 언제 무엇이 온라인이었는지, 무엇 때문에 내려갔는지.
- 통계
- 최근 1시간 또는 하루 동안의 요청, 실패, 응답 시간, 가장 많이 요청된 경로.
- 로그
- 요청, 람다가 출력한 내용, 문제가 생긴 곳의 스택 트레이스를 실시간으로.
- 코드
- 직접 작성하기. 검사는 컴파일, 저장은 버전 만들기, 배포는 온라인에 올리기예요. 초안에서는 저장을 누르면 초안에 저장되고, 미리 보기 배포를 누르면 초안 주소에서 온라인에 올라가요.
Ctrl-S로 저장하고,F12로 선언으로 이동해요.
모든 섹션은 똑같이 생겼어요. 제목, 설명을 보여 주는 ⓘ, 오른쪽의 작업 버튼, 그리고 보기가 여러 개인 섹션이라면 그 아래에 탭이 한 줄 있어요. 코드 섹션에서는 파일이 탭이에요.
트래픽과 로그는 보관용이 아니라 지켜보기용이라 메모리에만 있어요. 서버가 재시작되면 처음부터 다시 쌓여요. 버전과 배포 기록은 저장돼요.
이유 남기기
버전은 코드와, 선택적으로 붙는 메모 두 개로 이뤄져요. 요구 사항은 사용자가 원하는 것과 그 이유를 가능하면 사용자의 말로 적은 것이고, 변경 사항은 이 버전이 하는 일을 한 줄로 적은 거예요. 둘 다 버전 기록에서 diff 옆에 표시돼서, 무엇 옆에 왜가 함께 남아요. 나를 위해서도, 무언가를 바꾸기 전에 기록을 읽을 다음 에이전트를 위해서도요.
POST /api/v1/lambdas/{editorKey}/versions { "files": [ { "name": "lambda.cs", "code": "..." } ], "specification": "사람들이 글을 남길 수 있는 방명록. 재시작해도 글이 남아 있어야 함", "change": "재시작해도 남도록 방명록 글을 워크스페이스에 저장" }
에이전트도 같은 두 필드를 write_code에 넘겨요. 코드에서는 저장할 때 변경 사항을 물어봐요. 둘 다 선택 사항이에요. 너무 길어도 거부하지 않고, 요구 사항은 4,000자, 변경 사항은 500자에서 잘라요. 초안도 자기 메모 두 개를 갖고 있고, 초안을 확정해서 생긴 버전이 그 메모를 이어받아요.
안전하게 바꾸기
버전은 한 번 저장하면 바뀌지 않아요. 그래서 모든 버전을 남겨 둘 가치가 있어요. 어느 버전이든 비교할 수 있고, 저장했던 그대로 다시 온라인에 올릴 수 있으니까요. 사람들이 쓰는 람다를 바꾸려면, 대신 초안을 만드세요.
- 1초안 섹션에서, 또는 어느 버전에서든 만들어요. 그 버전의 코드와 에셋, 그리고 람다 데이터의 복사본이에요.
- 2코드에서 직접, 또는 에이전트에게 요청해서 필요한 만큼 몇 번이고 고치세요. 미리 보기 배포를 누르면 초안만의 데이터 복사본으로 전용 주소
/features/…/에서 온라인에 올라가요. 람다의 방문자에게는 아무것도 보이지 않고, 초안이 쓴 것은 람다의 데이터에 닿지 않아요. - 3제대로 되면 확정하세요. 메모와 함께 다음 버전이 되고, 원하면 바로 온라인에 올라가요. 초안(미리 보기와 데이터 복사본)은 함께 사라져요.
POST /api/v1/lambdas/{editorKey}/features { "name": "순위표" } PUT /api/v1/lambdas/{editorKey}/features/{feature}/files?deploy=true POST /api/v1/lambdas/{editorKey}/features/{feature}/merge { "deploy": true }
초안은 여러 개를 동시에 작업할 수 있어요. 하지만 최신 버전을 기준으로 한 초안만 확정할 수 있어요. 그래야 확정할 때 초안을 만든 뒤에 저장된 버전이 되돌려지지 않아요. 다른 초안이 먼저 확정됐다면, 그 변경 사항을 가져온 다음(에이전트에게 맡겨도 돼요) 초안의 기준을 최신 버전으로 바꾸세요. 저절로 확정되는 건 없어요. 일부러 그렇게 만들었어요.
여러 파일 쓰기
타입을 꼭 그 타입을 쓰는 코드 아래에 둘 필요는 없어요. 코드에서 파일 옆의 +를 누르면, 새 파일이 스니펫과 같은 네임스페이스에서 함께 컴파일돼요. 그래서 아무것도 임포트하지 않아도 쓸 수 있어요. 확장자가 없는 이름은 C# 파일로 취급해요.
var shelf = new Shelf(); return Inline.Create() .Get(() => shelf.All()) .Post((Book book) => shelf.Add(book));
public sealed class Shelf { private readonly List<Book> _books = []; public IEnumerable<Book> All() => _books; public Book Add(Book book) { _books.Add(book); return book; } } public record Book(string Title, string Author);
페이지 제공하기
페이지를 제공하는 방법은 두 가지이고, 사람들이 옆에 올리는 파일을 위한 방법이 하나 더 있어요.
인라인으로 쓴 페이지 하나
작은 거라면 이걸로 충분해요. 페이지가 스니펫의 일부가 돼요.
var page = Resource.FromString(""" <!doctype html> <title>Mine</title> <h1>It works</h1> """) .Type(new ContentType("text/html; charset=utf-8")); return Content.From(page);
실제 파일이 담긴 폴더
스타일시트와 스크립트가 있다면 이 방법이 맞아요. 파일은 C# 파일과 같은 방식으로 추가하고, 작성한 그대로 제공돼요. 컴파일은 하지 않아요.
return Layout.Create() .Add("api", api) .Add(Assets.App("site"));
업로드된 파일, 데이터에서
사람들이 업로드하거나 람다가 만든 것(사진, 문서)을 앱 옆에서 제공할 때. 앱 자체의 페이지용은 아니에요. 페이지는 파일 폴더에 두어야, 그 페이지가 필요한 코드와 함께 버전으로 관리돼요.
return Layout.Create() .Add("api", api) .Add("uploads", Workspace.Files("uploads")) .Add(Assets.App("site"));
프론트엔드 따라 만들기
두 번째 방법을 처음부터 끝까지 볼게요. 모든 데모가 web 폴더에서 이 방식으로 페이지를 제공해요. demo-crud 데모를 열어 직접 읽어 보세요. 데모는 읽기 전용이고, 에디터 키는 데모 이름과 같아요.
- 1코드에서 파일 옆의 +를 누르고
site/index.html파일을 만드세요. 이름에 슬래시가 있으면 파일이 폴더 안에 들어가고, 확장자가 있으면 그 확장자에 맞는 파일로 취급돼요. - 2
site/app.css파일과site/app.js파일도 같은 방법으로 추가하세요. 페이지에서는href="app.css"처럼 이름만으로 참조해요. 폴더는 주소의 일부가 아니라, 제공되는 파일의 루트이기 때문이에요. - 3이미지나 폰트처럼 텍스트가 아닌 파일은,
site안의 파일을 하나 연 다음 파일 옆의 업로드 버튼을 누르세요. 같은 폴더에 올라가요. PNG는 텍스트 에디터로 입력할 수 없으니, 이렇게 넣어야 해요. - 4
lambda.cs에서 폴더를 제공하세요.return Layout.Create().Add(Assets.App("site"));
- 5배포를 누르세요.
site/index.html파일은/주소에서,site/app.css파일은/app.css주소에서 응답해요. 어느 파일과도 맞지 않는 주소에는 페이지로 응답하기 때문에, 자체 라우팅을 하는 프론트엔드도 누군가 딥 링크에서 새로고침해도 잘 작동해요. - 6옆에 API를 추가하면 페이지가 통신할 상대가 생겨요.
var api = Inline.Create().Get("notes", () => notes); return Layout.Create() .Add("api", api) .Add(Assets.App("site"));
파일을 두는 두 곳
람다는 파일을 두 곳에 두고, 에디터도 둘을 따로 보여 줘요. 파일에는 버전의 파일, 즉 프로그램이 있고, 데이터에는 워크스페이스, 즉 프로그램이 보관하는 것이 있어요. 차이는 누구의 것이냐예요. 버전의 파일은 그 버전의 것이고, 데이터는 람다의 것이라 모든 버전이 함께 써요.
| 버전에 담긴 것 | 데이터에 담긴 것 | |
|---|---|---|
| 담는 것 | 코드와 에셋: 프론트엔드를 포함한 프로그램 | 람다가 쓰거나 누군가 업로드한 모든 것 |
| 바뀌는 때 | 바뀌지 않음: 수정하면 새 버전이 생김 | 무언가 쓰이는 즉시 |
| 배포하면 | 정확히 이 파일들이 온라인에 올라감 | 그대로 |
| 롤백하면 | 예전 파일로 돌아감 | 영향 없음: 모든 버전이 함께 씀 |
| 초안을 만들면 | 이 파일들의 복사본으로 시작 | 복사본으로 작업 |
| 사라지는 때 | 한도를 넘으면 오래된 버전과 함께 | 람다와 함께, 또는 끌 때 |
| 코드에서 부르는 이름 | Assets | Workspace |
둘을 한 곳으로 합칠 수는 없어요. 합치면 배포할 때마다 람다가 그동안 쓴 걸 모두 지우거나, 반대로 배포한 파일을 절대 지울 수 없게 돼요. 순위표를 저장하는 게임에는 뒤쪽이, 그 게임이 제공하는 페이지에는 앞쪽이 필요해요. 그래서 페이지는 버전에, 순위표는 데이터에 둬요.
데이터 저장하기
Workspace는 람다가 읽고 쓸 수 있는 비공개 디렉터리예요. 요청이나 배포가 끝난 뒤에도 남아야 하는 건 여기에 두세요.
var notes = new List<string>(); if (Workspace.Exists("notes.json")) { notes.AddRange(JsonSerializer.Deserialize<List<string>>(Workspace.ReadText("notes.json")) ?? []); } return Inline.Create() .Get("notes", () => notes) .Post("notes", (string text) => { notes.Add(text); Workspace.WriteText("notes.json", JsonSerializer.Serialize(notes)); return notes.Count; });
그 밖에 ReadBytes, WriteBytes, Delete, List, CreateFolder도 있고, 파일 제공용으로 Tree/Files/App도 있어요. 파일 시스템의 다른 곳에는 접근할 수 없어요.
웹소켓
지원해요. 나중에 대충 붙인 기능도 아니에요. demo-game 데모는 플레이어를 짝지어 주고 모든 게임을 서버에서 실행해요. 가장 간단한 형태는 콜백 세 개예요.
var room = new ConcurrentDictionary<IReactiveConnection, string>(); var socket = Websocket.Functional() .OnOpen(c => { room[c] = "someone"; return ValueTask.CompletedTask; }) .OnMessage(async (c, text) => { foreach (var other in room.Keys) { await other.WritePayloadAsync(text); } }) .OnClose((c, _) => { room.TryRemove(c, out _); return ValueTask.CompletedTask; }); return Layout.Create().Add("chat", socket);
누구나 한 번씩 걸리는 게 있어요. 브라우저는 웹소켓 핸드셰이크에 헤더를 설정할 수 없어요. 핸들러에 필요한 값은 쿼리로 넘겨서 connection.Request.Header.Query에서 읽게 하거나, 비밀 값은 첫 메시지로 보내세요.
할 수 없는 것
코드는 공유 서버에서 실행되기 때문에, C#의 일부 기능은 컴파일 전에 거부돼요. 프로세스 시작, 직접 소켓 열기, 어셈블리 로드, 워크스페이스 밖의 파일 시스템 접근, 그리고 이런 제한을 우회하려는 리플렉션이에요.
GenHTTP 모듈 API 전체를 포함해 나머지는 모두 쓸 수 있어요. 거부될 때는 그냥 실패했다고만 하지 않고, 어느 줄이 왜 거부됐는지 알려 줘요.
가지고 나가기
에디터에서 .NET 프로젝트로 다운로드를 누르면 전체를 통째로 받을 수 있어요. 바로 열어서 dotnet run으로 실행하고, 계속 가지고 있을 수 있는 솔루션이에요. 패키지 참조는 하나뿐이고, 이 플랫폼의 흔적은 전혀 없어요.
스니펫은 Program.cs의 본문이 되고, 반환하는 것을 제공하는 호스트로 감싸져요. 다른 파일은 작성한 그대로 옮겨져요. Workspace와 Assets는 코드 옆의 폴더 두 개가 되고, 메서드도 같아서 코드를 하나도 바꿀 필요가 없어요.
여기서 뭔가 만들기 전에 알아 두면 좋아요. 작성한 코드는 내 것이고, 통째로 가지고 나갈 수 있어요. 여기서 실행한다고 해서 여기에 묶이지 않아요.
에이전트에게 맡기기
/mcp에 MCP 엔드포인트가 있어요. 에이전트를 여기에 연결하면 에디터로 할 수 있는 건 모두 할 수 있어요. 가이드 읽기, 데모 전체 읽기, 파일 작성, 컴파일, 배포까지요. 내부적으로는 같은 API예요.
에이전트는 작업하면서 이유도 남겨요. write_code가 요구 사항과 변경 사항을 함께 받거든요. 배포한 결과도 직접 확인할 수 있어요. read_logs는 람다의 최근 요청, 출력한 내용, 던진 예외의 스택 트레이스를 돌려줘요. 에이전트는 이걸로 코드가 작동한다고 짐작하지 않고 직접 확인해요. 같은 내용을 관리 화면에서도 볼 수 있어요.