목록으로

프로그래밍 · Python

Python 완전정복 시리즈 #34: Gradio 완벽 이해하기

BeanCon
Python Gradio로 머신러닝과 생성형 AI 모델 웹 데모를 만드는 방법을 설명하는 대표 이미지

Python 완전정복 시리즈 34편입니다. Gradio로 머신러닝과 생성형 AI 모델을 빠르게 웹 데모로 만드는 방법을 실습 중심으로 정리했습니다. Interface, Blocks, ChatInterface, 입력·출력 컴포넌트, 전처리와 후처리, 여러 입력과 출력, Examples, Row와 Column, Tab과 Accordion, 이벤트 리스너, then·success·failure, 동적 컴포넌트 변경, 사용자 알림과 오류 처리, State, Queue, Progress, Generator 스트리밍, 오디오·영상 스트리밍, 챗봇, 멀티모달 챗봇, 파일 업로드와 다운로드, Dataframe·JSON·Gallery, 모델 자원 로딩, 캐시, API 사용, gradio_client, 임시 공유 링크, 인증, Hugging Face Spaces와 Docker 배포, 손글씨 숫자 분류 프로젝트, 성능과 보안 주의사항까지 다룹니다.

목차

메타 설명
머신러닝과 생성형 AI 모델을 브라우저에서 실행하려면 복잡한 웹 개발이 필요할까요? Gradio의 Interface, Blocks, 컴포넌트, 이벤트, State, Queue, Progress, 스트리밍, ChatInterface, API, Hugging Face Spaces 배포까지 실습 중심으로 알아봅니다.

지난 시간에는 Streamlit을 이용해 Python 데이터와 머신러닝 모델을 웹 애플리케이션으로 만들었습니다.

버튼, 입력창, 차트, 데이터프레임을 배치하고 다음과 같은 웹앱까지 완성했습니다.

CSV 파일 업로드

→ 데이터 분석

→ 머신러닝 예측

→ 결과 차트 표시

→ 예측 파일 다운로드

그런데 우리가 만든 것이 데이터 대시보드가 아니라 AI 모델 데모라면 어떨까요?

이미지를 업로드하면
어떤 물체인지 분류한다.

마이크에 말하면
음성을 문자로 변환한다.

문장을 입력하면
감정을 분석한다.

질문을 입력하면
AI가 답변을 생성한다.

그림을 올리면
객체의 위치를 표시한다.

이런 모델을 다른 사람에게 보여주기 위해 일반적인 웹사이트를 만든다면 화면, 서버, API를 모두 연결해야 합니다.

프런트엔드 화면 작성

→ 파일 업로드 구현

→ 서버 API 작성

→ 모델 연결

→ 결과 직렬화

→ 로딩 화면 구현

→ 오류 처리

→ 배포

AI 모델 하나를 보여주려다 웹 개발 프로젝트 하나가 새로 태어날 수 있습니다.

개발자:
“이미지 분류 모델을
잠깐 보여드리겠습니다.”

일주일 후:
“현재 로그인 화면을 개발 중입니다.”

이때 유용한 도구가 Gradio입니다.

Gradio는 Python 함수의 입력과 출력을 웹 컴포넌트에 연결해 머신러닝 모델, API, 일반 Python 함수를 빠르게 웹 데모로 만들 수 있는 오픈소스 Python 패키지입니다. JavaScript나 복잡한 프런트엔드 코드를 작성하지 않고도 실행 가능한 인터페이스를 구성할 수 있습니다.

다음 Python 함수가 있다고 가정해 보겠습니다.

def greet(name: str) -> str:
    return f"{name}님, 반갑습니다!"

Gradio를 연결하면 다음과 같습니다.

import gradio as gr


def greet(name: str) -> str:
    return f"{name}님, 반갑습니다!"


demo = gr.Interface(
    fn=greet,
    inputs=gr.Textbox(
        label="이름"
    ),
    outputs=gr.Textbox(
        label="인사 결과"
    )
)

demo.launch()

실행하면 브라우저에 입력창과 실행 버튼, 결과창이 나타납니다.

Python 함수

+ 입력 컴포넌트

+ 출력 컴포넌트

= 웹 데모

AI 모델에게 정장을 입히고 웹 무대에 올리는 시간이 찾아왔습니다. 🤖🎤

1. Gradio란?

Gradio는 Python 함수, 머신러닝 모델 또는 API 주위에 웹 기반 사용자 인터페이스를 빠르게 구성할 수 있도록 도와주는 오픈소스 Python 패키지입니다.

Gradio가 담당하는 부분은 다음과 같습니다.

입력창 생성

버튼 생성

파일 업로드

카메라·마이크 입력

함수 호출

결과 화면 표시

작업 대기열

진행 상태 표시

출력 스트리밍

API 엔드포인트

공유 링크

개발자는 주로 다음 두 가지에 집중할 수 있습니다.

어떤 입력을 받을 것인가?

Python 함수가 무엇을 반환할 것인가?

2026년 7월 30일 기준 공식 GitHub의 최신 릴리스는 Gradio 6.21.0이며, 2026년 7월 29일 공개되었습니다. Gradio 6은 현재 유지보수되는 주요 버전 계열로 안내되고 있습니다.

버전은 계속 변경될 수 있으므로 실제 프로젝트에서는 다음 명령어로 설치 버전을 확인합니다.

python -m pip show gradio

Python에서도 확인할 수 있습니다.

import gradio as gr


print(
    gr.__version__
)

2. Gradio 6의 현재 구조

Gradio에는 웹 데모를 만드는 여러 수준의 API가 있습니다.

Interface

Python 함수 하나를 빠르게 웹 화면으로 감쌉니다.

함수 하나

입력 컴포넌트

출력 컴포넌트

→ 빠른 모델 데모

Blocks

컴포넌트의 배치, 이벤트, 데이터 흐름을 직접 구성합니다.

복수의 입력

여러 버튼

탭

상태 관리

연결된 이벤트

→ 사용자 정의 웹앱

ChatInterface

챗봇 UI를 빠르게 구성합니다.

사용자 메시지

대화 기록

AI 응답 함수

→ 챗봇 완성

공식 문서에서 `Interface`는 함수 주위에 웹 기반 GUI를 만드는 고수준 클래스이며, `Blocks`는 레이아웃과 이벤트, 데이터 흐름을 더 세밀하게 제어할 수 있는 저수준 API로 설명됩니다. `ChatInterface`는 챗봇 UI를 위한 고수준 추상화입니다.

Interface:
빠른 조립 키트

Blocks:
부품을 직접 배치하는 작업대

ChatInterface:
챗봇 전용 완제품 조립 키트

3. Streamlit과 Gradio의 차이

Streamlit과 Gradio는 모두 Python으로 웹 인터페이스를 만들 수 있습니다.

하지만 출발점이 조금 다릅니다.

Streamlit의 중심

페이지를 위에서 아래로 작성

데이터 분석

대시보드

필터

지표

차트

업무용 데이터 앱

Gradio의 중심

Python 함수

머신러닝 모델

입력과 출력

이벤트

생성형 AI

빠른 웹 데모

비교

구분StreamlitGradio
중심 개념Python 스크립트함수와 이벤트
빠른 모델 데모편리함매우 편리함
데이터 대시보드매우 강함구성 가능
챗봇 UI구성 가능ChatInterface 제공
오디오·영상 AI구성 가능전문 컴포넌트 풍부
이벤트 연결재실행 구조이벤트 리스너 구조
상태 관리Session StateState·BrowserState
API 제공별도 구성이 필요할 수 있음앱 이벤트가 API로 노출 가능
공유 링크배포 필요임시 공유 링크 지원
대표 활용데이터 앱AI 모델 쇼케이스

둘 중 하나가 무조건 더 좋은 것은 아닙니다.

데이터 분석 대시보드
→ Streamlit이 자연스러운 경우가 많음

이미지·음성·LLM 모델 데모
→ Gradio가 자연스러운 경우가 많음

두 도구는 라이벌이라기보다 서로 다른 종류의 무대를 담당합니다.

4. Gradio를 사용하기 좋은 상황

Gradio는 다음 상황에서 특히 유용합니다.

머신러닝 모델 시연

표 데이터 분류

이미지 분류

객체 검출

음성 인식

번역

감정 분석

생성형 AI 데모

LLM 챗봇

이미지 생성

음성 생성

텍스트 요약

문서 질의응답

AI 에이전트

연구 결과 공유

동료에게 모델 테스트 화면 제공

논문 모델 데모

고객 POC

내부 모델 비교

하이퍼파라미터 실험

빠른 프로토타입

백엔드 함수는 완성됨

하지만 웹 화면은 아직 없음

→ Gradio로 즉시 연결

다음과 같은 서비스에는 추가적인 아키텍처가 필요할 수 있습니다.

복잡한 결제 시스템

대규모 회원 서비스

정교한 사용자 권한

매우 복잡한 프런트엔드

수천 개 화면의 업무 시스템

Gradio는 대형 백화점을 짓는 건설 장비라기보다 모델을 빠르게 공개하는 팝업스토어에 가깝습니다.

5. Gradio 설치하기

Gradio 6의 공식 빠른 시작 문서는 Python 3.10 이상을 요구하며 가상환경에서 `pip`로 설치할 것을 권장합니다.

Windows

python -m venv venv
venv\Scripts\activate

python -m pip install --upgrade pip
python -m pip install --upgrade gradio

macOS 또는 Linux

python3 -m venv venv
source venv/bin/activate

python -m pip install --upgrade pip
python -m pip install --upgrade gradio

머신러닝 실습 패키지 포함

python -m pip install \
    gradio \
    numpy \
    pillow \
    pandas \
    scikit-learn

Windows 명령 프롬프트에서는 한 줄로 실행할 수 있습니다.

python -m pip install gradio numpy pillow pandas scikit-learn

설치 확인

python -c "import gradio; print(gradio.__version__)"

6. 첫 번째 Gradio 앱

`app.py` 파일을 만듭니다.

import gradio as gr


def greet(
    name: str,
    intensity: int
) -> str:
    cleaned_name = name.strip()

    if not cleaned_name:
        return "이름을 입력해 주세요."

    exclamation_marks = (
        "!"
        * intensity
    )

    return (
        f"{cleaned_name}님, "
        f"반갑습니다"
        f"{exclamation_marks}"
    )


demo = gr.Interface(
    fn=greet,
    inputs=[
        gr.Textbox(
            label="이름",
            placeholder="이름을 입력하세요"
        ),
        gr.Slider(
            minimum=1,
            maximum=10,
            value=3,
            step=1,
            label="반가움 강도"
        )
    ],
    outputs=gr.Textbox(
        label="인사 결과"
    ),
    title="🎉 AI보다 먼저 만드는 인사 앱",
    description=(
        "이름과 반가움 강도를 입력하면 "
        "Python 함수가 인사를 생성합니다."
    ),
    api_name="greet"
)


if __name__ == "__main__":
    demo.launch()

`Interface`에는 세 가지 핵심 정보가 필요합니다.

fn
→ 실행할 Python 함수

inputs
→ 함수에 전달할 입력 컴포넌트

outputs
→ 함수 반환값을 표시할 출력 컴포넌트

이 구조는 공식 `Interface` API의 핵심 설계입니다.

7. Gradio 앱 실행하기

터미널에서 다음 명령을 실행합니다.

python app.py

일반적으로 로컬 서버가 실행되고 브라우저에서 앱에 접속할 수 있습니다.

코드를 수정할 때 자동 재실행 기능을 사용하려면 다음 명령을 활용할 수 있습니다.

gradio app.py

Gradio 공식 개발 가이드는 코드 변경을 빠르게 반영하는 reload 모드를 제공합니다.

외부 접속 허용

서버에서 실행한다면 다음처럼 설정할 수 있습니다.

demo.launch(
    server_name="0.0.0.0",
    server_port=7860
)

클라우드 환경에서는 추가로 다음 항목을 확인해야 합니다.

운영체제 방화벽

클라우드 보안 그룹

Docker 포트 연결

Nginx 리버스 프록시

HTTPS 인증서

8. Python 함수와 컴포넌트 연결

다음 함수는 숫자 두 개를 더합니다.

def add_numbers(
    number_a: float,
    number_b: float
) -> float:
    return number_a + number_b

Gradio에 연결합니다.

import gradio as gr


def add_numbers(
    number_a: float,
    number_b: float
) -> float:
    return number_a + number_b


demo = gr.Interface(
    fn=add_numbers,
    inputs=[
        gr.Number(
            label="첫 번째 숫자"
        ),
        gr.Number(
            label="두 번째 숫자"
        )
    ],
    outputs=gr.Number(
        label="계산 결과"
    )
)


demo.launch()

입력 컴포넌트의 순서는 함수 인수의 순서와 연결됩니다.

첫 번째 Number
→ number_a

두 번째 Number
→ number_b

함수가 값을 하나 반환하면 출력 컴포넌트도 하나를 지정합니다.

9. Interface 완벽 이해하기

`Interface`는 모델이나 함수 하나를 빠르게 시연할 때 적합합니다.

demo = gr.Interface(
    fn=predict,
    inputs=input_component,
    outputs=output_component
)

title

title="고양이·강아지 분류기"

description

description=(
    "사진을 업로드하면 "
    "AI가 고양이와 강아지를 분류합니다."
)

examples

examples=[
    ["examples/cat.jpg"],
    ["examples/dog.jpg"]
]

api_name

api_name="classify_image"

이벤트에 API 이름을 지정하면 Gradio Client나 외부 프로그램이 해당 기능을 호출할 수 있습니다.

allow_flagging 대신 현재 플래그 설정 확인

Gradio 주요 버전이 바뀌면서 일부 매개변수 이름과 위치가 변경될 수 있습니다.

Gradio 6에서는 앱 전체에 적용되는 테마와 CSS 같은 설정이 `Blocks` 생성자에서 `launch()`로 이동하는 등 호환성 변경이 있었습니다. 이전 버전의 예제를 그대로 복사하기보다 현재 버전의 문서를 확인해야 합니다.

10. 입력·출력 컴포넌트

Gradio 컴포넌트는 입력, 출력 또는 두 역할 모두로 사용될 수 있습니다.

문자열

gr.Textbox()

숫자

gr.Number()

슬라이더

gr.Slider(
    minimum=0,
    maximum=100,
    value=50
)

체크박스

gr.Checkbox(
    label="고급 분석 사용"
)

선택 메뉴

gr.Dropdown(
    choices=[
        "빠른 모델",
        "정확한 모델"
    ],
    value="빠른 모델",
    label="모델 선택"
)

`Dropdown`은 하나 또는 여러 항목을 선택하는 입력·출력 컴포넌트입니다.

라디오 버튼

gr.Radio(
    choices=[
        "긍정",
        "부정",
        "중립"
    ],
    label="감정"
)

이미지

gr.Image(
    type="pil"
)

파일

gr.File()

gr.Dataframe()

JSON

gr.JSON()

분류 결과

gr.Label(
    num_top_classes=5
)

11. 컴포넌트의 전처리와 후처리

Gradio는 브라우저의 입력을 Python 함수가 사용하기 좋은 형태로 변환합니다.

이를 전처리라고 합니다.

브라우저에서 이미지 업로드

→ Gradio 전처리

→ PIL 이미지 또는 NumPy 배열

→ Python 함수

Python 함수가 반환한 값은 브라우저에 표시할 형태로 다시 변환됩니다.

이를 후처리라고 합니다.

Python 함수가 PIL 이미지 반환

→ Gradio 후처리

→ 브라우저 이미지 표시

Gradio 컴포넌트는 입력 데이터를 Python 객체로 변환하는 `preprocess`, 함수 결과를 화면이 사용할 형태로 바꾸는 `postprocess` 단계를 갖습니다.

Image의 type

gr.Image(
    type="pil"
)

함수에 PIL 이미지 객체가 전달됩니다.

gr.Image(
    type="numpy"
)

함수에 NumPy 배열이 전달됩니다.

gr.Image(
    type="filepath"
)

임시 파일 경로가 전달됩니다.

공식 Image 컴포넌트 문서도 설정에 따라 NumPy 배열, PIL 이미지 또는 파일 경로를 함수에 전달한다고 설명합니다.

12. 여러 입력과 출력 연결하기

다음 함수는 텍스트를 분석해 글자 수와 단어 수를 반환합니다.

def analyze_text(
    text: str
) -> tuple[int, int, str]:
    cleaned_text = text.strip()

    character_count = len(
        cleaned_text
    )

    word_count = len(
        cleaned_text.split()
    )

    summary = (
        f"총 {character_count}글자이며 "
        f"{word_count}개 단어입니다."
    )

    return (
        character_count,
        word_count,
        summary
    )

Gradio 앱:

import gradio as gr


def analyze_text(
    text: str
) -> tuple[int, int, str]:
    cleaned_text = text.strip()

    character_count = len(
        cleaned_text
    )

    word_count = len(
        cleaned_text.split()
    )

    summary = (
        f"총 {character_count}글자이며 "
        f"{word_count}개 단어입니다."
    )

    return (
        character_count,
        word_count,
        summary
    )


demo = gr.Interface(
    fn=analyze_text,
    inputs=gr.Textbox(
        lines=8,
        label="분석할 문장"
    ),
    outputs=[
        gr.Number(
            label="글자 수"
        ),
        gr.Number(
            label="단어 수"
        ),
        gr.Textbox(
            label="분석 결과"
        )
    ]
)


demo.launch()

반환 순서와 출력 컴포넌트 순서가 일치해야 합니다.

character_count
→ 첫 번째 Number

word_count
→ 두 번째 Number

summary
→ Textbox

13. Examples 추가하기

사용자가 입력 형식을 쉽게 이해하도록 예제를 제공할 수 있습니다.

demo = gr.Interface(
    fn=analyze_text,
    inputs=gr.Textbox(
        label="문장"
    ),
    outputs=[
        gr.Number(),
        gr.Number(),
        gr.Textbox()
    ],
    examples=[
        [
            "Python은 배우기 즐거운 언어입니다."
        ],
        [
            "Gradio로 AI 데모를 만들어 봅시다."
        ]
    ]
)

예제를 클릭하면 입력값이 채워지고 설정에 따라 함수가 실행됩니다.

빈 입력창:
“무엇을 넣어야 하지?”

Examples:
“이런 값을 넣으면 됩니다.”

예제는 설명서보다 빠른 사용법 안내판이 될 수 있습니다.

14. Interface의 한계

Interface는 빠르지만 복잡한 화면에는 한계가 있습니다.

버튼을 여러 개 만들고 싶다.

입력과 출력을 원하는 위치에 배치하고 싶다.

탭을 만들고 싶다.

여러 함수를 연결하고 싶다.

특정 입력이 바뀔 때만 실행하고 싶다.

결과를 State에 저장하고 싶다.

이럴 때 `Blocks`를 사용합니다.

Interface:
한 함수의 빠른 데모

Blocks:
여러 컴포넌트와 이벤트로 만든 웹앱

15. Blocks란?

`Blocks`는 Gradio 컴포넌트의 레이아웃, 이벤트, 데이터 흐름을 세밀하게 구성하는 API입니다.

공식 문서에 따르면 Blocks는 Interface보다 화면 배치, 함수 실행 이벤트, 컴포넌트 사이의 데이터 흐름을 더 유연하게 제어할 수 있습니다.

기본 구조:

import gradio as gr


def greet(
    name: str
) -> str:
    return f"{name}님, 반갑습니다!"


with gr.Blocks() as demo:
    gr.Markdown(
        "# 🎤 Gradio 인사 무대"
    )

    name_input = gr.Textbox(
        label="이름"
    )

    greet_button = gr.Button(
        "인사하기",
        variant="primary"
    )

    result_output = gr.Textbox(
        label="결과"
    )

    greet_button.click(
        fn=greet,
        inputs=name_input,
        outputs=result_output
    )


demo.launch()

컴포넌트 생성

name_input = gr.Textbox()

이벤트 연결

greet_button.click(
    fn=greet,
    inputs=name_input,
    outputs=result_output
)

이벤트 리스너는 다음 관계를 정의합니다.

어떤 일이 발생하면

→ 어떤 함수를 실행하고

→ 무엇을 입력으로 전달하며

→ 어느 컴포넌트에 결과를 표시할 것인가

16. Row와 Column으로 화면 배치하기

기본적으로 컴포넌트는 위에서 아래로 배치됩니다.

가로 배치

with gr.Blocks() as demo:
    with gr.Row():
        name_input = gr.Textbox(
            label="이름"
        )

        age_input = gr.Number(
            label="나이"
        )

세로 영역

with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column():
            name_input = gr.Textbox()
            age_input = gr.Number()

        with gr.Column():
            result_output = gr.Textbox()

scale

with gr.Row():
    with gr.Column(
        scale=2
    ):
        main_content = gr.Textbox()

    with gr.Column(
        scale=1
    ):
        settings = gr.Dropdown(
            choices=[
                "빠름",
                "정확함"
            ]
        )

개념적인 비율:

메인 영역:
2

설정 영역:
1

화면의 약 2:1 비율로 배치됩니다.

17. Tab과 Accordion 활용하기

Tab

with gr.Blocks() as demo:
    with gr.Tab(
        "텍스트 분석"
    ):
        text_input = gr.Textbox()

    with gr.Tab(
        "이미지 분석"
    ):
        image_input = gr.Image()

    with gr.Tab(
        "모델 정보"
    ):
        gr.Markdown(
            "모델 설명"
        )

Accordion

with gr.Accordion(
    "고급 설정",
    open=False
):
    temperature = gr.Slider(
        minimum=0.0,
        maximum=2.0,
        value=0.7,
        label="Temperature"
    )

    max_tokens = gr.Slider(
        minimum=16,
        maximum=2048,
        value=512,
        step=16,
        label="최대 토큰"
    )

탭은 서로 다른 기능을 구분하고 Accordion은 자주 사용하지 않는 설정을 접어 둘 때 유용합니다.

모든 옵션을 화면에 펼침

→ 조종석 폭발

고급 옵션을 Accordion에 보관

→ 평온 회복

18. 이벤트 리스너 이해하기

Blocks에서 컴포넌트는 사용자 행동에 반응합니다.

버튼 클릭

텍스트 입력

값 변경

파일 업로드

Enter 입력

페이지 로딩

대표적인 이벤트 연결:

button.click(
    fn=process,
    inputs=input_component,
    outputs=output_component
)

이벤트 리스너의 기본 구성은 다음과 같습니다.

이벤트 발생 컴포넌트

이벤트 종류

실행 함수

입력 컴포넌트

출력 컴포넌트

19. click·change·input·submit 이벤트

click

버튼을 클릭할 때 실행합니다.

run_button.click(
    fn=analyze,
    inputs=text_input,
    outputs=result_output
)

submit

Textbox에서 Enter를 누를 때 실행합니다.

text_input.submit(
    fn=analyze,
    inputs=text_input,
    outputs=result_output
)

change

컴포넌트 값이 변경되었을 때 실행합니다.

사용자 입력뿐 아니라 함수가 컴포넌트 값을 변경해도 발생할 수 있습니다.

model_dropdown.change(
    fn=show_model_description,
    inputs=model_dropdown,
    outputs=model_description
)

input

사용자가 직접 값을 입력하거나 조작했을 때 실행합니다.

temperature_slider.input(
    fn=show_temperature,
    inputs=temperature_slider,
    outputs=temperature_text
)

실시간으로 입력할 때마다 무거운 모델을 실행하면 요청이 지나치게 많아질 수 있습니다.

사용자가 한 글자 입력

→ 대형 LLM 실행

한 글자 추가

→ 대형 LLM 다시 실행

GPU:
“문장을 다 쓰고 불러주세요.”

20. 이벤트 연결과 then·success·failure

이벤트가 끝난 뒤 다른 이벤트를 연속 실행할 수 있습니다.

first_event = run_button.click(
    fn=preprocess,
    inputs=input_data,
    outputs=processed_data
)

first_event.then(
    fn=predict,
    inputs=processed_data,
    outputs=result_output
)

then

앞 이벤트의 성공 여부와 관계없이 후속 이벤트를 실행합니다.

success

앞 이벤트가 성공한 경우에만 실행합니다.

first_event.success(
    fn=show_success_message,
    outputs=status_output
)

failure

앞 이벤트가 실패한 경우에만 실행합니다.

first_event.failure(
    fn=restore_previous_state,
    outputs=status_output
)

Gradio 공식 이벤트 가이드는 `.then()`, `.success()`, `.failure()`를 이용해 연속 실행과 성공·실패 분기를 구성할 수 있다고 설명합니다.

21. 컴포넌트 속성 동적으로 변경하기

함수에서 새 컴포넌트 설정을 반환해 화면 속성을 바꿀 수 있습니다.

def update_output_mode(
    mode: str
):
    if mode == "자세히":
        return gr.Textbox(
            label="상세 결과",
            lines=10,
            visible=True
        )

    return gr.Textbox(
        label="간단 결과",
        lines=2,
        visible=True
    )

연결:

mode_dropdown.change(
    fn=update_output_mode,
    inputs=mode_dropdown,
    outputs=result_output
)

간단한 속성 업데이트에는 `gr.update()`를 사용하는 이전 예제도 많이 검색되지만, 주요 버전에 따라 권장 방식이 달라질 수 있습니다.

현재 버전의 컴포넌트 반환 방식을 확인하는 것이 좋습니다.

22. 사용자 알림과 오류 처리

Gradio 함수 안에서 사용자에게 알림을 표시할 수 있습니다.

정보

gr.Info(
    "분석을 시작합니다."
)

경고

gr.Warning(
    "입력 문장이 너무 짧습니다."
)

오류

raise gr.Error(
    "이미지를 먼저 업로드해 주세요."
)

`gr.Error`는 예외로 발생시켜 실행을 중단하며, `gr.Info`와 `gr.Warning`은 알림을 표시하고 실행을 계속할 수 있습니다.

예:

def analyze_image(
    image
):
    if image is None:
        raise gr.Error(
            "이미지를 업로드해 주세요."
        )

    gr.Info(
        "이미지 분석을 시작합니다."
    )

    return run_model(
        image
    )

23. State가 필요한 이유

다음과 같은 값을 여러 이벤트에서 유지해야 할 수 있습니다.

대화 기록

예측 기록

장바구니

선택한 설정

업로드된 문서 목록

게임 점수

사용자별 모델 상태

일반 전역 변수에 저장하면 모든 사용자가 같은 값을 공유할 수 있습니다.

prediction_history = []

사용자 A의 예측이 사용자 B에게 보일 수 있습니다.

사용자 A:
고양이 사진 분석

사용자 B:
“제 기록에 왜 고양이가 있죠?”

사용자별 값을 관리하려면 `gr.State`를 사용할 수 있습니다.

24. 전역·세션·브라우저 상태

Gradio는 상태를 크게 세 종류로 구분합니다.

전역 상태

앱이 실행되는 동안 모든 사용자가 공유합니다.

visitor_count = 0

서버 전체에서 공유할 모델 객체나 공용 통계에 사용할 수 있습니다.

동시 접근 시 스레드 안전성을 고려해야 합니다.

세션 상태

현재 사용자의 페이지 세션에서만 유지합니다.

history_state = gr.State(
    []
)

이벤트 입력과 출력에 포함합니다.

def add_history(
    new_item: str,
    history: list[str]
):
    updated_history = (
        history
        + [
            new_item
        ]
    )

    return (
        updated_history,
        updated_history
    )

연결:

add_button.click(
    fn=add_history,
    inputs=[
        item_input,
        history_state
    ],
    outputs=[
        history_state,
        history_output
    ]
)

브라우저 상태

브라우저 저장소에 값을 유지할 수 있습니다.

user_settings = gr.BrowserState({
    "theme": "light",
    "language": "ko"
})

브라우저 상태는 페이지 새로고침이나 브라우저 종료 후에도 유지할 사용자 설정에 활용할 수 있습니다.

민감한 인증정보를 브라우저 상태에 무심코 저장해서는 안 됩니다.

25. Queue와 동시 실행 제어

머신러닝 추론은 시간이 오래 걸리거나 GPU를 사용할 수 있습니다.

사용자 여러 명이 동시에 요청하면 다음 문제가 발생할 수 있습니다.

GPU 메모리 부족

서버 과부하

응답 순서 혼란

모델 동시 실행 충돌

사용자가 진행 상태를 알 수 없음

Gradio는 이벤트 요청을 관리하는 Queue 기능을 제공합니다. Queue를 사용하면 대기 중인 작업을 순서대로 처리하고 동시 실행 제한을 설정할 수 있습니다.

demo.queue(
    default_concurrency_limit=2,
    max_size=50
)

실행:

demo.launch()

이벤트별 동시 실행 제한

predict_button.click(
    fn=predict,
    inputs=image_input,
    outputs=result_output,
    concurrency_limit=1
)

GPU 모델을 한 번에 하나씩만 실행하도록 제한할 수 있습니다.

사용자 1:
모델 실행 중

사용자 2:
대기열 1번

사용자 3:
대기열 2번

모델도 번호표가 필요합니다.

26. Progress 표시하기

오래 걸리는 함수에는 진행률을 표시할 수 있습니다.

함수 매개변수에 `gr.Progress()` 기본값을 추가합니다.

import time
import gradio as gr


def long_process(
    step_count: int,
    progress=gr.Progress()
) -> str:
    for index in progress.tqdm(
        range(step_count),
        desc="AI가 열심히 계산 중"
    ):
        time.sleep(
            0.1
        )

    return "작업 완료"

Gradio 공식 가이드에 따르면 `gr.Progress` 객체를 함수 매개변수로 주입하고 0부터 1 사이 값이나 `tqdm()` 방식으로 진행 상황을 갱신할 수 있습니다.

직접 비율을 지정할 수도 있습니다.

def process_files(
    files,
    progress=gr.Progress()
):
    total_count = len(
        files
    )

    for index, file in enumerate(
        files
    ):
        progress(
            (
                index + 1
            ) / total_count,
            desc=(
                f"{index + 1}/"
                f"{total_count} 처리 중"
            )
        )

        analyze_file(
            file
        )

    return "완료"

27. Generator와 출력 스트리밍

일반 함수는 최종 결과를 한 번 반환합니다.

def generate_text(
    prompt: str
) -> str:
    return (
        "최종 생성 문장"
    )

Generator는 중간 결과를 여러 번 전달합니다.

import time


def stream_text(
    prompt: str
):
    response = (
        f"입력한 문장은 "
        f"'{prompt}'입니다."
    )

    partial_text = ""

    for character in response:
        partial_text += character

        time.sleep(
            0.04
        )

        yield partial_text

Gradio에 연결하면 글자가 조금씩 출력됩니다.

demo = gr.Interface(
    fn=stream_text,
    inputs=gr.Textbox(),
    outputs=gr.Textbox()
)

demo.queue()
demo.launch()

Gradio는 함수가 `return` 대신 여러 번 `yield`하는 Generator일 때 순차적으로 출력을 갱신할 수 있습니다.

return:
완성품 한 번 배송

yield:
제작 과정을 조금씩 생중계

LLM의 토큰 스트리밍과 이미지 생성 중간 결과 표시에 활용할 수 있습니다.

28. 오디오·영상 스트리밍

Gradio는 Generator에서 오디오나 영상 조각을 순차적으로 전달하는 스트리밍 출력을 지원합니다.

출력 컴포넌트에 `streaming=True`를 설정하고 함수가 미디어 조각을 `yield`하도록 구성합니다.

오디오 출력

audio_output = gr.Audio(
    streaming=True,
    autoplay=True
)

영상 출력

video_output = gr.Video(
    streaming=True,
    autoplay=True
)

실시간 입력도 지원됩니다.

마이크 음성 조각

→ 음성 인식 모델

→ 부분 인식 결과

웹캠 프레임

→ 객체 검출 모델

→ 박스가 그려진 영상

Gradio 공식 스트리밍 입력 가이드는 카메라 이미지와 마이크 오디오 조각을 이벤트 함수에 전달하는 구조를 제공합니다.

실시간 처리는 일반 파일 업로드보다 복잡합니다.

처리 속도

네트워크 지연

프레임 누락

시간 제한

동시 사용자

GPU 처리량

처음에는 단일 파일 방식으로 기능을 확인한 뒤 스트리밍으로 확장하는 편이 좋습니다.

29. ChatInterface로 챗봇 만들기

`ChatInterface`는 챗봇 UI를 빠르게 구성합니다.

함수는 일반적으로 두 값을 받습니다.

message
→ 현재 사용자 메시지

history
→ 이전 대화 기록

기본 예:

import gradio as gr


def echo_chat(
    message: str,
    history: list[dict]
) -> str:
    return (
        f"방금 입력하신 메시지는 "
        f"'{message}'입니다."
    )


demo = gr.ChatInterface(
    fn=echo_chat,
    title="💬 메아리 챗봇",
    description=(
        "아직 AI는 아니지만 "
        "당신의 말을 매우 성실하게 반복합니다."
    ),
    examples=[
        "안녕하세요",
        "Gradio란 무엇인가요?",
        "Python 함수를 설명해 주세요"
    ],
    api_name="chat"
)


demo.launch()

현재 ChatInterface 함수의 `history`에는 OpenAI 스타일의 역할·내용 딕셔너리가 전달되며, 함수는 문자열, 메시지 딕셔너리, 파일이나 Gradio 컴포넌트 등을 반환할 수 있습니다.

30. 스트리밍 챗봇 만들기

import time
import gradio as gr


def stream_chat(
    message: str,
    history: list[dict]
):
    response = (
        "Gradio는 Python 함수에 "
        "웹 인터페이스를 연결하는 "
        "도구입니다."
    )

    partial_response = ""

    for character in response:
        partial_response += character

        time.sleep(
            0.03
        )

        yield partial_response


demo = gr.ChatInterface(
    fn=stream_chat,
    title="🤖 스트리밍 AI 데모",
    save_history=True,
    api_name="stream_chat"
)


demo.queue()
demo.launch()

`ChatInterface`는 Generator가 반환하는 부분 문자열을 이용해 스트리밍 응답을 표시할 수 있습니다. 또한 `save_history=True`를 설정하면 브라우저에 대화 기록을 보관하고 이전 대화를 선택할 수 있습니다.

실제 LLM 연결 위치

def chat(
    message,
    history
):
    response = llm.generate(
        message=message,
        history=history
    )

    return response

Gradio는 화면과 이벤트를 담당합니다.

모델 호출과 프롬프트, RAG, 도구 실행은 별도의 Python 로직에서 처리합니다.

31. 멀티모달 챗봇

텍스트뿐 아니라 이미지나 파일도 받는 챗봇을 만들 수 있습니다.

import gradio as gr


def multimodal_chat(
    message: dict,
    history: list[dict]
) -> str:
    text = message.get(
        "text",
        ""
    )

    files = message.get(
        "files",
        []
    )

    return (
        f"텍스트: {text}\n\n"
        f"첨부 파일: {len(files)}개"
    )


demo = gr.ChatInterface(
    fn=multimodal_chat,
    multimodal=True,
    title="📎 멀티모달 챗봇"
)


demo.launch()

`multimodal=True`일 때 첫 번째 함수 인수는 `text`와 `files`를 포함한 딕셔너리로 전달됩니다.

활용 예:

사진 + 질문
→ 이미지 질의응답

PDF + 질문
→ 문서 분석

음성 + 지시
→ 음성 비서

코드 파일 + 질문
→ 코드 리뷰

32. 이미지·오디오·비디오 컴포넌트

Image

gr.Image(
    sources=[
        "upload",
        "webcam",
        "clipboard"
    ],
    type="pil",
    image_mode="RGB",
    label="이미지 입력"
)

`Image`는 이미지 업로드 입력 또는 이미지 출력으로 사용할 수 있습니다.

Audio

gr.Audio(
    sources=[
        "upload",
        "microphone"
    ],
    type="filepath",
    label="음성 입력"
)

오디오를 NumPy 배열과 샘플링 주파수 형태로 받으려면 다음과 같이 설정할 수 있습니다.

gr.Audio(
    type="numpy"
)

Video

gr.Video(
    sources=[
        "upload",
        "webcam"
    ],
    label="영상 입력"
)

브라우저 재생을 위해서는 지원되는 컨테이너와 코덱 조합이 필요하며, Gradio가 가능한 경우 재생 가능한 형식으로 변환을 시도할 수 있습니다.

ImageEditor

gr.ImageEditor(
    type="pil",
    label="이미지 편집"
)

ImageEditor는 브러시, 자르기, 레이어 등 간단한 편집 기능을 제공할 수 있습니다.

33. 파일 업로드와 다운로드

파일 입력

file_input = gr.File(
    file_types=[
        ".pdf",
        ".txt",
        ".csv"
    ],
    file_count="single",
    label="분석할 파일"
)

`File` 컴포넌트는 하나 또는 여러 개의 일반 파일을 업로드하는 입력이나 파일 다운로드 출력으로 사용할 수 있습니다.

파일 처리 함수

from pathlib import Path


def inspect_file(
    file_path: str
) -> dict:
    path = Path(
        file_path
    )

    return {
        "file_name": path.name,
        "extension": path.suffix,
        "size_bytes": path.stat().st_size
    }

파일 출력

def create_result_file(
    text: str
) -> str:
    output_path = Path(
        "result.txt"
    )

    output_path.write_text(
        text,
        encoding="utf-8"
    )

    return str(
        output_path
    )

출력 컴포넌트:

gr.File(
    label="결과 다운로드"
)

서버 파일을 사용자에게 노출할 때는 허용 경로와 파일 접근 보안을 확인해야 합니다. Gradio는 임의의 서버 파일이 공개되지 않도록 파일 접근 검사를 수행하며 필요할 때 `allowed_paths`를 명시할 수 있습니다.

34. Dataframe·JSON·Gallery

Dataframe

gr.Dataframe(
    headers=[
        "이름",
        "점수",
        "상태"
    ],
    datatype=[
        "str",
        "number",
        "str"
    ],
    interactive=True
)

Dataframe은 표 데이터를 입력받거나 출력하는 스프레드시트형 컴포넌트입니다.

JSON

gr.JSON(
    label="API 결과"
)

Python 딕셔너리나 리스트를 구조화해 표시할 때 편리합니다.

gr.Gallery(
    label="생성 이미지",
    columns=3,
    rows=2
)

Gallery는 이미지와 영상을 격자로 표시하며 캡션을 함께 제공할 수 있습니다.

AnnotatedImage

객체 검출 박스나 세그멘테이션 마스크를 이미지 위에 표시할 수 있습니다.

gr.AnnotatedImage(
    label="객체 검출 결과"
)

AnnotatedImage는 기본 이미지와 사각형 또는 마스크 주석을 함께 표시합니다.

35. 모델과 무거운 자원 로딩하기

모델을 이벤트 함수 안에서 매번 불러오면 매우 느립니다.

좋지 않은 코드

def predict(
    image
):
    model = load_large_model()

    return model.predict(
        image
    )

요청할 때마다 모델을 로딩합니다.

사용자 요청

→ 모델 로딩

→ 예측

다음 사용자 요청

→ 모델 다시 로딩

앱 시작 시 한 번 로딩

model = load_large_model()


def predict(
    image
):
    return model.predict(
        image
    )

함수 캐시

from functools import lru_cache


@lru_cache(
    maxsize=1
)
def get_model():
    return load_large_model()


def predict(
    image
):
    model = get_model()

    return model.predict(
        image
    )

주의

여러 사용자가 같은 모델 객체를 공유할 수 있습니다.

모델이 추론 중 내부 상태를 수정한다면 스레드 안전성을 검토해야 합니다.

PyTorch 모델은 추론 전에 다음과 같이 설정합니다.

model.eval()

그리고 기울기 계산을 비활성화합니다.

with torch.inference_mode():
    output = model(
        input_tensor
    )

36. Gradio 캐시 사용하기

Gradio 6은 함수 결과를 저장하는 캐시 기능을 제공합니다.

자동 캐시

import gradio as gr


@gr.cache
def classify_image(
    image
):
    return model.predict(
        image
    )

입력 내용이 같으면 이전 결과를 재사용할 수 있습니다.

사용자별 캐시

@gr.cache(
    per_session=True,
    max_size=128
)
def analyze_document(
    document
):
    return run_analysis(
        document
    )

캐시가 적합한 작업

이미지 분류

음성 전사

임베딩 계산

문서 구조 추출

결정적인 전처리

캐시가 주의되는 작업

무작위 이미지 생성

Temperature가 있는 LLM 생성

현재 시간이 포함된 결과

외부 데이터가 계속 바뀌는 함수

Gradio는 자동 `@gr.cache`와 직접 제어하는 `gr.Cache()`를 제공하며, 입력 내용 해시, 크기 제한, 메모리 제한, 사용자 세션 분리 등을 설정할 수 있습니다.

민감한 사용자 데이터를 캐시한다면 사용자별 분리와 보관 정책을 반드시 검토해야 합니다.

37. Gradio 앱을 API로 사용하기

Gradio 앱의 이벤트는 웹 UI뿐 아니라 API로 호출할 수 있습니다.

이벤트에 `api_name`을 지정합니다.

predict_button.click(
    fn=predict,
    inputs=image_input,
    outputs=result_output,
    api_name="predict"
)

또는 Interface에 지정합니다.

demo = gr.Interface(
    fn=predict,
    inputs=gr.Image(),
    outputs=gr.Label(),
    api_name="predict"
)

Gradio 앱의 API 페이지에서 입력과 출력 구조, Python Client 예제를 확인할 수 있습니다.

Gradio 공식 가이드는 앱 이벤트를 HTTP 요청, Python Client 또는 JavaScript Client로 호출하는 방법을 제공합니다.

Gradio 화면:
사람이 사용

Gradio API:
다른 프로그램이 사용

웹 데모를 만들었는데 API도 함께 생기는 작은 보너스 상자입니다.

38. gradio_client 활용하기

`gradio_client`를 이용하면 다른 Python 프로그램에서 Gradio 앱을 호출할 수 있습니다.

설치

최근 Gradio를 설치했다면 의존성으로 포함될 수 있습니다.

별도로 설치하려면 다음 명령을 사용합니다.

python -m pip install --upgrade gradio_client

로컬 앱 호출

from gradio_client import Client


client = Client(
    "http://127.0.0.1:7860"
)

result = client.predict(
    "홍길동",
    3,
    api_name="/greet"
)

print(
    result
)

파일 입력

from gradio_client import (
    Client,
    handle_file
)


client = Client(
    "사용할 Gradio 앱 주소"
)

result = client.predict(
    handle_file(
        "sample.jpg"
    ),
    api_name="/predict"
)

`handle_file()`은 파일을 서버에 업로드하고 Gradio 컴포넌트가 처리할 수 있도록 준비합니다.

비동기 작업

job = client.submit(
    "분석할 문장",
    api_name="/analyze"
)

result = job.result()

`submit()`은 Job 객체를 반환하며 나중에 결과를 가져올 수 있습니다.

39. 임시 공유 링크

로컬 앱을 잠시 외부에 공유할 수 있습니다.

demo.launch(
    share=True
)

공개된 임시 링크를 다른 사람에게 전달하면 로컬에서 실행 중인 앱에 접속할 수 있습니다.

Gradio의 공유 링크는 로컬 서버로 연결되는 임시 접근 경로이며, 장기간 운영하는 영구 배포 대신 데모나 테스트에 적합합니다.

주의사항

컴퓨터를 종료하면 앱도 종료

Python 프로세스가 멈추면 접속 불가

임시 URL

로컬 파일 접근 주의

민감한 데이터 입력 금지

서버 자원은 내 컴퓨터 사용

임시 링크는 모델 시연을 위한 팝업 부스입니다.

회사 핵심 서비스를 팝업 부스에서 운영하면 안 됩니다.

40. 비밀번호 인증

간단한 사용자명과 비밀번호 인증을 설정할 수 있습니다.

demo.launch(
    auth=(
        "demo_user",
        "strong_password"
    )
)

여러 계정:

demo.launch(
    auth=[
        (
            "user1",
            "password1"
        ),
        (
            "user2",
            "password2"
        )
    ]
)

환경변수 사용:

import os


username = os.environ[
    "GRADIO_USERNAME"
]

password = os.environ[
    "GRADIO_PASSWORD"
]


demo.launch(
    auth=(
        username,
        password
    )
)

Gradio 내장 인증은 간단한 접근 제어에 적합하지만 다중 인증, 로그인 실패 잠금, 세밀한 권한 관리 같은 강력한 보안 기능을 완전히 제공하지는 않습니다. 엄격한 보안이 필요한 서비스는 별도의 인증 시스템을 검토해야 합니다.

41. Hugging Face Spaces 배포

Gradio 앱을 영구 주소로 공개하려면 Hugging Face Spaces를 활용할 수 있습니다.

공식 공유 가이드에서는 `gradio deploy` 명령이나 브라우저 업로드 방식으로 앱을 Spaces에 배포할 수 있다고 안내합니다.

프로젝트 구조

digit_gradio_app/
├─ app.py
├─ requirements.txt
└─ README.md

requirements.txt

gradio
numpy
pillow
pandas
scikit-learn

터미널 배포

gradio deploy

CLI가 저장소와 앱 정보를 확인한 뒤 현재 폴더의 파일을 Spaces에 업로드합니다.

브라우저 배포

Hugging Face 계정 생성

→ 새 Space 생성

→ SDK로 Gradio 선택

→ app.py 업로드

→ requirements.txt 업로드

→ 자동 빌드

비밀정보

API 키는 코드나 GitHub 저장소에 작성하지 않습니다.

Spaces 설정에서 Secret으로 등록하고 Python에서는 환경변수로 읽습니다.

import os


api_key = os.environ[
    "MY_API_KEY"
]

42. Docker 배포

자체 서버나 클라우드 VM에 배포하려면 Docker를 활용할 수 있습니다.

Gradio 공식 가이드는 앱과 Python 환경을 컨테이너로 패키징해 자체 서버에서 실행하는 방법을 제공합니다.

Dockerfile

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install \
    --no-cache-dir \
    -r requirements.txt

COPY . .

EXPOSE 7860

CMD [
    "python",
    "app.py"
]

app.py 실행 설정

if __name__ == "__main__":
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860
    )

이미지 생성

docker build \
    -t digit-gradio-app .

컨테이너 실행

docker run \
    --rm \
    -p 7860:7860 \
    digit-gradio-app

실제 운영 환경에서는 다음 요소도 필요합니다.

Nginx 리버스 프록시

HTTPS

도메인

로그 수집

프로세스 재시작

CPU·메모리 제한

GPU 컨테이너 설정

인증

모니터링

43. 실전 프로젝트 준비

이번에는 scikit-learn의 손글씨 숫자 데이터셋을 이용해 손글씨 숫자 AI 판독소를 만들어 보겠습니다.

프로젝트 기능

손글씨 숫자 이미지 업로드

이미지 흑백 변환

밝은 배경 자동 반전

숫자 영역 자르기

8×8 이미지 변환

머신러닝 숫자 예측

클래스별 확률 표시

전처리 이미지 확인

사용자별 예측 기록

모델 정확도 표시

API 엔드포인트 제공

Queue와 Progress 적용

사용 기술

Gradio
→ 웹 화면과 이벤트

Pillow
→ 이미지 전처리

NumPy
→ 픽셀 배열

scikit-learn
→ 숫자 분류 모델

Pandas
→ 예측 기록 표

프로젝트 구조

digit_gradio_app/
├─ app.py
└─ requirements.txt

44. 손글씨 숫자 분류 모델

scikit-learn의 `load_digits()` 데이터는 0부터 9까지 손글씨 숫자의 8×8 이미지 특성을 제공합니다.

다음 모델을 사용하겠습니다.

입력:
8 × 8 이미지

특성:
64개 픽셀값

모델:
표준화 + 로지스틱 회귀

출력:
숫자 0~9

모델 학습 함수:

from functools import lru_cache

from sklearn.datasets import (
    load_digits
)
from sklearn.linear_model import (
    LogisticRegression
)
from sklearn.metrics import (
    accuracy_score
)
from sklearn.model_selection import (
    train_test_split
)
from sklearn.pipeline import (
    make_pipeline
)
from sklearn.preprocessing import (
    StandardScaler
)


@lru_cache(
    maxsize=1
)
def train_digit_model() -> dict:
    digits = load_digits()

    (
        X_train,
        X_test,
        y_train,
        y_test
    ) = train_test_split(
        digits.data,
        digits.target,
        test_size=0.2,
        random_state=2026,
        stratify=digits.target
    )

    model = make_pipeline(
        StandardScaler(),
        LogisticRegression(
            max_iter=3000,
            random_state=2026
        )
    )

    model.fit(
        X_train,
        y_train
    )

    predictions = model.predict(
        X_test
    )

    accuracy = accuracy_score(
        y_test,
        predictions
    )

    return {
        "model": model,
        "accuracy": accuracy,
        "sample_count": (
            len(digits.data)
        ),
        "feature_count": (
            digits.data.shape[1]
        )
    }

`lru_cache`를 사용해 모델을 한 번 학습한 뒤 재사용합니다.

45. 업로드 이미지 전처리

학습 데이터는 8×8 크기이며 어두운 배경에 밝은 숫자가 그려져 있습니다.

사용자가 올린 이미지는 다음처럼 다를 수 있습니다.

흰 종이에 검은 숫자

검은 화면에 흰 숫자

숫자가 한쪽에 치우침

여백이 많음

사진 크기가 매우 큼

전처리 과정:

PIL 이미지 입력

→ 흑백 변환

→ 대비 조정

→ 배경이 밝으면 반전

→ 숫자 영역 탐색

→ 정사각형 여백 추가

→ 8×8 축소

→ 0~16 범위 변환

코드:

import numpy as np

from PIL import (
    Image,
    ImageOps
)


def prepare_digit_image(
    image: Image.Image
) -> tuple[
    np.ndarray,
    Image.Image
]:
    grayscale_image = ImageOps.grayscale(
        image
    )

    grayscale_image = ImageOps.autocontrast(
        grayscale_image
    )

    image_array = np.asarray(
        grayscale_image,
        dtype=np.float32
    )

    if image_array.mean() > 127:
        image_array = (
            255
            - image_array
        )

    threshold_value = max(
        20.0,
        float(
            image_array.max()
        ) * 0.15
    )

    foreground_mask = (
        image_array
        >= threshold_value
    )

    coordinates = np.argwhere(
        foreground_mask
    )

    if coordinates.size == 0:
        raise ValueError(
            "숫자 영역을 찾지 못했습니다."
        )

    (
        minimum_y,
        minimum_x
    ) = coordinates.min(
        axis=0
    )

    (
        maximum_y,
        maximum_x
    ) = coordinates.max(
        axis=0
    )

    cropped_array = image_array[
        minimum_y:maximum_y + 1,
        minimum_x:maximum_x + 1
    ]

    cropped_image = Image.fromarray(
        cropped_array.astype(
            np.uint8
        )
    )

    width, height = (
        cropped_image.size
    )

    square_size = max(
        width,
        height
    )

    padding = max(
        2,
        square_size // 5
    )

    canvas_size = (
        square_size
        + padding * 2
    )

    square_canvas = Image.new(
        mode="L",
        size=(
            canvas_size,
            canvas_size
        ),
        color=0
    )

    paste_x = (
        canvas_size - width
    ) // 2

    paste_y = (
        canvas_size - height
    ) // 2

    square_canvas.paste(
        cropped_image,
        (
            paste_x,
            paste_y
        )
    )

    resized_image = square_canvas.resize(
        (
            8,
            8
        ),
        Image.Resampling.LANCZOS
    )

    resized_array = np.asarray(
        resized_image,
        dtype=np.float32
    )

    model_input = (
        resized_array
        / 255.0
        * 16.0
    ).reshape(
        1,
        -1
    )

    preview_image = resized_image.resize(
        (
            256,
            256
        ),
        Image.Resampling.NEAREST
    )

    return (
        model_input,
        preview_image
    )

`NEAREST` 방식으로 확대하면 8×8 픽셀 구조를 선명하게 확인할 수 있습니다.

46. 숫자 예측 함수

from datetime import datetime

import gradio as gr
import pandas as pd


HISTORY_COLUMNS = [
    "시간",
    "예측 숫자",
    "예측 확률"
]


def predict_digit(
    image,
    history: list,
    progress=gr.Progress()
):
    if image is None:
        raise gr.Error(
            "손글씨 숫자 이미지를 "
            "업로드해 주세요."
        )

    progress(
        0.15,
        desc="이미지를 확인하고 있습니다."
    )

    try:
        (
            model_input,
            preview_image
        ) = prepare_digit_image(
            image
        )

    except ValueError as error:
        raise gr.Error(
            str(
                error
            )
        ) from error

    progress(
        0.6,
        desc="숫자 특징을 분석하고 있습니다."
    )

    model_bundle = train_digit_model()

    model = model_bundle[
        "model"
    ]

    prediction = int(
        model.predict(
            model_input
        )[0]
    )

    probabilities = (
        model.predict_proba(
            model_input
        )[0]
    )

    prediction_scores = {
        str(
            digit
        ): float(
            probability
        )
        for digit, probability in enumerate(
            probabilities
        )
    }

    confidence = float(
        probabilities[
            prediction
        ]
    )

    progress(
        0.9,
        desc="결과표를 정리하고 있습니다."
    )

    summary = (
        f"## 예측 숫자: **{prediction}**\n\n"
        f"- 예측 확률: **{confidence:.2%}**\n"
        f"- 입력 특성: 8×8 픽셀\n"
        f"- 참고: 업로드 이미지와 "
        f"학습 데이터 형태가 다르면 "
        f"정확도가 낮아질 수 있습니다."
    )

    updated_history = list(
        history
    )

    updated_history.append({
        "시간": (
            datetime.now()
            .strftime(
                "%H:%M:%S"
            )
        ),
        "예측 숫자": prediction,
        "예측 확률": confidence
    })

    history_dataframe = pd.DataFrame(
        updated_history,
        columns=HISTORY_COLUMNS
    )

    progress(
        1.0,
        desc="예측 완료"
    )

    return (
        prediction_scores,
        preview_image,
        summary,
        updated_history,
        history_dataframe
    )

`gr.Label`에 클래스 이름과 확률의 딕셔너리를 반환하면 순위와 확률을 표시할 수 있습니다.

47. Blocks 기반 화면 구성

import gradio as gr


with gr.Blocks(
    title="손글씨 숫자 AI 판독소",
    fill_width=True
) as demo:
    gr.Markdown(
        """
        # 🔢 손글씨 숫자 AI 판독소

        숫자 하나가 크게 보이는 이미지를 업로드하면
        머신러닝 모델이 0부터 9 중 어떤 숫자인지 예측합니다.
        """
    )

    history_state = gr.State(
        []
    )

    with gr.Row():
        with gr.Column(
            scale=1
        ):
            image_input = gr.Image(
                type="pil",
                image_mode="RGB",
                sources=[
                    "upload",
                    "webcam",
                    "clipboard"
                ],
                label="손글씨 숫자 이미지"
            )

            with gr.Row():
                predict_button = gr.Button(
                    "🔍 숫자 판독하기",
                    variant="primary"
                )

                clear_button = gr.Button(
                    "입력 지우기"
                )

        with gr.Column(
            scale=1
        ):
            prediction_output = gr.Label(
                label="숫자별 예측 확률",
                num_top_classes=10
            )

            processed_output = gr.Image(
                label="모델이 본 8×8 이미지",
                image_mode="L"
            )

            summary_output = gr.Markdown()

    with gr.Tab(
        "📜 예측 기록"
    ):
        history_output = gr.Dataframe(
            headers=HISTORY_COLUMNS,
            datatype=[
                "str",
                "number",
                "number"
            ],
            interactive=False,
            label="현재 사용자 세션 기록"
        )

        clear_history_button = gr.Button(
            "기록 초기화"
        )

    with gr.Tab(
        "🧠 모델 정보"
    ):
        model_bundle = train_digit_model()

        gr.Markdown(
            f"""
            ### 모델 구성

            - 알고리즘: 표준화 + 로지스틱 회귀
            - 학습 데이터: {model_bundle["sample_count"]:,}개
            - 입력 특성: {model_bundle["feature_count"]}개
            - 내부 평가 정확도: {model_bundle["accuracy"]:.2%}

            이 정확도는 scikit-learn의 원본 숫자 데이터에서
            분리한 평가 데이터에 대한 결과입니다.
            사용자가 촬영하거나 그린 이미지에서는
            배경, 글씨체, 크기에 따라 성능이 달라질 수 있습니다.
            """
        )

48. 예측 기록 State

이벤트를 연결합니다.

predict_event = predict_button.click(
    fn=predict_digit,
    inputs=[
        image_input,
        history_state
    ],
    outputs=[
        prediction_output,
        processed_output,
        summary_output,
        history_state,
        history_output
    ],
    api_name="predict_digit",
    concurrency_limit=2
)

이미지 입력 초기화:

clear_button.click(
    fn=lambda: (
        None,
        None,
        None,
        ""
    ),
    inputs=None,
    outputs=[
        image_input,
        prediction_output,
        processed_output,
        summary_output
    ]
)

기록 초기화 함수:

def clear_prediction_history():
    empty_dataframe = pd.DataFrame(
        columns=HISTORY_COLUMNS
    )

    return (
        [],
        empty_dataframe
    )

이벤트 연결:

clear_history_button.click(
    fn=clear_prediction_history,
    inputs=None,
    outputs=[
        history_state,
        history_output
    ]
)

예측 기록은 `gr.State`에 저장되므로 다른 사용자의 기록과 분리됩니다.

페이지를 새로고침하면 현재 세션 기록이 초기화될 수 있습니다.

49. 전체 프로젝트 코드

다음 내용을 `app.py`에 저장합니다.

from datetime import datetime
from functools import lru_cache

import gradio as gr
import numpy as np
import pandas as pd

from PIL import (
    Image,
    ImageOps
)
from sklearn.datasets import (
    load_digits
)
from sklearn.linear_model import (
    LogisticRegression
)
from sklearn.metrics import (
    accuracy_score
)
from sklearn.model_selection import (
    train_test_split
)
from sklearn.pipeline import (
    make_pipeline
)
from sklearn.preprocessing import (
    StandardScaler
)


HISTORY_COLUMNS = [
    "시간",
    "예측 숫자",
    "예측 확률"
]


@lru_cache(
    maxsize=1
)
def train_digit_model() -> dict:
    """손글씨 숫자 분류 모델을 학습합니다."""
    digits = load_digits()

    (
        X_train,
        X_test,
        y_train,
        y_test
    ) = train_test_split(
        digits.data,
        digits.target,
        test_size=0.2,
        random_state=2026,
        stratify=digits.target
    )

    model = make_pipeline(
        StandardScaler(),
        LogisticRegression(
            max_iter=3000,
            random_state=2026
        )
    )

    model.fit(
        X_train,
        y_train
    )

    predictions = model.predict(
        X_test
    )

    accuracy = accuracy_score(
        y_test,
        predictions
    )

    return {
        "model": model,
        "accuracy": accuracy,
        "sample_count": (
            len(digits.data)
        ),
        "feature_count": (
            digits.data.shape[1]
        )
    }


def prepare_digit_image(
    image: Image.Image
) -> tuple[
    np.ndarray,
    Image.Image
]:
    """업로드 이미지를 숫자 모델 입력으로 변환합니다."""
    grayscale_image = ImageOps.grayscale(
        image
    )

    grayscale_image = ImageOps.autocontrast(
        grayscale_image
    )

    image_array = np.asarray(
        grayscale_image,
        dtype=np.float32
    )

    if image_array.size == 0:
        raise ValueError(
            "빈 이미지입니다."
        )

    if image_array.mean() > 127:
        image_array = (
            255
            - image_array
        )

    maximum_pixel = float(
        image_array.max()
    )

    if maximum_pixel <= 0:
        raise ValueError(
            "숫자를 확인할 수 없습니다."
        )

    threshold_value = max(
        20.0,
        maximum_pixel * 0.15
    )

    foreground_mask = (
        image_array
        >= threshold_value
    )

    coordinates = np.argwhere(
        foreground_mask
    )

    if coordinates.size == 0:
        raise ValueError(
            "숫자 영역을 찾지 못했습니다."
        )

    (
        minimum_y,
        minimum_x
    ) = coordinates.min(
        axis=0
    )

    (
        maximum_y,
        maximum_x
    ) = coordinates.max(
        axis=0
    )

    cropped_array = image_array[
        minimum_y:maximum_y + 1,
        minimum_x:maximum_x + 1
    ]

    cropped_image = Image.fromarray(
        cropped_array.astype(
            np.uint8
        )
    )

    width, height = (
        cropped_image.size
    )

    square_size = max(
        width,
        height
    )

    padding = max(
        2,
        square_size // 5
    )

    canvas_size = (
        square_size
        + padding * 2
    )

    square_canvas = Image.new(
        mode="L",
        size=(
            canvas_size,
            canvas_size
        ),
        color=0
    )

    paste_x = (
        canvas_size - width
    ) // 2

    paste_y = (
        canvas_size - height
    ) // 2

    square_canvas.paste(
        cropped_image,
        (
            paste_x,
            paste_y
        )
    )

    resized_image = square_canvas.resize(
        (
            8,
            8
        ),
        Image.Resampling.LANCZOS
    )

    resized_array = np.asarray(
        resized_image,
        dtype=np.float32
    )

    model_input = (
        resized_array
        / 255.0
        * 16.0
    ).reshape(
        1,
        -1
    )

    preview_image = resized_image.resize(
        (
            256,
            256
        ),
        Image.Resampling.NEAREST
    )

    return (
        model_input,
        preview_image
    )


def predict_digit(
    image: Image.Image | None,
    history: list,
    progress=gr.Progress()
):
    """이미지의 손글씨 숫자를 예측합니다."""
    if image is None:
        raise gr.Error(
            "손글씨 숫자 이미지를 "
            "업로드해 주세요."
        )

    progress(
        0.15,
        desc="이미지를 확인하고 있습니다."
    )

    try:
        (
            model_input,
            preview_image
        ) = prepare_digit_image(
            image
        )

    except ValueError as error:
        raise gr.Error(
            str(
                error
            )
        ) from error

    progress(
        0.6,
        desc="숫자 특징을 분석하고 있습니다."
    )

    model_bundle = train_digit_model()

    model = model_bundle[
        "model"
    ]

    prediction = int(
        model.predict(
            model_input
        )[0]
    )

    probabilities = (
        model.predict_proba(
            model_input
        )[0]
    )

    prediction_scores = {
        str(
            digit
        ): float(
            probability
        )
        for digit, probability in enumerate(
            probabilities
        )
    }

    confidence = float(
        probabilities[
            prediction
        ]
    )

    progress(
        0.85,
        desc="결과를 정리하고 있습니다."
    )

    summary = (
        f"## 예측 숫자: **{prediction}**\n\n"
        f"- 예측 확률: **{confidence:.2%}**\n"
        f"- 모델 입력: 8×8 픽셀\n"
        f"- 분류 대상: 0부터 9\n\n"
        f"> 촬영 조건과 글씨체에 따라 "
        f"예측 결과가 달라질 수 있습니다."
    )

    updated_history = list(
        history
    )

    updated_history.append({
        "시간": (
            datetime.now()
            .strftime(
                "%H:%M:%S"
            )
        ),
        "예측 숫자": prediction,
        "예측 확률": confidence
    })

    history_dataframe = pd.DataFrame(
        updated_history,
        columns=HISTORY_COLUMNS
    )

    progress(
        1.0,
        desc="예측 완료"
    )

    return (
        prediction_scores,
        preview_image,
        summary,
        updated_history,
        history_dataframe
    )


def clear_inputs():
    """입력과 현재 예측 결과를 초기화합니다."""
    return (
        None,
        None,
        None,
        ""
    )


def clear_prediction_history():
    """현재 세션의 예측 기록을 초기화합니다."""
    empty_dataframe = pd.DataFrame(
        columns=HISTORY_COLUMNS
    )

    return (
        [],
        empty_dataframe
    )


model_bundle = train_digit_model()


with gr.Blocks(
    title="손글씨 숫자 AI 판독소",
    fill_width=True
) as demo:
    gr.Markdown(
        """
        # 🔢 손글씨 숫자 AI 판독소

        숫자 하나가 크게 보이는 이미지를 업로드하면
        머신러닝 모델이 **0부터 9** 중 어떤 숫자인지 예측합니다.

        흰 종이에 진한 색으로 숫자를 크게 적어 촬영하면
        비교적 좋은 결과를 얻을 수 있습니다.
        """
    )

    history_state = gr.State(
        []
    )

    with gr.Row():
        with gr.Column(
            scale=1
        ):
            image_input = gr.Image(
                type="pil",
                image_mode="RGB",
                sources=[
                    "upload",
                    "webcam",
                    "clipboard"
                ],
                label="손글씨 숫자 이미지"
            )

            with gr.Row():
                predict_button = gr.Button(
                    "🔍 숫자 판독하기",
                    variant="primary"
                )

                clear_button = gr.Button(
                    "입력 지우기"
                )

        with gr.Column(
            scale=1
        ):
            prediction_output = gr.Label(
                label="숫자별 예측 확률",
                num_top_classes=10
            )

            processed_output = gr.Image(
                label="모델이 본 8×8 이미지",
                image_mode="L"
            )

            summary_output = gr.Markdown(
                """
                이미지를 업로드하고
                **숫자 판독하기** 버튼을 누르세요.
                """
            )

    with gr.Tab(
        "📜 예측 기록"
    ):
        history_output = gr.Dataframe(
            headers=HISTORY_COLUMNS,
            datatype=[
                "str",
                "number",
                "number"
            ],
            interactive=False,
            label="현재 사용자 세션 기록"
        )

        clear_history_button = gr.Button(
            "기록 초기화"
        )

    with gr.Tab(
        "🧠 모델 정보"
    ):
        gr.Markdown(
            f"""
            ### 모델 구성

            - 알고리즘: 표준화 + 로지스틱 회귀
            - 학습 데이터: {model_bundle["sample_count"]:,}개
            - 입력 특성: {model_bundle["feature_count"]}개
            - 내부 평가 정확도: {model_bundle["accuracy"]:.2%}

            ### 주의사항

            내부 평가 정확도는 원본 숫자 데이터에서 분리한
            평가 데이터에 대한 결과입니다.

            사용자가 촬영한 이미지에는 글씨체, 그림자,
            배경, 카메라 각도 차이가 있으므로
            실제 정확도는 달라질 수 있습니다.
            """
        )

    predict_button.click(
        fn=predict_digit,
        inputs=[
            image_input,
            history_state
        ],
        outputs=[
            prediction_output,
            processed_output,
            summary_output,
            history_state,
            history_output
        ],
        api_name="predict_digit",
        concurrency_limit=2
    )

    clear_button.click(
        fn=clear_inputs,
        inputs=None,
        outputs=[
            image_input,
            prediction_output,
            processed_output,
            summary_output
        ]
    )

    clear_history_button.click(
        fn=clear_prediction_history,
        inputs=None,
        outputs=[
            history_state,
            history_output
        ]
    )


if __name__ == "__main__":
    demo.queue(
        default_concurrency_limit=2,
        max_size=30
    )

    demo.launch(
        theme=gr.themes.Soft(),
        footer_links=[
            "api",
            "settings"
        ]
    )

Gradio 6에서는 테마, CSS 등 앱 전체에 적용되는 설정을 `launch()`에 전달하는 방식이 사용됩니다.

50. 실행 및 배포 방법

requirements.txt

gradio
numpy
pillow
pandas
scikit-learn

가상환경

Windows

python -m venv venv
venv\Scripts\activate

macOS·Linux

python3 -m venv venv
source venv/bin/activate

설치

python -m pip install \
    -r requirements.txt

실행

python app.py

임시 공유

demo.launch(
    share=True
)

전체 코드에서는 `launch()` 부분을 다음처럼 변경할 수 있습니다.

demo.launch(
    share=True,
    theme=gr.themes.Soft()
)

Hugging Face Spaces

프로젝트 폴더에서 다음 명령을 실행합니다.

gradio deploy

또는 Space를 생성한 뒤 `app.py`와 `requirements.txt`를 업로드합니다.

51. 성능과 보안 주의사항

모델을 요청마다 로딩하지 않는다

앱 시작 시 한 번 로딩

→ 요청마다 추론만 실행

GPU 모델의 동시 실행을 제한한다

concurrency_limit=1

GPU 메모리와 모델 특성에 맞게 설정합니다.

Queue 크기를 무한히 늘리지 않는다

대기 사용자가 너무 많으면 응답 시간이 지나치게 길어집니다.

demo.queue(
    max_size=50
)

업로드 파일을 신뢰하지 않는다

확인할 내용:

확장자

MIME 유형

파일 크기

이미지 해상도

압축 폭탄

악성 파일

파일명

서버 내부 파일을 노출하지 않는다

사용자에게 반환할 파일 경로를 제한하고 `allowed_paths` 설정을 주의해서 사용합니다.

API 키를 코드에 작성하지 않는다

import os


api_key = os.environ[
    "MODEL_API_KEY"
]

기본 인증을 완전한 보안 시스템으로 간주하지 않는다

중요한 서비스에는 OAuth, 세션 관리, 접근 권한, 속도 제한을 별도로 설계합니다.

사용자별 데이터가 전역 변수에 섞이지 않게 한다

공용 모델:
전역 공유 가능

사용자 대화 기록:
gr.State 또는 외부 DB

공유 링크에 민감한 기능을 올리지 않는다

임시 공유 URL도 인터넷에서 접근할 수 있습니다.

생성형 AI 출력에 검증 절차를 둔다

잘못된 정보

유해한 출력

개인정보 노출

프롬프트 인젝션

도구 오작동

Gradio는 UI를 제공하지만 모델 출력의 안전성과 정확성을 보장하지는 않습니다.

52. 자주 발생하는 오류

오류 1. gradio를 찾을 수 없음

ModuleNotFoundError:
No module named 'gradio'

설치:

python -m pip install --upgrade gradio

확인:

python -m pip show gradio

현재 Python:

python -c "import sys; print(sys.executable)"

오류 2. Python 버전이 낮음

Gradio 6은 Python 3.10 이상을 요구합니다.

확인:

python --version

낮은 버전이라면 새 Python 환경을 구성합니다.

오류 3. 인터넷의 오래된 코드가 실행되지 않음

증상:

unexpected keyword argument

attribute not found

parameter has been removed

원인:

예제는 Gradio 3 또는 4 기준

현재 환경은 Gradio 6

버전 확인:

import gradio as gr


print(
    gr.__version__
)

Gradio 6 마이그레이션 문서에서 제거되거나 이동한 매개변수를 확인합니다.

오류 4. 함수 입력 개수가 맞지 않음

함수:

def predict(
    text,
    temperature
):
    ...

잘못된 연결:

inputs=[
    text_input
]

수정:

inputs=[
    text_input,
    temperature_slider
]

오류 5. 함수 반환값 개수가 맞지 않음

출력 컴포넌트가 두 개라면 함수도 두 값을 반환해야 합니다.

return (
    prediction,
    confidence
)

오류 6. 이미지 자료형이 예상과 다름

gr.Image(
    type="pil"
)

함수는 PIL 이미지 객체를 받습니다.

gr.Image(
    type="numpy"
)

함수는 NumPy 배열을 받습니다.

자료형을 출력해 확인합니다.

print(
    type(image)
)

오류 7. 버튼이 작동하지 않음

이벤트 연결이 `Blocks` 내부에서 정의되었는지 확인합니다.

with gr.Blocks() as demo:
    button = gr.Button()
    output = gr.Textbox()

    button.click(
        fn=run,
        outputs=output
    )

오류 8. State가 사용자 사이에 섞임

전역 리스트를 사용했을 수 있습니다.

history = []

수정:

history_state = gr.State(
    []
)

오류 9. 페이지를 새로고침하자 State가 사라짐

`gr.State`는 현재 페이지 세션을 위한 상태입니다.

새로고침 후에도 유지하려면 다음 중 하나를 검토합니다.

gr.BrowserState

데이터베이스

Redis

사용자 계정 기반 저장

오류 10. GPU 메모리 부족

CUDA out of memory

대응:

동시 실행 수 줄이기

입력 이미지 크기 줄이기

배치 크기 줄이기

작은 모델 사용

모델 양자화 검토

사용하지 않는 모델 해제
concurrency_limit=1

오류 11. 요청이 계속 대기함

확인할 내용:

앞 작업이 종료되지 않음

Queue 동시 실행 수가 너무 낮음

Generator가 끝나지 않음

외부 API 응답 지연

GPU 작업 멈춤

Queue 크기와 이벤트 실행 로그를 확인합니다.

오류 12. 공유 링크가 열리지 않음

로컬 Python 프로세스 종료

네트워크 차단

공유 터널 문제

앱 시작 오류

터미널 로그를 먼저 확인합니다.

임시 공유 대신 Hugging Face Spaces나 자체 서버 배포를 검토합니다.

오류 13. 다운로드 파일 접근이 차단됨

Gradio가 허용하지 않은 서버 경로일 수 있습니다.

사용자 업로드 파일, 현재 작업 디렉터리의 생성 파일 또는 명시적으로 허용된 경로인지 확인합니다.

오류 14. ChatInterface 함수 오류

기본 ChatInterface 함수는 다음 구조를 사용합니다.

def chat(
    message,
    history
):
    return "응답"

멀티모달 모드에서는 `message`가 딕셔너리입니다.

def chat(
    message: dict,
    history
):
    text = message["text"]
    files = message["files"]

오류 15. 스트리밍이 한 번에 출력됨

함수가 Generator인지 확인합니다.

잘못된 예:

def generate():
    result = ""

    for token in tokens:
        result += token

    return result

스트리밍:

def generate():
    result = ""

    for token in tokens:
        result += token

        yield result

Queue도 활성화합니다.

demo.queue()

53. 연습 문제

문제 1

이름과 직업을 입력받아 자기소개 문장을 반환하는 Interface를 만드세요.

문제 2

숫자 두 개와 연산자를 입력받는 계산기 앱을 만드세요.

더하기

빼기

곱하기

나누기

0으로 나누는 경우 `gr.Error`를 발생시키세요.

문제 3

Textbox에서 Enter를 누르면 문장 길이를 분석하도록 `submit` 이벤트를 연결하세요.

문제 4

Blocks와 Row를 사용해 입력창과 결과창을 좌우로 배치하세요.

문제 5

다음 세 개의 탭을 만드세요.

텍스트 분석

이미지 분석

모델 정보

문제 6

버튼 클릭 이벤트가 끝난 뒤 `.success()`로 완료 메시지를 표시하세요.

문제 7

예측 결과를 `gr.State`에 저장하고 Dataframe으로 출력하세요.

문제 8

현재 사용자의 설정값을 `gr.BrowserState`에 저장하세요.

문제 9

5초 동안 진행되는 가상 작업에 Progress 표시를 추가하세요.

문제 10

Generator를 이용해 문장을 한 글자씩 출력하세요.

문제 11

`ChatInterface`로 사용자의 메시지를 거꾸로 반환하는 챗봇을 만드세요.

문제 12

멀티모달 ChatInterface에서 첨부 파일 개수와 입력 문장을 출력하세요.

문제 13

이미지를 입력받아 흑백 이미지로 변환하는 앱을 만드세요.

문제 14

CSV 파일을 업로드하고 행 수, 열 수, 결측값 수를 JSON으로 반환하세요.

문제 15

예측 버튼에 다음 옵션을 적용하세요.

api_name:
predict

concurrency_limit:
1

문제 16

Gradio 앱을 `gradio_client`로 호출하세요.

문제 17

앱에 간단한 사용자명·비밀번호 인증을 추가하세요.

문제 18

손글씨 숫자 판독소에 다음 기능을 추가하세요.

예측 결과 CSV 다운로드

확률 50% 미만 경고

최근 기록 10개만 유지

숫자별 예측 횟수 차트

사용자 피드백 버튼

54. 핵심 요약

설치

python -m pip install --upgrade gradio

Interface

demo = gr.Interface(
    fn=predict,
    inputs=gr.Textbox(),
    outputs=gr.Textbox()
)

실행

demo.launch()

Blocks

with gr.Blocks() as demo:
    input_component = gr.Textbox()
    button = gr.Button()
    output_component = gr.Textbox()

    button.click(
        fn=process,
        inputs=input_component,
        outputs=output_component
    )

여러 입력

inputs=[
    text_input,
    slider_input
]

여러 출력

outputs=[
    result_output,
    score_output
]

State

history_state = gr.State(
    []
)

Queue

demo.queue(
    default_concurrency_limit=2
)

Progress

def process(
    value,
    progress=gr.Progress()
):
    progress(
        0.5,
        desc="처리 중"
    )

스트리밍

def stream():
    result = ""

    for token in tokens:
        result += token

        yield result

ChatInterface

demo = gr.ChatInterface(
    fn=chat
)

멀티모달 채팅

demo = gr.ChatInterface(
    fn=chat,
    multimodal=True
)

임시 공유

demo.launch(
    share=True
)

인증

demo.launch(
    auth=(
        "username",
        "password"
    )
)

Gradio Client

from gradio_client import Client


client = Client(
    "앱 주소"
)

result = client.predict(
    "입력값",
    api_name="/predict"
)

Spaces 배포

gradio deploy

55. 마무리

이번 시간에는 Gradio를 이용해 Python 함수와 머신러닝·생성형 AI 모델을 웹 데모로 만드는 방법을 알아보았습니다.

Gradio의 가장 기본적인 원리는 매우 단순합니다.

Python 함수

+ 입력 컴포넌트

+ 출력 컴포넌트

= 실행 가능한 웹 데모

빠른 데모에는 `Interface`를 사용했습니다.

gr.Interface(
    fn=predict,
    inputs=input_component,
    outputs=output_component
)

복잡한 화면에는 `Blocks`를 사용했습니다.

Row와 Column

Tab과 Accordion

여러 버튼

이벤트 연결

State

Queue

챗봇에는 `ChatInterface`를 사용했습니다.

gr.ChatInterface(
    fn=chat
)

Gradio의 핵심 구성 요소를 다시 정리해 보겠습니다.

Component
→ 데이터를 입력하고 결과를 표시

Event
→ 사용자 행동과 Python 함수 연결

Interface
→ 함수 하나를 빠르게 웹 데모로 변환

Blocks
→ 복잡한 화면과 데이터 흐름 구성

State
→ 사용자별 값 유지

Queue
→ 요청 순서와 동시 실행 관리

Progress
→ 작업 진행 상황 표시

Generator
→ 중간 결과 스트리밍

Client
→ Gradio 앱을 API로 호출

Gradio를 이용하면 모델 코드를 거의 변경하지 않고도 사용자가 직접 체험할 수 있는 화면을 만들 수 있습니다.

개발 전:

model.predict(image)

개발 후:

이미지 업로드

→ 분석 버튼

→ 확률 차트

→ 결과 다운로드

→ API 제공

같은 모델이지만 사용 가능성은 크게 달라집니다.

터미널 속 모델은 개발자만 사용할 수 있습니다.

웹 인터페이스를 얻은 모델은 동료, 고객, 연구자, 사용자와 대화할 수 있습니다.

모델:
“정확도는 97%입니다.”

사용자:
“그래서 어디에 이미지를 넣나요?”

Gradio:
“입력창과 버튼을 준비했습니다.”

다만 Gradio가 모델 자체를 더 정확하게 만들어 주는 것은 아닙니다.

좋은 UI
≠
좋은 모델

화려한 확률 표시
≠
검증된 예측

AI라는 제목
≠
지능적인 결과

데이터 품질, 모델 평가, 오류 분석, 보안, 개인정보 보호는 여전히 개발자의 책임입니다.

Gradio는 AI 모델을 포장하는 상자입니다.

상자가 아무리 예뻐도 안에 든 모델이 틀린 답을 내놓으면 사용자는 예쁘게 포장된 오답을 받게 됩니다.

그러므로 AI 데모를 만들 때는 다음 질문을 함께 던져야 합니다.

모델은 어떤 데이터로 학습했는가?

어떤 상황에서 잘못 예측하는가?

확률을 어떻게 해석해야 하는가?

사용자 데이터는 어디에 저장되는가?

동시 사용자 요청을 감당할 수 있는가?

오류가 발생했을 때 무엇을 보여줄 것인가?

이 질문에 답할 수 있을 때 Gradio 데모는 단순한 장난감에서 실제 검증 도구로 성장합니다.

이제 머신러닝 모델이 완성되었다면 터미널 결과만 캡처하지 마세요.

입력 컴포넌트를 연결하고, 버튼을 달고, 결과를 스트리밍해 보세요.

당신의 Python 함수가 브라우저 무대에서 첫 공연을 시작할 차례입니다. 🎭🤖

다음 편 예고

[Python 완전정복 시리즈 #35] Hugging Face Transformers 완벽 이해하기 | 사전 학습 AI 모델을 Python에서 사용하는 방법

다음 시간에는 Hugging Face 생태계와 Transformers 라이브러리를 이용해 사전 학습된 AI 모델을 불러오고 실행하는 방법을 알아봅니다.

Pipeline, Tokenizer, Model, 감정 분석, 문장 분류, 텍스트 생성, 요약, 번역, 이미지 분류, 모델 저장, GPU 실행, Gradio 데모 연결까지 실습해 보겠습니다.

#Python #파이썬 #Python강좌 #파이썬기초 #Gradio #그라디오 #머신러닝 #딥러닝 #생성형AI #AI웹앱 #웹데모 #Interface #Blocks #ChatInterface #AI챗봇 #멀티모달 #이미지분류 #음성인식 #LLM #State #Queue #Streaming #HuggingFace #HuggingFaceSpaces #scikitlearn #모델배포 #Python웹개발 #코딩공부 #프로그래밍

댓글

0

댓글을 불러오는 중입니다.