개요
범위: ComfyUI custom_nodes/ 아래에 새 노드 팩 하나를 만들고, 그 안의 노드 클래스가 UI 노드 검색 메뉴에 뜨는 지점까지. 실제 이미지·텐서 연산 로직은 구조가 잡힌 뒤 각자 채우는 영역이다.
대상: Python 기본기 있고, ComfyUI 워크플로에 기존 노드로는 표현 안 되는 로직(외부 API 호출, 독자 후처리, 파일 I/O 등)을 붙이고 싶은 사람.
전제: Python 3.12 · ComfyUI 설치 완료 (→ 환경 셋업 편). custom_nodes/ 가 뭐고 재시작 시 자동 로드되는 대충의 구조는 들어봤다는 가정.
배경
ComfyUI 는 시작 시 custom_nodes/ 바로 아래 각 폴더의 __init__.py 를 import 한다. 그 __init__.py 가 노출한 NODE_CLASS_MAPPINGS 딕셔너리에 들어있는 클래스들이 UI 노드 검색 목록에 등장한다. 그게 전부다 — 나머지(프런트엔드 위젯, 카테고리 분류, 타입 체크)는 노드 클래스가 선언한 메타데이터로부터 ComfyUI 가 자동 생성한다.
| 구성 | 역할 |
|---|---|
custom_nodes/<팩이름>/ | 팩 루트. 한 폴더 = 한 팩 |
__init__.py | 진입점. NODE_CLASS_MAPPINGS 노출 |
NODE_CLASS_MAPPINGS | {노드ID: 클래스} 딕셔너리 |
NODE_DISPLAY_NAME_MAPPINGS | {노드ID: UI 표시 이름} (선택) |
| 노드 클래스 | INPUT_TYPES · RETURN_TYPES · FUNCTION · CATEGORY 메타 + 실행 메서드 |
외부 라이브러리 감싸기 · 파이프라인 후처리 · 파일 포맷 변환 같은 것들은 이 골격 하나로 거의 다 커버된다. 독자 실행 엔진이나 GPU 코드를 쓸 일이 아니라면 복잡해질 이유가 없다.
작성 방법
6 단계. 14 는 “하나 떠오르게 하기”, 56 은 “유지보수 가능한 모양으로 정리”.
1. 팩 폴더 생성
ComfyUI 루트 기준 custom_nodes/ 아래에 팩 폴더를 만든다. 관례는 ComfyUI-<팩이름>.
custom_nodes/
└── ComfyUI-Foo/
├── __init__.py
└── foo_nodes/ ← 내부 서브패키지 (팩 이름 접두어 권장)
├── __init__.py
└── hello.py
왜: 서브패키지 이름에 nodes · comfy · server 같은 ComfyUI 코어가 이미 쓰는 이름을 피해야 하기 때문. 접두어를 붙이면 충돌이 원천 차단된다. 상세는 아래 트러블슈팅의 shadow collision 항목.
2. 최소 노드 클래스 작성
foo_nodes/hello.py:
class HelloNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": "hello"}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("echoed",)
FUNCTION = "run"
CATEGORY = "foo"
def run(self, text):
return (text + " from custom node",)필드별 의미:
| 필드 | 용도 |
|---|---|
INPUT_TYPES | UI 입력 슬롯 선언. STRING INT FLOAT IMAGE LATENT 등 타입 이름 |
RETURN_TYPES | 출력 타입 튜플. 타입 이름은 INPUT_TYPES 와 동일 어휘 |
RETURN_NAMES | 출력 슬롯 UI 이름 (선택). 없으면 타입 이름이 그대로 노출 |
FUNCTION | 실행 메서드 이름 (문자열). 클래스 내 메서드와 일치해야 함 |
CATEGORY | UI 노드 검색 트리에서의 위치 (foo · foo/util 등 슬래시로 깊이) |
왜: ComfyUI 는 이 메타만 보고 UI 위젯·타입 검사·라우팅을 자동 구성한다. 메타가 누락되면 노드는 뜨지만 연결이 안 되거나 실행 시 깨진다.
실행 메서드 run(self, text) 은 입력을 받아 튜플로 반환한다. 반환을 튜플로 감싸는 건 RETURN_TYPES 와 길이 맞추기 위한 규약.
3. 진입점 __init__.py 작성
팩 루트 ComfyUI-Foo/__init__.py:
from .foo_nodes.hello import HelloNode
NODE_CLASS_MAPPINGS = {
"HelloNode": HelloNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"HelloNode": "Hello (Foo)",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]왜: NODE_CLASS_MAPPINGS 의 키는 내부 식별자(워크플로 JSON 에 저장됨), 값은 클래스 참조. NODE_DISPLAY_NAME_MAPPINGS 는 UI 상에만 쓰는 예쁜 이름. 두 딕셔너리를 분리한 건 내부 ID 는 안정적으로 두고 UI 이름만 바꿀 수 있게 하려는 설계.
점(.) 이 붙은 상대 import 를 쓰는 게 결정적이다. 절대 import (from foo_nodes.hello ...) 는 다음 절 shadow 문제로 실패할 수 있다.
4. 재시작 + UI 확인
ComfyUI 서버 재시작. 브라우저에서 노드 검색창에 Hello 또는 foo 카테고리를 펼쳐 등록 여부 확인.
체크포인트: UI 노드 트리의 foo/ 카테고리에 Hello (Foo) 가 보이면 통과. 연결은 String 슬롯 하나 들어오고 나가는 형태.
안 뜨면 콘솔 로그를 위에서부터 읽어내려가며 ComfyUI-Foo 가 포함된 줄을 찾는다. import 에러가 있으면 traceback 이 보일 수 있고, 로그가 조용하다면 거의 확실히 트러블슈팅의 shadow 항목.
5. 노드가 여러 개로 늘어날 때
노드 한 개짜리 팩은 거의 없다. 보통 두 경로로 관리한다.
(a) 서브패키지에 파일 분리 + 진입점에서 모아 노출
foo_nodes/
├── __init__.py
├── _base.py ← 공통 유틸·믹스인
├── hello.py
└── image_util.py
__init__.py (팩 루트):
from .foo_nodes.hello import HelloNode
from .foo_nodes.image_util import ImageResizeNode, ImageFlipNode
NODE_CLASS_MAPPINGS = {
"HelloNode": HelloNode,
"ImageResizeNode": ImageResizeNode,
"ImageFlipNode": ImageFlipNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"HelloNode": "Hello (Foo)",
"ImageResizeNode": "Image Resize (Foo)",
"ImageFlipNode": "Image Flip (Foo)",
}(b) 서브패키지의 __init__.py 에서 자체 집계한 뒤 팩 루트가 병합
서브패키지가 자기 노드들을 자체적으로 수집하게 하면 팩 루트는 서브패키지별로 딕셔너리 두 개씩만 합친다. 노드 수십 개 넘어가면 이 쪽이 덜 지저분.
왜: 팩 루트의 __init__.py 는 ComfyUI 가 가장 먼저 읽는 파일이라 import 순서 꼬임·순환 참조가 여기서 가장 자주 터진다. import 대상이 한 곳에 모이게 해야 디버그가 쉽다.
6. 의존성 · pyproject.toml (선택)
외부 pip 패키지가 필요하면 팩 루트에 requirements.txt 를 둔다.
requests>=2.31
pillow>=10.0
ComfyUI-Manager 가 이 파일을 감지해 설치 UI 를 띄운다. 수동 설치는:
C:\path\to\ComfyUI\venv\Scripts\activate
pip install -r custom_nodes\ComfyUI-Foo\requirements.txtpyproject.toml 은 팩을 PyPI 등에 배포할 계획이 있을 때만. 로컬 custom_nodes 에서 돌리는 용도로는 불필요.
왜: ComfyUI venv 바깥에 설치하면 ComfyUI 가 못 찾는다. 항상 venv 활성화 후 설치.
트러블슈팅
증상 — 노드가 UI 에 안 뜬다 + 콘솔 로그가 조용하다 (Shadow collision)
custom_nodes/ComfyUI-Foo/ 재시작해도 검색 메뉴에 HelloNode 가 안 보이고, 콘솔엔 에러가 없거나 기껏해야 module has no attribute 류 애매한 한두 줄. print("HELLO") 를 __init__.py 에 박아도 안 찍히는 경우까지 있다.
원인: 서브패키지 이름이 ComfyUI 코어 모듈명과 겹침. 코어는 시작 시 레포 루트 nodes.py 를 sys.modules['nodes'] 에 등록해 둔다. 이후 커스텀 팩이 from nodes.foo_node import ... 식 절대 import 를 만나면 Python 이 이미 등록된 코어 nodes 를 집어간다. 코어 nodes 는 파일 하나짜리라 foo_node 서브모듈이 없어 ModuleNotFoundError 발생. ComfyUI 는 그 예외를 커스텀 노드 로딩 루프에서 조용히 삼키고 다음 팩으로 넘어간다.
해결 (세 가지, 위에서 아래로 권장 순):
(a) 서브패키지 이름 재명명 (권장)
git mv nodes foo_nodes그리고 내부 import 경로 전부 재작성 — from foo_nodes._base import ... 식으로. 내부 모듈끼리의 import 도 같은 접두어 통일. 팩 식별자를 접두어로 쓰면 충돌이 원천 차단된다.
(b) 상대 import 로 전환
# __init__.py
from .nodes.foo_node import FooNode # 안전
# from nodes.foo_node import FooNode # shadow 발생. 이 붙으면 sys.modules['nodes'] 를 우회하고 현재 패키지 내부에서 찾는다. 공개된 예제 코드가 nodes/ 를 쓰고 있어 이름을 못 바꿀 때 유용. 단 내부 모듈끼리 import 도 전부 상대로 통일.
# nodes/foo_node.py
from nodes._base import Base # shadow 여전히 발생
from ._base import Base # 안전(c) sys.path 에 팩 루트 끼우기 (비추)
# __init__.py
import os, sys
sys.path.insert(0, os.path.dirname(__file__))동작은 한다. 글로벌 sys.path 를 건드리는 부작용이 있어 다른 팩과 우연히 충돌할 수 있고 디버그가 어려워진다. 재명명·상대 import 둘 다 불가능할 때 마지막 수단.
재발 방지 · 확인 방법: 새 팩 만들 때 서브패키지 이름으로 ComfyUI 코어가 점유한 이름을 쓰지 않는다. 실전 피해야 할 이름 — nodes, comfy, execution, server, folder_paths, main. 팩 이름 접두어(foo_nodes, mypack_nodes) 가 안전하다. 그리고 __init__.py 최상단에 import 테스트 한 줄을 깔아두면 조용한 실패가 콘솔에 즉시 보인다.
try:
from .foo_nodes.hello import HelloNode
except Exception as e:
print(f"[ComfyUI-Foo] import failed: {type(e).__name__}: {e}")
raise증상 — 노드는 뜨는데 연결이 안 된다 / 빨간 점선
INPUT_TYPES 슬롯 타입을 바꿨거나 오타가 났을 때.
원인: ComfyUI 는 슬롯 타입 이름으로 호환성을 판정한다. IMAGE 대신 Image 처럼 대소문자가 다르거나, 존재하지 않는 타입 이름(TEXT — 실제론 STRING) 을 쓰면 UI 는 타입 불일치로 연결을 거부한다.
해결:
- 표준 타입 이름 확인 — 자주 쓰이는 건
IMAGE,LATENT,MASK,MODEL,CLIP,VAE,CONDITIONING,STRING,INT,FLOAT - 타입은 튜플 첫 요소.
("STRING",)에서 괄호·쉼표 누락 —("STRING")이면 문자열 자체가 타입이 된다
재발 방지 · 확인 방법: 처음 만들 때 기존 공식 노드를 참고해 타입 이름을 그대로 옮기는 게 안전. 타입 오타가 의심되면 서버 재시작 후 브라우저 새로고침도 같이.
증상 — NODE_CLASS_MAPPINGS 에 있는데 UI 에 안 뜸
팩 자체는 로드됐는데 특정 노드만 안 보임.
원인: 주로 CATEGORY 값이 비어있거나 문자열이 아닌 경우. 또는 NODE_CLASS_MAPPINGS 키 오타로 실제 클래스와 연결이 깨진 경우.
해결:
CATEGORY = "foo"가 반드시 문자열이고 비어있지 않음NODE_CLASS_MAPPINGS의 키가 UI 검색 문자열과 매치 — 검색창에 해당 키 직접 입력해 나오는지 확인- 코어
nodes.py의 기존 노드명과 키가 겹치면 한쪽이 숨겨짐. 접두어 붙여 충돌 회피
재발 방지 · 확인 방법: 팩 식별자를 노드 ID 접두어로 씀. HelloNode 보다 FooHelloNode 같이.
증상 — ModuleNotFoundError: No module named 'requests' (또는 기타 외부 패키지)
서버 시작 시 traceback.
원인: requirements.txt 의 의존성이 ComfyUI venv 에 설치 안 됨. 시스템 Python 에 설치해도 ComfyUI 는 자기 venv 만 본다.
해결:
cd C:\path\to\ComfyUI
venv\Scripts\activate
pip install -r custom_nodes\ComfyUI-Foo\requirements.txtComfyUI-Manager 설치돼 있으면 UI 에서 “Install Missing Custom Nodes” 로 일괄 처리 가능.
재발 방지 · 확인 방법: 팩 배포 전 빈 venv 에 requirements.txt 로 설치해보고 돌려봄.
관련
- ComfyUI 환경 셋업 편 — 이 가이드의 전제 환경
- ComfyUI 공식 커스텀 노드 가이드 — API 레퍼런스
- ComfyUI-Manager — 팩 설치·업데이트·의존성 자동화
- ComfyUI 코어
nodes.py— shadow 충돌 회피 위해 피해야 할 이름들의 원본