Skip to content

Split View: 2026년 새로운 파이썬 노트북 스택 — Marimo·Quarto, 그리고 Jupyter 이후의 데이터 워크플로 (Observable·Pluto·Polars·DuckDB까지)

|

2026년 새로운 파이썬 노트북 스택 — Marimo·Quarto, 그리고 Jupyter 이후의 데이터 워크플로 (Observable·Pluto·Polars·DuckDB까지)

프롤로그 — Jupyter의 숨은 상태 문제

2026년에도 매주 어딘가에서 같은 장면이 반복된다. 데이터 과학자가 노트북을 동료에게 넘기고, 동료가 "Run All"을 누르면 셀이 다섯 번째에서 폭발한다. 변수가 정의되지 않았다고 한다. 그런데 보낸 사람의 화면에서는 멀쩡히 돈다.

원인은 거의 항상 같다 — **숨은 상태(hidden state)**다. 셀을 위에서 아래로 한 번도 돌리지 않은 채, 중간 셀만 여러 번 실행하다 보면 노트북의 텍스트와 커널의 메모리 상태가 어긋난다. 보낸 사람의 커널에는 df가 살아 있지만, 노트북 파일에는 df를 만드는 셀이 지워져 있다.

이건 Jupyter의 버그가 아니다. 설계 결정이다. Jupyter는 셀을 임의 순서로 실행할 수 있게 해 주는 것이 자산이라고 봤다 — 그게 REPL의 본질이니까. 그러나 그 자유가 협업·재현성·CI에 들어가면 부채가 된다.

"노트북은 1인용 환경에서는 천국이고, 2인 이상의 환경에서는 지옥이다."

2024~2026년에 이 부채를 새 방식으로 풀려는 움직임이 본격화됐다. Marimo는 셀 사이 데이터플로 그래프를 정적으로 분석해서 의존성에 따라 자동으로 다시 실행한다 — 셀 순서가 아니라 데이터 의존성이 진실이다. Quarto는 노트북을 입력으로 받아 HTML·PDF·웹사이트·논문 보조자료로 퍼블리시하는 멀티-언어·멀티-포맷 엔진이다. Observable Framework는 JS-네이티브 노트북을 정적 사이트로 빌드한다. 그 아래의 컴퓨트 레이어도 Pandas에서 Polars·DuckDB로 옮겨가고 있다.

이 글은 그 스택을 깊게 본다 — Jupyter가 무엇을 잘못했고, Marimo는 어떻게 풀었고, Quarto로 어떻게 퍼블리시하고, 언제 여전히 Jupyter를 써야 하는지까지.


1장 · Jupyter의 네 가지 부채

Jupyter (정확히는 IPython 커널 + ipynb JSON + Notebook/Lab UI 조합)는 2014년 이래 데이터 사이언스의 표준 환경이었다. 잘 한 일이 많다 — 출력을 코드 옆에 두고, 인라인 시각화·Markdown·LaTeX를 합쳐서 "실행 가능한 문서"를 만들었다. 그러나 12년이 지나면서 네 가지 부채가 쌓였다.

부채 1 — 숨은 상태

셀을 In[14], In[3], In[27] 순서로 실행해도 노트북 파일에는 그 순서가 남지 않는다. 누군가가 "Run All"을 눌렀을 때 같은 결과가 나온다는 보장이 없다. 진실은 노트북 파일이 아니라 메모리 안에 사는 커널에 있고, 그 커널은 종료되는 순간 사라진다.

부채 2 — 비-diff 가능 (.ipynb는 JSON)

.ipynb는 코드·Markdown·이미지·출력·실행 카운터를 모두 한 JSON 파일에 박아 둔다. PR 리뷰어가 GitHub에서 diff를 열면 base64로 인코딩된 PNG 한 줄이 끝없이 펼쳐진다. nbstripout이나 jupytext 같은 우회책이 필요한 것 자체가 신호다 — 포맷이 git과 본질적으로 안 맞는다.

부채 3 — 재현 불가

위 두 부채가 합쳐지면 재현이 불가능해진다. "이 노트북을 돌렸다"는 말이 "이 환경, 이 순서, 이 데이터로 돌렸다"를 의미하지 않는다. 논문 부록 노트북을 6개월 뒤에 다시 열면 의존성이 깨져 있고, 출력은 코드와 어긋나 있다.

부채 4 — UI 단방향성

Jupyter는 "위→아래" 선형 모델을 가정한다. 데이터 의존성을 보여주는 그래프 뷰가 없다 — 이 셀의 출력이 어디로 흘러가는지, 어떤 셀이 어떤 셀을 부르는지 알려면 인간이 머리로 추적해야 한다. 셀이 50개를 넘으면 사실상 불가능하다.

이 네 부채는 서로를 강화한다. 숨은 상태가 있으니 재현이 안 되고, 재현이 안 되니 diff를 봐도 의미가 없고, diff가 의미가 없으니 협업이 안 된다.


2장 · Marimo — 노트북을 다시 설계하다

Marimo는 2023년 후반에 등장해 2026년 중반 기준 0.10.x 시리즈에 도달한 오픈소스 파이썬 노트북이다. 출발점은 단순하다 — "노트북을 처음부터 다시 설계하면 무엇이 달라질까?"

Marimo의 세 가지 핵심 설계 결정:

결정 1 — .py 파일로 저장

Marimo 노트북은 .ipynb가 아니라 순수 파이썬 파일이다. 각 셀은 데코레이터가 붙은 함수다.

import marimo as mo

app = mo.App()


@app.cell
def __():
    import polars as pl
    df = pl.read_csv("sales.csv")
    return (df,)


@app.cell
def __(df):
    summary = df.group_by("region").agg(pl.col("revenue").sum())
    return (summary,)


@app.cell
def __(summary):
    summary
    return ()


if __name__ == "__main__":
    app.run()

여기서 두 가지가 일어난다. 첫째, 이 파일은 그냥 파이썬 스크립트로도 돌아간다python notebook.py만 치면 끝이다. 둘째, git diff가 의미가 있다. 변수 이름이 바뀌면 diff에 변수 이름이 보인다. base64로 인코딩된 PNG는 어디에도 없다.

결정 2 — 데이터플로 그래프, 셀 순서가 아니라

Marimo는 노트북을 정적으로 분석해 각 셀이 어떤 변수를 정의하고 어떤 변수를 참조하는지 알아낸다. 이게 **방향성 비순환 그래프(DAG)**가 된다. 셀을 실행하면, Marimo는 그 셀에 의존하는 모든 셀을 자동으로 다시 실행한다 — Excel 스프레드시트와 같은 모델이다.

결과: 숨은 상태가 불가능하다. 변수를 새로 할당하면 그 변수를 쓰는 모든 셀이 즉시 갱신된다. 어떤 셀을 지워도, 그 셀이 정의한 변수를 쓰는 셀은 즉시 "정의되지 않음" 오류를 띄운다. "Run All"이 항상 작동하는 노트북 파일과, 메모리 안의 커널 상태가 일치한다.

결정 3 — 노트북-as-앱 모드

Marimo는 두 가지 모드로 같은 파일을 띄울 수 있다:

  • 편집 모드 — 셀을 작성·실행하는 일반 노트북.
  • 실행 모드marimo run notebook.py로 띄우면, 셀이 숨겨지고 UI 컴포넌트(슬라이더·드롭다운·테이블)만 보이는 인터랙티브 앱이 된다.

UI 컴포넌트는 mo.ui 모듈로 정의한다.

@app.cell
def __(mo):
    region = mo.ui.dropdown(
        options=["US", "EU", "APAC"],
        value="US",
        label="Region",
    )
    region
    return (region,)


@app.cell
def __(df, region):
    filtered = df.filter(pl.col("region") == region.value)
    filtered
    return (filtered,)

드롭다운 값이 바뀌면 그 값을 쓰는 셀이 자동으로 다시 실행된다. Streamlit/Gradio 없이 노트북 자체가 앱이 된다.

Marimo가 잘 푸는 추가 문제들

  • WASM 배포 — Pyodide를 통해 노트북을 그냥 정적 HTML로 export 할 수 있다. 서버가 필요 없다. 사람들이 브라우저에서 직접 만진다.
  • AI 보조 — 셀별 AI completion (모델 비종속)이 내장되어 있다.
  • 타입 검사.py 파일이라 mypy/pyright가 그냥 동작한다.
  • 테스트 — 노트북 셀을 pytest로 import해서 테스트할 수 있다.
  • 순수 함수 셀 — 각 셀이 명시적 입력·출력을 가진 함수라 단위 테스트가 자연스럽다.

3장 · Jupyter vs Marimo — 같은 작업, 다른 코드

같은 워크플로(데이터 로드 → 필터 → 시각화)를 두 노트북에서 어떻게 표현하는지 보자.

Jupyter 셀들

# Cell 1
import pandas as pd
df = pd.read_csv("sales.csv")
df.head()

# Cell 2
df_us = df[df["region"] == "US"]
df_us.shape

# Cell 3
import matplotlib.pyplot as plt
df_us.groupby("month")["revenue"].sum().plot(kind="bar")
plt.show()

# Cell 4 — 30분 뒤, df 정의를 잊고 다시 쓴다
df = pd.read_csv("sales_v2.csv")  # 이전 df_us는 여전히 옛 데이터를 가리킨다!

Cell 4를 실행하고 그래프(Cell 3)를 다시 보면 — df_us가 옛 df를 캡처했기 때문에 그래프가 갱신되지 않는다. 메모리 상태와 코드 상태가 어긋났다. 숨은 상태.

Marimo 셀들

@app.cell
def __():
    import polars as pl
    df = pl.read_csv("sales.csv")
    df
    return (df, pl)


@app.cell
def __(df, pl):
    df_us = df.filter(pl.col("region") == "US")
    df_us
    return (df_us,)


@app.cell
def __(df_us):
    chart = df_us.group_by("month").agg(pl.col("revenue").sum())
    chart
    return (chart,)

여기서 첫 셀의 dfsales_v2.csv로 바꾸면 — Marimo는 df에 의존하는 두 번째 셀, 거기 의존하는 세 번째 셀을 자동으로 다시 실행한다. 그래프가 즉시 갱신된다. 메모리 상태와 코드 상태가 본질적으로 동기화된다.

추가로, Marimo는 같은 변수를 두 셀에서 재정의하는 것을 막는다. "그 변수는 다른 셀에서 이미 정의되어 있다"는 오류를 내고, 작성자에게 둘 중 하나를 고르라고 강요한다. 이게 처음에는 답답하지만, 숨은 상태가 영원히 사라진다.


4장 · Quarto — 노트북을 퍼블리싱 가능한 문서로

Marimo가 "노트북을 다시 설계하기"라면, Quarto는 "노트북을 출판하기"다. Quarto는 RStudio의 RMarkdown 후계자로 2022년에 1.0을 찍었고, 2026년 기준 1.5+에 도달했다. 다국어(Python·R·Julia·Observable JS)·다포맷(HTML·PDF·EPUB·Word·웹사이트·책)을 한 도구로 처리한다.

Quarto 문서의 모양

Quarto 문서는 .qmd 확장자를 쓰고, YAML frontmatter + Markdown + 코드 펜스로 구성된다.

---
title: "2025 Sales Analysis"
author: "Data Team"
date: "2026-05-14"
format:
  html:
    code-fold: true
    toc: true
  pdf:
    documentclass: article
execute:
  echo: true
  warning: false
---

## 데이터 로드

이번 분기 매출을 분석한다.

\`\`\`{python}
import polars as pl
df = pl.read_csv("sales.csv")
df.head()
\`\`\`

## 지역별 합계

\`\`\`{python}
df.group_by("region").agg(pl.col("revenue").sum())
\`\`\`

(위 예시에서는 MDX 파서가 헷갈리지 않도록 백틱 펜스를 이스케이프했다. 실제 .qmd 파일에서는 그냥 백틱 세 개를 쓴다.)

이 파일 하나를 quarto render report.qmd --to html로 빌드하면 인터랙티브 HTML이, --to pdf로 빌드하면 LaTeX-품질의 PDF가 나온다. 코드는 실행되고, 출력이 문서에 박힌다.

Quarto의 출력 포맷

  • HTML — 검색·테마·인터랙티브 위젯·Mermaid 다이어그램·MathJax.
  • PDF (LaTeX) — 학술 논문 수준의 조판. book 클래스로 책도 만든다.
  • 웹사이트 — 여러 .qmd를 묶어 정적 사이트를 빌드한다 (Jekyll/Hugo와 같은 모델).
  • — 다챕터 PDF/HTML 책. 실제로 R for Data Science가 Quarto로 빌드된다.
  • Reveal.js 슬라이드 — 코드 실행 결과를 슬라이드에 넣는다.
  • Word·EPUB — 출판사 워크플로용.

다국어 — Python·R·Julia·Observable JS를 한 파일에서

Quarto는 코드 펜스의 언어 식별자를 보고 적절한 커널을 띄운다. python 펜스는 Jupyter 커널, r 펜스는 knitr, julia 펜스는 IJulia, {ojs} 펜스는 Observable JS 런타임으로 실행된다. 같은 문서 안에서 파이썬으로 데이터를 가공하고, R로 통계 모델을 돌리고, Observable JS로 인터랙티브 차트를 그리는 게 가능하다.

Quarto + Marimo, Quarto + Jupyter

Quarto는 입력으로 .qmd 외에도 Jupyter 노트북(.ipynb), Marimo 노트북을 받는다. 즉 Quarto는 노트북 포맷에 비종속적이다. "Marimo로 작업하고 Quarto로 출판한다"는 워크플로가 자연스럽다.

학술 논문·논문 부록

Quarto는 Journal of Statistical Software 같은 학술지의 공식 템플릿이 있다. APA·IEEE·Nature·Elsevier 스타일을 frontmatter 한 줄로 바꾼다. 인용은 BibTeX 또는 Zotero에서 직접 끌어오고, CSL 스타일로 포맷팅한다. 논문과 그 논문의 분석 코드가 같은 파일에 들어간다 — 재현성의 끝판왕.


5장 · Observable Framework — JS-네이티브 노트북의 진화

Observable은 2018년경 Mike Bostock(d3.js 창시자)이 만든 JS-네이티브 노트북이다. 셀이 반응형으로 묶이고 (Marimo의 영감 원천), 시각화가 일등 시민이다. 그러나 호스팅이 Observable.com에 종속되어 있다는 단점이 있었다.

2024년 발표된 Observable Framework는 이 모델을 정적 사이트 빌더로 가져왔다. .md 파일에 JS 셀을 박고, observable build를 치면 정적 HTML/CSS/JS가 나온다. 호스팅은 Vercel·Netlify·GitHub Pages 어디든 된다.

Observable Framework가 잘하는 영역:

  • 데이터 대시보드 — 회사 내부 분석 사이트, KPI 추적, 운영 대시보드.
  • 인터랙티브 데이터 스토리텔링 — d3 시각화, 사용자 입력에 반응하는 그래프, 데이터 저널리즘.
  • 빌드 타임 데이터 로더 — Python·R·Shell 스크립트로 빌드 시 데이터를 갱신하고, 결과를 정적 자산으로 굳힌다.

Marimo가 "Python 사용자가 인터랙티브를 원할 때"의 답이라면, Observable Framework는 "JS·시각화 중심의 데이터 출판"의 답이다. 둘은 경쟁이라기보다 다른 시장이다.


6장 · Pluto.jl — Julia의 반응형 노트북, 그리고 원조

Pluto.jl은 Julia용 반응형 노트북으로, Marimo의 직접적인 영감 원천이다. 2020년에 처음 등장했고, "셀 사이 데이터플로 그래프 → 자동 재실행" 모델을 노트북 세계에 최초로 도입했다.

Pluto가 제시한 핵심 원칙들:

  • 노트북 파일이 진실의 단일 원천이다 — 메모리 안 커널이 아니라 파일이 상태를 정의한다.
  • 셀 순서가 의미가 없다 — 파일 어디에 있어도 의존성 그래프로 실행 순서가 결정된다.
  • 같은 변수를 두 셀에서 재정의할 수 없다 — 그래프가 모호해지니까.
  • .jl 파일로 저장 — 그냥 Julia 스크립트로도 돈다.

Marimo가 이 모델을 Python 세계로 가져왔고, 그 사이 Pluto는 Julia 데이터 사이언스 커뮤니티(SciML·Plots.jl·DataFrames.jl)와 더 깊이 결합했다. Julia를 쓴다면 Pluto가 사실상 기본 선택이다.


7장 · 새로운 컴퓨트 레이어 — Polars·DuckDB

노트북 UX만 바뀐 게 아니다. 그 아래에서 데이터를 실제로 굴리는 컴퓨트 레이어도 바뀌었다.

Polars — Rust 기반 DataFrame

Polars는 Rust로 작성된 DataFrame 라이브러리로, Apache Arrow를 기본 메모리 포맷으로 쓴다. Pandas 대비:

  • 빠르다 — 멀티스레드 기본, 컬럼나 메모리 레이아웃, 쿼리 최적화기.
  • lazy evaluation — 쿼리를 빌드하고 .collect()로 한 번에 실행 (Spark·SQL과 같은 모델).
  • API가 일관적 — Pandas의 누적된 비일관성(apply vs applymap, 인덱스 지옥 등) 없음.
  • 메모리가 작다 — Arrow zero-copy로 같은 데이터를 여러 도구가 공유.

대용량 노트북에서 Polars로 옮기면 5~30배 빨라지는 게 흔하다.

DuckDB — 인-프로세스 OLAP DB

DuckDB는 SQLite의 OLAP(분석) 버전이다. 임베디드, 컬럼나, 단일 노드 분석에 최적화. 파일·Parquet·CSV·JSON·Arrow를 직접 쿼리한다.

노트북에서 DuckDB가 빛나는 순간:

import duckdb
duckdb.sql("""
    SELECT region, SUM(revenue)
    FROM 'sales/*.parquet'
    WHERE date >= '2026-01-01'
    GROUP BY region
""").to_df()

Spark 클러스터 없이 노트북에서 100GB Parquet를 직접 쿼리한다. Polars와 DuckDB는 Arrow를 통해 zero-copy로 데이터를 주고받는다 — 같은 파이프라인에서 둘을 섞어도 비용이 없다.

이 컴퓨트 레이어가 Marimo·Quarto 위에 깔리면, 단일 노트북이 처리할 수 있는 데이터 규모가 한 자릿수 GB에서 세 자릿수 GB로 올라간다.


8장 · 비교 매트릭스

항목JupyterMarimoQuartoObservable FrameworkPluto.jl
언어Python (커널 다수)Python다국어JS 중심Julia
파일 포맷.ipynb (JSON).py.qmd.md.jl
반응형 실행없음있음 (DAG)없음 (출판 도구)있음있음 (원조)
Git diff나쁨좋음좋음좋음좋음
노트북-as-앱별도 (Voila·Panel)내장별도정적 사이트 빌드부분적
정적 exportHTML (재실행 없음)HTML/WASMHTML·PDF·웹사이트·책정적 사이트HTML
학습 곡선낮음낮음~중간중간중간 (JS 필요)낮음
생태계 크기거대함성장 중시각화 중심Julia 중
대표 사용처EDA·교육·연구EDA·내부 도구·앱보고서·논문·책·블로그대시보드·데이터 저널리즘Julia 과학 컴퓨팅
라이선스BSD-3Apache-2.0MITISCMIT

9장 · 실제 워크플로 시나리오

시나리오 A · 데이터 과학자의 일일 EDA

  • 이전: Jupyter Lab + Pandas. 셀을 재실행하다 숨은 상태에 빠진다. 결과는 동료에게 PNG 스크린샷으로 보낸다.
  • 이후: Marimo + Polars + DuckDB. 노트북이 .py라 PR로 올린다. 동료가 marimo run으로 같은 결과를 즉시 재현한다. 셀에 슬라이더를 박아 "지역 선택" UI를 만들면, 비-데이터 동료도 그걸 직접 만진다.

시나리오 B · 분기 리포트 → 사내 임원진

  • 이전: Jupyter로 분석 → PowerPoint 캡처. 다음 분기에 데이터가 갱신되면 캡처를 다시 한다.
  • 이후: Quarto 웹사이트. 분기마다 데이터만 갱신하고 quarto render. 결과는 회사 인트라넷에 정적 HTML로 배포. 검색·테마·인쇄용 PDF까지 한 빌드에서 나온다.

시나리오 C · 학술 논문 + 보조자료

  • 이전: LaTeX로 논문, 별도 .ipynb로 분석 코드. 6개월 뒤 리뷰어 코멘트가 오면 어느 노트북에서 어느 그림이 나왔는지 찾지 못한다.
  • 이후: Quarto + Marimo. 논문 본문과 분석 코드가 같은 .qmd 파일에. quarto render --to pdf로 LaTeX 출력. 보조자료는 같은 파일을 --to html로 빌드. 재현 가능한 논문.

시나리오 D · 회사 내부 데이터 도구

  • 이전: Streamlit/Gradio로 짠 별도 앱. 데이터 과학자가 노트북에서 작업하고, 엔지니어가 그것을 앱으로 옮긴다 — 두 번 작성.
  • 이후: Marimo 노트북-as-앱. 노트북에 mo.ui 컴포넌트를 박고 marimo run으로 띄운다. 엔지니어링 핸드오프 없음. 같은 파일이 노트북이자 앱.

시나리오 E · 데이터 저널리즘·인터랙티브 스토리

  • Observable Framework. d3.js 시각화·사용자 입력에 반응하는 차트·빌드 타임 데이터 로더(Python으로 데이터 가공 → 정적 JSON으로 굳힘) → 정적 사이트.

시나리오 F · Julia 과학 컴퓨팅

  • Pluto.jl. SciML·DifferentialEquations.jl 같은 Julia 생태계가 깊고, 반응형 모델이 시뮬레이션·매개변수 탐색에 잘 맞는다.

10장 · 언제 여전히 Jupyter를 써야 하는가

Marimo·Quarto가 강력하지만, Jupyter가 더 나은 자리는 여전히 있다.

여전히 Jupyter

  • 다언어 커널 의존이 강할 때 — Spark·PyTorch Distributed·Wolfram Language 등 특수 커널이 필요한 환경. Jupyter의 커널 프로토콜이 표준이다.
  • JupyterHub·Binder 인프라가 이미 있을 때 — 학교·연구소가 운영 중인 시스템을 갈아엎기는 어렵다.
  • 빠른 일회용 스크래치 — "5줄짜리 실험" 하나라면 셋업 오버헤드가 작은 Jupyter가 빠르다.
  • 교육 — 학생들에게 셀 순서·In[N]/Out[N]의 멘탈 모델을 가르치는 게 강의 목적인 경우. (다만 2026년에는 점점 Marimo로 가르치는 강의도 늘었다.)
  • VS Code·Cursor의 통합 노트북 UI.ipynb를 그냥 열어서 보는 워크플로가 IDE에 깊이 박혀 있다.

Marimo로 가야 할 신호

  • 노트북을 협업 자산으로 쓰고 있다 (PR·리뷰).
  • "Run All"이 깨지는 일이 자주 생긴다.
  • 내부 도구로 노트북을 배포하고 싶다.
  • 비-데이터 동료가 노트북을 만질 일이 있다.

Quarto로 가야 할 신호

  • 분석 결과를 정기적으로 외부에 출판한다 (블로그·리포트·논문).
  • 같은 콘텐츠를 여러 포맷(HTML·PDF)으로 내야 한다.
  • 책·다챕터 문서를 만든다.
  • 다국어(Python·R·JS) 분석을 한 문서로 묶고 싶다.

11장 · 마이그레이션 — 어떻게 옮길까

Jupyter → Marimo

Marimo는 marimo convert notebook.ipynb로 자동 변환을 제공한다. 한 번에 깨끗하게 옮기진 않는다 — 같은 변수를 여러 셀에서 재정의하는 등 Jupyter에서 합법이었던 패턴이 Marimo에서는 오류다. 첫 변환 후 30분~한 시간 정도 손볼 각오는 해야 한다.

권장: 새 노트북부터 Marimo로 쓰고, 기존 .ipynb는 그대로 둔다. 한 번에 다 옮기지 말 것.

Jupyter → Quarto

Quarto는 .ipynb를 그대로 입력으로 받는다 — 변환이 사실상 필요 없다. quarto render notebook.ipynb만 치면 된다. 출력 포맷이 늘어나는 게 핵심 가치.

Marimo + Quarto 조합

가장 강력한 조합. Marimo로 작성·인터랙티브 탐색 → 결과를 Quarto로 출판. 둘 다 .py 또는 .ipynb를 입출력으로 쓰니까 파이프라인이 깨끗하다.


12장 · 안티-패턴

이 새 스택에서도 피해야 할 함정이 있다.

안티-패턴 1 — Marimo를 단순 Jupyter 대체품으로 쓰기

Marimo의 진짜 가치는 반응형·.py 저장·노트북-as-앱이다. 그걸 안 쓰고 그냥 셀 단위로 코드 쓰는 도구로만 쓰면, 학습 곡선만 지불하고 이득이 없다. UI 컴포넌트를 박고, 반응형 의존성을 활용하라.

안티-패턴 2 — Quarto 문서에 거대한 분석을 그대로 박기

Quarto 빌드 시간이 폭발한다. 무거운 데이터 가공은 별도 스크립트로 분리하고, Quarto에는 결과 데이터(Parquet/CSV)만 로드하게 한다. 또는 freeze: true로 캐시.

안티-패턴 3 — 노트북을 영원히 노트북으로 두기

Marimo·Quarto가 좋다고 모든 코드를 노트북으로 두는 건 함정이다. 안정적인 데이터 파이프라인은 일반 .py 모듈로 옮기고, 노트북은 탐색·문서·인터페이스 역할로 한정한다. 노트북은 종착지가 아니라 인터페이스다.

안티-패턴 4 — Polars/DuckDB를 안 써보고 Pandas만 고집

데이터가 GB대를 넘으면 Pandas는 메모리·속도 양쪽에서 무너진다. Polars로 옮기는 비용은 일주일 이하다. 그 일주일이 다음 1년의 대기 시간을 절반으로 줄인다.

안티-패턴 5 — Marimo의 변수 재정의 제약을 우회하려 들기

처음에 답답하다고 globals() 해킹으로 우회하지 마라. 그 제약이 숨은 상태를 막는 핵심 메커니즘이다. 답답하면 변수 이름을 새로 짓거나, 셀을 합쳐라.


에필로그 — 노트북의 다음 10년

Jupyter는 2014~2024년 데이터 사이언스 워크플로의 표준이었다. 2026년에 그 표준이 깨지고 있는 게 아니다 — 표준이 분화하고 있다.

  • 탐색·내부 도구·앱 → Marimo (또는 Pluto for Julia).
  • 출판·논문·책·다포맷 → Quarto.
  • JS-네이티브 시각화·정적 사이트 → Observable Framework.
  • 빠른 일회용·다언어 커널 → 여전히 Jupyter.

그리고 모두의 아래에 Polars·DuckDB·Arrow가 새 컴퓨트 레이어로 깔린다.

핵심은 노트북이 더는 "코드를 한 번 굴리는 도구"가 아니라는 점이다. 노트북은 재현 가능한 협업 자산(Marimo의 .py), 출판 가능한 문서(Quarto), 인터랙티브 앱(Marimo run mode), 데이터 스토리(Observable)의 네 가지 인터페이스를 한 파일이 모두 제공할 수 있는 시대가 됐다.

노트북은 종착지가 아니라 인터페이스다. 좋은 인터페이스는 모든 독자에게 다른 모양으로 열린다 — 데이터 과학자에게는 코드, 동료에게는 앱, 임원에게는 PDF, 인터넷에게는 정적 사이트.

체크리스트 — 새 노트북 워크플로로 옮길지 결정

  • 노트북을 PR로 리뷰하는 일이 있는가? (있으면 Marimo로 가라.)
  • "Run All"이 깨진 적이 있는가? (있으면 Marimo로 가라.)
  • 결과를 외부에 출판하는가? (있으면 Quarto를 추가하라.)
  • 비-데이터 동료가 결과를 직접 만지길 원하는가? (있으면 Marimo 노트북-as-앱.)
  • 데이터가 수 GB를 넘는가? (있으면 Polars·DuckDB로 옮겨라.)
  • Julia 생태계를 쓰는가? (있으면 Pluto.jl.)
  • JS·시각화 중심인가? (있으면 Observable Framework.)

안티-패턴 요약

  • Marimo를 그냥 Jupyter 대체품으로만 쓰기 (반응형·앱 모드를 안 쓰면 이득 없음).
  • Quarto에 무거운 가공 박기 (별도 스크립트로 분리).
  • 노트북을 영원히 노트북으로 두기 (안정화되면 모듈로).
  • Polars/DuckDB를 안 써보고 Pandas만 고집 (GB대 이상에서는 무너진다).
  • Marimo의 재정의 제약 우회 (숨은 상태가 다시 돌아온다).

다음 글 예고

  • 데이터 파이프라인의 새 표준 — Dagster·Prefect·Airflow를 2026년 관점에서 비교. 노트북이 탐색이라면, 파이프라인은 생산이다.
  • Polars 깊게 보기 — Pandas에서 옮길 때 알아야 할 표현식 모델·lazy 평가·Streaming engine.
  • DuckDB로 단일 노드 100GB Parquet 쿼리하기 — Spark 없는 분석 워크플로.

참고 / References

The New Python Notebook Stack in 2026 — Marimo, Quarto, and the Post-Jupyter Data Workflow (Observable, Pluto, Polars, DuckDB)

Prologue — Jupyter's hidden-state problem

Even in 2026, the same scene repeats weekly. A data scientist hands a notebook to a colleague, the colleague hits "Run All", and the fifth cell explodes. A variable is undefined. But on the sender's machine, it ran fine.

The cause is almost always the same — hidden state. If you execute cells in arbitrary order without ever running top-to-bottom, the notebook text and the kernel's memory drift apart. The sender's kernel still has df alive, but the notebook file no longer contains the cell that defined it.

This isn't a Jupyter bug. It's a design decision. Jupyter treats out-of-order execution as a feature — that's the essence of a REPL. But that freedom becomes a liability the moment two people collaborate, the moment you ship to CI, the moment six months pass.

"Notebooks are heaven for one person and hell for two or more."

Between 2024 and 2026, serious efforts emerged to pay down this debt with a different design. Marimo statically analyzes the dataflow between cells and re-runs cells based on dependencies — not cell order, but data dependency, is truth. Quarto takes notebooks as input and publishes them as HTML, PDF, websites, books, paper supplements — multi-language, multi-format. Observable Framework builds JS-native notebooks into static sites. Below all of them, the compute layer is moving from Pandas to Polars and DuckDB.

This post is a deep look at that stack — what Jupyter got wrong, how Marimo solved it, how Quarto publishes, and when you should still reach for Jupyter.


1. The four debts of Jupyter

Jupyter (more precisely: IPython kernel + ipynb JSON + Notebook/Lab UI) has been the standard data-science environment since 2014. It got many things right — putting outputs next to code, mixing inline visualizations and Markdown and LaTeX into "executable documents." But twelve years in, four debts have accumulated.

Debt 1 — Hidden state

Even if you execute cells in the order In[14], In[3], In[27], the notebook file does not record that order. There's no guarantee that "Run All" produces the same result later. Truth lives in the kernel running in memory, not in the notebook file, and that kernel evaporates the moment it shuts down.

Debt 2 — Not diffable (.ipynb is JSON)

.ipynb packs code, Markdown, images, outputs, and execution counters into one JSON file. When a PR reviewer opens the diff on GitHub, they see an endless single line of base64-encoded PNG. The fact that workarounds like nbstripout and jupytext exist is itself the signal — the format is fundamentally incompatible with git.

Debt 3 — Not reproducible

Combine the first two debts and reproducibility falls apart. "I ran this notebook" does not mean "in this environment, in this order, on this data." Open a paper-supplement notebook six months later and the dependencies are broken, outputs don't match code.

Debt 4 — One-directional UI

Jupyter assumes a "top-to-bottom" linear model. There's no graph view showing data dependencies — to know where the output of this cell flows, or which cell depends on which, you have to track it mentally. Past fifty cells, this becomes practically impossible.

These four debts reinforce each other. Hidden state means no reproducibility, no reproducibility means diffs are meaningless, meaningless diffs mean no collaboration.


2. Marimo — redesigning the notebook

Marimo is an open-source Python notebook that emerged in late 2023 and, as of mid-2026, sits in the 0.10.x series. Its starting point is simple — "What changes if we redesign the notebook from scratch?"

Three core design decisions:

Decision 1 — Store as .py files

Marimo notebooks are not .ipynb. They are plain Python files. Each cell is a decorated function.

import marimo as mo

app = mo.App()


@app.cell
def __():
    import polars as pl
    df = pl.read_csv("sales.csv")
    return (df,)


@app.cell
def __(df):
    summary = df.group_by("region").agg(pl.col("revenue").sum())
    return (summary,)


@app.cell
def __(summary):
    summary
    return ()


if __name__ == "__main__":
    app.run()

Two things follow. First, the file also runs as a Python script — just python notebook.py. Second, git diffs are meaningful. Rename a variable and you see the rename in the diff. There is no base64-encoded PNG anywhere.

Decision 2 — Dataflow graph, not cell order

Marimo statically analyzes the notebook to find which variables each cell defines and which it references. This becomes a directed acyclic graph (DAG). When you run a cell, Marimo automatically re-runs every cell that depends on it — the same model as an Excel spreadsheet.

The result: hidden state is impossible. Reassign a variable and every cell using it updates instantly. Delete a cell and any cell using its variables immediately raises "not defined." The notebook file and the kernel state stay in sync by construction.

Decision 3 — Notebook-as-app mode

Marimo can launch the same file in two modes:

  • Edit mode — a normal notebook for writing and running cells.
  • Run mode — launch with marimo run notebook.py and cells disappear, leaving only UI components (sliders, dropdowns, tables) as an interactive app.

UI components live in the mo.ui module.

@app.cell
def __(mo):
    region = mo.ui.dropdown(
        options=["US", "EU", "APAC"],
        value="US",
        label="Region",
    )
    region
    return (region,)


@app.cell
def __(df, region):
    filtered = df.filter(pl.col("region") == region.value)
    filtered
    return (filtered,)

Change the dropdown and every cell consuming the value re-runs automatically. The notebook itself becomes an app — no Streamlit or Gradio needed.

Additional wins

  • WASM deployment — Marimo exports notebooks as static HTML via Pyodide. No server. Users interact directly in the browser.
  • AI assist — Per-cell AI completion (model-agnostic) is built in.
  • Type checking — Because it's a .py file, mypy and pyright just work.
  • Testing — Import notebook cells into pytest and unit-test them.
  • Pure-function cells — Each cell is a function with explicit inputs and outputs, which makes unit testing natural.

3. Jupyter vs Marimo — same task, different code

Here is the same workflow (load data, filter, visualize) in both.

Jupyter cells

# Cell 1
import pandas as pd
df = pd.read_csv("sales.csv")
df.head()

# Cell 2
df_us = df[df["region"] == "US"]
df_us.shape

# Cell 3
import matplotlib.pyplot as plt
df_us.groupby("month")["revenue"].sum().plot(kind="bar")
plt.show()

# Cell 4 — thirty minutes later, forgot the original df
df = pd.read_csv("sales_v2.csv")  # df_us still points at the old data!

Run Cell 4, look at the chart in Cell 3 — it won't update, because df_us captured the old df. Memory state and code state are out of sync. Hidden state.

Marimo cells

@app.cell
def __():
    import polars as pl
    df = pl.read_csv("sales.csv")
    df
    return (df, pl)


@app.cell
def __(df, pl):
    df_us = df.filter(pl.col("region") == "US")
    df_us
    return (df_us,)


@app.cell
def __(df_us):
    chart = df_us.group_by("month").agg(pl.col("revenue").sum())
    chart
    return (chart,)

Change the first cell to load sales_v2.csv and Marimo automatically re-runs the second and third cells. The chart updates immediately. Memory state and code state stay synchronized by construction.

Also: Marimo forbids redefining the same variable in two cells. It raises "this variable is already defined elsewhere" and forces you to pick one. This is jarring at first — and hidden state vanishes forever.


4. Quarto — turning notebooks into publishable documents

If Marimo is "redesign the notebook," Quarto is "publish the notebook." Quarto is the successor to RStudio's RMarkdown, hit 1.0 in 2022, and is at 1.5+ as of 2026. It handles multiple languages (Python, R, Julia, Observable JS) and multiple formats (HTML, PDF, EPUB, Word, websites, books) from one tool.

A Quarto document

Quarto uses .qmd extension, with YAML frontmatter plus Markdown plus code fences.

---
title: "2025 Sales Analysis"
author: "Data Team"
date: "2026-05-14"
format:
  html:
    code-fold: true
    toc: true
  pdf:
    documentclass: article
execute:
  echo: true
  warning: false
---

## Load the data

We analyze this quarter's revenue.

\`\`\`{python}
import polars as pl
df = pl.read_csv("sales.csv")
df.head()
\`\`\`

## Regional totals

\`\`\`{python}
df.group_by("region").agg(pl.col("revenue").sum())
\`\`\`

(Backtick fences are escaped above so MDX doesn't get confused. In a real .qmd you just use three backticks.)

Build with quarto render report.qmd --to html for interactive HTML, or --to pdf for a LaTeX-quality PDF. The code runs, and outputs are embedded in the document.

Output formats Quarto supports

  • HTML — search, themes, interactive widgets, Mermaid diagrams, MathJax.
  • PDF (LaTeX) — academic-paper typography. The book class also generates books.
  • Websites — bundle multiple .qmd files into a static site (Jekyll/Hugo-style).
  • Books — multi-chapter PDF/HTML books. R for Data Science is built with Quarto.
  • Reveal.js slides — put executed code outputs into slides.
  • Word, EPUB — for publisher workflows.

Multi-language — Python, R, Julia, Observable JS in one file

Quarto reads the code-fence language identifier and dispatches to the right kernel. python fences run on the Jupyter kernel, r on knitr, julia on IJulia, {ojs} on the Observable JS runtime. You can preprocess data in Python, fit a model in R, and render an interactive chart in Observable JS — all in the same document.

Quarto + Marimo, Quarto + Jupyter

Quarto accepts not just .qmd but also Jupyter notebooks (.ipynb) and Marimo notebooks as input. Quarto is notebook-format-agnostic. The workflow "author in Marimo, publish with Quarto" is natural.

Academic papers and supplements

Quarto ships official templates for journals like Journal of Statistical Software — APA, IEEE, Nature, Elsevier formats are one frontmatter line. Citations pull straight from BibTeX or Zotero and format via CSL. The paper and its analysis code live in the same file — the endgame of reproducibility.


5. Observable Framework — the evolution of JS-native notebooks

Observable, started around 2018 by Mike Bostock (creator of d3.js), is a JS-native notebook. Cells form a reactive graph (the inspiration for Marimo) and visualization is a first-class citizen. The catch was lock-in to Observable.com hosting.

Observable Framework, announced in 2024, brought the model to a static-site builder. Embed JS cells in .md files, run observable build, and you get static HTML/CSS/JS. Host on Vercel, Netlify, GitHub Pages — wherever.

Where Observable Framework shines:

  • Data dashboards — internal analytics sites, KPI tracking, operational dashboards.
  • Interactive data storytelling — d3 visualizations, charts that respond to user input, data journalism.
  • Build-time data loaders — refresh data via Python/R/shell at build time, freeze the result as static assets.

If Marimo answers "I'm a Python user and I want interactivity," Observable Framework answers "I'm JS-and-visualization first and I want publishable artifacts." They aren't competitors — they serve different markets.


6. Pluto.jl — Julia's reactive notebook, and the original

Pluto.jl is a reactive notebook for Julia, and Marimo's direct inspiration. It launched in 2020 and was the first to bring the "cell-dataflow graph leads to auto-rerun" model to the notebook world.

Core principles Pluto introduced:

  • The notebook file is the single source of truth — not the in-memory kernel; the file defines state.
  • Cell order is irrelevant — wherever a cell lives in the file, the dependency graph determines execution order.
  • The same variable cannot be redefined across cells — that would make the graph ambiguous.
  • Stored as .jl files — also runs as a plain Julia script.

Marimo brought this model to Python. Pluto has meanwhile gone deeper with Julia's data-science stack (SciML, Plots.jl, DataFrames.jl). If you use Julia, Pluto is the default choice.


7. The new compute layer — Polars and DuckDB

It's not just the notebook UX that changed. The compute layer underneath has too.

Polars — Rust-based DataFrame

Polars is a DataFrame library written in Rust, with Apache Arrow as its native memory format. Compared to Pandas:

  • Fast — multithreaded by default, columnar memory layout, query optimizer.
  • Lazy evaluation — build a query, then .collect() to execute all at once (the Spark/SQL model).
  • Consistent API — none of Pandas' accumulated inconsistencies (apply vs applymap, index hell, etc.).
  • Small memory footprint — Arrow zero-copy so multiple tools share the same data.

In a heavy notebook, moving to Polars is commonly 5x–30x faster.

DuckDB — in-process OLAP database

DuckDB is the OLAP (analytic) cousin of SQLite. Embedded, columnar, optimized for single-node analytics. It queries files, Parquet, CSV, JSON, and Arrow directly.

Where DuckDB shines in a notebook:

import duckdb
duckdb.sql("""
    SELECT region, SUM(revenue)
    FROM 'sales/*.parquet'
    WHERE date >= '2026-01-01'
    GROUP BY region
""").to_df()

Query 100 GB of Parquet directly from a notebook — no Spark cluster needed. Polars and DuckDB exchange data via Arrow with zero copies, so mixing them in the same pipeline has no overhead.

When this compute layer sits below Marimo and Quarto, the data volume a single notebook can handle moves from single-digit GB to three-digit GB.


8. Comparison matrix

ItemJupyterMarimoQuartoObservable FrameworkPluto.jl
LanguagePython (many kernels)PythonMulti-languageJS-centricJulia
File format.ipynb (JSON).py.qmd.md.jl
Reactive executionNoYes (DAG)No (publishing tool)YesYes (original)
Git diffPoorGoodGoodGoodGood
Notebook-as-appSeparate (Voila, Panel)Built-inSeparateStatic-site buildPartial
Static exportHTML (no rerun)HTML/WASMHTML, PDF, sites, booksStatic sitesHTML
Learning curveLowLow to mediumMediumMedium (needs JS)Low
Ecosystem sizeMassiveGrowingLargeVisualization-focusedMid (Julia)
Primary useEDA, teaching, researchEDA, internal tools, appsReports, papers, books, blogsDashboards, data journalismJulia scientific computing
LicenseBSD-3Apache-2.0MITISCMIT

9. Real workflow scenarios

Scenario A · Daily EDA for a data scientist

  • Before: Jupyter Lab plus Pandas. Re-run cells, fall into hidden state. Send results to a colleague as PNG screenshots.
  • After: Marimo plus Polars plus DuckDB. Notebook is .py, so it lands as a PR. The colleague runs marimo run and reproduces the result immediately. Drop a slider into a cell to create a "select region" UI so non-data colleagues can also poke at it.

Scenario B · Quarterly report for executives

  • Before: Analyze in Jupyter, screenshot into PowerPoint. Next quarter, re-screenshot.
  • After: Quarto website. Refresh data each quarter and quarto render. Output ships as static HTML on the internal intranet. Search, themes, and print-ready PDF all in one build.

Scenario C · Academic paper plus supplement

  • Before: LaTeX for the paper, separate .ipynb for analysis. Reviewer comments arrive six months later and nobody can find which notebook produced which figure.
  • After: Quarto plus Marimo. Paper body and analysis code in the same .qmd. quarto render --to pdf for LaTeX output. Build the same file with --to html for the supplement. A reproducible paper.

Scenario D · Internal data tool

  • Before: Build a separate app in Streamlit/Gradio. Data scientist works in the notebook, engineer ports it to an app — written twice.
  • After: Marimo notebook-as-app. Drop mo.ui components into the notebook and serve with marimo run. No engineering handoff. One file is both notebook and app.

Scenario E · Data journalism, interactive stories

  • Observable Framework. d3.js visualizations, charts that respond to user input, build-time data loaders (preprocess in Python, freeze as static JSON), shipped as a static site.

Scenario F · Julia scientific computing

  • Pluto.jl. The Julia ecosystem (SciML, DifferentialEquations.jl) runs deep, and the reactive model fits simulations and parameter sweeps well.

10. When to still pick Jupyter

Powerful as Marimo and Quarto are, Jupyter is still the right call in places.

Stay on Jupyter when

  • You depend on specialty kernels — Spark, PyTorch Distributed, Wolfram Language. Jupyter's kernel protocol is the standard there.
  • You already run JupyterHub or Binder — Universities and research labs cannot rip these out overnight.
  • One-off scratch work — For a five-line experiment, Jupyter's lower setup cost wins.
  • Teaching — When the lecture's whole point is the cell-order, In[N]/Out[N] mental model. (Though Marimo-taught courses are growing in 2026.)
  • VS Code, Cursor integrated notebook UI.ipynb is deeply wired into the IDE-first workflow.

Signals to move to Marimo

  • You treat notebooks as collaboration assets (PRs, reviews).
  • "Run All" breaks frequently.
  • You want to ship notebooks as internal tools.
  • Non-data colleagues need to use the notebook.

Signals to move to Quarto

  • You publish results externally on a regular cadence (blog, report, paper).
  • The same content must ship in multiple formats (HTML and PDF).
  • You make books or multi-chapter documents.
  • You want to combine Python, R, and JS analysis in one document.

11. Migration — how to move

Jupyter to Marimo

Marimo provides marimo convert notebook.ipynb for automatic conversion. It will not be perfectly clean — patterns Jupyter permitted (redefining a variable across cells) error in Marimo. Budget thirty to sixty minutes of hand-tuning after the first conversion.

Recommendation: start new notebooks in Marimo, leave existing .ipynb alone. Do not migrate everything at once.

Jupyter to Quarto

Quarto accepts .ipynb directly as input — no conversion needed. Just run quarto render notebook.ipynb. The value-add is the expanded output formats.

Marimo plus Quarto together

The strongest combo. Author and explore interactively in Marimo, then publish with Quarto. Since both speak .py and .ipynb, the pipeline is clean.


12. Anti-patterns

The new stack has its own traps.

Anti-pattern 1 — Using Marimo as a Jupyter drop-in

Marimo's real value is reactivity, .py storage, and notebook-as-app. Skip those and you pay the learning curve for zero gain. Use UI components, lean on reactive dependencies.

Anti-pattern 2 — Embedding huge analysis in a Quarto doc

Quarto build times explode. Split heavy preprocessing into a separate script, and let Quarto only load the resulting data (Parquet or CSV). Or cache with freeze: true.

Anti-pattern 3 — Keeping notebooks as notebooks forever

Even with Marimo and Quarto, putting all code into notebooks is a trap. Stable data pipelines belong in regular .py modules. Notebooks are for exploration, documentation, and interface. Notebooks are not the destination — they are an interface.

Anti-pattern 4 — Refusing to try Polars/DuckDB and sticking with Pandas

Past a few GB, Pandas falls apart in both memory and speed. The cost of moving to Polars is under a week. That week halves your wait time for the next year.

Anti-pattern 5 — Hacking around Marimo's redefinition rule

When the rule feels annoying, do not hack around it with globals(). That rule is the core mechanism stopping hidden state. Rename the variable, or merge the cells.


Epilogue — the next decade of notebooks

Jupyter was the standard data-science workflow from 2014 to 2024. In 2026, that standard isn't broken — it is splitting.

  • Exploration, internal tools, apps → Marimo (or Pluto for Julia).
  • Publishing, papers, books, multi-format → Quarto.
  • JS-native visualization, static sites → Observable Framework.
  • Fast one-off scratch, multi-language kernels → still Jupyter.

And below them all, Polars, DuckDB, Arrow form the new compute layer.

The point: a notebook is no longer just "a tool to run code once." A notebook can simultaneously be a reproducible collaboration asset (Marimo's .py), a publishable document (Quarto), an interactive app (Marimo run mode), and a data story (Observable) — all four interfaces from one file.

The notebook is not the destination — it is an interface. A good interface opens differently for every reader: as code for the data scientist, as an app for the colleague, as a PDF for the executive, as a static site for the internet.

Checklist — should you move?

  • Do you review notebooks via PR? (If yes, move to Marimo.)
  • Has "Run All" ever broken? (If yes, move to Marimo.)
  • Do you publish results externally? (If yes, add Quarto.)
  • Do non-data colleagues need to use the result? (If yes, Marimo notebook-as-app.)
  • Is your data past a few GB? (If yes, move to Polars and DuckDB.)
  • Do you use the Julia ecosystem? (If yes, Pluto.jl.)
  • Are you JS-and-visualization first? (If yes, Observable Framework.)

Anti-pattern summary

  • Using Marimo as a Jupyter drop-in (skip reactivity and app mode, get no gain).
  • Stuffing heavy preprocessing into Quarto (split into a separate script).
  • Keeping notebooks as notebooks forever (stabilize, then promote to modules).
  • Refusing to try Polars/DuckDB and sticking with Pandas (falls apart past a few GB).
  • Hacking around Marimo's redefinition rule (hidden state comes right back).

Next posts

  • The new data-pipeline standard — Dagster, Prefect, Airflow compared from a 2026 lens. If notebooks are exploration, pipelines are production.
  • Polars in depth — the expression model, lazy evaluation, and the Streaming engine for Pandas migrants.
  • Querying 100 GB of Parquet on a single node with DuckDB — analytic workflows without Spark.

References