AI 데모는 어디에나 있습니다. 단 하나의 Python 스크립트로 정지된 사진의 얼굴을 바꿀 수 있고, 그 결과는 마법처럼 보입니다. 하지만 실제 사람들이 파일을 업로드하고, 잠시 자리를 비웠다가 다시 돌아와서 사용할 수 있는 무언가를 만드는 것은 완전히 다른 문제입니다. 최근 저는 애니메이션 GIF와 참조 얼굴를 입력받아, 모든 프레임의 얼굴이 바뀐 동일한 애니메이션을 반환하는 웹 도구를 만들었습니다. 모델이 시각적인 핵심 작업을 수행하지만, 진짜 엔지니어링 노력은 그 주변 아키텍처에 들어갔습니다. 즉, 연결을 유지하고, 페이지 새로고침을 견뎌내며, 2분짜리 추론(inference) 작업이 504 Gateway Timeout으로 사라지지 않도록 만드는 일입니다.
이것이 바로 프로토타입과 제품 사이의 간극입니다. 엔지니어들은 확산(diffusion) 아키텍처와 추론 파라미터에 대해 이야기하는 것을 좋아합니다. 하지만 사용자가 프레임이 200개나 되는 무거운 GIF를 업로드했을 때, 브라우저 탭이 30초 만에 멈춰버린다면 아무도 당신의 모델에는 관심이 없습니다. 추론 자체만큼이나 추론을 둘러싼 인프라가 중요합니다.
기다림의 문제
표준 웹 아키텍처는 빠른 응답을 가정합니다. 사용자가 버튼을 클릭하면 서버가 응답하고 페이지가 업데이트됩니다. 수십 개의 GIF 프레임에 걸쳐 얼굴을 바꾸는 작업은 이러한 가정을 즉시 깨뜨립니다. 모델은 제 서버가 아닌 Replicate의 GPU에서 실행되며, 긴 GIF는 처리하는 데 1분 이상 걸릴 수도 있습니다. HTTP 요청을 그렇게 오래 열어두려고 하면 문제가 생깁니다. 로드 밸런서는 유휴 연결을 끊어버립니다. 브라우저는 네트워크 오류로 간주하고 재시도합니다. 사용자는 멈춰버린 스피너를 바라보며 앱이 고장 났다고 생각하게 됩니다.
저는 제출과 결과를 분리함으로써 이 문제를 완전히 피했습니다. 사용자가 GIF와 얼굴 이미지를 업로드하면, 제 Next.js 백엔드는 Replicate에서 예측(prediction)을 시작하고 즉시 job ID를 반환합니다. 사용자는 즉각적인 확인을 받습니다. 실제 결과는 GPU 작업이 완료되면 웹훅(webhook)을 통해 나중에 도착합니다. 이 패턴은 생소한 것이 아니지만, 실행 시간이 긴 미디어 작업에는 절대적으로 필수적입니다. 이는 예측 불가능한 기다림을 확신을 주는 핸드셰이크로 바꿔줍니다. 즉, 작업이 접수되었으며 완료되면 알림을 받을 것이라는 확신입니다.
신뢰할 수 있는 상태 머신(State Machine) 구축하기
비동기 방식으로 전환하면 가시성(visibility)이 필요합니다. 사용자는 페이지를 새로고침할 것입니다. 탭을 닫았다가 다시 열 수도 있습니다. 링크를 복사해 동료에게 보내고, 그 동료는 두 시간 뒤에 확인할 수도 있습니다. 무슨 일이 일어났는지에 대한 내구성 있는 기록이 없다면 혼란이 발생합니다.
저는 Supabase를 단일 진실 공급원(single source of truth)으로 사용했습니다. 모든 업로드는 고유한 job ID를 가진 행(row)을 생성하며, 이 행은 queued, processing, succeeded, failed 또는 expired와 같은 특정 상태를 거칩니다. 사용자가 처음 제출을 누르면 행은 queued 상태가 됩니다. Replicate가 예측을 수락하는 즉시 processing으로 전환됩니다. 웹훅은 이를 succeeded 또는 failed로 변경합니다. 콜백 없이 너무 오래 머무는 작업을 위해 expired 상태를 추가하여, 시스템이 무한정 유령 작업을 쫓지 않도록 했습니다.
TypeScript로 구축된 프론트엔드는 짧은 간격으로 Supabase를 폴링(polling)하며 현재 상태에 따라 화면을 렌더링합니다. 이 폴링 방식이 원시적으로 보일 수 있지만, 새로고침 문제를 완벽하게 해결합니다. 사용자가 노트북을 닫았다가 내일 다시 열어도 진행 상황을 정확히 확인할 수 있는데, 이는 브라우저 메모리가 아닌 데이터베이스가 진행 상황을 관리하기 때문입니다. Supabase는 크레딧도 추적하므로, 상태를 추적하는 동일한 작업 기록에 결제 정보가 연결됩니다. 모든 것이 한 곳에서 관리됩니다.
브라우저에 무리를 주지 않고 파일 처리하기
GIF는 생각보다 용량이 큽니다. AI 파이프라인에 딱 맞는 크기의 파일이라도 여전히 수 메가바이트에 달할 수 있습니다. 이를 브라우저 내부에서 루프형 미리보기로 렌더링하면, 특히 저사양 기기에서는 성능이 크게 저하됩니다. 저는 AI를 위한 파이프라인과 사용자 인터페이스를 위한 파이프라인, 이렇게 완전히 분리된 두 개의 파일 파이프라인이 필요했습니다.
미리보기를 위해 WebAssembly로 컴파일된 FFmpeg를 사용하여 대용량 GIF를 애니메이션 WebP로 변환합니다. 이는 브라우저 내 클라이언트 측에서 완전히 실행됩니다. 그 결과 원본 파일을 건드리지 않고도 인터페이스를 빠릿하게 유지하는 가벼운 미리보기가 만들어집니다. Replicate로 전송되는 GIF는 수정되지 않은 상태로 유지됩니다. 이러한 분리가 중요합니다. 미리보기의 압축 아티팩트(artifacts)가 학습 데이터나 최종 렌더링 결과물에 섞여 들어가는 것을 원치 않을 것이며, AI가 생각하는 동안 사용자 인터페이스가 수 메가바이트짜리 데이터 덩어리 때문에 멈추는 것도 원치 않을 것입니다.
FFmpeg WASM은 업로드가 브라우저를 떠나기 전 다른 GIF 작업들도 처리합니다. 저는 이를 사용하여 프레임 수를 파싱하고, 크기를 검증하며, 손상된 파일을 조기에 찾아냅니다. GPU 크레딧을 소모하기 전에 문제를 잡아내는 것은 비용과 사용자의 인내심을 모두 아껴줍니다.
데모를 사람들이 신뢰하는 소프트웨어로 바꾸는 법
여기에는 시장에 나온 거의 모든 생성형 AI 도구에 적용되는 더 넓은 교훈이 담겨 있습니다. 모델 자체는 작업의 30%에 불과할지도 모릅니다. 나머지 70%는 아무도 트위터에 올리지 않는, 화려하지는 않지만 필수적인 기반 작업들입니다. 즉, 상태 복구, 웹훅 서명, 파일 변환, 크레딧 추적, 그리고 오류 발생 시의 유연한 대응(graceful failure) 같은 것들 말이죠.
제 스택은 단순하면서도 의도적입니다. Next.js와 TypeScript가 인터페이스와 API 라우트를 처리합니다. Replicate는 모델을 실행합니다. Supabase는 상태, 데이터, 크레딧을 관리합니다. FFmpeg WASM은 클라이언트 측 미디어 작업을 처리합니다. Animated WebP는 UI를 빠르게 유지합니다. 각 구성 요소는 하나의 역할만 수행하며, 취약하고 오래 지속되는 요청 대신 명시적인 상태 전환을 통해 서로 연결됩니다.
사용자가 크레딧을 사용하여 페이스 스왑을 할 때, 그들이 기대하는 것은 연구 논문이 아니라 신뢰성입니다. 만약 결과 생성에 실패한다면, 시스템은 이를 인지하고 사용자에게 알려야 합니다. 사용자가 기다려야 한다면, 볼 수 있는 가벼운 미리보기가 있어야 하고 브라우저를 재시작해도 유지되는 상태 정보가 있어야 합니다. 이러한 디테일은 잘 작동할 때는 눈에 보이지 않지만, 제대로 작동하지 않을 때는 치명적인 문제가 됩니다.
페이스 스왑 자체는 멋진 기술입니다. 하지만 이 도구가 실제 서비스처럼 느껴지는 이유는 사용자가 파일을 업로드하고, 잠시 자리를 비웠다가 다시 돌아와도 하던 작업을 잃지 않고 이어갈 수 있기 때문입니다. 이것이 바로 단순한 API 호출을 사람들이 실제로 신뢰하는 소프트웨어로 바꾸는 핵심입니다.
