Hooks 심층 분석 | Workflow SDK

VVercel
컴퓨터/소프트웨어창업/스타트업AI/미래기술

Transcript

00:00:00안녕하세요, 오늘 함께해주셔서 정말 감사합니다.
00:00:02저는 Vercel 워크플로 팀의 Praneet입니다.
00:00:05안녕하세요, 저도 워크플로 팀의 Nate입니다.
00:00:08Nate, 당신과 저 모두 처음부터 워크플로 팀에 있었죠.
00:00:12지난 6개월 동안 우리가 출시한 많은 기능 중에서
00:00:15저는 훅(hook)과 웹훅이 가장 마음에 드는 기능 중 하나예요.
00:00:18오늘 당신이 이야기할 주제도 바로 그것이고요.
00:00:21저도 훅과 웹훅을 가장 좋아합니다.
00:00:23정말 강력한 기능인데, 왜 그런지 몇 가지 데모를 통해 보여드릴게요.
00:00:28첫 번째 데모는 우리 모두에게 익숙한 '매직 링크'입니다.
00:00:33매직 링크는 로그인 양식입니다. 이메일을 입력하면 편지함으로 메일을 받고,
00:00:40그 링크를 클릭하면 서비스에 로그인이 되죠.
00:00:44네, 기억하기로 Vercel이 Vercel이라 불리기 전,
00:00:48그러니까 Zeit였던 시절에도 매직 링크가 유일한 인증 방식이었어요.
00:00:52당시에는 이 시스템 전체를 우리가 직접 구축했었죠.
00:00:56맞아요, 그래서 아직도 그때의 고생이 기억나네요.
00:01:01워크플로 없이는 이런 시스템을 구현하는 게 겉보기보다 훨씬 복잡하거든요.
00:01:08로직이 여러 파일에 흩어지게 됩니다.
00:01:12상태를 추적하기 위해 데이터베이스도 필요하고, 금방 지저분해지죠.
00:01:19네, 저도 이걸 어떻게 구조화하고 어떤 데이터베이스를 쓸지 고민했었는데,
00:01:24이런 종류의 문제는 전에도 만들어 본 적이 있는 아주 흔한 고민이죠.
00:01:28그래서 어떻게 구현됐는지 정말 보고 싶네요.
00:01:30네, 제가 말씀드린 그 불편한 점들을 보여드리기 위해,
00:01:38우선 워크플로 없이 구현한 "전통적인" 버전의 매직 링크 로그인을 준비했습니다.
00:01:43여기에는 세 개의 엔드포인트가 관여합니다.
00:01:47첫 번째는 로그인 양식이 제출될 때인데,
00:01:50세션을 생성하고 이를 Redis 같은 데이터베이스에 저장해야 합니다.
00:01:57TTL을 구현해야 하고, 데이터를 영원히 둘 수 없으니 만료도 시켜야 하죠.
00:02:06그리고 이메일을 보내는데, 이게 실패하면 로그인이 안 되어 사용자에게 답답한 경험을 줍니다.
00:02:14맞아요, 그러면 다시 돌아가서 크론 잡을 돌리거나 인턴을 시켜서 DB를 정리해야 하죠.
00:02:19그 당시엔 제가 그 인턴이었을지도 모르겠네요.
00:02:22하지만 두 번째 엔드포인트도 있습니다. 사용자가 이메일의 링크를 클릭할 때 실행되죠.
00:02:28이건 데이터베이스를 쿼리해서 첫 번째 엔드포인트에서 생성된 상태를 복구해야 합니다.
00:02:36이미 코드가 굉장히 복잡한 '스파게티 코드'가 되어가고 있네요.
00:02:38이게 어떤 모습일지 상상해 봤을 때, 이 코드는 정말 익숙하고 저도 이렇게 짰을 것 같아요.
00:02:48아주 간단한 개념인데도 얼마나 빨리 복잡해지는지 알 수 있죠.
00:02:54그럼 이제 워크플로에서는 어떻게 구현하는지 살펴봅시다.
00:02:59Workflow SDK를 사용한 매직 링크 구현은 이런 모습입니다.
00:03:05여기 함수가 있고 'useWorkflow' 디렉티브가 있는데, 이것이 우리의 워크플로 함수라는 뜻입니다.
00:03:11가장 먼저 하는 일은 워크플로 패키지의 'createWebhook' 함수를 호출하는 것입니다.
00:03:18여기서는 'respondWithManual' 옵션을 사용하고 있는데, 이는 웹훅을 트리거하는 HTTP 요청에 대한 응답을 워크플로 함수가 직접 작성한다는 의미입니다.
00:03:36이건 사용자가 로그인한 후에 리다이렉트 등을 처리하기 위한 건가요?
00:03:40네, 어떤 응답을 보낼지 결정하는 데 필요한 정보가 워크플로 함수 안에 있을 때 사용합니다.
00:03:51첫 번째 엔드포인트와 마찬가지로 로그인 이메일을 보냅니다. 이건 'useStep' 함수입니다.
00:03:57이렇게 하면 실패하더라도 Workflow SDK가 자동으로 재시도합니다.
00:04:03이러한 내구성이 이미 기존 방식보다 이점을 제공하고 있죠.
00:04:10그러니까 'sendLoginEmails'가 하나의 단계이고, 이메일 전송에 실패하면 이미 생성된 웹훅 URL을 사용해서 전송을 재시도하는 거군요.
00:04:21그리고 여기를 보시면 아주 흥미로운 패턴이 있습니다.
00:04:265분의 'sleep'과 함께 'promise.race'를 사용하고 있죠.
00:04:30이 웹훅 객체가 프로미스(promise)를 구현하고 있기 때문에 가능한 일입니다.
00:04:35웹훅 요청을 기다리려면 그냥 'await Webhook'을 하면 됩니다.
00:04:40여기서는 'race'와 함께 사용했죠. 웹훅 기능에 타임아웃 옵션이 따로 있을 거라 생각했는데 참 멋지네요.
00:04:50타임아웃을 웹훅과 'sleep' 사이의 'race'로 모델링하니 코드가 훨씬 깔끔해 보여요.
00:04:58이걸로 훨씬 많은 걸 할 수 있을 것 같아요. 두 개의 서로 다른 웹훅으로 'race'를 시킬 수도 있겠고요.
00:05:02함수 인자가 몇 개만 있을 때는 할 수 있는 게 많지 않죠.
00:05:06하지만 그냥 프로미스라서 'promise.race'를 'sleep'이나 다른 단계와 함께 쓸 수 있다는 게 좋네요.
00:05:12이 패턴 정말 마음에 들어요. 이걸로 무엇을 만들 수 있을지 머릿속에 아이디어가 샘솟네요.
00:05:16맞아요, 그게 바로 Workflow SDK가 제공하는 프리미티브(primitives)의 아름다움입니다.
00:05:21모든 것이 프로미스로 노출됩니다.
00:05:23그게 가장 핵심이죠.
00:05:28그리고 여기서 주목할 점은 Redis도, 데이터베이스도 없다는 겁니다.
00:05:33전통적인 예제에서는 타임아웃 구현을 위해 Redis의 TTL을 사용했었죠.
00:05:41이 경우에는 워크플로의 'sleep' 프리미티브를 사용하고 있습니다.
00:05:44또한 나중에 지저분한 DB를 정리해야 할 인턴도 필요 없고요.
00:05:50그게 제일 좋은 부분이네요.
00:05:51보시다시피 워크플로는 로그인 성공 페이지로 리다이렉트하여 공용 요청에 응답합니다.
00:05:59그런 다음 로그인을 시작한 클라이언트에 반환할 사용자 정보를 가져옵니다.
00:06:07이것이 워크플로 전체입니다. 매직 링크 구현이 단 50줄의 코드로 끝났죠.
00:06:12정말 놀랍네요. 실제로 작동하는 걸 볼 수 있을까요?
00:06:17네, 여기 매직 링크 데모가 있습니다. 제 이메일을 입력해 볼게요.
00:06:24워크플로가 시작되어 이메일을 보냈습니다. 그리고 웹훅이 대기 중이죠.
00:06:31사실 지금 워크플로는 중단(suspended)된 상태입니다. 사용자가 링크를 클릭하길 기다리는 동안 컴퓨팅 자원을 전혀 소모하지 않죠.
00:06:41오, Vercel에서는 어떤 모습인가요? 대기 중인 실행 상태를 볼 수 있을까요?
00:06:47네, 이메일이 왔네요. 클릭하기 전에 관찰 도구(observability)를 살펴봅시다.
00:06:52제가 좀 서두르는 것 같지만, 이걸 보는 게 정말 즐겁네요.
00:06:57좋아요, 여기 실행 기록이 있고 40초 전에 시작된 걸 볼 수 있습니다.
00:07:02들어가 보면 워크플로가 제공하는 표준 관찰 기능들이 있습니다.
00:07:08워크플로 실행에 입력된 값들을 볼 수 있죠. 로그인 양식에 입력한 제 이메일 주소가 보입니다.
00:07:13그리고 흥미롭게도 여기 훅이 그냥 기다리고 있는 것을 볼 수 있습니다.
00:07:17지금 컴퓨팅 자원이 실행되지 않는다고 하셨죠. 상태는 보이지만 실제로 제가 훅을 클릭하기를 기다리며 상주하는 프로세스는 없다는 거죠?
00:07:25맞습니다. 훅은 대기 중이고 'sleep'은 작동 중이지만, 두 가지 모두 실제 컴퓨팅 자원을 사용하지 않습니다.
00:07:39하지만 여기 훅을 보시면, 기억하시겠지만 이 둘은 'promise.race'에서 경합 중입니다.
00:07:46워크플로가 계속 진행되려면 둘 중 하나가 먼저 완료되어야 합니다.
00:07:50자, 링크를 클릭하면... 로그인 성공 페이지로 리다이렉트되었습니다. 우리 워크플로 로직의 단계 중 하나였죠.
00:07:59다시 로그인 양식으로 돌아가 보면...
00:08:01네, 대시보드에서도 완료된 것으로 나올 겁니다.
00:08:05맞습니다. 워크플로가 완료되었습니다.
00:08:08훅이 승리하자마자 타이머도 바로 멈추는군요.
00:08:11네, 약 50줄의 코드로 매직 링크를 구현할 수 있었습니다.
00:08:17정말 깔끔하네요. 화이트보드에 매직 링크가 어떻게 작동하는지 그려서 설명하는 것과 같아요.
00:08:27코드에 있는 단계들이 구상했던 로직과 정확히 일치하는데, 그게 곧 최종 코드가 된 셈이니까요.
00:08:34중간에 별도의 데이터베이스도 없고 API 라우트가 여러 개일 필요도 없죠. 보여주신 코드가 정말 직관적입니다.
00:08:41제 생각에 Workflow SDK의 가장 강력한 면은, 인프라에 맞추기 위해 억지로 늘리는 게 아니라 로직이 논리적으로 흐르도록 애플리케이션 구조를 짜게 해준다는 점입니다.
00:08:59맞아요. 또한 여기서 웹훅이라는 이름을 쓴 게 마음에 들어요. 웹훅에 대해 완전히 다르게 생각하게 해주거든요.
00:09:07그저 생성하고 중단할 수 있는 일회성 URL인 거죠.
00:09:10사실 이게 좋은 연결고리가 되는데, Vercel에서 에이전트를 많이 만들잖아요.
00:09:16Slack이나 GitHub 에이전트들을 만드는데, 보통 GitHub나 Slack에서 오는 웹훅을 구독하죠?
00:09:23PR에 새 댓글이 달릴 때마다 Vercel 에이전트를 가동하고 싶을 때, GitHub가 보내는 웹훅 이벤트에 기반해서 하잖아요.
00:09:31워크플로 웹훅을 사용해서 GitHub 같은 곳의 이벤트를 구독할 수도 있나요?
00:09:36Slack이나 GitHub에서 발송되는 웹훅의 경우, 보통 대시보드에 들어가서 정적인 콜백 URL을 수동으로 설정해야 합니다.
00:09:49그렇죠. 이메일처럼 일회성 URL을 줄 수는 없으니까요.
00:09:54맞습니다. 'webhook 생성' 기능은 그보다 좀 더 고수준의 기능으로, 무작위로 생성된 고유한 웹훅 URL을 제공합니다.
00:10:04그것은 하나의 특정 워크플로우 실행에 매핑됩니다.
00:10:07우리의 GitHub나 Slack 웹훅 경로의 경우, 수많은 워크플로우 실행에 매핑될 수 있습니다.
00:10:14맞아요. 여러 개의 풀 리퀘스트가 있어도 모두 동일한 엔드포인트로 가도록 미리 설정해야 하죠.
00:10:20그래서 Workflow SDK로 이를 구현하기 위해, 한 단계 더 내려가서 더 저수준의 'hook' 프리미티브를 사용할 것입니다.
00:10:28그것을 보여드리기 위한 데모를 준비했습니다.
00:10:31한번 보시죠.
00:10:32좋습니다. 이것은 '스토리타임(storytime)' 봇입니다.
00:10:35제가 1년 조금 전쯤에 Workflow SDK로 작성한 아주 초기 애플리케이션 중 하나입니다.
00:10:40작동 방식은 'storytime/' 명령어를 입력하면 스레드가 생성되는 것을 볼 수 있습니다.
00:10:47각 스레드는 개별 워크플로우 실행으로 표현됩니다.
00:10:52스레드를 확장하면 LLM이 우리를 위해 이야기를 시작한 것을 볼 수 있고, 저나 당신, 또는 이 채널에 있는 누구든 이야기를 이어갈 수 있습니다.
00:11:05그러면 LLM이 이야기가 최종 결론에 도달할 수 있도록 도와줍니다.
00:11:09좋아요. 루나가 마법의 씨앗을 가졌고, 다음에 무슨 일이 일어날까요? 그녀는 씨앗을 심습니다.
00:11:13네, 여기서 어떤 활동이 일어나는 것을 볼 수 있습니다.
00:11:17다음에 무슨 일이 생길까요? 마법 같은 일이요.
00:11:20이야기가 끝났고 최종 결과물이 나왔습니다. 작은 이미지도 함께 생성될 거예요.
00:11:26하지만 그 부분은 나중에 다시 살펴보겠습니다.
00:11:28벌써 정말 궁금한데요, 웹훅 요청이 한 번일 줄 알았는데 메시지가 두 개라서 최소 두 번의 요청이 있었거든요.
00:11:35그래서 코드에서는 어떤 모습일지 정말 궁금합니다.
00:11:38좋습니다. 이것이 우리 스토리타임 봇의 워크플로우 함수입니다.
00:11:44스토리타임 봇 채널인 채널 ID를 입력으로 받는 것을 볼 수 있습니다.
00:11:50전달할 수 있는 몇 가지 구성 옵션들이 있고요.
00:11:53흥미롭게도 'messages' 배열이 있는데, AI SDK에 익숙하시다면 이것이 AI 대화를 저장하는 데이터 형식이라는 걸 아실 겁니다.
00:12:04우리가 여기서 만든 것과 같은 전형적인 Slack 봇 애플리케이션에서는 보통 이런 데이터를 데이터베이스에 저장하고, 각 반복이나 웹훅 이벤트마다, 즉 스레드에 메시지가 입력될 때마다 상태를 복원하고 데이터베이스에서 대화 내용을 조회해야 합니다.
00:12:23그런데 여기서는 그렇지 않습니다. 그냥 함수 내의 배열일 뿐이죠.
00:12:27네. 아까 도입부에서 "엄마, 큐(queue)나 키-값 저장소(KV)가 없어요"라는 주석을 보고 웃음이 났거든요.
00:12:34데이터베이스 임포트(import)도 없네요. 그냥 Workflow만 임포트하고 계시고요.
00:12:40그리고 마지막 메시지 부분으로 돌아가면, 자칫 놓치기 쉽지만 여기 'final story'라는 변수가 있습니다. 아마 시간이 지나면서 이 배열에 메시지를 푸시(push)하겠죠.
00:12:55그러면 최종 이야기가 여기서 문자열로 나타날 텐데, 데이터베이스로 갈 필요가 없습니다. 마치 'let'이 여기서 당신의 데이터베이스인 것 같네요.
00:13:04네, "let이 당신의 데이터베이스다"라는 표현 좋네요. 우리가 만든 용어로 써야겠어요.
00:13:10제가 당신에게서 훔쳐온 말일 수도 있겠지만요.
00:13:14여기서 흥미로운 점이자 우리가 이야기하러 온 부분은 'hook' 기능입니다. 여기서 hook을 생성하고 있죠. 매직 링크가 있던 웹훅 예제와 다른 점은 여기서 '토큰'을 제공한다는 것입니다.
00:13:28이 토큰은 해당 워크플로우 실행에 고유한 식별 정보를 포함하는 문자열입니다.
00:13:35TS는 스레드 ID입니다. 따라서 이 문자열은 이 워크플로우 실행을 유일하게 식별하는 토큰이 됩니다.
00:13:44웹훅 경로의 코드를 보면 Slack이 보내는 이벤트 페이로드가 이 식별자를 결정론적으로 재현하는 데 필요한 모든 정보를 포함하고 있음을 알 수 있습니다.
00:13:58그것이 바로 웹훅이 개별 워크플로우 실행으로 다시 매핑되는 마법 같은 방식입니다.
00:14:04맞아요, 웹훅을 봤을 때 궁금했거든요. 매직 링크 예제에서는 새 URL을 생성했지만, Slack 봇을 만들어 본 경험상 모든 스레드마다 새 URL을 줄 수는 없으니까요.
00:14:17여기서 하시는 방식을 이해하자면, API가 이미 Slack에 연결되어 있지만 메시지를 받을 때마다 재개(resumption) 측에서 동일한 토큰을 계산하는 것이군요.
00:14:29그래서 워크플로우는 기본적으로 이 토큰을 기다릴 수 있고, 메시지 페이로드로부터 동일한 토큰을 구성하여 이 워크플로우 실행을 재개할 수 있는 거네요.
00:14:37정확합니다. Slack 봇은 대시보드에서 수동으로 한 번 설정되었고, 정적인 웹훅 콜백 URL을 정의해야 합니다.
00:14:50그래서 토큰을 동적으로 재현할 수 있는 저수준의 hook 프리미티브가 이 경우에 더 잘 작동하는 것입니다.
00:14:59잠깐 살펴보면, 이것이 웹훅 경로인데 실제로 별로 대단한 건 없습니다.
00:15:07핵심은 Slack에서 전달된 데이터로부터 토큰을 어떻게 재현하느냐입니다.
00:15:13그런 다음 재개(resume) 함수를 호출하면 해당 실행에 고유한 워크플로우 실행이 재개됩니다.
00:15:20정말 멋지네요. 제 생각에는 웹훅으로 하시는 일도 기본적으로는 같은 방식인 것 같아요.
00:15:28웹훅이 기본적으로 그냥 무작위 토큰을 만들고, 동일한 무작위 토큰을 해결하는 HTTP 엔드포인트를 갖는 건가요?
00:15:35네, 웹훅 기능과의 차이점은 코드에 해당 API 경로를 정의할 필요가 없다는 것입니다.
00:15:44Workflow SDK가 실제로 웹훅 기능을 위한 기본 경로를 대신 구현해 줍니다.
00:15:50하지만 그 외에는 특정 워크플로우 실행에 고유한 무작위로 생성된 토큰이라는 점은 같습니다.
00:15:55하지만 이 경우 토큰이 있는 hook을 사용하고, 방금 언급하신 것처럼 이 hook은 데이터를 여러 번 받을 수 있습니다.
00:16:06이는 한 번만 트리거되면 되었던 매직 링크 예제와는 다릅니다.
00:16:11이 경우에는 누군가 Slack 스레드에 입력하는 각각의 고유한 메시지에 대해 hook이 실행되기를 원합니다.
00:16:17그렇게 하기 위해 비동기 이터레이터(async iterator)에서 흔히 사용하는 JavaScript의 'for await' 구문을 사용합니다.
00:16:25이 경우, 우리의 hook을 사용하여 Slack 웹훅으로부터 여러 이벤트 페이로드를 수신하게 됩니다.
00:16:33정말 멋지네요. 비동기 이터레이터와 제너레이터를 좋아해서 오래전에 관련 발표도 했었지만, 마땅한 사용 사례를 찾지 못했었거든요.
00:16:42데모에는 좋았지만 실제 활용법을 찾기 어려웠죠.
00:16:46여기서는 그냥 루프를 돌리는 것처럼 보이네요.
00:16:50고정된 항목들이나 타임스탬프를 루프 도는 대신, 'for await'을 hook에 사용함으로써 정확히 매핑되는 루프가 생겼습니다.
00:17:01루프 내부의 모든 것이 하나의 사용자 메시지에 매핑되네요.
00:17:05새로운 사용자 메시지가 이 루프의 또 다른 반복을 일으키고, 그것이 계속 쌓여서 진행된다는 점이 생각하기에 참 좋은 방식입니다.
00:17:12이 방식의 아름다운 점은 사용자가 다음 메시지를 입력하기를 기다리는 동안, 각 반복 사이에 컴퓨팅 자원이 전혀 소모되지 않는다는 것입니다.
00:17:22워크플로우는 완전히 중단(suspend)된 상태이며, 다음 메시지는 몇 분 뒤에 올 수도, 며칠 뒤에 올 수도, 혹은 영영 오지 않을 수도 있지만 그래도 상관없습니다.
00:17:33그럼 그 Slack 채널의 스레드 중에, 아무도 대답하지 않아서 몇 주 동안 그냥 대기 중인 워크플로우 실행이 있을 수도 있겠네요.
00:17:42정말 멋집니다.
00:17:43그리고 아까 언급한 messages 배열로 돌아가서, 이제 배열을 수정합니다.
00:17:48새 사용자 메시지를 푸시하는데, messages 배열이 로컬 변수이기 때문에 이것이 곧 우리의 데이터베이스 수정이 되는 셈이죠.
00:17:57대단하네요. 그리고 중간에 더 많은 단계를 병렬화하기 위해 'promise.all'을 더 사용하시는 것도 보이네요.
00:18:03Slack의 모든 루프와 모든 메시지에 대해 코드가 정말 깔끔하게 읽힙니다.
00:18:08해커톤 같은 곳에서 이걸 만든다면 딱 이렇게 모델링하고 싶을 것 같아요.
00:18:12모든 메시지에서 일어나는 일을 그냥 그대로 적어 내려가는 식이니까요.
00:18:16네, 'promise.all' 모델은 그냥 일반적인 step 함수들이고 이들을 병렬로 실행하려는 아이디어입니다.
00:18:23Slack 메시지에 리액션을 추가하는 것 같은 작업은 사용자에게 무언가 진행 중이라는 즉각적인 피드백을 주기 위한 것입니다.
00:18:32동시에 LLM을 시작해서 이야기 생성 과정이 진행되도록 하고 싶고요.
00:18:39나중에 기회가 되면 관측성(observability)이 어떤 모습일지도 정말 보고 싶네요. 그런 스팬(span)들이 동시에 시작되는 게 아주 명확하게 보일 것 같거든요.
00:18:49스토리타임에 대한 관측성 화면이 여기 있습니다.
00:18:52완료되었으므로 아까 그 이미지도 확인해 봐야겠네요.
00:18:56우리의 hook을 볼 수 있습니다.
00:18:58여기서 흥미로운 점은 이 경우 두 번의 hook 수신 이벤트가 있다는 것입니다.
00:19:05이것은 제가 Slack 스레드에 입력한 두 개의 메시지에 매핑됩니다.
00:19:10그리고 관측성을 통해 hook에 제공된 개별 데이터를 확인할 수 있습니다.
00:19:14오, 정말 멋지네요.
00:19:16이게 기본적으로 Slack 페이로드군요. 추가로 로그를 남길 필요 없이 Slack 페이로드가 이벤트로 대시보드에 나타나서 다시 점검할 수 있다는 거죠.
00:19:25맞습니다. hook 페이로드가 수신될 때마다 워크플로우 실행이 이어지고 단계들이 계속 진행되는 것을 볼 수 있습니다.
00:19:34그리고 마지막으로 스토리보드 이미지를 생성한 결과가 저기에 나타납니다.
00:19:40이것이 스토리타임 봇입니다.
00:19:42정말 멋집니다.
00:19:43매직 링크로 생성된 웹훅과, 이제 더 저수준의 hook 프리미티브를 사용하여 루프 내에서 여러 이벤트를 처리하는 것까지 모두 보게 되어 정말 좋네요.
00:19:54정말 훌륭합니다.
00:19:55웹훅을 이용한 휴먼 오퍼레이션(human operations)이 어떤 모델인지 확실히 와닿네요.
00:20:02hook을 다른 용도로도 사용할 수 있을까요?
00:20:05네, 물론입니다.
00:20:06제가 준비한 마지막 데모는 Slack 웹훅에 응답하는 것과 매우 유사한 패턴입니다.
00:20:17하지만 이 경우에는 웹훅을 우리 애플리케이션 코드의 실행을 잠시 넘겨주고, 워크플로우가 중단된 상태에서 외부의 연산이 끝나기를 기다리는 용도로 사용할 것입니다.
00:20:32그런 다음 해당 웹훅 URL을 사용해 다시 호출하면, 우리 애플리케이션 로직의 나머지 작업을 마무리할 수 있습니다.
00:20:41이 예제에서는 Vercel Sandbox를 사용해 FFmpeg로 파일을 변환하는 것과 같은 오래 걸리는 연산 작업을 수행할 것입니다.
00:20:51이것이 우리의 FFmpeg 변환 워크플로우입니다.
00:20:56가장 먼저 일어나는 일 중 하나는 Vercel Sandbox를 생성하는 것입니다.
00:21:00흥미로운 점은 실제로 Sandbox NPM 패키지가 제공하는 함수들 내부에서 'use step'을 사용하고 있다는 것입니다.
00:21:09그래서 실제로 이 작업 자체가 하나의 Step입니다.
00:21:12즉, NPM 패키지를 출시할 수 있는 거죠.
00:21:15Sandbox는 기본적으로 함수 내부에 'use Step' 디렉티브가 포함된 NPM 패키지를 배포하는 방식입니다.
00:21:21따라서 워크플로우 내에서 이를 임포트해 사용하면, 별도의 코드를 작성하지 않아도 Sandbox가 자동으로 Step이 됩니다.
00:21:29그렇다고 워크플로우 외부에서 Sandbox를 생성할 수 없다는 뜻은 아닙니다.
00:21:32워크플로우 없이 호출하면 어떻게 되나요?
00:21:35아시다시피 디렉티브는 그냥 문자열일 뿐이라서, 워크플로우 컴파일러 없이 실행하면 그 문자열은 아무런 기능도 하지 않습니다.
00:21:47그래서 그냥 정상적으로 작동하죠.
00:21:49NPM 패키지에 'use Step'을 추가하는 것은 워크플로우 SDK 없이도 전혀 문제가 없습니다.
00:21:55그리고 워크플로우 SDK 내부에서 해당 함수를 사용하면, 즉시 내구성(durability)이라는 추가적인 혜택을 얻게 됩니다.
00:22:03알겠습니다, Sandbox는 일반적인 작업들을 수행하는군요.
00:22:07기본적으로 제공되지 않는 FFmpeg을 설치하고,
00:22:11우리가 지정할 파일의 URL을 다운로드합니다.
00:22:14그럼 이 각각의 실행 과정들도 지금은 다 Step인가요?
00:22:17네, Sandbox에서 개별 명령어를 실행하는 것들이 모두 Step이며, 관찰 도구(observability)에서 확인할 수 있습니다.
00:22:29그리고 다시 'create-webhook' 호출로 돌아가는데, 매직 링크 데모에서 보셨던 것과 비슷할 겁니다.
00:22:36하지만 이번에는 웹훅 URL을 Sandbox에서 실행할 Bash 스크립트에 전달할 것입니다.
00:22:43여기서 일어나는 일은 FFmpeg을 실행하여 UI에서 요청한 형식으로 파일을 변환하는 것입니다.
00:22:53변환이 완료되면 Bash 스크립트가 웹훅의 콜백 URL로 cURL 요청을 보냅니다.
00:22:59해당 cURL 요청이 발생하면 워크플로우 로직이 다시 재개됩니다.
00:23:04아, 이해했습니다. 정말 멋지네요. 제가 조금 앞서 살펴봤는데, 이 실행 부분에 'AND'가 있는 걸 봤어요.
00:23:11FFmpeg Step은 시간이 꽤 걸릴 수 있기 때문에, 실제로 스크립트를 작성해서 백그라운드에서 실행하시는 거군요.
00:23:17단순히 Step이 가만히 앉아서 완료를 기다리게 하고 싶지 않으신 거죠.
00:23:20맞습니다. 바로 이 줄이 백그라운드에서 FFmpeg 변환 스크립트를 시작하는 부분입니다.
00:23:28그러면 워크플로우 함수는 일시 중단(suspend)되고 웹훅이 재개되기를 기다립니다.
00:23:34그리고 다시 1시간의 sleep과 함께 promise race를 사용하는군요. 정말 멋진 패턴입니다.
00:23:40그렇죠. 이번 FFmpeg 변환 프로세스는 꽤 오래 걸릴 수도 있습니다.
00:23:46매우 큰 미디어 파일일 수도 있으니까요. 그래서 이 경우에는 1시간의 타임아웃을 지정했습니다.
00:23:51워크플로우에서는 사실상 무제한으로 sleep 상태를 유지할 수 있으므로 전혀 문제없습니다.
00:23:56다시 말씀드리지만, 웹훅이 재개되기를 기다리는 동안 돌아가는 연산(compute) 비용은 0입니다.
00:24:01이걸 직접 볼 수 있을까요? 실행되는 데모가 있나요?
00:24:04네, 있습니다.
00:24:05약간 뻔한 예시이긴 하지만요.
00:24:07아니요, 바로 'Big Buck Bunny' 예시라는 걸 알아봤어요. Blender에서 만든 거죠.
00:24:12맞아요. 아주 예전에 Blender를 배울 때 이 영상들을 봤던 기억이 나네요.
00:24:16와, 부러운데요.
00:24:19미디어 파일 URL을 붙여넣었습니다. 이번에는 여기서 오디오 레이어만 추출해 보겠습니다.
00:24:26버튼을 클릭하면 워크플로우가 시작되고, 관찰 도구 페이지로 이동해서 확인할 수 있습니다.
00:24:33아, 여기 있네요. Sandbox 생성을 확인할 수 있습니다.
00:24:37그리고 Sandbox 인스턴스가 반환되네요. 정말 멋집니다.
00:24:42이건 워크플로우의 모든 요소가 직렬화(serializable) 가능해야 하기 때문이죠.
00:24:46말씀하신 대로 Sandbox가 직렬화를 구현했기 때문에 워크플로우에서도 직렬화되어 나타나는 거군요.
00:24:53맞습니다. Vercel Sandbox NPM 패키지에는 Sandbox 클래스가 있고, 그 클래스가 워크플로우 직렬화 함수를 구현하고 있습니다.
00:25:03그래서 관찰 도구에서도 바로 잘 작동하는 것이죠.
00:25:06어떤 패키지든 이렇게 할 수 있다는 거죠? Sandbox만 특별한 게 아니라, 워크플로우 내부에서 작동하고 싶은 클래스라면 동일한 심볼을 구현하고 'use Step' 디렉티브를 가질 수 있겠네요.
00:25:17네, 맞습니다. 이번에는 20초 만에 웹훅이 다시 호출된 것을 볼 수 있습니다.
00:25:25파일이 작아서 변환이 좀 빨랐지만, 시간이 얼마나 걸렸든 상관없었을 겁니다.
00:25:31Sandbox가 생성되고 초기화된 후, 웹훅이 생성되어 Sandbox에 전달되었고 FFmpeg 명령이 시작된 것을 확인할 수 있습니다.
00:25:43그리고 작업이 끝났을 때 Sandbox로부터 페이로드를 전달받았습니다.
00:25:48이게 아까 Bash 스크립트 내부에서 실행된 cURL이군요. 명령을 작성한 뒤 Sandbox에서 말 그대로 cURL을 사용해 웹훅을 완료하는 거죠.
00:25:57맞습니다. Sandbox가 할 일을 마쳤으므로 제어권을 다시 워크플로우로 넘겨주는 것입니다.
00:26:04이제 제가 이해하기로는, 워크플로우의 Step은 Step을 실행하고 백그라운드에서 코드를 돌린 뒤 워크플로우를 이어가는 방식인데요.
00:26:13하지만 Hook과 Webhook은 더 로우 레벨(lower level)처럼 느껴집니다. 토큰이나 URL을 생성하고 무엇이든 기다릴 수 있으니까요.
00:26:21그게 사람의 매직 링크일 수도, 이메일일 수도, Sandbox나 어떤 연산 작업일 수도 있겠죠.
00:26:27이벤트가 발생할 때까지 모든 상태를 유지한 채 워크플로우를 일시 정지시키니, Step 자체보다 더 근본적인 기능처럼 느껴지네요.
00:26:34네. 제가 생각하는 Webhook과 Hook은 외부 페이로드를 워크플로우로 전달하는 방법입니다.
00:26:42Step은 워크플로우가 일시 중단된 후 특정 연산이 끝나기를 기다렸다가 재개하는 방식이라고 생각하고요.
00:26:50하지만 Hook과 Webhook은 토큰이나 URL을 만들어서 어디로든, 여기서는 Sandbox였지만, 보낼 수 있다는 점에서 훨씬 더 로우 레벨 같습니다.
00:27:01사람에게 보낼 수도 있고, 이메일이나 혹은 다른 워크플로우로 보낼 수도 있겠죠.
00:27:05그리고 그 작업이 완료될 때마다 부모 워크플로우가 깨어나서 중단된 지점부터 바로 재개됩니다.
00:27:12그러니 Step보다 더 낮은 단계의 기능인 셈이죠. 어떤 종류의 외부 작업에 대해서도 워크플로우를 중단시킬 수 있는 방법이니까요.
00:27:19네. 저는 Hook을 워크플로우를 일시 중단하고 외부 페이로드가 다시 전달되기를 기다리는 방법으로 생각하는데, 이는 매우 강력한 기능입니다.
00:27:31정말 멋지네요. 오늘 시간이 다 됐지만, 이 데모들을 통해 왜 Hook이 제가 가장 좋아하는 기능인지 다시 한번 확인했고 계속해서 무언가를 만들어보고 싶어지네요.
00:27:42좋습니다. 즐거우셨다니 다행이네요.

Key Takeaway

Vercel Workflow SDK는 웹훅과 훅 프리미티브를 통해 외부 이벤트를 프로미스 형태로 노출하며, 복잡한 인프라 설정 없이 상태가 유지되는 비동기 비즈니스 로직을 단일 함수 내에서 구현하도록 돕는다.

Highlights

  • Vercel Workflow SDK의 웹훅 기능을 사용하면 Redis나 데이터베이스 없이 50줄의 코드로 매직 링크 로그인 시스템을 구현한다.

  • 웹훅 객체는 프로미스(Promise)를 구현하므로 JavaScript의 'promise.race'를 사용하여 타임아웃 로직을 5분 단위로 정교하게 모델링한다.

  • 저수준의 'hook' 프리미티브와 'for await' 구문을 결합하면 Slack 스레드처럼 여러 번의 외부 이벤트를 수신하는 루프형 워크플로를 구축한다.

  • Vercel Sandbox와 워크플로를 연동하면 FFmpeg을 이용한 대용량 미디어 변환처럼 시간이 오래 걸리는 작업을 외부 컴퓨팅 자원에서 처리한 후 결과를 회신받는다.

  • 워크플로가 외부 페이로드를 기다리는 '중단(suspended)' 상태에서는 실제 실행되는 프로세스가 없으므로 컴퓨팅 자원 소모와 비용이 0이다.

Timeline

기존 방식과 워크플로 기반 매직 링크 구현 비교

  • 전통적인 매직 링크 구현에는 세션 저장용 데이터베이스, TTL 설정, 이메일 전송 실패 처리 등 복잡한 인프라가 요구된다.
  • 로직이 여러 엔드포인트와 파일에 흩어지면 상태 추적이 어려워지고 코드가 스파게티처럼 복잡해진다.
  • 데이터베이스를 직접 관리하는 방식은 만료된 데이터를 수동으로 정리해야 하는 운영 부담을 초래한다.

Vercel 초기 시절의 경험을 바탕으로 매직 링크 시스템 구축의 어려움을 분석한다. 데이터베이스와 Redis를 사용하여 상태를 저장하고 만료 시간을 직접 관리하는 기존 방식은 구현 속도를 늦추고 오류 발생 가능성을 높인다. 워크플로는 이러한 복잡성을 추상화하여 개발자가 비즈니스 로직에만 집중할 수 있는 환경을 제공한다.

Webhook SDK를 활용한 무상태 워크플로 설계

  • createWebhook 함수는 워크플로 실행에 매핑되는 고유한 일회성 URL을 생성한다.
  • promise.race를 활용해 5분간의 대기 시간과 웹훅 수신 중 먼저 발생하는 이벤트를 기반으로 흐름을 결정한다.
  • 사용자가 이메일 링크를 클릭할 때까지 워크플로는 컴퓨팅 자원을 전혀 사용하지 않는 중단 상태를 유지한다.

단 50줄의 코드로 작성된 매직 링크 예제를 통해 SDK의 강력함을 증명한다. useStep을 사용하여 이메일 전송 실패 시 자동 재시도를 보장하며, Redis의 TTL 기능을 JavaScript의 sleep 함수로 대체한다. Vercel 대시보드의 관찰 도구(observability)를 통해 실행 입력값과 중단된 상태의 훅을 시각적으로 모니터링한다.

저수준 Hook 프리미티브와 AI 에이전트 연동

  • 정적 콜백 URL이 필요한 서비스에는 무작위 URL 대신 결정론적으로 생성된 토큰 기반의 hook을 사용한다.
  • for await 구문을 통해 Slack 스레드의 연속적인 메시지들을 비동기 이터레이터로 처리한다.
  • 대화 기록을 데이터베이스가 아닌 로컬 변수 배열에 저장하여 상태를 유지하는 'let as a database' 패턴을 적용한다.

스토리타임 봇 데모를 통해 다중 이벤트 처리 방식을 설명한다. Slack 메시지가 올 때마다 워크플로가 깨어나 로컬 변수 배열에 데이터를 추가하고 AI SDK와 연동하여 이야기를 이어간다. 여러 개의 풀 리퀘스트나 스레드가 있더라도 고유 토큰을 통해 각 실행 단위가 정확히 식별되고 재개된다.

Vercel Sandbox를 이용한 장시간 연산 처리

  • useStep 디렉티브가 포함된 NPM 패키지는 워크플로 내에서 자동으로 내구성을 갖춘 단계로 작동한다.
  • 오래 걸리는 FFmpeg 변환 작업을 백그라운드 Sandbox에서 실행하고 워크플로는 결과 보고를 기다리며 일시 정지한다.
  • Bash 스크립트 내부의 cURL 명령으로 웹훅 URL을 호출하여 제어권을 다시 워크플로로 넘긴다.

미디어 변환과 같이 리소스 집약적인 작업을 외부 샌드박스에서 처리하는 모델을 제시한다. 워크플로는 1시간 이상의 긴 sleep 상태를 무제한으로 유지할 수 있어 대용량 파일 처리에도 적합하다. 최종적으로 웹훅과 훅이 단순히 URL 생성을 넘어 외부 페이로드를 워크플로 내부로 전달하는 가장 근본적인 로우 레벨 도구임을 강조한다.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video