ComfyUI REST API로 Unity 인게임 UI·아이콘을 런타임에 동적 생성하는 파이프라인
Unity 클라이언트에서 ComfyUI REST API를 호출해 아이콘과 UI 이미지를 생성·다운로드·캐시하는 방법을 다룬다. 비동기 폴링, 안전한 서버 중계, 메모리 관리, 실패 대응까지 실전 파이프라인으로 정리한다.

핵심 요약
ComfyUI REST API 연동은 Unity 클라이언트가 워크플로 JSON을 직접 실행하는 방식이 아니라 게임 서버 또는 게이트웨이가 검증한 요청을 ComfyUI의 POST /prompt로 전달하고 결과 이미지를 받아 캐시하는 구조로 구현하는 것이 안전하다. 생성 요청에는 고유 client_id를 넣고 GET /history/{prompt_id} 또는 WebSocket /ws로 완료를 확인한 뒤 GET /view 결과를 Texture2D와 Sprite로 변환하면 인게임 UI에 적용할 수 있다.
인게임 런타임 이미지 생성은 언제 적합한가?
런타임 생성은 모든 UI 에셋을 대체하는 기술이 아니다. 로딩 시간, GPU 자원, 생성 실패를 감안하면 플레이어 이름이 반영된 퀘스트 배지, 유저 제작 아이템의 미리보기, 이벤트 한정 카드 일러스트처럼 개별성이 가치가 되는 이미지에 적합하다.
| 사용 사례 | 런타임 생성 적합도 | 이유 |
|---|---|---|
| 고정 메뉴 아이콘 | 낮음 | 빌드 에셋 또는 Addressables가 더 빠르고 예측 가능하다. |
| 유저 제작 장비 아이콘 | 높음 | 조합 결과에 따라 이미지가 달라진다. |
| NPC 초상화 변형 | 중간 | 변형 수가 많을 때 사전 제작 비용을 줄일 수 있다. |
| 전투 중 즉시 표시할 스킬 아이콘 | 낮음 | 생성 지연과 실패가 게임플레이를 방해한다. |
| 로비·상점의 이벤트 카드 | 높음 | 생성 결과를 서버 캐시해 재사용하기 쉽다. |
생성 지연을 사용자 경험에 노출하지 않으려면 전투 화면이 아닌 로비, 제작 완료 화면, 다음 스테이지 로딩 구간에서 요청을 예약한다. 이미 생성된 결과가 있으면 즉시 캐시를 보여주고 없을 때만 기본 플레이스홀더를 표시한다.

ComfyUI REST API 파이프라인은 어떻게 동작하는가?
ComfyUI의 기본 API 흐름은 프롬프트 큐 등록, 완료 상태 확인, 결과 파일 조회의 세 단계다. 워크플로는 API 형식 JSON이어야 하며 UI에서 만든 워크플로를 Save (API Format) 으로 내보내 노드 입력값만 서버에서 교체하는 방식이 유지보수에 유리하다.
flowchart LR
U[Unity UI] --> G[게임 API 게이트웨이]
G -->|검증된 workflow JSON| C[ComfyUI POST /prompt]
C --> Q[실행 큐]
Q --> H["GET /history/{prompt_id} 또는 WebSocket"]
H --> V[GET /view 결과 PNG/WebP]
V --> S[오브젝트 스토리지·CDN 캐시]
S --> U
1. API 형식 워크플로를 준비한다
ComfyUI 워크플로의 노드 ID와 입력 이름은 워크플로마다 달라진다. 예를 들어 CLIPTextEncode 노드의 text, KSampler 노드의 seed, EmptyLatentImage 노드의 width, height만 허용 목록으로 바꾸도록 서버를 설계한다. 클라이언트가 임의 노드나 모델 경로를 전달하게 하면 모델 실행 정책을 통제하기 어렵다.
아래는 개념적인 API 형식 워크플로 일부다.
{
"6": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "flat game inventory icon, glowing blue potion, isolated, transparent background"
}
},
"3": {
"class_type": "KSampler",
"inputs": {
"seed": 184467,
"steps": 20,
"cfg": 6.5,
"sampler_name": "euler",
"scheduler": "normal"
}
}
}
아이콘은 결과 일관성이 중요하므로 프롬프트 템플릿, 고정 모델, 고정 해상도, seed 정책을 함께 버전 관리한다. 예를 들어 아이템 ID와 스타일 버전을 해시해 seed를 만들면 같은 아이템 요청에서 재현 가능한 결과를 얻는다.
해시 바이트를 정수로 바꿀 때는 바이트 순서를 고정해야 합니다. 런타임마다 결과가 달라질 수 있는 기본 문자열 해시를 사용하지 말고 안정적인 해시 함수를 선택합니다.
2. POST /prompt로 생성 작업을 큐에 등록한다
아래 C# 예제는 Unity 2020.2 이상과 C# 8의 using 선언을 전제로 한다. 이전 Unity 버전에서는 using 블록으로 바꾸고 해당 버전의 UnityWebRequest API를 확인한다.
ComfyUI는 prompt에 API 워크플로를 넣어 받는다. client_id는 요청 추적과 WebSocket 이벤트 구분에 사용한다.
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
public static class ComfyApiClient
{
public static async Task<string> QueuePromptAsync(
string comfyBaseUrl,
string apiWorkflowJson,
CancellationToken cancellationToken)
{
var clientId = Guid.NewGuid().ToString("N");
var requestBody = "{\"prompt\":" + apiWorkflowJson +
",\"client_id\":\"" + clientId + "\"}";
using var request = new UnityWebRequest(
comfyBaseUrl + "/prompt", UnityWebRequest.kHttpVerbPOST);
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(requestBody));
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
var operation = request.SendWebRequest();
while (!operation.isDone)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Yield();
}
if (request.result != UnityWebRequest.Result.Success)
throw new Exception($"ComfyUI 요청 실패: {request.error}");
return JsonUtility.FromJson<PromptResponse>(request.downloadHandler.text).prompt_id;
}
[Serializable]
private class PromptResponse
{
public string prompt_id;
}
}
실서비스 Unity 앱에서 ComfyUI 주소를 직접 호출하면 내부 IP, 인증 정보, 워크플로 구조가 노출될 수 있다. 위 코드는 사내 프로토타입 또는 신뢰된 네트워크를 위한 예시이며 배포 환경에서는 Unity가 게임 백엔드의 POST /generated-icons 같은 제한된 API를 호출하도록 바꾼다.
Unity에서 생성 완료를 어떻게 기다릴까?
가장 단순한 방식은 GET /history/{prompt_id}를 짧은 간격으로 폴링하는 것이다. 동시 요청이 많아지면 WebSocket GET /ws?clientId={client_id}로 완료 이벤트를 받아 폴링 트래픽을 줄일 수 있다. 다만 Unity WebSocket 라이브러리와 재연결 처리가 추가되므로 아이콘 생성량이 적은 초기 버전은 폴링부터 시작하는 편이 실용적이다.
3. 폴링 간격과 제한 시간을 둔다
폴링 간격을 너무 짧게 잡으면 ComfyUI와 게이트웨이에 불필요한 요청이 쌓인다. 1초 전후의 간격과 전체 제한 시간을 두고 시간 초과 시 기본 아이콘으로 안전하게 되돌린다.
public static async Task<string> WaitForResultAsync(
string comfyBaseUrl,
string promptId,
float timeoutSeconds,
CancellationToken cancellationToken)
{
var startedAt = Time.realtimeSinceStartup;
while (Time.realtimeSinceStartup - startedAt < timeoutSeconds)
{
using var request = UnityWebRequest.Get(
$"{comfyBaseUrl}/history/{UnityWebRequest.EscapeURL(promptId)}");
var operation = request.SendWebRequest();
while (!operation.isDone)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Yield();
}
if (request.result == UnityWebRequest.Result.Success &&
request.downloadHandler.text.Contains("\"outputs\""))
{
return request.downloadHandler.text;
}
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
throw new TimeoutException("ComfyUI 이미지 생성 시간이 제한을 초과했습니다.");
}
실제 구현에서는 문자열 검색 대신 JSON 파서를 사용해 history[prompt_id].outputs 아래의 이미지 메타데이터를 추출한다. filename, subfolder, type 값으로 /view?filename=...&subfolder=...&type=output URL을 구성해야 한다. 파일명만 신뢰하지 말고 URL 인코딩과 서버 측 경로 검증을 적용한다.

생성 이미지를 Unity Sprite로 적용하는 방법
UnityWebRequestTexture.GetTexture로 PNG 또는 JPEG를 받아 Texture2D로 변환한 후 Sprite를 만들 수 있다. UI용 이미지는 읽기 가능 텍스처가 꼭 필요한 경우가 아니라면 다운로드 이후 CPU 복사본을 오래 유지하지 않도록 관리한다.
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public sealed class GeneratedIconView : MonoBehaviour
{
[SerializeField] private Image targetImage;
[SerializeField] private Sprite fallbackSprite;
public async Task LoadAsync(string imageUrl, CancellationToken cancellationToken)
{
targetImage.sprite = fallbackSprite;
using var request = UnityWebRequestTexture.GetTexture(imageUrl, nonReadable: true);
var operation = request.SendWebRequest();
while (!operation.isDone)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Yield();
}
if (request.result != UnityWebRequest.Result.Success)
return;
var texture = DownloadHandlerTexture.GetContent(request);
targetImage.sprite = Sprite.Create(
texture,
new Rect(0, 0, texture.width, texture.height),
new Vector2(0.5f, 0.5f),
100f);
}
}
Sprite를 매번 새로 만들면 메모리 누수처럼 보이는 현상이 발생할 수 있다. 같은 키의 결과는 Dictionary<string, Sprite>에 재사용하고 화면을 떠날 때 참조 수가 0인 텍스처와 Sprite에 Destroy를 호출하는 LRU 캐시 정책을 둔다. 서버 CDN URL을 캐시 키로 삼기보다 itemId:styleVersion:promptVersion 같은 논리 키를 사용하면 워크플로 변경 시 무효화하기 쉽다.
런타임 생성 UI의 성능 예산은 어떻게 잡을까?
생성 자체는 ComfyUI 서버의 GPU 작업이지만 클라이언트도 다운로드와 텍스처 메모리 비용을 부담한다. 모바일과 PC를 함께 지원한다면 먼저 최대 해상도, 동시 생성 수, 메모리 상한을 정해야 한다.
| 항목 | 권장 출발값 | 점검 방법 |
|---|---|---|
| 아이콘 출력 해상도 | 256×256 또는 512×512 | UI 실제 표시 크기와 비교한다. |
| 동시 생성 요청 | 계정당 1~2개 | 서버 큐 대기 시간과 실패율을 기록한다. |
| 폴링 주기 | 1초 | 요청 수와 완료 감지 지연을 함께 측정한다. |
| 클라이언트 메모리 캐시 | 기기 등급별 개수 제한 | Unity Memory Profiler로 Texture2D 사용량을 확인한다. |
| 네트워크 재시도 | 지수 백오프 2~3회 | 4xx는 재시도하지 않고 5xx·시간 초과만 대상화한다. |
아이콘 수가 많다면 생성 결과를 백엔드가 오브젝트 스토리지에 저장하고 CDN으로 제공하는 구조가 좋다. ComfyUI의 /view를 게임 클라이언트가 계속 호출하면 생성 서버가 파일 제공까지 떠안게 되어 GPU 작업과 다운로드 작업이 서로 영향을 준다.
보안과 운영에서 반드시 막아야 할 문제
- ComfyUI를 인터넷에 직접 공개하지 않는다. API 게이트웨이 또는 사설망 뒤에 두고 인증과 요청 제한을 적용한다.
- 클라이언트가 자유 프롬프트, 노드 JSON, checkpoint 이름을 보내지 못하게 한다. 서버는 템플릿의 허용된 변수만 치환한다.
- 프롬프트 길이, 해상도, steps, batch size에 상한을 둔다. 특히 해상도와 batch size는 GPU 메모리 사용량을 급격히 늘린다.
- 결과 이미지와 요청 메타데이터에 프롬프트 버전, 모델 버전, seed를 기록한다. 품질 문제와 재생성을 추적할 수 있다.
- NSFW 필터, 금칙어, 저작권·개인정보 정책은 생성 전과 결과 배포 전 단계 모두에 적용한다.
자주 묻는 질문 (FAQ)
ComfyUI REST API를 Unity 클라이언트에서 직접 호출해도 될까?
개발용 로컬 환경에서는 가능하지만 운영 환경에서는 권장하지 않는다. ComfyUI는 게임용 공개 API가 아니므로 인증, 요청 제한, 워크플로 검증, 파일 배포를 담당하는 중계 서버를 둬야 한다.
GET /history/{prompt_id}에 결과가 바로 나타나지 않는 이유는 무엇인가?
POST /prompt는 작업을 큐에 넣는 요청이며 이미지 생성 완료를 보장하지 않는다. 큐 대기와 GPU 실행이 끝난 뒤에만 history의 outputs를 신뢰할 수 있으므로 폴링 또는 WebSocket 완료 이벤트가 필요하다.
생성된 아이콘을 매번 다시 만들어야 할까?
아니다. 아이템 정의와 스타일 버전이 같다면 서버와 클라이언트 캐시를 우선 조회한다. 재생성이 필요한 경우는 프롬프트, 모델, 워크플로, seed 정책이 바뀌었을 때다.
정리
ComfyUI 기반 런타임 UI 생성의 핵심은 이미지 생성 호출 자체보다 요청을 제한하고 완료를 비동기로 처리하고 결과를 재사용하는 운영 구조다. Unity는 플레이스홀더와 Sprite 적용에 집중하고 워크플로 실행·검증·CDN 캐시는 서버 계층으로 분리하면 생성형 UI를 게임 경험에 무리 없이 연결할 수 있다.



