> ## Documentation Index
> Fetch the complete documentation index at: https://dripart-chore-sync-comfy-api-v2-spec-12fd5b4.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# V3 마이그레이션

> 기존 V1 노드를 새 V3 스키마로 마이그레이션하는 방법.

## 개요

ComfyUI V3 스키마는 노드를 정의하는 방식을 보다 체계적으로 개선했으며, 향후 노드 기능 확장은 오직 V3 스키마에만 추가됩니다. 이 가이드를 사용해 기존 V1 노드를 새 V3 스키마로 마이그레이션할 수 있습니다.

## 핵심 개념

V3 스키마는 새로운 버전화된 Comfy API에 유지되며, 이는 스키마의 향후 수정사항이 하위 호환성을 보장한다는 것을 의미합니다. `comfy_api.latest`는 여전히 개발 중인 최신 번호의 API를 가리키며, 최신 버전 바로 전 버전이 '안정적'이라고 볼 수 있습니다. 현재(첫 번째) API 버전은 `v0_0_2`이며, 이 버전에는 경고 없이 더 많은 변경이 이루어질 것입니다. 안정적인 버전으로 간주되면, `latest`가 가리키는 버전이 새롭게 `v0_0_3`으로 변경됩니다.

```python theme={null}
# 최신 ComfyUI API 사용
from comfy_api.latest import ComfyExtension, io, ui

# 특정 버전의 ComfyUI API 사용
from comfy_api.v0_0_2 import ComfyExtension, io, ui
```

### V1 vs V3 아키텍처

V3 스키마에서 가장 큰 변화는 다음과 같습니다:

* 입력과 출력이 사전 대신 객체로 정의됨.
* 실행 방식이 'execute'라는 이름으로 고정되고 클래스 메서드임.
* `def comfy_entrypoint()` 함수는 ComfyExtension 객체를 반환하며, 노출되는 노드를 NODE\_CLASS\_MAPPINGS/NODE\_DISPLAY\_NAME\_MAPPINGS 대신 정의함.
* 노드 객체는 '상태'를 노출하지 않음 - `def __init__(self)`는 노드의 함수에서 노출되는 내용에 아무런 영향을 미치지 않으며, 모든 함수는 클래스 메서드임. 또한 노드 클래스는 실행 전에 정제됨.

#### V1 (레거시)

```python theme={null}
class MyNode:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": {...}}

    RETURN_TYPES = ("IMAGE",)
    FUNCTION = "execute"
    CATEGORY = "my_category"

    def execute(self, ...):
        return (result,)

NODE_CLASS_MAPPINGS = {"MyNode": MyNode}
```

#### V3 (현대)

```python theme={null}
from comfy_api.latest import ComfyExtension, io

class MyNode(io.ComfyNode):
    @classmethod
    def define_schema(cls) -> io.Schema:
        return io.Schema(
            node_id="MyNode",
            display_name="My Node",
            category="my_category",
            inputs=[...],
            outputs=[...]
        )

    @classmethod
    def execute(cls, ...) -> io.NodeOutput:
        return io.NodeOutput(result)

class MyExtension(ComfyExtension):
    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return [MyNode]

async def comfy_entrypoint() -> ComfyExtension:
    return MyExtension()
```

## 마이그레이션 단계

대부분의 경우 V1에서 V3으로의 이동은 간단하며, 단순한 문법 변경에 불과합니다.

### 단계 1: 기본 클래스 변경

모든 V3 스키마 노드는 `ComfyNode`를 상속해야 합니다. 여러 계층의 상속도 괜찮으며, 체인의 맨 위에 `ComfyNode` 부모가 있으면 됩니다.

**V1:**

```python theme={null}
class Example:
    def __init__(self):
        pass
```

**V3:**

```python theme={null}
from comfy_api.latest import io

class Example(io.ComfyNode):
    # __init__ 필요 없음
```

### 단계 2: INPUT\_TYPES를 define\_schema로 변환

노드 ID, 표시 이름, 카테고리 등 코드 내 다양한 위치에서 할당되었던 노드 속성들은 이제 `Schema` 클래스를 통해 함께 관리됩니다.

`define_schema(cls)` 함수는 V1에서 INPUT\_TYPES(s)가 작동한 방식과 매우 유사하게 `Schema` 객체를 반환해야 합니다.

지원되는 핵심 입력/출력 유형은 `comfy_api/{version}`의 `_io.py`에 저장 및 문서화되어 있으며, 기본적으로 `io`로 네임스페이스가 지정됩니다. 이제 입력/출력이 사전이나 문자열 대신 클래스로 정의되므로, 사용자 정의 유형은 직접 클래스를 정의하거나 `io`의 `Custom` 헬퍼 함수를 사용하여 지원됩니다.

사용자 정의 유형에 대한 자세한 내용은 아래 별도의 섹션에서 다룹니다.

유형 클래스는 다음과 같은 속성을 가집니다:

* 입력에 대한 `class Input` (예: `Model.Input(...)`)
* 출력에 대한 `class Output` (예: `Model.Output(...)`). 모든 유형이 출력을 지원하지는 않습니다.
* 유형의 타입 힌트를 얻기 위한 `Type` (예: `Model.Type`). 일부 타입 힌트는 단순히 `any`이며, 이는 향후 업데이트될 수 있습니다. 이러한 타입 힌트는 강제되지 않으며 유용한 문서 역할을 합니다.

**V1:**

```python theme={null}
@classmethod
def INPUT_TYPES(s):
    return {
        "required": {
            "image": ("IMAGE",),
            "int_field": ("INT", {
                "default": 0,
                "min": 0,
                "max": 4096,
                "step": 64,
                "display": "number"
            }),
            "string_field": ("STRING", {
                "multiline": False,
                "default": "Hello"
            }),
            # V1 처리: 임의의 유형
            "custom_field": ("MY_CUSTOM_TYPE",),
        },
        "optional": {
            "mask": ("MASK",)
        }
    }
```

**V3:**

```python theme={null}
@classmethod
def define_schema(cls) -> io.Schema:
    return io.Schema(
        node_id="Example",
        display_name="Example Node",
        category="examples",
        description="Node description here",
        inputs=[
            io.Image.Input("image"),
            io.Int.Input("int_field",
                default=0,
                min=0,
                max=4096,
                step=64,
                display_mode=io.NumberDisplay.number
            ),
            io.String.Input("string_field",
                default="Hello",
                multiline=False
            ),
            # V3 처리: 임의의 유형
            io.Custom("my_custom_type").Input("custom_input"),
            io.Mask.Input("mask", optional=True)
        ],
        outputs=[
            io.Image.Output()
        ]
    )
```

### 단계 3: Execute 메서드 업데이트

V3의 모든 실행 함수는 `execute`라는 이름을 가지며 클래스 메서드입니다.

**V1:**

```python theme={null}
def test(self, image, string_field, int_field):
    # Process
    image = 1.0 - image
    return (image,)
```

**V3:**

```python theme={null}
@classmethod
def execute(cls, image, string_field, int_field) -> io.NodeOutput:
    # Process
    image = 1.0 - image

    # 선택적 UI 미리보기와 함께 반환
    return io.NodeOutput(image, ui=ui.PreviewImage(image, cls=cls))
```

### 단계 4: 노드 속성 변환

다음은 속성 이름의 몇 가지 예시입니다. 자세한 내용은 `comfy_api.latest._io`의 소스 코드를 참조하세요.

| V1 속성          | V3 스키마 필드              | 비고           |
| -------------- | ---------------------- | ------------ |
| `RETURN_TYPES` | 스키마의 `outputs`         | 출력 객체 목록     |
| `RETURN_NAMES` | 출력의 `display_name`     | 출력별 표시 이름    |
| `FUNCTION`     | 항상 `execute`           | 메서드 이름이 표준화됨 |
| `CATEGORY`     | 스키마의 `category`        | 문자열 값        |
| `OUTPUT_NODE`  | 스키마의 `is_output_node`  | 불리언 플래그      |
| `DEPRECATED`   | 스키마의 `is_deprecated`   | 불리언 플래그      |
| `EXPERIMENTAL` | 스키마의 `is_experimental` | 불리언 플래그      |

### 단계 5: 특수 메서드 처리

V1과 동일한 특수 메서드가 지원되지만, 더 명확하게 하기 위해 소문자화되거나 완전히 이름이 변경되었습니다. 사용법은 동일합니다.

#### 검증 (V1 → V3)

입력 검증 함수는 `validate_inputs`로 이름이 변경되었습니다.

**V1:**

```python theme={null}
@classmethod
def VALIDATE_INPUTS(s, **kwargs):
    # 검증 로직
    return True
```

**V3:**

```python theme={null}
@classmethod
def validate_inputs(cls, **kwargs) -> bool | str:
    # 유효하면 True, 아니면 오류 메시지 반환
    if error_condition:
        return "Error message"
    return True
```

<h4 id="lazy-evaluation-v1--v3">
  지연 평가 (V1 → V3)
</h4>

`check_lazy_status` 함수는 클래스 메서드이며, 그 외에는 동일하게 유지됩니다.

**V1:**

```python theme={null}
def check_lazy_status(self, image, string_field, ...):
    if condition:
        return ["string_field"]
    return []
```

**V3:**

```python theme={null}
@classmethod
def check_lazy_status(cls, image, string_field, ...):
    if condition:
        return ["string_field"]
    return []
```

#### 캐시 제어 (V1 → V3)

캐시 제어의 기능은 V1과 동일하게 유지되지만, 원래 이름은 작동 방식을 매우 오해하기 쉽게 만들었습니다.

V1의 `IS_CHANGED` 함수는 반환값이 노드가 마지막으로 실행되었을 때와 동일한 경우 노드 재실행을 트리거하지 않도록 신호를 보냅니다.

따라서 `IS_CHANGED` 함수는 `fingerprint_inputs`로 이름이 변경되었습니다. 개발자들이 저지르는 가장 흔한 실수 중 하나는 `True`를 반환하면 노드가 항상 재실행된다고 생각하는 것이었습니다. `True`가 항상 반환되면, 실제로는 노드를 한 번만 실행하고 캐시된 값을 재사용하는 반대 효과가 발생합니다.

이 함수의 사용 예시는 LoadImage 노드입니다. 이 노드는 선택된 파일의 해시를 반환하여, 파일이 변경되면 노드가 강제로 재실행되도록 합니다.

**V1:**

```python theme={null}
@classmethod
def IS_CHANGED(s, **kwargs):
    return "unique_value"
```

**V3:**

```python theme={null}
@classmethod
def fingerprint_inputs(cls, **kwargs):
    return "unique_value"
```

### 단계 6: 확장 프로그램 및 진입점 생성

노드 ID를 노드 클래스/표시 이름에 연결하는 사전을 정의하는 대신, 이제 `ComfyExtension` 클래스와 정의해야 할 `comfy_entrypoint` 함수가 있습니다.

향후 `get_node_list`를 통해 노드만 등록하는 것 이상의 기능을 등록하기 위해 ComfyExtension에 더 많은 함수가 추가될 수 있습니다.

`comfy_entrypoint`는 비동기이거나 아니어도 되지만, `get_node_list`는 반드시 비동기로 정의해야 합니다.

**V1:**

```python theme={null}
NODE_CLASS_MAPPINGS = {
    "Example": Example
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "Example": "Example Node"
}
```

**V3:**

```python theme={null}
from comfy_api.latest import ComfyExtension

class MyExtension(ComfyExtension):
    # 반드시 비동기로 선언해야 함
    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return [
            Example,
            # 여기에 더 많은 노드 추가
        ]

# 비동기 또는 비동기가 아닌 형태로 선언 가능, 둘 다 작동함
async def comfy_entrypoint() -> MyExtension:
    return MyExtension()
```

## 입력 유형 참조

이미 단계 2에서 설명했지만, V1 대비 V3의 몇 가지 유형 참조 비교를 제공합니다. 전체 유형 선언은 `comfy_api.latest._io`를 참조하세요.

### 기본 유형

| V1 유형       | V3 유형                | 예시                                                           |
| ----------- | -------------------- | ------------------------------------------------------------ |
| `"INT"`     | `io.Int.Input()`     | `io.Int.Input("count", default=1, min=0, max=100)`           |
| `"FLOAT"`   | `io.Float.Input()`   | `io.Float.Input("strength", default=1.0, min=0.0, max=10.0)` |
| `"STRING"`  | `io.String.Input()`  | `io.String.Input("text", multiline=True)`                    |
| `"BOOLEAN"` | `io.Boolean.Input()` | `io.Boolean.Input("enabled", default=True)`                  |

#### control\_after\_generate

Int 및 Combo 입력은 각 생성 후 값을 자동으로 변경하기 위한 제어 위젯을 추가하는 `control_after_generate` 파라미터를 지원합니다. V1에서는 이 값이 단순한 `bool`이었지만, V3에서는 `io.ControlAfterGenerate` 열거형을 사용하여 명시적으로 제어할 수 있습니다. `True`를 전달하는 것은 `io.ControlAfterGenerate.randomize`와 동일합니다.

| 값                                   | 동작                    |
| ----------------------------------- | --------------------- |
| `io.ControlAfterGenerate.fixed`     | 각 생성 후 값이 동일하게 유지됩니다. |
| `io.ControlAfterGenerate.increment` | 각 생성 후 값이 단계만큼 증가합니다. |
| `io.ControlAfterGenerate.decrement` | 각 생성 후 값이 단계만큼 감소합니다. |
| `io.ControlAfterGenerate.randomize` | 각 생성 후 값이 무작위화됩니다.    |

```python theme={null}
# 제어 위젯 활성화 (UI에서 사용자가 모드 선택)
io.Int.Input("seed", default=0, min=0, max=0xFFFFFFFFFFFFFFFF, control_after_generate=True)

# 특정 기본 모드 설정
io.Int.Input("seed", default=0, min=0, max=0xFFFFFFFFFFFFFFFF,
    control_after_generate=io.ControlAfterGenerate.randomize)
```

### ComfyUI 유형

| V1 유형            | V3 유형                     | 예시                                          |
| ---------------- | ------------------------- | ------------------------------------------- |
| `"IMAGE"`        | `io.Image.Input()`        | `io.Image.Input("image", tooltip="입력 이미지")` |
| `"MASK"`         | `io.Mask.Input()`         | `io.Mask.Input("mask", optional=True)`      |
| `"LATENT"`       | `io.Latent.Input()`       | `io.Latent.Input("latent")`                 |
| `"CONDITIONING"` | `io.Conditioning.Input()` | `io.Conditioning.Input("positive")`         |
| `"MODEL"`        | `io.Model.Input()`        | `io.Model.Input("model")`                   |
| `"VAE"`          | `io.VAE.Input()`          | `io.VAE.Input("vae")`                       |
| `"CLIP"`         | `io.CLIP.Input()`         | `io.CLIP.Input("clip")`                     |

### Combo (드롭다운/선택 목록)

V3의 Combo 유형은 명시적인 클래스 정의가 필요합니다.

**V1:**

```python theme={null}
"mode": (["option1", "option2", "option3"],)
```

**V3:**

```python theme={null}
io.Combo.Input("mode", options=["option1", "option2", "option3"])
```

## 스키마 참조

`Schema` 데이터클래스는 V3 노드의 모든 속성을 정의합니다. 사용 가능한 모든 필드에 대한 완전한 참조입니다:

| 필드                  | 유형             | 기본값     | 설명                                                        |
| ------------------- | -------------- | ------- | --------------------------------------------------------- |
| `node_id`           | `str`          | *필수*    | 전역적으로 고유한 노드 ID. 커스텀 노드는 충돌을 피하기 위해 접두사/접미사를 추가해야 합니다.    |
| `display_name`      | `str`          | `None`  | UI에 표시되는 표시 이름. 설정하지 않으면 `node_id`로 대체됩니다.                |
| `category`          | `str`          | `"sd"`  | "노드 추가" 메뉴의 카테고리 (예: `"image/transform"`).                |
| `description`       | `str`          | `""`    | 노드 위에 마우스를 올리면 표시되는 툴팁.                                   |
| `inputs`            | `list[Input]`  | `[]`    | 입력 정의 목록.                                                 |
| `outputs`           | `list[Output]` | `[]`    | 출력 정의 목록.                                                 |
| `hidden`            | `list[Hidden]` | `[]`    | 요청할 히든 입력 목록 (참조: [히든 입력](#hidden-inputs)).               |
| `search_aliases`    | `list[str]`    | `[]`    | 검색을 위한 대체 이름. 동의어나 이름 변경 후의 이전 이름에 유용합니다.                 |
| `is_output_node`    | `bool`         | `False` | 노드를 출력 노드로 표시하여, 해당 노드와 그 의존성이 실행되도록 합니다.                 |
| `is_input_list`     | `bool`         | `False` | True이면 모든 입력이 전달된 항목 수에 관계없이 `list[type]`이 됩니다.           |
| `is_deprecated`     | `bool`         | `False` | 노드를 지원 중단됨으로 표시하여, 사용자에게 대안을 찾도록 알립니다.                    |
| `is_experimental`   | `bool`         | `False` | 노드를 실험적으로 표시하여, 사용자에게 변경될 수 있음을 경고합니다.                    |
| `is_dev_only`       | `bool`         | `False` | 개발자 모드가 활성화되지 않은 경우 검색/메뉴에서 노드를 숨깁니다.                     |
| `is_api_node`       | `bool`         | `False` | Comfy API 서비스를 위한 API 노드로 표시합니다.                          |
| `not_idempotent`    | `bool`         | `False` | True이면, 그래프의 다른 동일한 노드에서 생성된 캐시 출력을 재사용하지 않고 항상 다시 실행합니다. |
| `enable_expand`     | `bool`         | `False` | `NodeOutput`이 노드 확장을 위한 `expand` 속성을 포함할 수 있도록 허용합니다.     |
| `accept_all_inputs` | `bool`         | `False` | True이면, 스키마에 정의되지 않은 입력이라도 프롬프트의 모든 입력이 kwargs로 전달됩니다.    |

### 공통 입력 파라미터

모든 입력 유형은 다음과 같은 기본 파라미터를 공유합니다:

| 파라미터           | 유형     | 기본값     | 설명                                                          |
| -------------- | ------ | ------- | ----------------------------------------------------------- |
| `id`           | `str`  | *필수*    | 입력의 고유 식별자, `execute`에서 kwarg 이름으로 사용됩니다.                   |
| `display_name` | `str`  | `None`  | UI에 표시되는 레이블. 기본값은 `id`입니다.                                 |
| `optional`     | `bool` | `False` | 입력이 선택 사항인지 여부.                                             |
| `tooltip`      | `str`  | `None`  | 마우스 오버 툴팁 텍스트.                                              |
| `lazy`         | `bool` | `None`  | 지연 평가를 위한 입력을 표시합니다 (참조: [지연 평가](#lazy-evaluation-v1--v3)). |
| `raw_link`     | `bool` | `None`  | True이면, 해석된 값 대신 원시 링크 정보를 전달합니다.                           |
| `advanced`     | `bool` | `None`  | True이면, 입력은 UI의 "고급" 토글 뒤에 숨겨집니다.                           |

위젯 입력(Int, Float, String, Boolean, Combo)은 추가로 다음을 지원합니다:

| 파라미터          | 유형     | 기본값    | 설명                                    |
| ------------- | ------ | ------ | ------------------------------------- |
| `default`     | 다양함    | `None` | 위젯의 기본값.                              |
| `socketless`  | `bool` | `None` | True이면 입력 소켓을 숨깁니다 (위젯만, 들어오는 연결 없음). |
| `force_input` | `bool` | `None` | True이면, 대신 위젯이 소켓 입력으로 표시되도록 강제합니다.   |

## 고급 기능

<h3 id="hidden-inputs">
  히든 입력
</h3>

히든 입력은 프롬프트 메타데이터, 노드 ID 및 기타 내부 값과 같은 실행 컨텍스트에 대한 접근을 제공합니다. 이들은 UI에 표시되지 않습니다.

V1에서는 히든 입력이 `INPUT_TYPES`의 `"hidden"` 키로 선언되었습니다. V3에서는 스키마의 `hidden` 파라미터를 통해 선언되며, 해당 값은 `cls.hidden`을 통해 접근됩니다.

**V1:**

```python theme={null}
@classmethod
def INPUT_TYPES(s):
    return {
        "required": {...},
        "hidden": {
            "unique_id": "UNIQUE_ID",
            "prompt": "PROMPT",
            "extra_pnginfo": "EXTRA_PNGINFO",
        }
    }

def execute(self, unique_id, prompt, extra_pnginfo, ...):
    # 히든 값은 일반 인자로 전달됨
    print(unique_id)
```

**V3:**

```python theme={null}
@classmethod
def define_schema(cls) -> io.Schema:
    return io.Schema(
        node_id="MyNode",
        inputs=[...],
        hidden=[io.Hidden.unique_id, io.Hidden.prompt, io.Hidden.extra_pnginfo],
    )

@classmethod
def execute(cls, ...) -> io.NodeOutput:
    # 히든 값은 cls.hidden을 통해 접근
    print(cls.hidden.unique_id)
    print(cls.hidden.prompt)
    print(cls.hidden.extra_pnginfo)
```

사용 가능한 히든 값:

| 히든 열거형                           | 설명                                               |
| -------------------------------- | ------------------------------------------------ |
| `io.Hidden.unique_id`            | 클라이언트 측 ID와 일치하는 노드의 고유 식별자.                     |
| `io.Hidden.prompt`               | 클라이언트가 보낸 전체 프롬프트.                               |
| `io.Hidden.extra_pnginfo`        | 저장된 `.png` 파일의 메타데이터에 복사되는 사전.                   |
| `io.Hidden.dynprompt`            | 실행 중에 변경될 수 있는 `DynamicPrompt` 인스턴스.             |
| `io.Hidden.auth_token_comfy_org` | 프론트엔드에서 ComfyOrg 계정에 로그인하여 획득한 토큰.               |
| `io.Hidden.api_key_comfy_org`    | ComfyOrg에서 생성한 API 키로, 프론트엔드 로그인을 건너뛸 수 있게 해줍니다. |

<Note>
  일부 히든 값은 스키마 플래그에 따라 자동으로 추가됩니다. 출력 노드(`is_output_node=True`)는 자동으로 `prompt`와 `extra_pnginfo`를 받습니다. API 노드(`is_api_node=True`)는 자동으로 인증 토큰을 받습니다.
</Note>

### UI 헬퍼

V3는 `ui` 모듈에서 미리보기 및 파일 저장과 같은 일반적인 패턴을 처리하기 위한 내장 UI 헬퍼를 제공합니다. `ui` 파라미터를 통해 `io.NodeOutput`에 전달하세요.

#### 미리보기 헬퍼

미리보기 헬퍼는 임시 파일을 저장하고 노드 내 표시를 위한 UI 데이터를 반환합니다.

```python theme={null}
from comfy_api.latest import ui

# 노드에서 이미지 미리보기
return io.NodeOutput(images, ui=ui.PreviewImage(images, cls=cls))

# 마스크 미리보기 (표시를 위해 자동으로 3채널로 변환)
return io.NodeOutput(mask, ui=ui.PreviewMask(mask, cls=cls))

# 오디오 미리듣기
return io.NodeOutput(audio, ui=ui.PreviewAudio(audio, cls=cls))

# 텍스트 미리보기
return io.NodeOutput(ui=ui.PreviewText("Some text value"))

# 3D 모델 미리보기
return io.NodeOutput(ui=ui.PreviewUI3D(model_file, camera_info))
```

#### 저장 헬퍼

저장 헬퍼는 적절한 메타데이터를 포함하여 파일을 출력 디렉터리에 저장하는 메서드를 제공합니다. 일반적으로 출력 노드에서 사용합니다.

```python theme={null}
from comfy_api.latest import ui, io

# Save images and return UI data (most common pattern)
return io.NodeOutput(
    ui=ui.ImageSaveHelper.get_save_images_ui(
        images=images,
        filename_prefix=filename_prefix,
        cls=cls,  # passes hidden prompt/extra_pnginfo for metadata
    )
)

# Save animated PNG
return io.NodeOutput(
    ui=ui.ImageSaveHelper.get_save_animated_png_ui(
        images=images,
        filename_prefix=filename_prefix,
        cls=cls,
        fps=6.0,
        compress_level=4,
    )
)

# Save animated WebP
return io.NodeOutput(
    ui=ui.ImageSaveHelper.get_save_animated_webp_ui(
        images=images,
        filename_prefix=filename_prefix,
        cls=cls,
        fps=6.0,
        lossless=True,
        quality=80,
        method=4,
    )
)

# Save audio (supports flac, mp3, opus)
return io.NodeOutput(
    ui=ui.AudioSaveHelper.get_save_audio_ui(
        audio=audio,
        filename_prefix=filename_prefix,
        cls=cls,
        format="flac",
    )
)
```

<Tip>
  저장 또는 미리보기 헬퍼에 `cls=cls`를 전달하면 저장 파일에 워크플로 메타데이터(prompt, extra\_pnginfo)를 자동으로 포함할 수 있습니다. 스키마의 hidden 목록에 `io.Hidden.prompt`와 `io.Hidden.extra_pnginfo`를 포함하거나, 두 항목을 자동으로 추가하는 `is_output_node=True`를 설정하세요.
</Tip>

#### 원시 UI 딕셔너리 반환

헬퍼가 없는 UI 데이터를 반환해야 한다면 딕셔너리를 직접 전달할 수 있습니다.

```python theme={null}
return io.NodeOutput(ui={"images": results})
```

### 출력 노드

파일 저장처럼 부작용을 만드는 노드에 사용합니다. V1과 마찬가지로 노드를 출력 노드로 표시하면 노드의 컨텍스트 창에 `run` 재생 버튼이 표시되어 그래프를 부분적으로 실행할 수 있습니다.

```python theme={null}
@classmethod
def define_schema(cls) -> io.Schema:
    return io.Schema(
        node_id="SaveNode",
        inputs=[...],
        outputs=[],  # Does not need to be empty.
        is_output_node=True  # Mark as output node
    )
```

### 사용자 정의 유형

클래스를 정의하거나 `Custom` 헬퍼 함수를 사용하여 사용자 정의 입력/출력 유형을 만들 수 있습니다.

```python theme={null}
from comfy_api.latest import io

# Method 1: Using decorator with class
@io.comfytype(io_type="MY_CUSTOM_TYPE")
class MyCustomType:
    Type = torch.Tensor  # Python type hint

    class Input(io.Input):
        def __init__(self, id: str, **kwargs):
            super().__init__(id, **kwargs)

    class Output(io.Output):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)

# Method 2: Using Custom helper
# The helper can be used directly without saving to a variable first for convenience as well
MyCustomType = io.Custom("MY_CUSTOM_TYPE")
```

### MultiType 입력

`MultiType` allows an input to accept more than one type. This is useful when a node can operate on different data types through the same input slot.

첫 번째 인수(`id`)가 문자열이 아니라 `Input` 클래스의 인스턴스이면 해당 입력의 재정의된 값으로 위젯을 만듭니다. 그렇지 않으면 소켓 전용 입력입니다.

```python theme={null}
# Socket-only multi-type input (no widget)
io.MultiType.Input("input", types=[io.Image, io.Mask])

# Multi-type input with a widget fallback (String widget shown when nothing is connected)
io.MultiType.Input(
    io.String.Input("model_file", default="", multiline=False),
    types=[io.File3DGLB, io.File3DGLTF, io.File3DOBJ],
    tooltip="3D model file or path string",
)
```

### MatchType (일반 유형 매칭)

`MatchType` creates type-linked inputs and outputs. When a user connects a specific type to a MatchType input, all other inputs and outputs sharing the same template automatically constrain to that type. This is how nodes like Switch and Create List work with any type.

```python theme={null}
@classmethod
def define_schema(cls):
    # Create a template - all inputs/outputs sharing the same template will match types
    template = io.MatchType.Template("switch")
    return io.Schema(
        node_id="SwitchNode",
        display_name="Switch",
        category="logic",
        inputs=[
            io.Boolean.Input("switch"),
            io.MatchType.Input("on_false", template=template, lazy=True),
            io.MatchType.Input("on_true", template=template, lazy=True),
        ],
        outputs=[
            io.MatchType.Output(template=template, display_name="output"),
        ],
    )
```

You can also restrict which types are allowed:

```python theme={null}
# Only allow Image, Mask, or Latent types
template = io.MatchType.Template("input_type", allowed_types=[io.Image, io.Mask, io.Latent])
```

### 동적 입력

V3에서는 사용자 상호작용에 따라 사용 가능한 입력이 변경되는 동적 입력 유형을 도입합니다. V1에는 이에 해당하는 기능이 없습니다.

#### Autogrow

`Autogrow` creates a variable number of inputs that automatically grow as the user connects more. There are two template types:

**TemplatePrefix** generates inputs with a numbered prefix (e.g. `image0`, `image1`, `image2`...):

```python theme={null}
@classmethod
def define_schema(cls):
    autogrow_template = io.Autogrow.TemplatePrefix(
        input=io.Image.Input("image"),  # template for each input
        prefix="image",                  # prefix for generated input names
        min=2,                           # minimum number of inputs shown
        max=50,                          # maximum number of inputs allowed
    )
    return io.Schema(
        node_id="BatchImagesNode",
        display_name="Batch Images",
        category="image",
        inputs=[io.Autogrow.Input("images", template=autogrow_template)],
        outputs=[io.Image.Output()],
    )

@classmethod
def execute(cls, images: io.Autogrow.Type) -> io.NodeOutput:
    # 'images' is a dict mapping input names to their values
    image_list = list(images.values())
    return io.NodeOutput(batch(image_list))
```

**TemplateNames** generates inputs with specific names:

```python theme={null}
template = io.Autogrow.TemplateNames(
    input=io.Float.Input("float"),
    names=["x", "y", "z"],  # explicit names for each input
    min=1,                    # minimum number of inputs shown
)
```

Autogrow can be combined with MatchType to create lists of type-matched inputs:

```python theme={null}
@classmethod
def define_schema(cls):
    template_matchtype = io.MatchType.Template("type")
    template_autogrow = io.Autogrow.TemplatePrefix(
        input=io.MatchType.Input("input", template=template_matchtype),
        prefix="input",
    )
    return io.Schema(
        node_id="CreateList",
        display_name="Create List",
        category="logic",
        is_input_list=True,
        inputs=[io.Autogrow.Input("inputs", template=template_autogrow)],
        outputs=[
            io.MatchType.Output(
                template=template_matchtype,
                is_output_list=True,
                display_name="list",
            ),
        ],
    )
```

#### DynamicCombo

`DynamicCombo` creates a dropdown that shows/hides different inputs depending on the selected option. This is useful for nodes where different modes require different parameters.

```python theme={null}
@classmethod
def define_schema(cls):
    return io.Schema(
        node_id="ResizeNode",
        display_name="Resize",
        category="transform",
        inputs=[
            io.Image.Input("image"),
            io.DynamicCombo.Input("resize_type", options=[
                io.DynamicCombo.Option("scale by dimensions", [
                    io.Int.Input("width", default=512, min=0, max=8192),
                    io.Int.Input("height", default=512, min=0, max=8192),
                ]),
                io.DynamicCombo.Option("scale by multiplier", [
                    io.Float.Input("multiplier", default=1.0, min=0.01, max=8.0),
                ]),
                io.DynamicCombo.Option("scale to megapixels", [
                    io.Float.Input("megapixels", default=1.0, min=0.01, max=16.0),
                ]),
            ]),
        ],
        outputs=[io.Image.Output()],
    )

@classmethod
def execute(cls, image, resize_type: dict) -> io.NodeOutput:
    # resize_type is a dict containing the selected option key and its inputs
    selected = resize_type["resize_type"]
    if selected == "scale by dimensions":
        width = resize_type["width"]
        height = resize_type["height"]
        # ...
    elif selected == "scale by multiplier":
        multiplier = resize_type["multiplier"]
        # ...
```

DynamicCombo 옵션은 중첩할 수도 있습니다.

```python theme={null}
io.DynamicCombo.Input("combo", options=[
    io.DynamicCombo.Option("option1", [io.String.Input("string")]),
    io.DynamicCombo.Option("option2", [
        io.DynamicCombo.Input("subcombo", options=[
            io.DynamicCombo.Option("sub_opt1", [io.Float.Input("x"), io.Float.Input("y")]),
            io.DynamicCombo.Option("sub_opt2", [io.Mask.Input("mask", optional=True)]),
        ])
    ]),
])
```

### 비동기 Execute

V3는 비동기 `execute` 메서드를 지원합니다. I/O 작업, API 호출 또는 기타 비동기 작업을 수행하는 노드에 유용합니다. `execute`를 `async`로 선언하기만 하면 됩니다.

```python theme={null}
@classmethod
async def execute(cls, prompt, **kwargs) -> io.NodeOutput:
    result = await some_async_operation(prompt)
    return io.NodeOutput(result)
```

### ComfyAPI

`ComfyAPI` 클래스는 진행률 보고 및 노드 교체 등록과 같은 ComfyUI 런타임 서비스에 접근할 수 있게 합니다. 가져온 후 인스턴스를 생성하세요.

```python theme={null}
from comfy_api.latest import ComfyAPI

api = ComfyAPI()
```

#### 진행률 보고

노드의 `execute` 메서드 안에서 실행 진행률을 보고합니다. 진행률 표시줄은 ComfyUI 인터페이스에 표시됩니다. 이는 V1에서 `comfy.utils.PROGRESS_BAR_HOOK`를 사용하던 방식을 대체합니다.

```python theme={null}
from comfy_api.latest import ComfyAPI

api = ComfyAPI()

@classmethod
async def execute(cls, images, **kwargs) -> io.NodeOutput:
    total = len(images)
    for i, image in enumerate(images):
        process(image)
        await api.execution.set_progress(
            value=i + 1,
            max_value=total,
            preview_image=image,  # optional: show preview during progress
        )
    return io.NodeOutput(result)
```

<Note>
  `set_progress` can accept a PIL Image, an `ImageInput` tensor, or `None` for the `preview_image` parameter. When called from within `execute`, the `node_id` is automatically determined from the executing context.
</Note>

#### 노드 교체

노드 교체를 사용하면 이전 노드나 더 이상 사용되지 않는 노드를 새 노드에 매핑하여 기존 워크플로를 자동으로 업그레이드할 수 있습니다. 확장의 `on_load` 메서드에서 `ComfyAPI`를 사용해 교체를 등록하세요.

```python theme={null}
from comfy_api.latest import ComfyAPI, ComfyExtension, io

api = ComfyAPI()

class MyExtension(ComfyExtension):
    async def on_load(self) -> None:
        await api.node_replacement.register(io.NodeReplace(
            new_node_id="MyNewNode",
            old_node_id="MyOldNode",
            old_widget_ids=["param1", "param2"],  # ordered widget IDs for positional mapping
            input_mapping=[
                {"new_id": "image", "old_id": "input_image"},       # rename input
                {"new_id": "method", "set_value": "lanczos"},       # set a fixed value
            ],
            output_mapping=[
                {"new_idx": 0, "old_idx": 0},  # map output by index
            ],
        ))

    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return [MyNewNode]
```

The `old_widget_ids` parameter is important: workflow JSON stores widget values by position index, not by name. This list maps those positional indexes to input IDs so the replacement system can correctly identify widget values during migration.

For nodes using dynamic inputs (like Autogrow), use dotted paths in the mapping:

```python theme={null}
input_mapping=[
    {"new_id": "images.image0", "old_id": "image1"},
    {"new_id": "images.image1", "old_id": "image2"},
]
```

### 확장 수명 주기

`ComfyExtension` 클래스는 `get_node_list` 외에도 다음 수명 주기 훅을 지원합니다.

```python theme={null}
from comfy_api.latest import ComfyExtension, io

class MyExtension(ComfyExtension):
    async def on_load(self) -> None:
        """Called when the extension is loaded.
        Use for one-time initialization: registering node replacements,
        setting up global resources, etc.
        """
        pass

    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        """Return the list of node classes this extension provides."""
        return [MyNode]

async def comfy_entrypoint() -> MyExtension:
    return MyExtension()
```

### NodeOutput

`NodeOutput` 클래스는 `execute`의 표준 반환 값입니다. 다음과 같은 여러 패턴을 지원합니다.

```python theme={null}
# Return a single output value
return io.NodeOutput(image)

# Return multiple output values (order matches outputs list in schema)
return io.NodeOutput(width, height, batch_size)

# Return only UI data (no output values)
return io.NodeOutput(ui=ui.PreviewImage(images, cls=cls))

# Return both output values and UI data
return io.NodeOutput(image, ui=ui.PreviewImage(image, cls=cls))

# Return None/empty (for nodes with no outputs)
return io.NodeOutput()
```

## 전체 예시

다음은 여러 노드를 포함하는 V3 확장 파일의 전체 예시입니다.

```python theme={null}
from comfy_api.latest import ComfyExtension, io, ui

class InvertImage(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="MyPack_InvertImage",  # prefixed to avoid clashes
            display_name="Invert Image",
            category="my_pack/image",
            description="Inverts the colors of an image.",
            inputs=[
                io.Image.Input("image"),
            ],
            outputs=[
                io.Image.Output(display_name="inverted"),
            ],
        )

    @classmethod
    def execute(cls, image) -> io.NodeOutput:
        inverted = 1.0 - image
        return io.NodeOutput(inverted, ui=ui.PreviewImage(inverted, cls=cls))


class SaveImage(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="MyPack_SaveImage",
            display_name="Save Image",
            category="my_pack/image",
            is_output_node=True,
            inputs=[
                io.Image.Input("images"),
                io.String.Input("filename_prefix", default="ComfyUI"),
            ],
            outputs=[],
        )

    @classmethod
    def execute(cls, images, filename_prefix) -> io.NodeOutput:
        return io.NodeOutput(
            ui=ui.ImageSaveHelper.get_save_images_ui(
                images=images,
                filename_prefix=filename_prefix,
                cls=cls,
            )
        )


class MyPackExtension(ComfyExtension):
    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return [InvertImage, SaveImage]

async def comfy_entrypoint() -> MyPackExtension:
    return MyPackExtension()
```
