GenHTTP Lambda

작동 방식

C# 스니펫 하나를 작성하세요. 스니펫이 반환하는 것은 몇 초 만에 HTTPS 공개 주소에 호스팅돼요. 필요한 내용을 모두, 실제로 만나게 될 순서대로 정리했어요.

람다란?

람다는 GenHTTP 핸들러를 반환하는 스니펫이에요. 플랫폼이 스니펫을 컴파일하고 불러와서, 반환된 것을 내 주소 아래에 마운트해요. 프로젝트도, 빌드 파일도, using 문도 필요 없어요. GenHTTP 모듈은 이미 모두 임포트되어 있어요.

return Content.From(Resource.FromString("hello"));

이게 완전한 람다예요. /lambda/your-key/에 배포하면 모든 요청에 hello라고 답해요.

스니펫은 클래스가 아니라 문(statement)의 나열이에요. 마지막에는 요청을 처리할 수 있는 것, 즉 핸들러나 핸들러 빌더를 반환해요.

첫 람다 만들기

  1. 1
    람다 만들기를 누르세요. 공개 주소와 에디터 키가 나와요. 키는 다시 들어올 수 있는 유일한 방법이니 꼭 보관하세요. 누구도 대신 찾아 줄 수 없어요.
  2. 2
    곧바로 람다의 관리 화면으로 들어가요. 첫 버전으로 작은 REST 서비스가 이미 작성되어 있어요. 시작점일 뿐이에요.
  3. 3
    에디터 키를 에이전트에게 주고 무엇을 만들지 말하세요. 에이전트는 MCP로 새 버전을 써요. 아니면 코드를 열고 직접 작성하세요. 검사를 누르면 아무것도 저장하지 않고 컴파일해서, 컴파일러가 뭐라고 하는지 파일과 줄 단위로 알려 줘요.
  4. 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. 1
    초안 섹션에서, 또는 어느 버전에서든 만들어요. 그 버전의 코드와 에셋, 그리고 람다 데이터의 복사본이에요.
  2. 2
    코드에서 직접, 또는 에이전트에게 요청해서 필요한 만큼 몇 번이고 고치세요. 미리 보기 배포를 누르면 초안만의 데이터 복사본으로 전용 주소 /features/…/에서 온라인에 올라가요. 람다의 방문자에게는 아무것도 보이지 않고, 초안이 쓴 것은 람다의 데이터에 닿지 않아요.
  3. 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# 파일로 취급해요.

lambda.cs
var shelf = new Shelf();

return Inline.Create()
             .Get(() => shelf.All())
             .Post((Book book) => shelf.Add(book));
Shelf.cs
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. 1
    코드에서 파일 옆의 +를 누르고 site/index.html 파일을 만드세요. 이름에 슬래시가 있으면 파일이 폴더 안에 들어가고, 확장자가 있으면 그 확장자에 맞는 파일로 취급돼요.
  2. 2
    site/app.css 파일과 site/app.js 파일도 같은 방법으로 추가하세요. 페이지에서는 href="app.css"처럼 이름만으로 참조해요. 폴더는 주소의 일부가 아니라, 제공되는 파일의 루트이기 때문이에요.
  3. 3
    이미지나 폰트처럼 텍스트가 아닌 파일은, site 안의 파일을 하나 연 다음 파일 옆의 업로드 버튼을 누르세요. 같은 폴더에 올라가요. PNG는 텍스트 에디터로 입력할 수 없으니, 이렇게 넣어야 해요.
  4. 4
    lambda.cs에서 폴더를 제공하세요.
    return Layout.Create().Add(Assets.App("site"));
  5. 5
    배포를 누르세요. site/index.html 파일은 / 주소에서, site/app.css 파일은 /app.css 주소에서 응답해요. 어느 파일과도 맞지 않는 주소에는 페이지로 응답하기 때문에, 자체 라우팅을 하는 프론트엔드도 누군가 딥 링크에서 새로고침해도 잘 작동해요.
  6. 6
    옆에 API를 추가하면 페이지가 통신할 상대가 생겨요.
    var api = Inline.Create().Get("notes", () => notes);
    
    return Layout.Create()
                 .Add("api", api)
                 .Add(Assets.App("site"));

파일을 두는 두 곳

람다는 파일을 두 곳에 두고, 에디터도 둘을 따로 보여 줘요. 파일에는 버전의 파일, 즉 프로그램이 있고, 데이터에는 워크스페이스, 즉 프로그램이 보관하는 것이 있어요. 차이는 누구의 것이냐예요. 버전의 파일은 그 버전의 것이고, 데이터는 람다의 것이라 모든 버전이 함께 써요.

버전에 담긴 것데이터에 담긴 것
담는 것코드와 에셋: 프론트엔드를 포함한 프로그램람다가 쓰거나 누군가 업로드한 모든 것
바뀌는 때바뀌지 않음: 수정하면 새 버전이 생김무언가 쓰이는 즉시
배포하면정확히 이 파일들이 온라인에 올라감그대로
롤백하면예전 파일로 돌아감영향 없음: 모든 버전이 함께 씀
초안을 만들면이 파일들의 복사본으로 시작복사본으로 작업
사라지는 때한도를 넘으면 오래된 버전과 함께람다와 함께, 또는 끌 때
코드에서 부르는 이름AssetsWorkspace

둘을 한 곳으로 합칠 수는 없어요. 합치면 배포할 때마다 람다가 그동안 쓴 걸 모두 지우거나, 반대로 배포한 파일을 절대 지울 수 없게 돼요. 순위표를 저장하는 게임에는 뒤쪽이, 그 게임이 제공하는 페이지에는 앞쪽이 필요해요. 그래서 페이지는 버전에, 순위표는 데이터에 둬요.

데이터 저장하기

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는 람다의 최근 요청, 출력한 내용, 던진 예외의 스택 트레이스를 돌려줘요. 에이전트는 이걸로 코드가 작동한다고 짐작하지 않고 직접 확인해요. 같은 내용을 관리 화면에서도 볼 수 있어요.

더 알아보기 →