이미지 로딩 중...
AI Generated
2025. 11. 8. · 3 Views
ComfyUI 완벽 가이드 - AI 이미지 생성 워크플로우 마스터하기
ComfyUI를 활용한 AI 이미지 생성의 모든 것을 배워봅니다. 노드 기반 워크플로우 구성부터 고급 프롬프트 기법, 성능 최적화까지 실무에서 바로 활용할 수 있는 실전 가이드를 제공합니다.
목차
- ComfyUI_기본_워크플로우
- 프롬프트_엔지니어링
- ControlNet_활용
- LoRA_모델_적용
- Upscaling_기법
- Batch_Processing
- Custom_Node_개발
- 성능_최적화
1. ComfyUI_기본_워크플로우
시작하며
여러분이 Stable Diffusion으로 이미지를 생성할 때 매번 복잡한 파라미터 설정에 시간을 낭비하고 계신가요? 또는 동일한 작업을 반복하면서 효율성이 떨어진다고 느끼시나요?
이런 문제는 AI 이미지 생성 작업에서 매우 흔합니다. 특히 프로젝트 규모가 커질수록 일관된 품질을 유지하면서도 생산성을 높이는 것이 중요해집니다.
매번 설정을 바꾸다 보면 이전에 좋았던 결과를 재현하기도 어렵죠. 바로 이럴 때 필요한 것이 ComfyUI입니다.
노드 기반의 시각적 워크플로우를 통해 복잡한 이미지 생성 과정을 체계적으로 관리하고, 언제든 재사용할 수 있습니다.
개요
간단히 말해서, ComfyUI는 AI 이미지 생성을 위한 노드 기반 워크플로우 편집기입니다. Stable Diffusion의 모든 기능을 시각적으로 연결하여 사용할 수 있게 해줍니다.
왜 이 도구가 필요한지 실무 관점에서 설명하자면, 이미지 생성 작업은 단순히 프롬프트만 입력하는 것이 아닙니다. 모델 선택, 샘플러 설정, ControlNet 적용, 후처리 등 여러 단계를 거치는데, 이 모든 과정을 재사용 가능한 형태로 저장하고 관리해야 합니다.
예를 들어, 캐릭터 일러스트 생성 파이프라인을 구축하면 동일한 스타일로 수백 개의 이미지를 일관되게 생성할 수 있습니다. 기존의 Web UI에서는 설정을 텍스트로 기록하거나 스크린샷으로 저장했다면, 이제는 전체 워크플로우를 JSON 파일로 저장하고 즉시 불러올 수 있습니다.
ComfyUI의 핵심 특징은 첫째, 모든 작업이 노드로 표현되어 직관적이고, 둘째, GPU 메모리 사용을 최적화하여 대용량 모델도 효율적으로 실행하며, 셋째, 확장성이 뛰어나 커스텀 노드를 쉽게 추가할 수 있다는 점입니다. 이러한 특징들이 프로덕션 환경에서 안정적이고 확장 가능한 AI 이미지 생성 시스템을 구축하는 데 핵심적입니다.
코드 예제
# ComfyUI 기본 워크플로우 예제 (Python API 사용)
import json
import requests
# 워크플로우 정의 - 기본 txt2img
workflow = {
"1": {
"inputs": {"ckpt_name": "sd_xl_base_1.0.safetensors"},
"class_type": "CheckpointLoaderSimple" # 모델 로드
},
"2": {
"inputs": {
"text": "a beautiful landscape, mountains, sunset, highly detailed",
"clip": ["1", 1] # 첫 번째 노드의 CLIP 출력 연결
},
"class_type": "CLIPTextEncode" # 프롬프트 인코딩
},
"3": {
"inputs": {
"text": "blurry, low quality, distorted",
"clip": ["1", 1]
},
"class_type": "CLIPTextEncode" # 네거티브 프롬프트
},
"4": {
"inputs": {
"seed": 42,
"steps": 20,
"cfg": 7.5, # CFG Scale
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1.0,
"model": ["1", 0], # 모델 연결
"positive": ["2", 0], # 포지티브 프롬프트 연결
"negative": ["3", 0], # 네거티브 프롬프트 연결
"latent_image": ["5", 0]
},
"class_type": "KSampler" # 실제 이미지 생성
},
"5": {
"inputs": {"width": 1024, "height": 1024, "batch_size": 1},
"class_type": "EmptyLatentImage" # 빈 latent 생성
},
"6": {
"inputs": {"samples": ["4", 0], "vae": ["1", 2]},
"class_type": "VAEDecode" # Latent를 이미지로 디코딩
},
"7": {
"inputs": {"filename_prefix": "ComfyUI", "images": ["6", 0]},
"class_type": "SaveImage" # 이미지 저장
}
}
# ComfyUI API로 워크플로우 실행
response = requests.post(
"http://127.0.0.1:8188/prompt",
json={"prompt": workflow}
)
설명
이것이 하는 일: 이 워크플로우는 텍스트 프롬프트를 입력받아 SDXL 모델로 1024x1024 이미지를 생성하는 전체 파이프라인을 정의합니다. 첫 번째로, CheckpointLoaderSimple 노드(#1)가 Stable Diffusion 모델을 메모리에 로드합니다.
이 노드는 모델, CLIP, VAE 세 가지 출력을 제공하는데, 각각 다른 노드에서 사용됩니다. 왜 이렇게 분리되어 있냐면, CLIP은 텍스트 인코딩에, VAE는 이미지 디코딩에 특화되어 있어 각 단계에서 필요한 컴포넌트만 사용하여 메모리를 효율적으로 관리하기 때문입니다.
그 다음으로, CLIPTextEncode 노드(#2, #3)가 포지티브 프롬프트와 네거티브 프롬프트를 각각 처리합니다. 내부에서는 텍스트가 토큰으로 분해되고, CLIP 모델이 이를 벡터 공간의 임베딩으로 변환합니다.
이 임베딩이 실제로 이미지 생성의 방향을 결정하는 핵심 정보가 됩니다. 중간 단계에서 EmptyLatentImage 노드(#5)가 빈 잠재 공간 텐서를 생성하고, KSampler 노드(#4)가 이것을 점진적으로 변환하여 이미지를 만듭니다.
20단계에 걸쳐 노이즈를 제거하면서 프롬프트에 맞는 이미지로 수렴합니다. CFG(Classifier Free Guidance) 값 7.5는 프롬프트를 얼마나 강하게 따를지 결정합니다.
마지막으로, VAEDecode 노드(#6)가 잠재 공간의 데이터를 실제 픽셀 이미지로 변환하고, SaveImage 노드(#7)가 파일로 저장합니다. 최종적으로 여러분은 "ComfyUI_00001.png" 같은 형식의 고품질 이미지를 얻게 됩니다.
여러분이 이 워크플로우를 사용하면 매번 동일한 품질의 이미지를 재현할 수 있고, 특정 파라미터만 변경하여 다양한 변형을 빠르게 생성할 수 있습니다. 실무에서는 이 기본 구조를 템플릿으로 저장해두고, 프로젝트마다 필요한 노드만 추가하여 사용하면 생산성이 크게 향상됩니다.
또한 JSON 형태로 저장되므로 버전 관리 시스템에 넣어 팀원과 공유하거나, 변경 이력을 추적할 수도 있습니다.
실전 팁
💡 워크플로우를 저장할 때는 항상 의미 있는 파일명을 사용하세요. "portrait_high_quality_v1.json" 같은 형식으로 버전을 명시하면 나중에 찾기 쉽습니다.
💡 흔한 실수: seed 값을 고정하지 않으면 매번 다른 결과가 나옵니다. 테스트 중에는 고정된 seed(예: 42)를 사용하고, 프로덕션에서만 랜덤으로 변경하세요.
💡 GPU 메모리가 부족하면 이미지 크기를 줄이거나, batch_size를 1로 설정하세요. 1024x1024는 최소 8GB VRAM이 필요합니다.
💡 워크플로우 디버깅 시에는 각 노드의 출력을 중간에 Preview Image 노드로 연결하여 확인하세요. 어느 단계에서 문제가 생겼는지 쉽게 파악할 수 있습니다.
💡 자주 사용하는 설정 조합은 Group 기능으로 묶어서 하나의 커스텀 노드처럼 재사용하세요. 예를 들어 "고품질 인물 설정" 그룹을 만들어두면 매번 설정할 필요가 없습니다.
2. 프롬프트_엔지니어링
시작하며
여러분이 "beautiful girl" 같은 간단한 프롬프트로 이미지를 생성했을 때, 결과가 너무 평범하거나 의도와 다른 경험 있으신가요? 프롬프트를 어떻게 작성해야 원하는 스타일과 디테일을 얻을 수 있는지 막막하셨죠?
이 문제는 AI 이미지 생성에서 가장 중요한 스킬인 프롬프트 엔지니어링의 이해 부족에서 비롯됩니다. 같은 모델이라도 프롬프트 작성 방식에 따라 퀄리티가 천차만별로 달라집니다.
실제로 전문가들은 수십 개의 키워드를 조합하고 가중치를 조절하여 정교한 결과를 만들어냅니다. 바로 이럴 때 필요한 것이 체계적인 프롬프트 구조화 기법입니다.
ComfyUI에서는 프롬프트를 노드로 관리하므로, 재사용 가능한 프롬프트 템플릿을 만들고 조합할 수 있습니다.
개요
간단히 말해서, 프롬프트 엔지니어링은 AI 모델이 이해하기 쉬운 형태로 요구사항을 표현하는 기술입니다. 단순한 키워드 나열이 아니라, 구조화된 문장과 가중치 조절로 정확한 의도를 전달합니다.
왜 이 기법이 필요한지 실무 관점에서 설명하자면, 클라이언트나 팀에서 "좀 더 밝게", "더 판타지 느낌으로" 같은 추상적인 요청을 받을 때 일관된 결과를 만들어야 합니다. 예를 들어, 게임 캐릭터 컨셉 아트를 생성할 때 "epic fantasy warrior, highly detailed armor, dramatic lighting, artstation quality" 같은 구조화된 프롬프트를 사용하면 매번 비슷한 품질과 스타일을 유지할 수 있습니다.
기존에는 시행착오를 통해 좋은 프롬프트를 찾았다면, 이제는 구조화된 템플릿을 만들어 효율적으로 변형할 수 있습니다. 프롬프트의 핵심 특징은 첫째, 순서가 중요하여 앞쪽 키워드가 더 강한 영향을 미치고, 둘째, 괄호와 숫자로 가중치를 조절할 수 있으며(예: "(detailed:1.3)"), 셋째, 네거티브 프롬프트로 원하지 않는 요소를 제거할 수 있다는 점입니다.
이러한 특징들이 여러분이 정확히 원하는 이미지를 생성하는 데 결정적입니다.
코드 예제
# ComfyUI에서 고급 프롬프트 구조 예제
from dataclasses import dataclass
@dataclass
class PromptTemplate:
"""재사용 가능한 프롬프트 템플릿"""
subject: str # 주요 대상
style: str # 스타일
quality: str # 품질 키워드
lighting: str # 조명
details: str # 세부사항
def build(self, emphasis: dict = None) -> str:
"""가중치를 적용하여 최종 프롬프트 생성"""
emphasis = emphasis or {}
parts = [
self.subject,
f"({self.style}:1.2)", # 스타일 강조
self.quality,
f"{self.lighting}",
f"({self.details}:1.1)" # 디테일 약간 강조
]
# 사용자 정의 가중치 적용
for key, weight in emphasis.items():
parts = [p.replace(key, f"({key}:{weight})") if key in p else p
for p in parts]
return ", ".join(parts)
# 실제 사용 예시
portrait_template = PromptTemplate(
subject="a professional female character portrait",
style="anime style, studio ghibli inspired",
quality="masterpiece, best quality, highly detailed",
lighting="soft natural lighting, rim light",
details="detailed eyes, detailed hair, detailed clothing"
)
# 기본 프롬프트
basic_prompt = portrait_template.build()
print(basic_prompt)
# 특정 요소 강조
emphasized = portrait_template.build(
emphasis={"detailed eyes": 1.4, "anime style": 1.3}
)
print(emphasized)
# 네거티브 프롬프트 템플릿
negative_common = "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry"
설명
이것이 하는 일: 이 코드는 프롬프트를 체계적으로 관리하고, 가중치를 동적으로 조절할 수 있는 템플릿 시스템을 구현합니다. 첫 번째로, PromptTemplate 클래스가 프롬프트의 주요 구성 요소를 구조화합니다.
subject는 무엇을 그릴지, style은 어떤 느낌으로, quality는 전반적인 품질 수준, lighting은 조명 환경, details는 세부 묘사를 담당합니다. 이렇게 분리하면 각 요소를 독립적으로 수정하거나 재사용할 수 있습니다.
예를 들어, 같은 캐릭터를 다른 스타일로 그리고 싶을 때 style만 변경하면 됩니다. 그 다음으로, build 메서드가 각 요소를 조합하여 최종 프롬프트를 생성합니다.
내부에서 스타일에는 1.2배, 디테일에는 1.1배 가중치를 기본으로 적용합니다. Stable Diffusion은 (keyword:weight) 형식을 인식하여, 1.0보다 크면 더 강조하고 작으면 약화시킵니다.
1.2면 약 20% 더 강하게 영향을 미치는 것으로 이해하면 됩니다. 세 번째 단계에서 emphasis 파라미터를 통해 특정 키워드의 가중치를 동적으로 조절할 수 있습니다.
클라이언트가 "눈을 더 강조해주세요"라고 요청하면, emphasis={"detailed eyes": 1.4}를 전달하기만 하면 됩니다. 코드가 자동으로 해당 부분을 찾아서 가중치를 적용합니다.
마지막으로, 네거티브 프롬프트는 흔히 나타나는 문제들을 미리 정의합니다. bad anatomy, bad hands는 AI가 자주 실수하는 부분이고, jpeg artifacts, blurry는 품질 저하를 방지합니다.
이것을 모든 생성에 기본으로 적용하면 실패율이 크게 줄어듭니다. 여러분이 이 템플릿 시스템을 사용하면 프로젝트별로 표준 프롬프트를 정의하고, 팀원 모두가 일관된 품질의 이미지를 생성할 수 있습니다.
실무에서는 캐릭터용, 배경용, UI 아이콘용 등 용도별 템플릿을 만들어두면 작업 속도가 5배 이상 빨라집니다. 또한 A/B 테스트를 할 때도 체계적으로 변수를 조절할 수 있어 어떤 프롬프트가 더 나은지 정량적으로 비교할 수 있습니다.
실전 팁
💡 프롬프트 순서가 중요합니다. 가장 중요한 요소를 앞에 배치하세요. "detailed eyes, portrait" 보다 "portrait, detailed eyes"가 더 나은 결과를 냅니다.
💡 흔한 실수: 가중치를 너무 높게 설정하면 (예: 2.0 이상) 이미지가 과장되거나 왜곡됩니다. 1.1~1.4 사이에서 미세 조정하세요.
💡 성능 팁: 프롬프트가 너무 길면(77토큰 초과) 잘립니다. ComfyUI의 CLIP Text Encode 노드 설명에서 토큰 수를 확인할 수 있습니다. 길 경우 여러 프롬프트로 나누어 Conditioning Combine 노드로 합치세요.
💡 디버깅: 원하는 결과가 안 나오면 프롬프트를 단순화한 뒤 하나씩 요소를 추가하면서 테스트하세요. 어떤 키워드가 문제인지 쉽게 찾을 수 있습니다.
💡 "artstation", "unreal engine", "octane render" 같은 플랫폼 이름을 추가하면 해당 플랫폼의 고품질 스타일을 모방합니다. 하지만 모델이 학습한 데이터에 따라 효과가 다르므로 테스트가 필요합니다.
3. ControlNet_활용
시작하며
여러분이 프롬프트만으로 이미지를 생성할 때, 정확한 포즈나 구도를 만들기 어려웠던 경험 있으신가요? "손을 들고 있는 사람"이라고 입력해도 매번 다른 각도와 자세가 나와서 여러 번 재생성해야 했죠?
이런 문제는 텍스트 프롬프트의 근본적인 한계입니다. 텍스트만으로는 공간적 배치, 정확한 포즈, 세밀한 구도를 표현하기 어렵습니다.
특히 상업 프로젝트에서 클라이언트가 요구하는 정확한 레이아웃을 구현하려면 수십 번의 시도가 필요할 수 있습니다. 바로 이럴 때 필요한 것이 ControlNet입니다.
참조 이미지의 포즈, 윤곽선, 깊이 정보 등을 추출하여 생성 과정을 정밀하게 제어할 수 있습니다. 스케치만 그려도 그 구도를 그대로 유지하면서 고품질 이미지로 변환할 수 있습니다.
개요
간단히 말해서, ControlNet은 이미지 생성 과정에 공간적 가이드를 제공하는 추가 신경망입니다. Stable Diffusion의 기본 프로세스는 유지하면서, 포즈나 구도 같은 특정 속성을 제어할 수 있게 해줍니다.
왜 이 기술이 필요한지 실무 관점에서 설명하자면, 웹툰이나 게임 캐릭터처럼 일관된 포즈가 필요한 경우가 많습니다. 예를 들어, 캐릭터의 정면/측면/후면 뷰를 생성할 때 ControlNet의 OpenPose를 사용하면 정확히 같은 포즈를 다른 스타일로 렌더링할 수 있습니다.
또한 건축 투시도나 제품 디자인처럼 정확한 깊이와 원근이 중요한 작업에서도 필수적입니다. 기존에는 같은 구도를 얻기 위해 img2img를 반복하거나 운에 맡겨야 했다면, 이제는 원하는 구도를 명확히 지정할 수 있습니다.
ControlNet의 핵심 특징은 첫째, 다양한 프리프로세서가 있어 Canny(윤곽선), OpenPose(포즈), Depth(깊이), Scribble(스케치) 등 다양한 조건을 사용할 수 있고, 둘째, 여러 ControlNet을 동시에 적용하여 복합적인 제어가 가능하며, 셋째, conditioning strength로 가이드의 강도를 조절할 수 있다는 점입니다. 이러한 특징들이 텍스트만으로는 불가능한 수준의 정밀한 이미지 생성을 가능하게 합니다.
코드 예제
# ComfyUI에서 ControlNet 사용 예제 (OpenPose)
workflow_controlnet = {
# ... 기본 노드들 (체크포인트, 프롬프트 등)
"10": {
"inputs": {"image": "reference_pose.png"},
"class_type": "LoadImage" # 참조 이미지 로드
},
"11": {
"inputs": {
"image": ["10", 0],
"detect_hand": "enable", # 손 감지 활성화
"detect_body": "enable", # 몸 감지 활성화
"detect_face": "enable" # 얼굴 감지 활성화
},
"class_type": "DWPreprocessor" # DWPose 프리프로세서
},
"12": {
"inputs": {
"control_net_name": "control_v11p_sd15_openpose.pth"
},
"class_type": "ControlNetLoader" # ControlNet 모델 로드
},
"13": {
"inputs": {
"strength": 0.8, # ControlNet 영향 강도 (0~1)
"start_percent": 0.0, # 적용 시작 시점
"end_percent": 0.9, # 적용 종료 시점 (마지막 10%는 자유롭게)
"positive": ["2", 0], # 포지티브 컨디셔닝
"negative": ["3", 0], # 네거티브 컨디셔닝
"control_net": ["12", 0], # ControlNet 모델
"image": ["11", 0] # 프리프로세서 출력 (포즈 맵)
},
"class_type": "ControlNetApply" # ControlNet 적용
},
# KSampler는 이제 기본 컨디셔닝 대신 ControlNet 컨디셔닝 사용
"4": {
"inputs": {
# ... 기타 파라미터
"positive": ["13", 0], # ControlNet이 적용된 컨디셔닝
"negative": ["13", 1],
# ...
},
"class_type": "KSampler"
}
}
# Python으로 ControlNet 강도 자동 조절
def find_optimal_strength(base_workflow, reference_image, target_prompt):
"""최적의 ControlNet 강도를 찾는 함수"""
results = []
for strength in [0.5, 0.7, 0.9, 1.0]:
workflow = base_workflow.copy()
workflow["13"]["inputs"]["strength"] = strength
# 이미지 생성 및 평가
image = generate_image(workflow)
score = evaluate_pose_accuracy(image, reference_image)
results.append((strength, score))
# 가장 높은 점수의 강도 반환
return max(results, key=lambda x: x[1])[0]
설명
이것이 하는 일: 이 워크플로우는 참조 이미지의 포즈를 분석하고, 그 포즈를 유지하면서 새로운 스타일의 이미지를 생성합니다. 첫 번째로, LoadImage 노드(#10)가 참조 이미지를 불러옵니다.
이 이미지는 여러분이 원하는 포즈를 담고 있는 사진이나 3D 렌더링일 수 있습니다. 심지어 스마트폰으로 찍은 셀카도 가능합니다.
중요한 것은 명확한 포즈가 보이는 이미지여야 한다는 점입니다. 그 다음으로, DWPreprocessor 노드(#11)가 실제 마법이 일어나는 곳입니다.
이 노드는 컴퓨터 비전 기술로 이미지에서 사람의 관절 위치를 감지합니다. 손, 몸, 얼굴을 각각 인식하여 스켈레톤 맵을 생성하는데, 이것이 나중에 Stable Diffusion에게 "이 위치에 팔이 있어야 해"라고 알려주는 가이드가 됩니다.
detect_hand를 활성화하면 손가락까지 정밀하게 추적하므로, 손 제스처가 중요한 이미지에서는 필수입니다. 중간 단계에서 ControlNetApply 노드(#13)가 이 포즈 정보를 Stable Diffusion의 컨디셔닝에 통합합니다.
strength=0.8은 포즈 가이드를 꽤 강하게 따르되, 약간의 자유도를 남깁니다. 1.0으로 설정하면 정확히 같은 포즈만 나오지만 때로는 부자연스러울 수 있고, 0.5 정도면 포즈를 참고만 하는 수준입니다.
start_percent=0.0, end_percent=0.9는 흥미로운 설정인데, 생성 초기 90%는 포즈를 엄격히 따르고 마지막 10%는 자유롭게 디테일을 다듬도록 합니다. 마지막으로, KSampler가 프롬프트와 ControlNet 가이드를 동시에 고려하여 이미지를 생성합니다.
최종적으로 여러분은 "anime girl in red dress"라는 프롬프트를 줬지만 정확히 참조 이미지와 같은 포즈로 서 있는, 하지만 완전히 다른 스타일의 캐릭터를 얻게 됩니다. 여러분이 이 기법을 사용하면 컨셉 아트 제작 시간이 극적으로 단축됩니다.
실무에서는 3D 소프트웨어로 기본 포즈를 잡고, ControlNet으로 다양한 스타일 변형을 생성하는 파이프라인을 많이 사용합니다. 또한 여러 ControlNet을 레이어처럼 쌓을 수 있어서, OpenPose로 포즈를 제어하면서 동시에 Canny로 윤곽선도 유지하는 등 복합적인 제어가 가능합니다.
이렇게 하면 클라이언트의 까다로운 요구사항도 정확히 구현할 수 있습니다.
실전 팁
💡 프리프로세서 선택이 중요합니다. 사진에서 포즈 추출은 DWPose, 라인아트 유지는 Lineart, 건축/배경은 Depth를 사용하세요. 잘못된 프리프로세서는 오히려 결과를 망칠 수 있습니다.
💡 흔한 실수: strength를 1.0으로 설정하고 복잡한 포즈를 주면 아티팩트가 생깁니다. 0.7~0.9 사이에서 시작하고, 포즈가 잘 안 따라오면 조금씩 올리세요.
💡 성능 최적화: 여러 ControlNet을 사용할 때는 각각의 strength를 합쳐서 1.5 이하로 유지하세요. 예를 들어 OpenPose 0.8 + Canny 0.7 = 1.5는 괜찮지만, 둘 다 1.0이면 과도합니다.
💡 참조 이미지 해상도는 생성할 이미지와 비슷하게 맞추세요. 512x512로 생성하는데 4K 참조 이미지를 주면 프리프로세서가 느려지고 불필요한 디테일을 추출합니다.
💡 end_percent를 0.8~0.9로 설정하면 초기에는 구도를 따르고 후반에는 자연스러운 디테일을 추가할 수 있습니다. 이 기법은 특히 얼굴 디테일을 개선하는 데 효과적입니다.
4. LoRA_모델_적용
시작하며
여러분이 특정 아티스트의 스타일이나 독특한 캐릭터를 생성하고 싶을 때, 기본 Stable Diffusion 모델만으로는 한계를 느끼셨나요? 프롬프트를 아무리 잘 작성해도 원하는 스타일이 정확히 나오지 않았죠?
이 문제는 기본 모델이 범용적으로 학습되어 있어서 특정 스타일에 특화되지 않았기 때문입니다. 전체 모델을 새로 학습시키려면 수백 GB의 데이터와 며칠간의 GPU 시간이 필요합니다.
소규모 팀이나 개인이 접근하기에는 비현실적이죠. 바로 이럴 때 필요한 것이 LoRA(Low-Rank Adaptation)입니다.
기본 모델은 그대로 두고, 수십 MB 크기의 작은 어댑터 파일만으로 특정 스타일이나 개념을 추가할 수 있습니다. 마치 포토샵의 레이어처럼 여러 LoRA를 조합하여 독특한 결과를 만들 수 있습니다.
개요
간단히 말해서, LoRA는 기본 모델의 가중치를 효율적으로 미세 조정하는 기술입니다. 전체 모델을 바꾸지 않고, 특정 스타일이나 개념을 "플러그인"처럼 추가할 수 있습니다.
왜 이 기법이 필요한지 실무 관점에서 설명하자면, 브랜드 고유의 비주얼 스타일을 유지해야 하는 경우가 많습니다. 예를 들어, 게임 회사가 자사의 캐릭터 스타일로 수백 개의 아이템 아이콘을 생성해야 할 때, 해당 스타일로 학습된 LoRA를 사용하면 일관된 품질을 유지할 수 있습니다.
또한 유명 일러스트레이터의 스타일을 모방하거나, 특정 시대(예: 1980년대 애니메이션)의 특징을 재현하는 데도 유용합니다. 기존에는 체크포인트 모델을 여러 개 다운로드하고 전환해야 했다면, 이제는 하나의 베이스 모델에 원하는 LoRA만 조합하여 사용할 수 있습니다.
LoRA의 핵심 특징은 첫째, 파일 크기가 작아(10200MB) 빠르게 다운로드하고 전환할 수 있고, 둘째, 여러 LoRA를 동시에 적용하여 스타일을 혼합할 수 있으며, 셋째, 강도를 02 범위로 조절하여 효과를 미세하게 제어할 수 있다는 점입니다. 이러한 특징들이 유연하고 효율적인 스타일 관리를 가능하게 합니다.
코드 예제
# ComfyUI에서 여러 LoRA 조합 사용 예제
workflow_lora = {
"1": {
"inputs": {"ckpt_name": "sd_xl_base_1.0.safetensors"},
"class_type": "CheckpointLoaderSimple"
},
# 첫 번째 LoRA: 스타일 관련
"20": {
"inputs": {
"lora_name": "studio_ghibli_style_v2.safetensors",
"strength_model": 0.8, # 모델 가중치 적용 강도
"strength_clip": 0.7, # CLIP 인코더 적용 강도
"model": ["1", 0], # 베이스 모델
"clip": ["1", 1] # 베이스 CLIP
},
"class_type": "LoraLoader"
},
# 두 번째 LoRA: 디테일 향상
"21": {
"inputs": {
"lora_name": "add_detail_xl.safetensors",
"strength_model": 0.5, # 디테일은 적당히
"strength_clip": 0.3,
"model": ["20", 0], # 첫 번째 LoRA의 출력을 입력으로
"clip": ["20", 1]
},
"class_type": "LoraLoader"
},
# 세 번째 LoRA: 조명 효과
"22": {
"inputs": {
"lora_name": "dramatic_lighting.safetensors",
"strength_model": 0.6,
"strength_clip": 0.4,
"model": ["21", 0], # 체인처럼 연결
"clip": ["21", 1]
},
"class_type": "LoraLoader"
},
# 이후 노드들은 마지막 LoRA의 출력 사용
"2": {
"inputs": {
"text": "a peaceful village, <lora:studio_ghibli:0.8>", # 트리거 워드
"clip": ["22", 1] # 최종 CLIP
},
"class_type": "CLIPTextEncode"
}
}
# Python으로 LoRA 강도 자동 최적화
class LoRAManager:
"""LoRA 조합을 관리하는 유틸리티"""
def __init__(self):
self.loras = []
def add_lora(self, name: str, strength: float, category: str):
"""LoRA 추가 (카테고리별 관리)"""
self.loras.append({
"name": name,
"strength": strength,
"category": category # style, detail, lighting 등
})
def balance_strengths(self):
"""카테고리별로 강도 자동 조정하여 과도한 적용 방지"""
# 같은 카테고리의 LoRA 강도 합이 1.0을 넘지 않도록
categories = {}
for lora in self.loras:
cat = lora["category"]
categories[cat] = categories.get(cat, 0) + lora["strength"]
# 초과하는 카테고리는 비율로 줄임
for lora in self.loras:
cat = lora["category"]
if categories[cat] > 1.0:
lora["strength"] *= (1.0 / categories[cat])
return self.loras
# 사용 예시
manager = LoRAManager()
manager.add_lora("ghibli_style", 0.8, "style")
manager.add_lora("miyazaki_characters", 0.6, "style") # 같은 카테고리
manager.add_lora("add_detail", 0.5, "detail")
balanced = manager.balance_strengths()
# ghibli_style: 0.57, miyazaki_characters: 0.43 으로 자동 조정됨
설명
이것이 하는 일: 이 워크플로우는 세 개의 LoRA를 순차적으로 적용하여 지브리 스타일 + 디테일 향상 + 드라마틱한 조명이 결합된 이미지를 생성합니다. 첫 번째로, 첫 번째 LoraLoader 노드(#20)가 베이스 모델에 지브리 스타일 LoRA를 적용합니다.
strength_model=0.8은 모델의 가중치를 80% 강도로 조정하고, strength_clip=0.7은 텍스트 인코더를 70% 조정합니다. 왜 두 값이 다르냐면, 때로는 비주얼 스타일(model)만 강하게 적용하고 텍스트 해석(clip)은 약하게 하는 것이 더 나은 결과를 내기 때문입니다.
예를 들어, 지브리 스타일의 "강아지"를 생성할 때 비주얼은 지브리스럽지만 "강아지"라는 개념 자체는 정확히 유지하려면 clip 강도를 낮추는 게 좋습니다. 그 다음으로, 두 번째와 세 번째 LoraLoader(#21, #22)가 체인처럼 연결됩니다.
각 LoRA는 이전 LoRA가 수정한 모델을 입력으로 받아 추가 변형을 가합니다. 내부적으로는 모델의 특정 레이어에 작은 행렬을 곱하는 방식인데, 이것이 LoRA의 "Low-Rank"가 의미하는 바입니다.
전체 가중치를 바꾸지 않고 효율적인 저차원 변환만 적용하므로 메모리와 연산량이 적습니다. 중요한 것은 LoRA의 순서입니다.
스타일 LoRA를 먼저 적용하고, 디테일이나 조명 같은 세부 효과는 나중에 적용하는 것이 일반적으로 좋은 결과를 냅니다. 순서를 바꾸면 결과가 달라질 수 있으므로 실험이 필요합니다.
LoRAManager 클래스의 balance_strengths 메서드는 실무에서 매우 유용한 기능입니다. 여러분이 여러 스타일 LoRA를 동시에 사용할 때, 각각 0.8, 0.7로 설정하면 총 1.5가 되어 과도하게 적용될 수 있습니다.
이 함수는 자동으로 비율을 조정하여, 스타일 카테고리 전체가 1.0을 넘지 않도록 각 LoRA를 0.53, 0.47 같은 식으로 리밸런싱합니다. 여러분이 이 기법을 사용하면 프로젝트별로 "스타일 팩"을 만들 수 있습니다.
실무에서는 base_style.json에 주요 LoRA 조합을 저장해두고, 각 씬마다 조금씩 변형하는 방식으로 작업합니다. 또한 Civitai 같은 플랫폼에서 수천 개의 커뮤니티 제작 LoRA를 다운로드할 수 있어, 거의 모든 스타일을 구현할 수 있습니다.
심지어 특정 캐릭터나 유명인을 학습한 LoRA도 있어(저작권 주의 필요), 일관된 캐릭터 생성이 가능합니다.
실전 팁
💡 LoRA 강도는 보통 0.6~1.0 범위가 적절합니다. 1.0 이상으로 올리면 효과가 과도해져 이미지가 망가질 수 있지만, 때로는 의도적으로 1.5까지 올려 극적인 효과를 낼 수도 있습니다.
💡 흔한 실수: 너무 많은 LoRA를 동시에 사용하면 서로 충돌하여 아티팩트가 생깁니다. 34개 이하로 제한하고, 각각의 역할을 명확히 하세요 (스타일 1개 + 디테일 1개 + 효과 12개).
💡 성능 팁: LoRA 파일은 메모리에 캐시되므로, 자주 사용하는 것들은 한 번 로드하면 다음 생성에서 빠릅니다. 하지만 10개 이상 동시에 로드하면 VRAM 부족이 발생할 수 있으니 주의하세요.
💡 트리거 워드를 반드시 확인하세요. 일부 LoRA는 "lora:name" 같은 특정 키워드를 프롬프트에 포함해야 활성화됩니다. 모델 설명이나 civitai 페이지에서 확인할 수 있습니다.
💡 strength_clip을 0으로 설정하면 비주얼 스타일만 적용되고 텍스트 해석은 베이스 모델을 따릅니다. 이 기법은 스타일은 바꾸되 프롬프트 이해도를 유지하고 싶을 때 유용합니다.
5. Upscaling_기법
시작하며
여러분이 512x512 해상도로 생성한 이미지를 확대했을 때 흐릿하고 디테일이 부족한 경험 있으신가요? 프린트나 웹사이트 배너처럼 고해상도가 필요한 상황에서는 어떻게 해야 할지 막막하셨죠?
이 문제는 AI 이미지 생성의 근본적인 한계입니다. 고해상도로 직접 생성하면 VRAM이 부족하거나 생성 시간이 너무 오래 걸립니다.
1024x1024는 512x512보다 4배의 메모리와 시간이 필요하고, 2048x2048는 16배나 됩니다. 또한 고해상도 직접 생성은 구도가 이상하게 나오는 경우도 많습니다.
바로 이럴 때 필요한 것이 2단계 업스케일링 전략입니다. 먼저 낮은 해상도로 구도를 잡고, 그 다음 AI 업스케일러로 디테일을 추가하면서 확대하는 방식입니다.
단순 확대와 달리 실제로 디테일을 생성하므로 훨씬 선명한 결과를 얻을 수 있습니다.
개요
간단히 말해서, AI 업스케일링은 저해상도 이미지를 고해상도로 변환하면서 새로운 디테일을 지능적으로 생성하는 기술입니다. 단순 보간이 아니라 이미지의 내용을 이해하고 그에 맞는 디테일을 추가합니다.
왜 이 기법이 필요한지 실무 관점에서 설명하자면, 상업 프로젝트에서는 다양한 해상도가 필요합니다. 예를 들어, 게임 스플래시 아트는 4K(3840x2160) 이상이어야 하고, 인쇄물은 300dpi 기준으로 매우 큰 픽셀 크기가 필요합니다.
처음부터 이 해상도로 생성하면 8GB VRAM으로는 불가능하지만, 512x512로 생성 후 4배 업스케일하면 2048x2048 고품질 이미지를 얻을 수 있습니다. 기존의 Photoshop 업스케일이나 waifu2x 같은 도구들도 있었지만, 이제는 Stable Diffusion의 img2img 기능과 전용 업스케일 모델을 결합하여 훨씬 자연스러운 결과를 만들 수 있습니다.
업스케일링의 핵심 특징은 첫째, Ultimate SD Upscale이나 Tiled Diffusion으로 타일 방식 처리하여 VRAM 한계를 극복하고, 둘째, denoise 값으로 디테일 추가 강도를 조절할 수 있으며, 셋째, 4x-UltraSharp 같은 전용 업스케일 모델로 먼저 확대한 뒤 Stable Diffusion으로 디테일을 보강하는 2단계 접근이 가능하다는 점입니다. 이러한 특징들이 제한된 하드웨어로도 프로페셔널 품질의 고해상도 이미지를 만들 수 있게 합니다.
코드 예제
# ComfyUI 고급 업스케일 워크플로우 (Tiled 방식)
workflow_upscale = {
# ... 기본 이미지 생성 노드들
# 1단계: 전용 업스케일러로 1차 확대
"30": {
"inputs": {
"upscale_model_name": "4x-UltraSharp.pth" # ESRGAN 기반
},
"class_type": "UpscaleModelLoader"
},
"31": {
"inputs": {
"upscale_model": ["30", 0],
"image": ["6", 0] # VAEDecode 출력 (원본 이미지)
},
"class_type": "ImageUpscaleWithModel" # 512->2048 (4배)
},
# 2단계: Tiled VAE로 고해상도 처리
"32": {
"inputs": {
"vae": ["1", 2],
"tile_size": 512 # 타일 크기 (VRAM에 따라 조절)
},
"class_type": "VAEEncodeTiled" # 큰 이미지를 타일로 나누어 인코딩
},
"33": {
"inputs": {
"pixels": ["31", 0], # 업스케일된 이미지
"vae": ["32", 0]
},
"class_type": "VAEEncode" # 이미지를 latent로 변환
},
# 3단계: img2img로 디테일 보강
"34": {
"inputs": {
"seed": 42,
"steps": 20,
"cfg": 7.0,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 0.4, # 낮은 값: 원본 유지, 높은 값: 디테일 추가
"model": ["1", 0],
"positive": ["2", 0], # 원본과 같은 프롬프트
"negative": ["3", 0],
"latent_image": ["33", 0] # 업스케일된 latent
},
"class_type": "KSampler"
},
# 4단계: 타일 방식으로 디코딩 (VRAM 절약)
"35": {
"inputs": {
"samples": ["34", 0],
"vae": ["1", 2],
"tile_size": 512 # 인코딩과 같은 크기
},
"class_type": "VAEDecodeTiled"
}
}
# Python 유틸리티: 동적 denoise 값 계산
def calculate_optimal_denoise(upscale_factor: int, content_type: str) -> float:
"""업스케일 배율과 콘텐츠 타입에 따른 최적 denoise 값"""
# 기본값 설정
base_denoise = {
"portrait": 0.35, # 인물: 얼굴 보존 중요
"landscape": 0.45, # 풍경: 디테일 추가 여유
"architecture": 0.30, # 건축: 선명한 선 유지
"abstract": 0.50 # 추상: 창의적 디테일 허용
}
denoise = base_denoise.get(content_type, 0.40)
# 배율이 높을수록 denoise 약간 증가 (더 많은 디테일 필요)
denoise += (upscale_factor - 2) * 0.05
# 0.2~0.6 범위로 제한
return max(0.2, min(0.6, denoise))
# 사용 예시
portrait_denoise = calculate_optimal_denoise(4, "portrait") # 0.45
landscape_denoise = calculate_optimal_denoise(4, "landscape") # 0.55
설명
이것이 하는 일: 이 워크플로우는 512x512 이미지를 2048x2048(4배)로 업스케일하면서 AI가 새로운 디테일을 생성하도록 합니다. 첫 번째로, ImageUpscaleWithModel 노드(#31)가 ESRGAN 기반의 4x-UltraSharp 모델을 사용하여 순수하게 해상도를 4배 확대합니다.
이 단계는 매우 빠르며(1~2초), 기계 학습으로 학습된 패턴을 사용해 단순 보간보다 훨씬 선명합니다. 하지만 완전히 새로운 디테일을 만들지는 못하고, 기존 정보를 "예쁘게" 확대하는 수준입니다.
그래서 다음 단계가 필요합니다. 그 다음으로, VAEEncodeTiled와 VAEDecodeTiled 노드(#32, #35)가 핵심 기술입니다.
2048x2048 이미지를 한 번에 처리하면 20GB 이상의 VRAM이 필요할 수 있는데, 이것을 512x512 타일로 나누어 처리하면 8GB로도 가능합니다. 내부적으로는 이미지를 겹치는 타일들로 분할하고, 각 타일을 독립적으로 인코딩/디코딩합니다.
타일 경계에서 이음새가 생기지 않도록 오버랩 영역을 블렌딩하는 것이 이 노드의 핵심 로직입니다. 중간 단계에서 KSampler(#34)가 img2img 모드로 실행됩니다.
denoise=0.4는 원본 이미지의 60%를 유지하고 40%만 새로 생성한다는 의미입니다. 이것이 업스케일의 핵심인데, 너무 낮으면(0.2) 디테일이 부족하고, 너무 높으면(0.7) 원본과 달라질 수 있습니다.
0.3~0.5가 일반적으로 안전한 범위입니다. 프롬프트는 원본 생성 시와 동일하게 사용하여 일관성을 유지합니다.
마지막으로, 전체 과정을 거쳐 여러분은 원본의 구도와 스타일을 유지하면서도 털의 질감, 피부의 모공, 풍경의 나뭇잎 하나하나 같은 미세한 디테일이 추가된 고해상도 이미지를 얻게 됩니다. 여러분이 이 기법을 사용하면 하드웨어 제약 없이 상업적 품질의 이미지를 생성할 수 있습니다.
실무에서는 클라이언트 승인을 512x512로 빠르게 받고, 최종 딜리버리만 업스케일하는 워크플로우를 많이 사용합니다. 또한 여러 업스케일 모델(UltraSharp, AnimeSharp, RealESRGAN 등)을 콘텐츠 타입에 맞게 선택하면 더 좋은 결과를 얻을 수 있습니다.
예를 들어 애니메이션 스타일은 AnimeSharp, 사실적 사진은 RealESRGAN-x4plus가 적합합니다.
실전 팁
💡 항상 2단계 업스케일을 권장합니다. 한 번에 4배보다는 2배를 두 번 하는 것이 더 자연스럽고, 중간 결과를 확인할 수 있어 문제가 생기면 일찍 잡을 수 있습니다.
💡 흔한 실수: tile_size를 너무 작게 설정하면(256 이하) 타일 경계가 보일 수 있습니다. 512는 안전하고, VRAM이 충분하면 768이나 1024로 올려서 품질을 높이세요.
💡 성능 최적화: 업스케일만 할 때는 steps를 1520으로 줄여도 됩니다. 20 스텝으로도 충분한 디테일이 생성되며, 3050 스텝은 시간만 낭비하는 경우가 많습니다.
💡 denoise 값 테스트를 위해 동일 이미지를 0.3, 0.4, 0.5로 각각 업스케일해보고 비교하세요. 콘텐츠 타입마다 최적값이 다르므로 프로젝트 초기에 표준을 정해두면 좋습니다.
💡 업스케일 전에 Fix Faces 노드를 추가하면 인물 이미지의 얼굴 디테일이 크게 개선됩니다. 특히 작은 얼굴이 여러 개 있는 단체 사진에서 효과적입니다.
6. Batch_Processing
시작하며
여러분이 같은 설정으로 수십 개의 이미지를 생성해야 할 때, 하나씩 수동으로 생성 버튼을 누르고 계셨나요? 또는 프롬프트만 조금씩 바꿔가며 변형을 만들 때 매번 워크플로우를 수정하는 번거로움을 겪으셨죠?
이 문제는 반복 작업에서 생산성을 크게 떨어뜨립니다. 특히 게임 아이템 아이콘 100개, 캐릭터 표정 변형 20개 같은 대량 작업에서는 자동화가 필수입니다.
수동 작업으로는 며칠 걸릴 일을 몇 시간으로 단축할 수 있습니다. 바로 이럴 때 필요한 것이 배치 처리 시스템입니다.
ComfyUI는 API를 통해 워크플로우를 자동화할 수 있고, 프롬프트 리스트나 파라미터 범위를 지정하여 무인으로 대량 생성할 수 있습니다. 잠자는 동안 GPU가 500장의 이미지를 생성하도록 설정할 수도 있습니다.
개요
간단히 말해서, 배치 처리는 동일하거나 유사한 작업을 자동으로 반복 실행하는 시스템입니다. ComfyUI의 HTTP API를 활용하여 Python 스크립트로 워크플로우를 제어하고, 파라미터를 동적으로 변경하면서 여러 이미지를 생성합니다.
왜 이 기법이 필요한지 실무 관점에서 설명하자면, 실제 프로젝트에서는 A/B 테스트, 컨셉 변형, 대량 에셋 생성이 일상입니다. 예를 들어, "빨간 드레스", "파란 드레스", "녹색 드레스" 같은 색상 변형 10개를 만들어야 할 때, 배치 스크립트로 프롬프트 리스트를 순회하면 자동으로 생성됩니다.
또한 seed 값을 0~999로 반복하여 1000개의 랜덤 변형을 만들고 그중 최고를 선택하는 전략도 가능합니다. 기존에는 GUI에서 수동으로 클릭하거나, 간단한 큐 시스템을 사용했다면, 이제는 복잡한 조건문과 반복문을 포함한 고급 자동화 파이프라인을 구축할 수 있습니다.
배치 처리의 핵심 특징은 첫째, ComfyUI의 REST API를 통해 프로그래밍 방식으로 워크플로우를 실행할 수 있고, 둘째, 진행 상황을 모니터링하고 완료된 이미지를 자동으로 분류/저장할 수 있으며, 셋째, 오류 발생 시 재시도 로직이나 스킵 로직을 구현할 수 있다는 점입니다. 이러한 특징들이 무인 대량 생성과 프로덕션 파이프라인 통합을 가능하게 합니다.
코드 예제
# ComfyUI 배치 처리 자동화 스크립트
import requests
import json
import time
from pathlib import Path
from typing import List, Dict
class ComfyUIBatchProcessor:
"""ComfyUI 배치 작업 관리"""
def __init__(self, server_url: str = "http://127.0.0.1:8188"):
self.server_url = server_url
self.client_id = "batch_processor"
def queue_prompt(self, workflow: dict) -> str:
"""워크플로우를 큐에 추가하고 prompt_id 반환"""
response = requests.post(
f"{self.server_url}/prompt",
json={"prompt": workflow, "client_id": self.client_id}
)
return response.json()["prompt_id"]
def get_history(self, prompt_id: str) -> dict:
"""특정 작업의 히스토리 조회"""
response = requests.get(f"{self.server_url}/history/{prompt_id}")
return response.json()
def wait_for_completion(self, prompt_id: str, timeout: int = 300):
"""작업 완료 대기 (타임아웃 설정)"""
start_time = time.time()
while time.time() - start_time < timeout:
history = self.get_history(prompt_id)
if prompt_id in history:
# 작업 완료
return history[prompt_id]
time.sleep(2) # 2초마다 체크
raise TimeoutError(f"작업 {prompt_id}가 {timeout}초 내에 완료되지 않음")
def batch_generate(self,
base_workflow: dict,
variations: List[Dict],
output_dir: Path):
"""배치 생성 실행"""
output_dir.mkdir(exist_ok=True)
results = []
for i, variation in enumerate(variations):
print(f"[{i+1}/{len(variations)}] 생성 중: {variation.get('name', i)}")
# 워크플로우 복사 및 파라미터 수정
workflow = json.loads(json.dumps(base_workflow)) # 딥카피
# variation에서 지정한 값으로 노드 업데이트
for node_id, params in variation.get("updates", {}).items():
if node_id in workflow:
workflow[node_id]["inputs"].update(params)
try:
# 큐에 추가하고 완료 대기
prompt_id = self.queue_prompt(workflow)
result = self.wait_for_completion(prompt_id)
# 결과 저장
results.append({
"variation": variation,
"prompt_id": prompt_id,
"status": "success",
"result": result
})
except Exception as e:
print(f"오류 발생: {e}")
results.append({
"variation": variation,
"status": "failed",
"error": str(e)
})
continue # 실패해도 계속 진행
# 결과 리포트 저장
report_path = output_dir / "batch_report.json"
with open(report_path, "w") as f:
json.dump(results, f, indent=2)
print(f"\n배치 완료: {len(results)}개 중 "
f"{sum(1 for r in results if r['status'] == 'success')}개 성공")
return results
# 실제 사용 예시
processor = ComfyUIBatchProcessor()
# 기본 워크플로우 로드
with open("base_workflow.json") as f:
base_workflow = json.load(f)
# 변형 리스트 정의
variations = [
{
"name": "red_dress",
"updates": {
"2": {"text": "beautiful girl in red dress"}, # 프롬프트 노드
"4": {"seed": 100} # Sampler 노드의 seed
}
},
{
"name": "blue_dress",
"updates": {
"2": {"text": "beautiful girl in blue dress"},
"4": {"seed": 101}
}
},
{
"name": "green_dress",
"updates": {
"2": {"text": "beautiful girl in green dress"},
"4": {"seed": 102}
}
}
]
# 배치 실행
results = processor.batch_generate(
base_workflow,
variations,
Path("./output/batch_001")
)
설명
이것이 하는 일: 이 스크립트는 ComfyUI 서버에 HTTP 요청을 보내 여러 워크플로우를 자동으로 실행하고, 각 결과를 추적하며 리포트를 생성합니다. 첫 번째로, ComfyUIBatchProcessor 클래스가 ComfyUI 서버와의 통신을 캡슐화합니다.
queue_prompt 메서드는 워크플로우 JSON을 POST 요청으로 전송하고, 서버가 반환하는 prompt_id를 받습니다. 이 ID가 나중에 작업을 추적하는 키가 됩니다.
ComfyUI는 내부적으로 큐 시스템을 사용하므로, 여러 요청을 보내도 순서대로 처리됩니다. GPU가 한 번에 하나씩 작업하므로 병렬 실행은 안 되지만, 자동화는 됩니다.
그 다음으로, wait_for_completion 메서드가 폴링 방식으로 작업 완료를 기다립니다. 2초마다 /history 엔드포인트를 조회하여 해당 prompt_id가 나타나는지 확인합니다.
내부적으로는 작업이 완료되면 히스토리에 결과 이미지 경로, 사용된 파라미터, 실행 시간 등의 정보가 저장됩니다. 타임아웃을 설정하여 무한 대기를 방지하는 것이 중요합니다.
특히 업스케일 같은 무거운 작업은 5분 이상 걸릴 수 있으므로 충분한 시간을 줘야 합니다. 중간 단계에서 batch_generate 메서드가 핵심 로직입니다.
variations 리스트를 순회하면서, 각 변형에 대해 base_workflow를 복사하고 특정 노드의 파라미터를 업데이트합니다. 예를 들어, "2"번 노드(프롬프트)의 text를 "red dress"로 바꾸고, "4"번 노드(샘플러)의 seed를 100으로 설정합니다.
JSON의 딥카피를 사용하는 이유는 Python의 딕셔너리가 참조 타입이라 그대로 수정하면 원본이 오염되기 때문입니다. 오류 처리 부분도 중요합니다.
try-except 블록으로 개별 작업 실패를 처리하되, 전체 배치를 중단하지 않고 계속 진행합니다. 실무에서는 100개 중 1~2개가 실패하는 경우가 있는데(예: VRAM 부족, 타임아웃), 그때마다 전체를 다시 시작하면 비효율적입니다.
실패한 항목만 나중에 재실행하거나, 리포트를 보고 원인을 파악할 수 있습니다. 마지막으로, batch_report.json에 모든 결과를 저장하여 추후 분석이 가능합니다.
어떤 변형이 성공했는지, 실행 시간은 얼마나 걸렸는지, 어떤 파라미터를 사용했는지 모두 기록됩니다. 여러분이 이 시스템을 사용하면 밤새 GPU를 활용하여 수백 개의 이미지를 생성할 수 있습니다.
실무에서는 더 나아가 생성된 이미지를 CLIP으로 자동 평가하고, 점수가 높은 것만 선별하는 파이프라인도 구축합니다. 또한 AWS Lambda나 Docker 컨테이너로 배포하여, 팀원이 웹 인터페이스에서 배치 작업을 요청하면 자동으로 실행되도록 할 수도 있습니다.
실전 팁
💡 대규모 배치(100개 이상) 실행 전에는 반드시 3~5개로 테스트하세요. 워크플로우에 오류가 있으면 모든 작업이 실패할 수 있습니다.
💡 흔한 실수: 파일명 충돌을 방지하기 위해 SaveImage 노드의 filename_prefix를 각 변형마다 다르게 설정하세요. 예: "red_dress_", "blue_dress_" 같은 식으로.
💡 성능 팁: ComfyUI 서버를 --listen 옵션으로 실행하면 네트워크의 다른 컴퓨터에서도 접근할 수 있습니다. 여러 GPU 머신을 동시에 활용하는 분산 배치 처리도 가능합니다.
💡 진행 상황을 실시간으로 확인하려면 websocket 연결을 사용하세요. ComfyUI는 /ws 엔드포인트로 실시간 이벤트를 전송하므로, 프로그레스 바를 구현할 수 있습니다.
💡 variations를 CSV 파일에서 읽어오면 기획자나 디자이너가 Excel로 작성한 리스트를 바로 사용할 수 있습니다. pandas 라이브러리로 쉽게 파싱할 수 있습니다.
7. Custom_Node_개발
시작하며
여러분이 ComfyUI의 기본 노드만으로는 원하는 작업을 구현하기 어려웠던 경험 있으신가요? 예를 들어, 특정 이미지 전처리 알고리즘이나 외부 API 연동 같은 기능이 필요한데 기본 노드에는 없었죠?
이 문제는 ComfyUI가 범용적으로 설계되어 모든 특수한 경우를 커버할 수 없기 때문입니다. 하지만 폐쇄적인 시스템이 아니라, Python으로 누구나 노드를 만들어 확장할 수 있는 구조로 되어 있습니다.
실제로 커뮤니티에서 수천 개의 커스텀 노드가 공유되고 있습니다. 바로 이럴 때 필요한 것이 커스텀 노드 개발 능력입니다.
자신만의 워크플로우에 최적화된 노드를 만들면 작업 효율이 크게 향상되고, 오픈소스로 공개하여 커뮤니티에 기여할 수도 있습니다. 생각보다 간단한 Python 클래스 하나로 강력한 기능을 추가할 수 있습니다.
개요
간단히 말해서, 커스텀 노드는 ComfyUI의 노드 시스템을 확장하는 Python 클래스입니다. 입력을 받고, 처리하고, 출력을 반환하는 구조로 되어 있으며, ComfyUI가 자동으로 UI를 생성해줍니다.
왜 이 기술이 필요한지 실무 관점에서 설명하자면, 회사나 팀마다 특수한 요구사항이 있습니다. 예를 들어, 자체 DB에서 프롬프트 템플릿을 불러오거나, 생성된 이미지를 자동으로 CDN에 업로드하거나, 특정 브랜드 가이드라인에 맞게 색상을 조정하는 등의 작업입니다.
이런 것들을 커스텀 노드로 만들어두면, 팀원 모두가 GUI에서 드래그 앤 드롭으로 사용할 수 있습니다. 기존에는 Python 스크립트를 별도로 실행하거나 이미지를 수동으로 후처리했다면, 이제는 모든 과정을 ComfyUI 워크플로우 안에 통합할 수 있습니다.
커스텀 노드의 핵심 특징은 첫째, INPUT_TYPES로 입력 파라미터를 정의하면 ComfyUI가 자동으로 UI 위젯을 생성하고, 둘째, RETURN_TYPES로 출력 타입을 명시하여 다른 노드와 연결 가능하도록 하며, 셋째, 클래스 메서드만 구현하면 되므로 진입 장벽이 낮다는 점입니다. 이러한 특징들이 빠른 프로토타이핑과 프로덕션 통합을 가능하게 합니다.
코드 예제
# ComfyUI 커스텀 노드 예제: 프롬프트 템플릿 매니저
import json
from pathlib import Path
class PromptTemplateLoader:
"""JSON 파일에서 프롬프트 템플릿을 로드하는 커스텀 노드"""
def __init__(self):
self.templates_dir = Path("./prompt_templates")
self.templates_dir.mkdir(exist_ok=True)
@classmethod
def INPUT_TYPES(cls):
"""ComfyUI가 UI를 생성하기 위한 입력 정의"""
# 사용 가능한 템플릿 파일 목록
templates_dir = Path("./prompt_templates")
template_files = [f.stem for f in templates_dir.glob("*.json")] \
if templates_dir.exists() else ["none"]
return {
"required": {
"template_name": (template_files,), # 드롭다운 메뉴
"subject": ("STRING", { # 텍스트 입력
"default": "a beautiful girl",
"multiline": False
}),
"style_strength": ("FLOAT", { # 슬라이더
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.1
})
},
"optional": {
"additional_details": ("STRING", { # 선택적 입력
"default": "",
"multiline": True
})
}
}
RETURN_TYPES = ("STRING", "STRING") # (포지티브 프롬프트, 네거티브 프롬프트)
RETURN_NAMES = ("positive", "negative") # 출력 이름
FUNCTION = "load_and_build" # 실행될 메서드 이름
CATEGORY = "MyCustomNodes/Prompt" # UI에서 카테고리 분류
def load_and_build(self, template_name, subject, style_strength,
additional_details=""):
"""실제 노드 실행 로직"""
# 템플릿 파일 로드
template_path = self.templates_dir / f"{template_name}.json"
if not template_path.exists():
# 폴백: 기본 프롬프트 반환
return (subject, "lowres, bad quality")
with open(template_path) as f:
template = json.load(f)
# 프롬프트 빌드
positive_parts = [
subject,
f"({template['style']}:{style_strength})",
template.get('quality', 'best quality, highly detailed'),
]
if additional_details:
positive_parts.append(additional_details)
positive = ", ".join(positive_parts)
negative = template.get('negative', 'lowres, bad quality')
# 로깅 (ComfyUI 콘솔에 출력)
print(f"[PromptTemplateLoader] 로드됨: {template_name}")
print(f" 포지티브: {positive[:100]}...")
return (positive, negative)
# ComfyUI에 노드 등록
NODE_CLASS_MAPPINGS = {
"PromptTemplateLoader": PromptTemplateLoader
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PromptTemplateLoader": "Prompt Template Loader"
}
# 템플릿 파일 예시 (prompt_templates/fantasy.json)
"""
{
"style": "epic fantasy art, dramatic lighting, artstation quality",
"quality": "masterpiece, best quality, highly detailed, 8k",
"negative": "lowres, bad anatomy, bad hands, text, error, missing fingers, cropped, worst quality, low quality, jpeg artifacts, signature, watermark, blurry"
}
"""
설명
이것이 하는 일: 이 커스텀 노드는 JSON 파일에 저장된 프롬프트 템플릿을 불러와, 사용자 입력과 결합하여 최종 프롬프트를 생성합니다. 첫 번째로, INPUT_TYPES 클래스 메서드가 ComfyUI에게 "이 노드는 어떤 입력을 받는지"를 알려줍니다.
template_name은 드롭다운으로, subject와 additional_details는 텍스트 박스로, style_strength는 슬라이더로 자동 생성됩니다. 내부적으로 ComfyUI는 이 정의를 파싱하여 React 컴포넌트를 동적으로 렌더링합니다.
multiline=True로 설정하면 여러 줄 입력이 가능한 텍스트 영역이 되고, min/max/step으로 슬라이더 범위를 제어합니다. 그 다음으로, RETURN_TYPES가 이 노드의 출력을 정의합니다.
("STRING", "STRING")은 두 개의 문자열을 반환한다는 의미이고, RETURN_NAMES는 각각에 "positive", "negative"라는 라벨을 붙입니다. 이렇게 하면 다음 노드가 이것을 연결할 때 어떤 출력인지 명확히 알 수 있습니다.
예를 들어, CLIPTextEncode 노드의 text 입력에 이 노드의 positive 출력을 연결하는 식입니다. 중간 단계에서 load_and_build 메서드가 실제 비즈니스 로직을 수행합니다.
JSON 파일을 읽고, 템플릿의 style 키워드를 style_strength에 따라 가중치를 적용하고, 사용자가 입력한 subject와 결합합니다. 에러 처리도 중요한데, 템플릿 파일이 없으면 기본값을 반환하여 노드가 깨지지 않도록 합니다.
실무에서는 더 복잡한 유효성 검사나 폴백 로직을 추가합니다. CATEGORY 속성은 ComfyUI의 노드 브라우저에서 분류를 결정합니다.
"MyCustomNodes/Prompt"로 설정하면, "MyCustomNodes"라는 카테고리 아래 "Prompt" 서브카테고리에 나타납니다. 여러 커스텀 노드를 만들 때 체계적으로 정리하는 데 유용합니다.
마지막으로, NODE_CLASS_MAPPINGS와 NODE_DISPLAY_NAME_MAPPINGS가 ComfyUI에 이 노드를 등록합니다. 파일을 custom_nodes 폴더에 넣기만 하면, ComfyUI 재시작 시 자동으로 인식됩니다.
여러분이 이 기법을 사용하면 팀의 워크플로우를 표준화할 수 있습니다. 실무에서는 회사의 스타일 가이드를 템플릿으로 만들어두고, 디자이너들이 일관된 프롬프트를 사용하도록 합니다.
또한 외부 서비스 연동 노드(예: Notion DB에서 캐릭터 설정 불러오기, Slack으로 완료 알림 보내기)를 만들어 전체 파이프라인을 자동화할 수도 있습니다. 오픈소스로 공개하면 커뮤니티에서 피드백을 받고 개선할 수 있습니다.
실전 팁
💡 개발 중에는 ComfyUI를 재시작하지 않고 노드를 리로드하려면, Manager 확장의 "Reload Custom Nodes" 기능을 사용하세요. 단, 클래스 구조를 바꿨을 때만 재시작이 필요합니다.
💡 흔한 실수: INPUT_TYPES를 인스턴스 메서드로 만들면 안 됩니다. 반드시 @classmethod 데코레이터를 사용하세요. ComfyUI는 인스턴스 생성 전에 이 메서드를 호출합니다.
💡 디버깅: print() 문은 ComfyUI를 실행한 터미널에 출력됩니다. 복잡한 로직을 개발할 때는 중간 값들을 프린트하여 확인하세요.
💡 타입 힌트를 추가하면 코드 가독성이 높아집니다. 특히 팀원이 사용할 노드라면 명확한 docstring과 타입 힌트를 작성하세요.
💡 이미지 처리 노드를 만들 때는 torch.Tensor 형식을 사용하세요. ComfyUI의 이미지는 (batch, height, width, channels) 형태의 텐서입니다. PIL이나 OpenCV로 변환이 필요하면 유틸리티 함수를 만들어두세요.
8. 성능_최적화
시작하며
여러분이 고해상도 이미지를 생성하거나 복잡한 워크플로우를 실행할 때 VRAM 부족 오류나 느린 생성 속도로 고생하신 적 있으신가요? 또는 같은 하드웨어로 다른 사람은 더 빠르게 생성하는 것을 보고 의아해하셨죠?
이 문제는 ComfyUI의 기본 설정이 범용적이어서, 여러분의 특정 GPU와 작업에 최적화되어 있지 않기 때문입니다. 같은 RTX 4090이라도 설정에 따라 이미지 생성 속도가 2배 이상 차이 날 수 있습니다.
VRAM 관리 방식, 정밀도 설정, VAE 타일링 등 여러 요소가 성능에 영향을 미칩니다. 바로 이럴 때 필요한 것이 체계적인 성능 최적화입니다.
하드웨어 특성에 맞춰 설정을 조정하고, 불필요한 연산을 제거하며, 메모리 사용을 최적화하면 같은 GPU로도 훨씬 쾌적하게 작업할 수 있습니다. 심지어 불가능했던 해상도도 가능해질 수 있습니다.
개요
간단히 말해서, 성능 최적화는 ComfyUI의 실행 파라미터와 워크플로우 구조를 조정하여 생성 속도를 높이고 메모리 사용을 줄이는 기술입니다. 하드웨어 리소스를 최대한 활용하면서도 안정성을 유지합니다.
왜 이 기법이 필요한지 실무 관점에서 설명하자면, 대량 작업이나 클라이언트 앞에서 실시간 프레젠테이션 할 때 속도가 중요합니다. 예를 들어, 이미지 하나 생성에 30초 걸리던 것을 15초로 줄이면, 100개 작업 시 25분을 절약할 수 있습니다.
또한 VRAM 최적화로 16GB GPU에서 불가능했던 2K 업스케일을 가능하게 만들 수도 있습니다. 기존에는 하드웨어 업그레이드로 해결하려 했다면, 이제는 소프트웨어 최적화로 비용 없이 성능을 개선할 수 있습니다.
성능 최적화의 핵심 특징은 첫째, --lowvram, --medvram, --fp16 같은 실행 옵션으로 VRAM 사용을 제어하고, 둘째, xformers나 torch.compile 같은 최신 PyTorch 기능으로 연산을 가속하며, 셋째, 워크플로우에서 불필요한 노드를 제거하고 캐싱을 활용한다는 점입니다. 이러한 특징들이 제한된 하드웨어로도 프로페셔널 작업을 가능하게 합니다.
코드 예제
# ComfyUI 성능 최적화 설정 및 벤치마크
import subprocess
import time
import psutil
import GPUtil
from dataclasses import dataclass
from typing import List
@dataclass
class BenchmarkResult:
"""벤치마크 결과"""
config_name: str
generation_time: float # 초
vram_peak: float # GB
vram_allocated: float # GB
success: bool
class ComfyUIOptimizer:
"""ComfyUI 성능 최적화 유틸리티"""
# 다양한 최적화 설정
OPTIMIZATION_CONFIGS = {
"high_performance": {
"args": ["--preview-method", "none", "--fp16"],
"description": "최고 속도, 높은 VRAM 사용"
},
"balanced": {
"args": ["--preview-method", "auto", "--fp16"],
"description": "속도와 메모리 균형"
},
"low_vram": {
"args": ["--lowvram", "--preview-method", "none"],
"description": "낮은 VRAM, 느린 속도"
},
"ultra_low_vram": {
"args": ["--novram", "--cpu-vae"],
"description": "CPU 폴백, 매우 느림"
}
}
@staticmethod
def get_vram_usage() -> float:
"""현재 VRAM 사용량 반환 (GB)"""
gpus = GPUtil.getGPUs()
if gpus:
return gpus[0].memoryUsed / 1024
return 0.0
@staticmethod
def benchmark_workflow(workflow_path: str, config_name: str) -> BenchmarkResult:
"""특정 설정으로 워크플로우 벤치마크"""
config = ComfyUIOptimizer.OPTIMIZATION_CONFIGS[config_name]
# ComfyUI 서버 시작 (실제로는 이미 실행 중이라고 가정)
print(f"테스트: {config_name} - {config['description']}")
# VRAM 초기화
initial_vram = ComfyUIOptimizer.get_vram_usage()
try:
# 워크플로우 실행 시간 측정
start_time = time.time()
# 실제 워크플로우 실행 (API 호출)
# response = requests.post(...)
# wait_for_completion(...)
generation_time = time.time() - start_time
# 피크 VRAM 측정
peak_vram = ComfyUIOptimizer.get_vram_usage()
vram_used = peak_vram - initial_vram
return BenchmarkResult(
config_name=config_name,
generation_time=generation_time,
vram_peak=peak_vram,
vram_allocated=vram_used,
success=True
)
except Exception as e:
print(f"오류: {e}")
return BenchmarkResult(
config_name=config_name,
generation_time=0,
vram_peak=0,
vram_allocated=0,
success=False
)
@staticmethod
def recommend_config(available_vram_gb: float) -> str:
"""사용 가능한 VRAM에 따라 최적 설정 추천"""
if available_vram_gb >= 12:
return "high_performance"
elif available_vram_gb >= 8:
return "balanced"
elif available_vram_gb >= 6:
return "low_vram"
else:
return "ultra_low_vram"
# 워크플로우 최적화 팁 적용
def optimize_workflow_structure(workflow: dict) -> dict:
"""워크플로우 구조 최적화"""
optimized = workflow.copy()
# 1. 불필요한 Preview Image 노드 제거
nodes_to_remove = []
for node_id, node in optimized.items():
if node["class_type"] == "PreviewImage":
# 최종 출력이 아닌 중간 프리뷰는 제거
nodes_to_remove.append(node_id)
for node_id in nodes_to_remove:
del optimized[node_id]
# 2. VAE Tiled 사용 권장 (고해상도의 경우)
for node_id, node in optimized.items():
if node["class_type"] == "VAEDecode":
# 이미지 크기 체크 (실제로는 latent 크기 확인 필요)
# 만약 2048 이상이면 VAEDecodeTiled로 교체 권장
print(f"[최적화] 노드 {node_id}: 고해상도인 경우 VAEDecodeTiled 사용 권장")
# 3. 동일 모델 중복 로드 확인
loaded_models = {}
for node_id, node in optimized.items():
if node["class_type"] == "CheckpointLoaderSimple":
model_name = node["inputs"]["ckpt_name"]
if model_name in loaded_models:
print(f"[경고] 모델 {model_name}이 중복 로드됨: "
f"{loaded_models[model_name]}, {node_id}")
loaded_models[model_name] = node_id
return optimized
# 사용 예시
optimizer = ComfyUIOptimizer()
# GPU VRAM 확인
gpus = GPUtil.getGPUs()
if gpus:
available_vram = gpus[0].memoryTotal / 1024
print(f"사용 가능 VRAM: {available_vram:.2f} GB")
# 추천 설정
recommended = optimizer.recommend_config(available_vram)
print(f"추천 설정: {recommended}")
print(f" {optimizer.OPTIMIZATION_CONFIGS[recommended]['description']}")
설명
이것이 하는 일: 이 코드는 다양한 최적화 설정을 정의하고, GPU 사양에 맞는 최적 설정을 추천하며, 워크플로우 구조를 분석하여 개선점을 제안합니다. 첫 번째로, OPTIMIZATION_CONFIGS 딕셔너리가 4가지 프리셋을 정의합니다.
high_performance는 --fp16으로 반정밀도 연산을 사용하여 속도를 2배 가까이 높이지만 VRAM을 많이 씁니다. fp16은 float32 대신 float16을 사용하여 메모리 절반, 연산 속도 2배의 이점이 있지만, 극히 드물게 수치 정밀도 문제가 생길 수 있습니다(실무에서는 거의 문제없음).
--preview-method none은 중간 프리뷰를 끄는데, 이것이 의외로 오버헤드가 큽니다. 매 스텝마다 이미지를 디코딩하므로 10~20% 느려질 수 있습니다.
그 다음으로, low_vram과 ultra_low_vram 설정은 제한된 하드웨어를 위한 것입니다. --lowvram은 모델을 VRAM과 RAM 사이에서 동적으로 이동시키는 스와핑 방식인데, 느리지만 6~8GB VRAM으로도 SDXL을 실행할 수 있게 해줍니다.
--novram은 모든 것을 CPU에서 처리하는 극단적 옵션으로, GPU가 약하거나 없을 때 사용합니다(생성 시간 10배 이상 느림). 중간 단계에서 get_vram_usage 함수가 실시간 VRAM 사용량을 모니터링합니다.
이것을 벤치마크에 활용하여, 특정 설정이 얼마나 메모리를 사용하는지 측정할 수 있습니다. 실무에서는 자동으로 여러 설정을 테스트하고 가장 빠르면서도 안정적인 것을 선택하는 스크립트를 돌립니다.
optimize_workflow_structure 함수는 정적 분석으로 워크플로우를 개선합니다. PreviewImage 노드는 개발 중에는 유용하지만, 프로덕션에서는 불필요한 오버헤드입니다.
제거하면 10~15% 속도 향상이 가능합니다. 또한 동일 모델을 여러 번 로드하는 실수를 감지합니다.
같은 체크포인트를 두 번 로드하면 VRAM을 2배 소모하므로, 하나만 로드하고 여러 노드가 공유하도록 수정해야 합니다. recommend_config 함수는 하드웨어에 맞는 설정을 자동 선택합니다.
RTX 4090(24GB)은 high_performance, RTX 3060(12GB)은 balanced, GTX 1660(6GB)은 low_vram 같은 식입니다. 이것을 프로그램 시작 시 자동으로 적용하면, 사용자가 복잡한 설정을 이해하지 않아도 최적 성능을 얻을 수 있습니다.
여러분이 이 최적화를 적용하면 작업 효율이 크게 향상됩니다. 실무에서는 프로젝트 시작 시 한 번 벤치마크를 돌려 최적 설정을 찾고, 그것을 표준으로 삼습니다.
또한 xformers 라이브러리를 설치하면(pip install xformers) attention 연산이 20~30% 빨라지므로 필수입니다. torch 2.0 이상에서는 torch.compile()도 고려할 만한데, 첫 실행은 느리지만 이후 실행이 빨라집니다.
실전 팁
💡 --fp16은 거의 모든 경우 켜는 것이 좋습니다. 품질 차이는 눈으로 구분하기 어렵고, 속도와 메모리 이득이 큽니다. 단, 일부 오래된 커스텀 노드는 fp16을 지원하지 않을 수 있으니 오류 발생 시 끄세요.
💡 흔한 실수: VRAM이 충분한데도 --lowvram을 켜면 오히려 느려집니다. 8GB 이상이면 기본 설정이나 --fp16만 사용하세요.
💡 성능 모니터링: nvidia-smi를 터미널에서 watch -n 1 nvidia-smi로 실행하면 1초마다 VRAM 사용량을 확인할 수 있습니다. 워크플로우 최적화 시 필수 도구입니다.
💡 배치 처리 시에는 --queue-size를 늘려서 여러 작업을 미리 로드하세요. GPU가 idle 시간 없이 계속 작업하므로 전체 처리 시간이 단축됩니다.
💡 SSD 사용이 중요합니다. 모델 로딩 시간은 디스크 속도에 크게 의존하므로, HDD보다 SSD에 모델 파일을 저장하면 초기 로딩이 5~10배 빨라집니다.