DuckDB in Action·Manning, 2024· 제9장 정독
9 BUILDING AND DEPLOYING DATA APPS
원서 193–221쪽

SQL을 모르는 사람에게 데이터를 건네는 두 가지 길

제8장이 클라우드에 데이터를 발행해두었다. 이제 그 위에 제품을 짓는다. 많은 데이터 분석가는 SQL로 DuckDB의 데이터를 다루는 데 만족하겠지만, 다른 사용자들은 코드를 쓰지 않아도 되는 인터페이스를 선호한다. 그들에게 값을 주려면 DuckDB에서 정보를 꺼내오는 애플리케이션을 만들어야 한다. 이 장은 그 길을 둘로 나눠 걷는다. 파이썬만으로 대화형 웹 앱을 짓는 Streamlit과, 코드를 거의 쓰지 않고 눌러서 대시보드를 만드는 Apache Superset이다. 한쪽은 코드가 먼저이고 다른 쪽은 클릭이 먼저인데, DuckDB를 다루는 방식은 양쪽에서 조금도 달라지지 않는다.

이 장이 다루는 것
Streamlit으로 대화형 웹 애플리케이션 짓기 · Streamlit Community Cloud로 배포 · Plot.ly로 대화형 차트 렌더링 · Apache Superset으로 BI 대시보드 만들기 · 맞춤 SQL 질의로 차트 만들기
절 구성
9.1 – 9.2 및 하위 절 열 개, 요약
실린 것
도판 29점(그중 스물여덟이 화면 갈무리) · 목록 5건 · NOTE 5건 · 사이드바 1건
다루는 데이터
제8장이 만든 atp.duckdb · matches · players · levels · rounds
matches tourney_id: varchar tourney_name: varchar surface: varchar draw_size: bigint tourney_level: varchar tourney_date: date match_num: bigint winner_id: bigint loser_id: bigint winner_rank: varchar winner_rank_points: varchar loser_rank: varchar loser_rank_points: varchar ⋯ 원서 도판은 여기까지만 적었다 levels name: varchar short_name: varchar rank: bigint players player_id: bigint name_first: varchar name_last: varchar hand: varchar dob: date ioc: varchar height: bigint wikidata_id: varchar name_full: varchar rounds name: varchar order: bigint tourney_level winner_id loser_id round matches를 가운데 두고 세 테이블이 붙는다. 등급 이름은 levels에서, 선수 정보는 players에서, 라운드의 순서는 rounds에서 온다. 점선으로 표시한 rounds는 제8장에서 한 번도 만들어지지 않은 테이블이다. 뒤에서 따로 다룬다.
〈도판 9.1〉ATP 데이터베이스 스키마. 원서 그림의 구성을 옮겨 새로 작도했다. 관계에 관여하는 컬럼을 색으로 구분했다.
← 좌우로 밀어 보기 →

어떤 데이터셋은 표 형태로 표상할 수 있지만, 차트로 시각화하고 요약하는 편이 도움이 되는 경우가 더 잦다. 그렇게 하면 데이터가 더 접근하기 쉬워지고, 사용자가 질문에 답하고 통찰을 제시하고 결정을 뒷받침할 수 있게 된다. 대부분의 데이터 기반 쓰임에 결정적인 일이다.

그런데 저자들이 여기서 선을 하나 긋는다. 데이터셋의 뜻을 왜곡하지 않는 이상적인 차트를 만드는 일은 다른 책의 주제다. 이 장은 기술적인 면에 초점을 두고, 접근 가능한 프론트엔드를 짓기 위한 기존 도구들과 DuckDB의 통합을 탐색한다. 할 수 있는 것과 할 수 없는 것을 미리 갈라놓는 이 절제가 미덥다.

먼저 Streamlit으로 최종 사용자를 향한 애플리케이션을 만들고, 나중에 Apache Superset으로 비즈니스 인텔리전스(BI) 대시보드를 만든다. 그리고 둘의 차이를 이렇게 규정한다.

대화형 애플리케이션은 통상 완성된 보고서에 초점을 두고 그 보고서를 걸러내는 정도로 상호작용을 제한한다. BI 대시보드는 통상 가능한 여러 원천의 데이터를 총체적 개관이 가능한 방식으로 집계하며, 원시 숫자가 아니라 전용 차트에 기반한다. 제9장 도입, 두 도구의 성격 구분
이 장을 읽는 방식에 대하여

이 장에는 도판이 스물아홉 점 실려 있는데, 그중 스물여덟이 Streamlit과 Superset의 화면 갈무리다. 재현할 수 없고 재현해서도 안 되는 것들이므로, 이 문서는 그 화면들을 점선 상자에 무엇을 어떻게 누르는지의 서술로 대신했다.

재현 가능한 것은 둘뿐이다. 〈도판 9.1〉의 스키마 도해〈도판 9.12〉의 Superset 개념 관계도다. 그 둘만 원서 구성을 옮겨 새로 작도했다.

대신 이 문서는 원서가 하지 않은 일을 하나 한다. 9.1절이 열 쪽에 걸쳐 조각조각 흘려놓은 app.py를 한자리에 모아 조립 과정으로 보여주는 것이다. 원서는 완성된 파일을 끝까지 한 번도 보여주지 않는다.

9.1

Streamlit으로 맞춤 데이터 앱 짓기Building a custom data app with Streamlit

ATP 데이터는 제5장과 제8장에서 이미 다뤘다. 제5장에서는 여러 CSV 원천의 랭킹을 한 테이블로, 결국 하나의 Parquet 파일로 집계했다. 제8장에서는 데이터 파이프라인의 일부로 웹 자원에서 선수 데이터를 소비했다. 그 결과가 〈도판 9.1〉의 DuckDB 데이터베이스다.

그런데 문제가 있다. 이제 여러분은 이 데이터베이스를 쓰는 법을 알지만, 여러분의 사용자는 반드시 그렇지 않다. 그들은 SQL을 모를 수도 있고, 데이터베이스에 직접 접근할 수 없을 수도 있다. 애플리케이션이 그들에게 데이터와 구조를 항해하도록 도와줄 수 있고, 대화형 필터와 유용한 시각화를 결합해줄 수 있다. 목표는 사용자가 선수 간 상대 전적 통계를 검색하고 분석할 수 있는 플랫폼이다.

Streamlit이란 무엇인가 9.1.1

Streamlit은 파이썬만 써서 대화형 웹 앱을 만들 수 있게 해주는 라이브러리다. 자바스크립트 기반 프론트엔드 라이브러리나 프레임워크에 대한 지식이 전혀 필요하지 않다. 흔히 풀스택 엔지니어가 아닌 데이터 엔지니어, 데이터 과학자, 백엔드 개발자에게 이것은 데이터 주도 애플리케이션을 빠르게 프로토타이핑하고 짓는 좋은 방법이다.

그리고 이 절에서 못 박아둘 문장이 나온다. 데이터·BI 분야의 다른 시각화 도구들이 로우코드 시각 빌더인 것과 달리, Streamlit은 코드 우선(code-first) 도구다. pandas, scikit-learn, matplotlib, DuckDB처럼 이미 익숙한 도구와 라이브러리로 파이썬을 써서 앱을 쓴다. 그래서 데이터나 기계학습 앱의 프론트엔드를 만드는 데 특히 요긴하다. 덧붙여 Streamlit을 만든 회사 Snowflake Inc.가 앱을 무료로 배포하고 관리할 수 있는 Streamlit Community Cloud를 제공한다.

앱 짓기 9.1.2

Streamlit은 애플리케이션의 골격을 만들어주지 않는다. 대신 파이썬 스크립트로 표상되는 애플리케이션을 직접 만들며, 그것이 차트와 텍스트와 위젯과 표를 그려 넣을 캔버스가 된다. 이 비유가 이 도구의 성격을 정확히 말한다. 위에서 아래로 스크립트가 실행되고, 실행된 순서대로 화면에 그려진다.

아래는 원서 9.1절이 열 쪽에 걸쳐 조각조각 제시한 app.py를 한자리에 모은 것이다. 걸음마다 무엇이 더해지는지, 그리고 그 걸음이 어떤 목록에 해당하는지 확인할 수 있다.

이 걸음이 하는 일
해당 목록
더해진 것
앞 장과의 연결
← 좌우로 밀어 보기 →

터미널로 돌아가 streamlit run app.py를 실행하면 로컬 Streamlit 서버가 떠오르고 기본 웹 브라우저의 새 탭에서 앱이 열린다. 출력에는 Local URL: http://localhost:8501과 네트워크 URL이 함께 나온다.

TIP — 이 배치가 왜 특별한가

연결 한 줄에 대해 저자들이 붙인 단서가 이 장에서 기술적으로 가장 중요한 대목이다. 이 배치는 클라이언트–서버 기반 데이터베이스 배포와 다르다. 데이터베이스가 Streamlit 애플리케이션과 같은 프로세스 안에 임베드되어 돌아간다.

그 결과가 이것이다. 렌더링될 프론트엔드가 어떤 원격 데이터베이스 접속도 요구하지 않는다. 제1장에서 배운 임베디드 데이터베이스의 성질이 배포 아키텍처를 한 단계 단순하게 만들어버린 것이다. 데이터베이스 서버도, 접속 풀도, 네트워크 지연도 없다.

〈도판 9.2〉·〈도판 9.3〉 · 화면 갈무리

앞의 것은 제목만 있는 초기 Streamlit 앱의 화면이다. 저자들의 평이 담백하다. "여기까지는 그리 흥미롭지 않지만, 시작은 시작이다."

뒤의 것은 st.write(search_players("Novak"))를 실행했을 때의 화면으로, 검색 결과가 목록으로 렌더링된 모습이다. Streamlit은 데이터를 여러 형태로 써내는 일에 관한 것이며 리스트, 맵, 데이터프레임 등을 지원한다.

python — 준비된 문장으로 SQL 주입을 막는다 목록 9.2
def search_players(search_term):
    query = '''
    SELECT DISTINCT winner_name AS player
    FROM matches
    WHERE player ilike '%' || $search_term || '%'
    UNION
    SELECT DISTINCT loser_name AS player
    FROM matches
    WHERE player ilike '%' || $search_term || '%'
    '''
    values = atp_duck.execute(query, {"search_term":search_term}).fetchall()
    return [value[0] for value in values]
열두 줄 안에 이 책이 가르친 것이 셋이나 들어 있다. 첫째, 두 WHERE 절 모두 컬럼에 나중에 붙인 별칭을 참조한다. winner_name AS player로 별칭을 준 다음 WHERE player ilike …로 쓰는 것, 제3장 3.5.3절의 DuckDB 확장이다. 표준 SQL이라면 WHERE winner_name ilike …라고 두 번 적어야 한다.
둘째, 이름 붙은 매개변수 $search_term을 쓴 준비된 문장이다. SQL 주입의 가능성을 없애기 위한 것이며, 제6장 6.2.3절에서 그 원리를 더 자세히 다뤘다. 검색어를 문자열로 이어 붙이지 않는다는 원칙이 사용자 입력을 받는 첫 화면에서 곧바로 적용된다.
셋째, UNION이지 UNION ALL이 아니다. 제3장에서 그 차이를 배웠다. 한 선수가 이긴 경기와 진 경기에 모두 있으면 이름이 두 번 나올 텐데, UNION이 그 중복을 없애준다. 검색 자동완성 목록에 같은 이름이 두 번 뜨지 않는 이유다.
9.1.3

Streamlit 컴포넌트 쓰기Using Streamlit components

특정 선수의 경기만 보여준다면 앱이 꽤 심심할 것이다. 검색어 입력 필드를 만든다. 여기서 이 도구의 사고 방식이 드러난다. Streamlit은 컴포넌트로 생각한다. 클라이언트 쪽 상호작용을 위해 개별 HTML 조각과 자바스크립트 코드를 쓰는 대신, 컴포넌트의 사용을 선언하고 매개변수를 주면, 컴포넌트가 필요한 조각들을 웹 페이지에 렌더링해준다.

그런 컴포넌트 하나가 streamlit-searchbox다. GitHub의 m-wrzr/streamlit-searchbox로 공개된 라이브러리이며, 자동완성처럼 사용자가 이런 것에 기대하는 통상의 기능을 모두 제공한다.

목록 9.3에서 두 열짜리 배치를 만들 때 HTML을 어떻게 쓸지 생각할 필요가 없다. Streamlit에게 그렇게 해달라고 부탁하면 된다. 그리고 컴포넌트가 그 배치 안에 어떻게 놓이는지도 파이썬 키워드 with로 처리된다. leftright 변수를 정의해두고, 그 안에서 두 개의 범위를 만드는 것이다.

가장 중요한 것은 st_searchbox첫 인수다. 앞에서 정의한 search_players 함수 자체이며, 사용자가 렌더링된 검색 상자의 내용을 바꿀 때 그 함수가 호출된다. 함수를 값으로 넘기는 파이썬의 성질이 UI의 반응성으로 바뀌는 자리다.

〈도판 9.4〉·〈도판 9.5〉 · 화면 갈무리

앞의 것은 기본값이 채워진 두 개의 검색 상자다. Player 1은 Roger Federer, Player 2는 Rafael Nadal이다.

뒤의 것은 Andy Murray를 검색하는 모습이다. 저자들의 설명이 흐름을 잘 보여준다. Murr을 입력하는 순간 밑에 깔린 search_players가 호출되고, 결과가 UI로 되돌아온다. 결과를 클릭하는 순간 그것이 player1 변수에 할당되며, 그 변수는 컴포넌트와 렌더링된 검색 상자 양쪽을 표상한다.

선수 이름 목록은 좋지만 사용자가 관심 있는 것은 대개 그것이 아니다. 선택한 선수들 사이의 모든 경기를, 대회 등급과 그 경기가 치러진 라운드 정보와 함께 담은 보고서를 원할 것이다. 여기서 제6장이 되돌아온다. Streamlit은 pandas 데이터프레임을 대화형 표로 렌더링하는 데 훌륭한 지원을 하며, 추가 코딩이 전혀 필요하지 않다. 그리고 DuckDB에는 데이터프레임에 대한 고유 지원이 있다. 두 선수 이름을 매개변수로 받아 fetchdf 메서드로 결과를 데이터프레임으로 돌려주면 된다.

python — 세 테이블을 조인해 데이터프레임으로 받기 목록 9.4
matches_for_players = atp_duck.execute("""
SELECT
    tourney_date, tourney_name, surface, round,
    rounds.order AS roundOrder,                        -- 라운드의 순서
    levels.name AS level, levels.rank AS levelRank,    -- 대회 등급과 그 순위
    winner_name, score
FROM matches
JOIN levels ON levels.short_name = matches.tourney_level   -- 등급 이름을 가져온다
JOIN rounds ON rounds.name = matches.round                 -- 라운드 정보를 가져온다
WHERE (loser_name = $player1 AND winner_name = $player2) OR
      (loser_name = $player2 AND winner_name = $player1)
ORDER BY tourney_date DESC
""", {"player1":player1, "player2":player2}).fetchdf()
WHERE 절의 두 갈래가 이 질의의 요점이다. 누가 이겼는지 모르는 상태로 두 선수의 모든 맞대결을 찾아야 하므로 양방향을 다 적는다. 상대 전적을 다루는 질의의 정석이다.
rounds.order를 눈여겨볼 만하다. order는 SQL의 예약어인데 인용부호 없이 컬럼 이름으로 쓰였다. DuckDB가 맥락으로 구별해준 것이지만, 다른 데이터베이스로 옮길 때 걸릴 수 있는 자리이니 알아두는 편이 좋다.
이 질의는 제8장을 따라온 독자의 데이터베이스에서 실패한다. 제8장이 만든 테이블은 matches, players, levels 셋뿐이다. rounds라는 낱말은 제8장에 한 번도 나오지 않는다. 그런데 〈도판 9.1〉은 rounds를 스키마의 일원으로 그려놓고, 이 목록은 그것을 조인한다. 저장소에서 내려받는 atp.duckdb에는 들어 있겠지만, 제8장의 코드를 손으로 따라온 독자는 이 테이블을 손수 만들어야 한다.
〈도판 9.1〉의 matches 필드 목록도 온전하지 않다. 이 질의가 쓰는 round, score, winner_name, loser_name이 그 목록에 없다. 지면을 위해 줄인 것으로 보이는데, 하필 rounds로 가는 관계선이 목록에 없는 round 컬럼에서 나온다. 스키마 그림을 보고 질의를 쓰려는 독자에게는 곤란한 생략이다.
← 좌우로 밀어 보기 →
python — 세 열로 이름과 전적을 앉히기 9.1.3
left, middle, right = st.columns(3)          # 세 열짜리 컨테이너를 만든다
with left:
    st.markdown(
        f"<h2 style='text-align: left; '>{player1}</h1>",
        unsafe_allow_html=True
    )
with right:
    st.markdown(
        f"<h2 style='text-align: right; '>{player2}</h1>",
        unsafe_allow_html=True
    )

p1_wins = matches_for_players[                       # 선수 1의 승수를 센다
    matches_for_players.winner_name == player1].shape[0]
p2_wins = matches_for_players[                       # 선수 2의 승수를 센다
    matches_for_players.winner_name == player2].shape[0]

with middle:
    st.markdown(
        f"<h2 style='text-align: center; '>{p1_wins} vs {p2_wins}</h1>",
        unsafe_allow_html=True
    )

st.markdown(f'### Matches')
st.dataframe(
    matches_for_players.drop(["roundOrder", "level", "levelRank"], axis=1)
)
승수를 세는 방식이 간결하다. 데이터프레임을 winner_name == player1로 걸러 .shape[0]으로 행 수를 얻는다. 같은 일을 SQL로도 할 수 있었지만, 이미 손에 데이터프레임이 있으니 pandas로 하는 편이 짧다. 어느 쪽으로 계산할지 고를 수 있다는 것이 제6장이 만들어준 여유다.
렌더링할 데이터프레임에서 roundOrder, level, levelRank 셋을 떨어낸다. 화면이 너무 어수선해지기 때문이며, 그 필드들은 나중에 요긴해지지만 아직은 필요하지 않다. 실제로 roundOrder는 다음 절에서 점의 크기로 쓰인다. 질의로는 가져오고 표에서는 감춘다는 이 분리가 실무의 요령이다.
세 개의 st.markdown 호출이 모두 <h2>로 열고 </h1>로 닫는다. 태그가 어긋나 있다. 브라우저가 관용적으로 처리해주므로 화면은 나오지만 올바른 HTML이 아니다. 그리고 바로 옆 사이드바가 "이 마크업은 구조적으로 유효한 HTML로 변환된다"고 적어놓았으니, 설명과 예제가 같은 지면에서 어긋난 셈이다.
← 좌우로 밀어 보기 →
SIDEBAR

마크다운

마크다운은 쓰고 읽기 쉽도록 설계된, 평문 형식 문법을 가진 경량 마크업 언어다. 이 마크업은 구조적으로 유효한 HTML로 변환된다. 통상 README 파일의 문서를 형식화하는 데 쓰이지만, Streamlit에서도 markdown 함수로 지원된다.

여기서는 그것으로 선수 이름과 승수를 세 개의 제목—왼쪽, 오른쪽, 가운데 정렬—으로 렌더링하며, 마크다운 안에 맞춤 스타일을 준 HTML을 넣는다. 그래서 unsafe_allow_html=True도 설정해야 한다.

〈도판 9.6〉 · 화면 갈무리

앤디 머리와 라파엘 나달의 맞대결 표다. 위에는 두 이름과 그 사이의 전적이 큰 글씨로 놓이고, 아래에 경기 목록이 대화형 표로 놓인다. 사용자가 다른 선수 조합을 선택하면 내용이 자동으로 새로 고쳐진다.

이 자동 갱신이 Streamlit의 실행 모델에서 나온다. 위젯이 바뀌면 스크립트 전체가 위에서 아래로 다시 실행된다. 그래서 상태 관리 코드를 쓰지 않아도 화면이 데이터와 맞아 있게 된다.

9.1.4

plot.ly로 데이터 시각화하기Visualizing data using plot.ly

표 형태 표상에 만족할 사람도 있지만 더 시각적인 방식을 선호할 사람도 있다. 저자들이 이 절을 여는 문장이 정직하다. 훌륭한 차트는 데이터 중심 애플리케이션과 대시보드 양쪽에서 중요하지만, 훌륭한 차트를 만드는 일은 기술적으로도 내용적으로도 어렵다. 그러나 바퀴를 다시 발명할 필요는 없다. 상업 라이선스와 오픈소스 라이선스 양쪽으로 이미 만들어진 해법이 여럿 있다.

저자들이 좋아하는 라이브러리가 plot.ly다. 직관적인 API로 보기 좋은 차트와 도표를 만들 수 있게 해주는 데이터 시각화 도구이며, Streamlit이 plot.ly 차트 사용을 직접 지원한다.

머릿속에 그린 시각화는 산점도다. y축에 대회 이름을(열린 월·일 순으로 정렬), x축에 날짜를 놓는다. 차트의 점은 경기를 표상하며, 승자에 따라 다르게 색을 칠한다.

python — 데이터프레임을 SQL로 다시 정렬하고 점을 찍는다 9.1.4
import plotly.express as px

# 대회를 열린 월·일 순으로 정렬한 새 데이터프레임을 만든다.
sorted_matches_for_players = atp_duck.sql("""
FROM matches_for_players
ORDER BY strftime(tourney_date, '%m-%d')
""").fetchdf()

fig = px.scatter(sorted_matches_for_players,
    x="tourney_date",
    y="tourney_name",
    color="winner_name",
    size="roundOrder",                       # 라운드의 중요도가 점의 크기가 된다
    color_discrete_sequence=px.colors.qualitative.Plotly,
    category_orders={
        "tourney_name": (                     # plot.ly가 대회 이름을
            sorted_matches_for_players['tourney_name']   # 제멋대로 다시 정렬하지
            .drop_duplicates()                            # 못하게 한다
            .tolist()
        )
    },
)

st.plotly_chart(fig, use_container_width=True)
첫 줄이 이 장에서 가장 눈여겨볼 자리다. FROM matches_for_playersmatches_for_players는 테이블이 아니라 파이썬의 pandas 데이터프레임 변수 이름이다. 제6장의 오리 시험—테이블처럼 보이면 질의할 수 있다—이 여기서 또 쓰인다. 데이터프레임을 SQL로 정렬해 다시 데이터프레임으로 받는 왕복이 한 문장에 담겼다.
color_discrete_sequence를 지정한 이유를 저자들이 밝혀두었다. 기본값이 밝은 파랑 둘인데, 인쇄에서 구별하기 어렵다고 판단했기 때문이다. 책을 만드는 사람의 사정이 코드에 남은 자리인데, 색약을 고려한 팔레트 선택이라는 더 일반적인 교훈으로도 읽힌다.
NOTE Streamlit에는 Altair, Bokeh, PyDeck 같은 다른 데이터 시각화 도구로 만든 차트를 렌더링하는 함수도 있다. plot.ly가 유일한 선택은 아니다.
python — 연도마다 세로선을 그어 격자를 만든다 9.1.4
min_year = sorted_matches_for_players['tourney_date'].dt.year.min()
max_year = sorted_matches_for_players['tourney_date'].dt.year.max()
unique_years = list(range(min_year, max_year+2))   # 최대 연도 +1년까지 포함하려면 +2

for year in unique_years:
    fig.add_shape(
        type="line",
        x0=f"{year}-01-01", x1=f"{year}-01-01",
        y0=0, y1=1,
        yref="paper",                # y좌표를 도표 전체 높이의 비율로 해석한다
        layer="below",               # 점 아래에 깔린다
        line=dict(color="#efefef", width=2)
    )
max_year+2가 눈에 걸릴 수 있는데 옳다. 파이썬 range의 상한이 배타적이므로 최대 연도의 다음 해까지 선을 그으려면 +2가 필요하다. 제4장 4.9절과 제8장 목록 8.12에서 되풀이된 그 규칙이 여기서는 제대로 쓰였다.
yref="paper"layer="below" 두 옵션이 요령이다. 전자는 y좌표 0과 1을 도표 높이의 0%와 100%로 읽게 해 대회 이름 축과 무관하게 위아래로 꽉 찬 선을 만들고, 후자는 그 선을 데이터 점 아래에 깔아 격자가 데이터를 가리지 않게 한다.
〈도판 9.7〉·〈도판 9.8〉 · 화면 갈무리

앞의 것은 세로선 없는 산점도이고 뒤의 것은 연도 세로선을 넣은 산점도다. 무엇이 나아졌는지를 저자들이 직접 읽어준다.

지난 15년쯤 동안 두 선수가 언제 맞붙었는지 보기가 훨씬 쉬워졌다. 경기 대부분이 2008–2009년과 2010–2011년에 치러졌고 나머지는 흩어져 있다. 결과 표를 볼 때는 알아채기 어려웠던 사실이다. 나달이 이 라이벌 관계를 지배했다는 것은 이미 알고 있었지만, 이 시각화가 그 사실을 한층 더 강조한다.

차트가 표보다 나은 까닭을 이보다 구체적으로 보여주기 어렵다. 표에는 있었지만 보이지 않던 것—시간의 뭉침—이 눈에 들어온다.

데이터프레임을 표로 표상할지 차트로 표상할지가 DuckDB에서 그 데이터프레임을 가져오는 방식을 바꾸지 않는다. 9.1.4절을 맺는 문단

이어지는 설명이 이 장의 뼈대를 다시 짚는다. 표 형태 표상을 위해 Streamlit에 곧바로 넘길지, 아니면 plot.ly에 먼저 넘겨 차트를 만들고 그것을 Streamlit에 넘길지는 선택의 문제다. 그리고 저자들이 직접 비유를 댄다. 제6장에서 pandas에서 Apache Arrow로, Polars 데이터프레임으로 갈아타면서도 데이터베이스 기술과 상호작용하는 방식을 바꾸지 않았던 그 경험과 비슷하다. 갈아 끼울 수 있는 것은 바깥의 도구이고, 안쪽의 접점은 그대로다.

9.1.5

Community Cloud에 앱 배포하기Deploying our app on the Community Cloud

배포는 정적 파일을 웹 서버에 복사하는 것만큼 단순할 수도, 컨테이너와 서비스와 온갖 움직이는 부품을 세우는 것만큼 복잡할 수도 있다. 양쪽 극단에 다 그럴 만한 이유가 있을 때도 있지만, 흔히는 중간 어딘가가 답이다. 저자들이 그 중간을 이렇게 묘사한다. 어쩌면 누군가 이미 그 복잡한 인프라 전체를 세워두고 여러분의 애플리케이션에 "밀면 돌아가는(push-to-run)" 시나리오를 제공했을 수도 있다. Snowflake Inc.가 뒤를 받치는 Streamlit Community Cloud가 정확히 그것을 제공한다.

약관에 동의하면 로컬 Streamlit 서버 안에서 바로, UI 오른쪽 위의 Deploy 버튼을 눌러 새 애플리케이션을 운영에 밀어 넣을 수 있다. 다만 저장소에 requirements.txt가 없으면 만들어 넣어야 한다.

requirements.txt — 의존 넷 목록 9.5
streamlit
duckdb
streamlit-searchbox
plotly
네 줄이 이 애플리케이션의 전부다. 프론트엔드 프레임워크도, 번들러도, 패키지 매니저 설정도 없다. 제9장이 앞의 여덟 장에 붙는 방식이 이 파일에 압축되어 있다. DuckDB가 그 넷 가운데 하나로 들어간다. 데이터베이스가 애플리케이션의 의존 목록에 한 줄로 적히는 것이 임베디드 데이터베이스라는 말의 실제 뜻이다.
판본을 못 박지 않은 점을 눈여겨볼 만하다. 예제로는 간결하지만 운영에서는 판본을 고정하는 편이 안전하다. 제8장 사이드바에서 dagster-webserver의 의존 충돌을 urllib3==1.26.15로 못 박아 해결했던 일을 떠올릴 만하다.
〈도판 9.9〉·〈도판 9.10〉·〈도판 9.11〉 · 화면 갈무리

배포 흐름의 세 화면이다.

  1. 배포 방식 선택 — 도구가 Community Cloud와 맞춤 배포 사이의 선택을 준다.
  2. 오류 화면프로젝트가 원격 GitHub 저장소에 연결되어 있어야 한다. 그렇지 않으면 오류 메시지를 본다.
  3. 배포 화면앱의 URL을 고르고, 필요하면 브랜치나 주 파일 경로를 조정한다. Advanced Settings에서 쓸 파이썬 판본도 바꿀 수 있다.

완성된 앱은 mneedham/atp-head-to-head 저장소에 있고 atp-head-to-head.streamlit.app에 배포되어 있다.

프론트엔드 프레임워크의 세계로 들어가지 않고도, 우리는 한 줌의 Streamlit 컴포넌트로—이미 아는 하나의 언어로—대화형 웹 애플리케이션을 지을 수 있었다. 9.1절을 맺는 문단

그리고 저자들이 곧바로 단서를 붙인다. "그렇지만 인정하자면, 이 모든 것을 작동시키려면 우리는 여전히 코드를 좀 써야 했다!" 코드를 쓰는 데 그리 편안하지 않거나, 자신이나 사용자를 위해 뭔가를 빨리 띄우고 싶을 뿐이라면 다른 선택지가 있다. 대화형 애플리케이션이 아니라 대시보드를 만들고 싶다면 로우코드나 BI 도구로 그것을 이룰 수 있다. 차트를 확대하고 이동하고 표상을 바꾸는 정도의 상호작용은 여전히 얻으면서, 애플리케이션 코드는 하나도 쓰지 않아도 된다.

9.2

Apache Superset으로 BI 대시보드 짓기Building a BI dashboard with Apache Superset

Streamlit이 전문적인 프론트엔드 지식 없이도 애플리케이션에 대한 많은 통제권을 주지만, 때로는 프론트엔드를 위한 맞춤 코드를 쓰는 데 그만한 시간을 들이고 싶지 않다. DuckDB는 Hex, Tableau를 포함한 다양한 BI 도구와 통합되며, 이 장에서 쓸 것은 Apache Superset이다.

Apache Superset이란 무엇인가 9.2.1

오픈소스 데이터 탐색·시각화 플랫폼이며, Apache Airflow를 만든 것으로 가장 잘 알려진 막심 보슈맹(Maxime Beauchemin)이 개발했다. 제8장에서 조율 도구를 열거할 때 Airflow가 첫자리에 있었으니, 같은 사람이 만든 도구로 그 파이프라인의 결과를 보게 되는 셈이다.

Superset은 DuckDB를 포함한 많은 데이터베이스와 통합되고, 모든 것을 UI로 설정할 수 있다. 대부분의 쓰임을 감당할 시각화 유형이 미리 설치되어 있지만, 자바스크립트로 맞춤 시각화를 만들 수도 있다.

DuckDB와 붙는 접점이 SQLAlchemy다. 데이터베이스와 상호작용하기 위한 고수준 객체 지향 인터페이스를 제공하는 파이썬 라이브러리이며, 데이터베이스 접속을 관리하고 데이터베이스 스키마를 정의하고 그 데이터베이스에 대해 질의를 수행한다. 제6장이 "SQLAlchemy는 이 책의 범위를 벗어난다"고 미뤄두었던 그것이, 여기서 다리로 되돌아온다.

도구 선택의 실용적 근거도 밝혀둔다. BI 도구가 많지만 그중 다수가 계정 등록을 요구하는 클라우드 서비스다. 반면 Superset은 내 기계에서 시험해볼 수 있고, 나중에 운영에 배포하기로 정하면 Preset 호스팅 서비스를 쓸 선택지가 있다.

shell — 설치와 초기화 여섯 걸음 9.2.1
$ pip install apache-superset==3.1.2
# NOTE 집필 시점에 Superset은 Python 3.10에서만 작동한다.

$ pip install duckdb-engine     # DuckDB의 SQLAlchemy 드라이버

# 이 변수 없이는 Superset이 돌지 않는다. 새 터미널 세션마다 설정해야 한다.
$ export SUPERSET_SECRET_KEY="sYBpNA2+bQHvmXcojOVp53b8xbmN3ZQ"
$ export FLASK_APP=superset

# Superset의 메타데이터 데이터베이스를 초기화한다. 이것 없이는 작동하지 않는다.
$ superset init
$ superset db upgrade

# 관리자 사용자를 만든다.
$ superset fab create-admin \
    --username admin \
    --firstname Superset \
    --lastname Admin \
    --email admin@example.com \
    --password admin

logging was configured successfully
...
Recognized Database Authentications.
Admin User admin created.

# 8088 포트로 웹 서버를 띄운다.
$ superset run -p 8088 --with-threads --reload --debugger
# → http://localhost:8088 에서 admin/admin으로 로그인한다.
Superset은 Flask 애플리케이션을 담고 있으므로 Flask가 찾을 파일 이름을 정의하는 FLASK_APP 변수를 설정해야 한다. 그리고 더 안전한 설치를 위해 무작위 문자열로 된 비밀 키를 지정해야 한다.
Flask app builder(FAB)가 Superset이 그 위에 지어진 프레임워크다. 인증, 사용자 관리, 권한, 역할을 제공한다. 그래서 사용자 생성 명령이 superset fab …인 것이다. 저자들도 짚어두었듯 기계가 인터넷에 노출된다면 더 안전한 자격 증명을 써야 한다.
두 가지를 확인해두는 편이 좋다. 첫째, 원서는 superset initsuperset db upgrade보다 먼저 놓았다. init이 하는 일은 기본 역할과 권한을 만드는 것이고 그것은 메타데이터 테이블이 이미 있어야 가능하니, 새로 설치하는 경우라면 db upgrade가 먼저여야 순서가 맞는다. 공식 문서로 확인해볼 만한 자리다.
둘째, 비밀 키가 책에 문자열 그대로 인쇄되어 있다. 본문이 "무작위 문자열로"라고 적어두었으니 예시일 뿐이지만, 그대로 복사해 쓰면 공개된 비밀 키를 쓰는 셈이 된다. 자기 것을 새로 만들어야 한다.
← 좌우로 밀어 보기 →
9.2.2

Superset의 작업 흐름Superset's workflow

무엇을 만들기 전에 Superset이 쓰는 용어와 그것들이 서로 어떻게 관계하는지 이해해두는 편이 도움이 된다. 개념이 다섯이다.

SUPERSET CONCEPTS 데이터베이스 DuckDB 테이블 개념이 아니다 저장된 SQL 질의 SQL Saved Query 데이터셋 Dataset 차트 Chart 대시보드 Dashboard 데이터셋이 병목이 아니라 관문이다. 테이블에서 오든 저장된 SQL 질의에서 오든, 차트는 반드시 데이터셋 위에 세워진다.
〈도판 9.12〉Superset이 쓰는 용어와 그 관계. 원서 그림의 구성을 옮겨 새로 작도했다.
← 좌우로 밀어 보기 →
  • 데이터베이스 Database — 밑에 깔린 데이터 원천. 여기서는 DuckDB다.
  • 저장된 SQL 질의 SQL Saved Query — 데이터베이스의 한 테이블 또는 여러 테이블에 대한 맞춤 SQL 질의.
  • 데이터셋 Dataset저장된 SQL 질의 또는 데이터베이스 테이블을 감싼 껍데기.
  • 차트 Chart — 데이터셋에 기반한 시각화.
  • 대시보드 Dashboard — 차트의 모음.

그리고 한 가지를 분명히 해둔다. 데이터베이스를 이루는 테이블들은 Superset의 개념 가운데 하나가 아니지만, 데이터셋이 그것을 쓴다. 그래서 흐름은 이렇게 된다. 먼저 데이터베이스를 만들면 Superset이 스키마와 그에 따른 테이블을 자동으로 감지한다. 그다음 그 테이블에 기반한 데이터셋을 만들거나, SQL 질의를 써서 그것을 데이터셋으로 바꾼다. 마지막으로 그 데이터셋 위에 차트를 만들고, 그 차트가 대시보드에 쓰인다.

9.2.3

첫 대시보드 만들기Creating our first dashboard

대시보드는 중요한 업무 지표를 시각화해 상황을 재빨리 그려볼 수 있게 해준다. 그리고 문제가 있다면 데이터를 더 깊이 파고들 수 있도록 대시보드와 상호작용할 수 있기를 바란다.

NOTE

테니스 데이터셋 위에 만드는 어떤 대시보드도 업무 데이터셋 위에 만드는 것만큼 중요하지는 않을 것이다. 그래도 이 기법을 여러분 자신의 데이터에 어떻게 적용할지는 보이기를 바란다는 것이 저자들의 말이다.

〈도판 9.13〉–〈도판 9.17〉 · 화면 갈무리 다섯 점

DuckDB를 Superset에 붙이고 데이터셋을 만드는 흐름이다.

  1. 오른쪽 위의 Settings를 눌러 데이터베이스 접속 화면으로 간다.
  2. + Database 버튼을 누르면 새 데이터베이스 접속 모달이 뜬다.
  3. DuckDB를 누르고 SQL Alchemy URI 칸에 접속 문자열을 넣는다. 형식은 duckdb:///<your-database>이고 여기서는 duckdb:///atp.duckdb다.
  4. Data 링크 → Create Dataset데이터베이스·스키마·테이블을 고른다. matches를 고른다.
  5. 고르면 그 테이블의 모든 필드를 보여주는 미리보기가 나온다.

Test Connection 버튼으로 모든 것이 제대로 연결되었는지 확인할 수 있다.

TIP — 앞 절의 앱을 먼저 닫아야 한다

저자들이 붙인 실무 단서다. 앞 절의 Streamlit 애플리케이션이 아직 돌고 있다면, 데이터베이스 파일의 잠금을 풀기 위해 먼저 그것을 닫아야 한다.

9.1.2절에서 read_only=True로 열었는데도 잠금이 문제가 되는 것은, Superset이 그 파일을 쓰기 가능하게 열려 하기 때문이다. DuckDB는 여러 읽기 전용 접속은 함께 허용하지만 읽기 전용 접속과 읽기·쓰기 접속을 같은 파일에 동시에 두지는 않는다. 제7장에서 업로드 전에 .open md:로 잠금을 풀어야 했던 것, 제8장에서 Dagster는 그럴 필요가 없었던 것과 나란히 놓고 볼 만한 자리다.

NOTE

MotherDuck 데이터베이스에도 접속할 수 있다. duckdb:///md:<my_database>?motherduck_token=<my_token> 형식을 쓴다. 제7장 7.3.7절이 말한 "md: 접두사를 끼워 넣고 토큰 매개변수를 덧붙이는 것뿐"이라는 규칙이 SQLAlchemy URI 안에서도 그대로 성립한다.

〈도판 9.18〉–〈도판 9.22〉 · 화면 갈무리 다섯 점

차트를 만들어 대시보드에 앉히는 흐름이다.

  1. Create Dataset and Create Chart를 누르면 어떤 차트를 만들지 고르는 화면이 나온다. Bar Chart를 고른다.
  2. 세 칸을 설정한다. X-Axis는 tourney_date, Time Grain은 Year, Metrics는 COUNT(*)다.
  3. 차트를 만들면 오른쪽에 미리보기가 나온다.
  4. 마음에 들면 저장 화면에서 저장한다. 기존 대시보드에 넣을지 새 대시보드에 넣을지 묻는다. ATP Dashboard라는 새 대시보드에 넣는다.
  5. Save & Go to Dashboard를 누르면 차트를 담은 대시보드가 보인다.

설정한 것이 세 줄뿐이라는 점을 눈여겨볼 만하다. 축, 시간 단위, 지표. 9.1절에서 산점도 하나에 스무 줄 남짓의 파이썬을 쓴 것과 대비된다.

NOTE

대시보드 칸에 아무것도 입력하지 않으면 차트가 어느 대시보드에도 배정되지 않지만, 나중에 언제든 대시보드에 더할 수 있다.

이 장의 유일한 데이터 발견

만든 차트는 1967년부터 2023년까지 해마다 치러진 경기 수를 보여준다. 저자들이 질문을 먼저 세워둔다. 요즘 테니스 경기가 예전보다 훨씬 많이 치러진다는 말이 흔히 있으니, 데이터가 무엇을 보여줄지 흥미로울 것이다. 그리고 답이 통념을 뒤집는다.

요즘 경기가 더 많이 치러진다는 증거는 보이지 않는다. 오히려 치러지는 경기 수에 약간의 하향 추세가 있어 보인다. 9.2.3절, 막대 차트를 읽은 결과

여기에 이상치 하나를 짚는다. 2020년에 이상치가 있으며, 그것은 코로나19 대유행이 원인이다. 그해 3월부터 8월까지 테니스 시즌이 멈췄다. 데이터의 튀는 값을 세계의 사건으로 설명해내는 이 한 줄이, 대시보드를 만드는 일의 목적을 잘 보여준다. 차트는 숫자를 보여주고, 사람이 그 숫자에 이유를 붙인다.

9.2.4

SQL 질의로 데이터셋 만들기Creating a dataset from an SQL query

테이블에서 데이터셋을 만드는 것에 더해 SQL 질의에서도 데이터셋을 만들 수 있다. 새 데이터셋을 더할 때 상단 메뉴의 SQL Lab 링크를 누른다. 새 질의 탭을 더하고, 데이터베이스와 스키마를 고르고, 질의를 입력한다.

물음은 이것이다. 그랜드 슬램 우승자들이 시간이 흐르며 나이가 많아졌는가. 이 질문에 답하려면 matchesplayers 두 테이블에서 데이터를 가져와야 한다. 앞 절의 차트가 한 테이블에서 나온 것과 달라지는 지점이며, 그것이 이 절이 따로 있는 이유다.

sql — 서른 넘은 그랜드 슬램 우승자의 누적 수 9.2.4
SELECT
    winner_name,
    tourney_date,
    (tourney_date - dob)/365 AS age,                        -- 우승자의 나이를 계산한다
    COUNT(*) OVER (ORDER BY tourney_date) AS cumulative_count  -- 우승자의 누적 수
FROM matches
JOIN players ON players.player_id = matches.winner_id
WHERE round='F'              -- 각 대회의 결승만 돌려준다
  AND tourney_level = 'G'    -- 그랜드 슬램만 포함한다
  AND age >= 30              -- 서른 이상의 우승자만 돌려준다
ORDER BY tourney_date;
아홉 줄에 이 책의 여러 장이 겹쳐 있다. 날짜끼리 빼서 365로 나눠 나이를 얻고(제3장), WHERE 절에서 선택 목록의 별칭 age를 참조하고(제3장 3.5.3절), 프레임 절 없는 COUNT(*) OVER (ORDER BY …)로 누적 수를 세고(제4장 4.5절, 4.8절의 누적 수익과 같은 꼴), 두 테이블을 조인한다(제3장). 이 장이 앞의 장들 없이 성립하지 않는다는 것이 이 질의에 다 담겼다.
WHERE가 윈도 함수보다 먼저 적용되므로 cumulative_count는 필터를 통과한 행만 셈한다. 곧 "서른 넘은 그랜드 슬램 결승 우승" 사건만 순서대로 누적된다. 의도한 대로다. 제4장 4.6절이 WHERE·HAVING·QUALIFY의 적용 순서를 가르쳐둔 값이 여기서 나온다.
tourney_level = 'G'가 그랜드 슬램을 고른다. 제8장 목록 8.19의 등급 표에서 여섯 코드 가운데 유일하게 옳게 짝지어져 있던 코드가 G였는데, 이 질의가 쓰는 것이 바로 그 하나다. 운이 좋았다.
← 좌우로 밀어 보기 →
〈도판 9.23〉–〈도판 9.26〉 · 화면 갈무리 네 점

질의를 데이터셋으로 만들어 차트로 세우는 흐름이다.

  1. 질의를 붙여 넣고 Run을 누르면 서른 넘은 그랜드 슬램 우승자 목록이 나온다. 마음에 들면 Save Dataset을 골라 이름을 준다.
  2. Charts 페이지로 돌아가 이번에는 Big Number with Trendline을 고르고 새로 만든 데이터셋을 선택한다.
  3. 세 칸을 설정한다. X-Axis는 tourney_date, Time Grain은 Day, Metrics는 SUM(cumulative_count)이며 이것은 맞춤 지표다.
  4. Update Chart를 누르면 오른쪽에 시각화가 나온다. 저장해 ATP Dashboard에 더한다.
살펴본 김에 — 누적 수를 다시 합하는 지표

지표가 SUM(cumulative_count)다. cumulative_count는 이미 누적된 값이므로, 그것을 다시 SUM하면 일반적으로 부풀려진 숫자가 나온다. 값이 1, 2, 3, …, N으로 늘어난다면 그 합은 N이 아니라 N(N+1)/2다.

도판 9.25의 설명이 "서른 넘은 그랜드 슬램 우승자의 "라고 되어 있으니, 의도는 총 몇 명인가일 것이다. 그렇다면 MAX(cumulative_count)COUNT(*)가 의도에 맞는다. Big Number 계열 차트가 마지막 시간 구간의 값을 큰 숫자로 보여주기 때문에, 마지막 날에 해당 사건이 하나뿐이면 우연히 총계와 같아진다. 그러나 같은 날 결승이 둘 있으면 어긋난다.

차트 설정 화면을 재현할 수 없으니 실제로 어떤 숫자가 표시되었는지는 확인할 수 없다. 그러나 누적값에 다시 합계를 씌우는 지표는 의도를 다시 확인해볼 자리임은 분명하다.

〈도판 9.27〉 · 화면 갈무리

완성된 ATP Dashboard다. 저자들은 이 절과 앞 절의 방식을 따라 차트를 몇 개 더 더했다고 밝힌다. "Superset으로 무엇을 할 수 있는지 감을 주려고 몇 가지 다른 차트 유형을 썼다"는 것이다. 다음 절에서 이 대시보드의 사본을 가져오는 법을 설명한다.

9.2.5

대시보드 내보내고 가져오기Exporting and importing dashboards

데이터베이스, 데이터셋, 차트, 대시보드를 통상 Superset의 UI로 설정한다. 그런데 그 대시보드를 다른 곳에 배포하고 싶다면—또는 책의 독자들과 공유하고 싶다면—처음부터 전 과정을 다시 거치고 싶지는 않을 것이다. 여기서 내보내기·가져오기 기능이 등장한다.

shell — 눌러 만든 것이 결국 YAML이다 9.2.5
$ unzip dashboard_export_20231203T162310.zip
$ tree dashboard_export_20231203T162310

dashboard_export_20231203T162310
├── charts
│   ├── Grand_Slam_Winners__30_11.yaml
│   ├── Grand_Slam_winners__25_9.yaml
│   ├── Lowest_Ranked_Grand_Slam_Winners_8.yaml
│   ├── Matches_Played_7.yaml
│   └── Multi_Title_Winners_6.yaml
├── dashboards
│   └── ATP_Dashboard_1.yaml
├── databases
│   └── DuckDB.yaml
├── datasets
│   └── DuckDB
│       ├── Grand_Slam_Winners__30.yaml
│       ├── Multi_Title_Winners.yaml
│       ├── Young_Grand_Slam_Winners.yaml
│       └── matches.yaml
└── metadata.yaml
9.2.2절에서 배운 개념마다 디렉터리가 하나씩 있다. 데이터베이스 접속 설정은 databases/DuckDB.yaml, 데이터셋은 datasets/DuckDB, 차트는 charts, 대시보드 설정은 dashboards/ATP_Dashboard_1.yaml이다. 개념 도해가 그대로 디렉터리 구조가 되었다.
이 목록에서 이 장 전체를 되짚을 만한 사실이 하나 드러난다. UI로 눌러 만든 것이 결국 YAML이다. 제8장에서 dbt가 YAML로 시작해 파이프라인을 서술했는데, Superset은 클릭으로 시작해 YAML로 끝난다. 도착지가 같으니 버전 관리도, 코드 리뷰도, 배포 자동화도 가능하다. 노코드 도구가 코드를 배제하는 것이 아니라 코드를 쓰는 시점을 뒤로 미룰 뿐이라는 것이 여기서 보인다.
차트가 다섯인데 데이터셋은 넷이다. matches 테이블에서 온 데이터셋 하나와 SQL 질의에서 온 데이터셋 셋이며, 한 데이터셋 위에 차트 둘이 서 있다는 뜻이다. Grand_Slam_Winners__30 데이터셋과 그 위의 차트 이름이 짝을 이루는 것이 보인다.
공유하기 전에 확인해둘 것이 하나 있다. databases/DuckDB.yaml접속 설정을 담는다. 9.2.3절의 NOTE가 알려준 대로 MotherDuck에 붙었다면 그 접속 문자열에 ?motherduck_token=…이 들어 있다. 내보낸 ZIP을 그대로 남에게 주거나 저장소에 올리기 전에 그 파일에 토큰이 남아 있지 않은지 확인하는 편이 안전하다.
← 좌우로 밀어 보기 →
〈도판 9.28〉·〈도판 9.29〉 · 화면 갈무리

앞의 것은 내보내기다. Dashboards 페이지에서 해당 행의 Actions 열에 마우스를 올리면 내보내기 버튼이 나타난다. 누르면 Superset이 데이터베이스·데이터셋·차트·대시보드를 담은 ZIP 파일을 만들어준다.

뒤의 것은 가져오기다. Dashboard 페이지 오른쪽 위의 아래쪽 화살표 버튼을 누르고 ZIP 파일을 고르면 그 설정이 Superset으로 들어온다.

덧붙인 표  같은 일을 두 길로 하면 무엇이 달라지는가. 원서에는 이런 대조표가 없다.
해야 할 일Streamlit · 코드 우선Superset · 클릭 우선
DuckDB 붙이기 duckdb.connect('atp.duckdb',
  read_only=True)
Settings → + Database → DuckDB →
SQL Alchemy URI에 duckdb:///atp.duckdb
검색 입력 받기 st_searchbox(search_players, …)
+ 준비된 문장 함수 12줄
해당 개념이 없다. 대시보드의 상호작용은
확대·이동·차트 표상 변경으로 제한된다
표 보여주기 st.dataframe(df.drop([…], axis=1)) 데이터셋을 만들면 미리보기가 표로 나온다
차트 그리기 px.scatter(…) 20줄 남짓 +
st.plotly_chart(fig)
차트 유형 선택 → 축·Time Grain·Metrics
세 칸 설정 → Update Chart
여러 테이블 합치기 SQL 문자열에 JOIN을 적어 execute SQL Lab에서 질의를 쓰고 Save Dataset
결국 SQL은 쓴다
배포·공유 requirements.txt 네 줄 +
GitHub 저장소 + Deploy 버튼
대시보드 내보내기 → ZIP(YAML 묶음) →
다른 Superset에서 가져오기

표를 옆으로 읽으면 이 장의 결론이 보인다. Superset이 없애준 것은 프레젠테이션 코드이고, 없애주지 못한 것은 SQL이다. 여러 테이블을 합쳐야 하는 순간 SQL Lab이 열리고, 거기서 쓰는 것은 제3·4장에서 배운 그 SQL이다. 요약의 마지막 문장이 그것을 인정한다. Apache Superset을 쓰며 통상 써야 하는 유일한 코드는 시각화에 흘러 들어가는 맞춤 SQL 질의다.

SUMMARY

제9장이 남긴 여섯 문장Summary

  1. Streamlit은 로우코드 환경으로, 웹 애플리케이션을 쓸 때 되풀이되는 과제를 푸는 이미 만들어진 재사용 가능한 컴포넌트를 여럿 제공한다.
  2. Streamlit은 파이썬으로 쓰였으므로 DB-API 2.0, 관계형 API, 데이터프레임을 통해 DuckDB의 파이썬 API와 여러 방식으로 통합된다.
  3. 노코드이고 순전히 선언적인 환경과 달리, Streamlit 애플리케이션에는 맞춤 파이썬을 써서 능력을 확장할 수 있다.
  4. plot.ly는 보기 좋고 대화형인 시각화를 만드는 비슷한 로우코드 접근을 제공하며 Streamlit과 매끄럽게 함께 쓸 수 있다.
  5. Apache Superset은 스펙트럼의 반대쪽 끝에 있다. 보기 좋은 대시보드를 짓기 위한 사실상 노코드의 끌어놓기 대안이다.
  6. Apache Superset을 쓰며 통상 써야 하는 유일한 코드는 시각화에 흘러 들어가는 맞춤 SQL 질의다.
살펴본 김에 — 요약이 본문과 어긋난다

요약의 첫 문장은 Streamlit을 "로우코드 환경(a low-code environment)"이라 부른다. 그런데 9.1.1절은 정반대로 못 박아두었다.

"데이터·BI 분야의 다른 시각화 도구들이 로우코드 시각 빌더인 것과 달리, Streamlit은 코드 우선(code-first) 도구다."

같은 장 안에서 한 도구를 로우코드가 아니라고 규정했다가 로우코드라고 요약한 셈이다. 요약의 세 번째 문장이 "노코드이고 순전히 선언적인 환경과 달리 맞춤 파이썬을 쓸 수 있다"고 적은 것을 보면, 요약이 세우려던 대비는 노코드(Superset) ↔ 코드를 쓸 수 있음(Streamlit)이었던 듯하다. 그렇다면 첫 문장의 "로우코드"는 "코드 우선"이어야 아귀가 맞는다.

사소한 낱말 하나로 보이지만 이 장의 구도 전체가 그 낱말에 걸려 있다. 두 도구를 스펙트럼의 양 끝에 놓는 것이 이 장의 뼈대인데, 첫 문장이 한쪽 끝을 가운데로 끌어당겨버린다.