iBetter Books
수정

anywidget으로 커스텀 위젯 만들기

mo.ui.*에 없는 위젯이 필요할 때 어떻게 할까요. marimo는 anywidget 생태계를 통해 커스텀 위젯을 만들거나 기존 anywidget 패키지를 가져다 쓸 수 있도록 지원합니다. 이 챕터에서는 anywidget이 무엇인지, marimo에서 어떻게 통합하는지를 살펴봅니다.

anywidget란

anywidget은 ipywidgets 생태계에서 탄생한 경량 위젯 프레임워크입니다. JavaScript(또는 TypeScript)로 렌더링 로직을 작성하고, Python에서 traitlets로 상태를 정의하면, 두 쪽이 자동으로 동기화됩니다. 원래 Jupyter를 타깃으로 만들어졌지만, marimo도 mo.ui.anywidget()을 통해 anywidget을 완전히 지원합니다.

anywidget이 필요한 상황은 주로 다음과 같습니다.

  • mo.ui.*에 없는 특수 UI 컴포넌트가 필요할 때
  • 기존 anywidget 패키지(예: 데이터 시각화, 지도, 커스텀 컨트롤)를 marimo에서 사용하고 싶을 때
  • 조직 내부용 커스텀 입력 컴포넌트를 직접 만들고 싶을 때

패키지 설치

pip install anywidget traitlets

이미 marimo 환경에 있다면 traitlets는 설치되어 있을 가능성이 높습니다. anywidget만 추가로 설치하면 됩니다.

간단한 카운터 위젯 만들기

anywidget의 구조를 이해하기 위해 버튼을 클릭하면 숫자가 오르는 카운터를 만들어봅니다.

Python 쪽: anywidget.AnyWidget을 상속하고 traitlets로 상태 변수를 정의합니다. _esm 속성에 JavaScript 렌더링 코드를 넣습니다.

import anywidgetimport traitletsclass CounterWidget(anywidget.AnyWidget):    _esm = """    function render({ model, el }) {        let btn = document.createElement("button");        btn.innerHTML = `count: ${model.get("count")}`;        btn.addEventListener("click", () => {            model.set("count", model.get("count") + 1);            model.save_changes();        });        model.on("change:count", () => {            btn.innerHTML = `count: ${model.get("count")}`;        });        el.appendChild(btn);    }    export default { render };    """    count = traitlets.Int(0).tag(sync=True)
  • _esm에는 ES module 형식의 JavaScript를 적습니다. render 함수가 진입점입니다.
  • model.get("count")로 Python 쪽 count 값을 읽습니다.
  • model.set("count", 새값) 다음에 model.save_changes()를 호출하면 Python 쪽으로 값이 동기화됩니다.
  • model.on("change:count", 콜백)은 Python 쪽에서 count가 바뀌면 JavaScript 쪽 UI를 갱신합니다.
  • traitlets.Int(0).tag(sync=True)sync=True가 있어야 Python과 JavaScript 사이에 값이 오갑니다.

mo.ui.anywidget()으로 래핑하기

anywidget 인스턴스를 mo.ui.anywidget()으로 감싸야 marimo에서 반응형으로 동작합니다.

import marimo as mowidget = mo.ui.anywidget(CounterWidget())widget

다른 셀에서 widget.value를 참조하면 위젯 상태가 바뀔 때 그 셀이 재실행됩니다.

mo.md(f"현재 카운트: **{widget.value}**")

mo.ui.anywidget()이 반환한 객체에서 .valuecount traitlet의 현재 값을 담은 딕셔너리입니다.

# widget.value 예시 (count가 3이라면)# {"count": 3}current = widget.valuecount_val = current["count"]

개별 traitlet 속성 접근

widget.value 대신 traitlet 이름으로 직접 접근할 수도 있습니다.

# widget.count는 현재 count 값을 정수로 반환widget.count

widget.value는 전체 상태를 딕셔너리로, widget.{속성이름}은 개별 값을 해당 타입으로 반환합니다. 어느 방식이든 marimo의 반응형 추적 대상이 됩니다.

더 복잡한 예시 — 색상 선택기

여러 traitlet을 쓰는 예시입니다. JavaScript로 색상을 선택하고 Python에서 그 값을 읽습니다.

import anywidgetimport traitletsclass ColorPickerWidget(anywidget.AnyWidget):    _esm = """    function render({ model, el }) {        let input = document.createElement("input");        input.type = "color";        input.value = model.get("color");        input.addEventListener("input", () => {            model.set("color", input.value);            model.save_changes();        });        model.on("change:color", () => {            input.value = model.get("color");        });        el.appendChild(input);    }    export default { render };    """    color = traitlets.Unicode("#3498db").tag(sync=True)
import marimo as mocolor_picker = mo.ui.anywidget(ColorPickerWidget())color_picker
mo.md(f"선택된 색상: `{color_picker.color}`")

색상을 선택하면 color_picker.color 값이 바뀌고, 아래 셀의 텍스트가 자동으로 갱신됩니다.

기존 anywidget 패키지 사용하기

직접 만들지 않고 PyPI에 있는 anywidget 기반 패키지를 marimo에서 그대로 쓸 수도 있습니다. 설치 후 mo.ui.anywidget()으로 래핑하면 됩니다.

# 예시: 가상의 외부 anywidget 패키지 사용# pip install some-anywidget-package# from some_anywidget_package import SomeWidget# wrapped = mo.ui.anywidget(SomeWidget())# wrapped

anywidget 생태계가 활발하게 성장하고 있으므로, 필요한 UI 컴포넌트가 이미 패키지로 나와 있는 경우가 많습니다. anywidget 공식 문서에서 갤러리를 확인할 수 있습니다.

anywidget과 ipywidgets의 관계

anywidget은 ipywidgets 생태계의 일부이지만, 기존 ipywidgets(widgets.IntSlider 등)와는 다릅니다. 기존 ipywidgets를 marimo에서 mo.ui.anywidget()으로 래핑하는 것은 지원되지 않습니다. anywidget 규격(_esm 정의 + AnyWidget 상속)으로 만든 위젯만 통합됩니다.

ipywidgets에서 marimo 표준 위젯(mo.ui.*)으로 이전하는 방법은 PART 05 Ch 03에서 다룹니다.

정리

  • anywidget은 JavaScript와 Python 상태를 동기화하는 경량 위젯 프레임워크입니다.
  • anywidget.AnyWidget을 상속하고 traitlets로 상태를 정의한 뒤, _esm에 JavaScript 렌더링 코드를 작성합니다.
  • mo.ui.anywidget(위젯_인스턴스)으로 래핑하면 marimo의 반응형 실행 체계 안으로 들어옵니다.
  • widget.value는 전체 traitlet 상태를 딕셔너리로, widget.{속성}은 개별 값을 반환합니다.
  • 기존 anywidget 생태계 패키지도 같은 방식으로 통합할 수 있습니다.