iBetter Books
수정

레이아웃과 콜백

Dash를 처음 보는 사람들이 가장 낯설어하는 것이 콜백입니다. 함수 위에 데코레이터가 붙고, Input과 Output이라는 낯선 객체가 등장합니다.

지윤은 팀원들에게 이렇게 설명했습니다. "커피숍 키오스크 생각해봐. 음료 선택하면 화면이 바뀌잖아. 그게 콜백이야. 사용자가 뭔가 선택하면 → 파이썬 함수가 실행되고 → 결과로 화면이 업데이트돼."

html 컴포넌트

from dash import html로 불러옵니다. HTML 태그 이름과 1:1로 대응됩니다.

from dash import html# html 컴포넌트 예시layout = html.Div([    html.H1("대시보드 제목"),           # <h1>    html.H2("소제목"),                  # <h2>    html.P("단락 텍스트"),              # <p>    html.Hr(),                          # <hr>    html.Br(),                          # <br>    html.Span("인라인 텍스트"),         # <span>    html.A("링크", href="https://plotly.com"),  # <a>    html.Ul([                           # <ul>        html.Li("항목 1"),              # <li>        html.Li("항목 2"),    ]),])

스타일을 인라인으로 지정할 때는 딕셔너리를 씁니다. CSS 속성 이름을 카멜케이스로 바꿉니다. font-sizefontSize, background-colorbackgroundColor.

html.Div(    "스타일 적용",    style={        "backgroundColor": "#f0f4f8",        "padding": "20px",        "borderRadius": "8px",        "fontSize": "16px"    })

dcc 컴포넌트

from dash import dcc로 불러옵니다. Dash Core Components의 약자입니다. 사용자 입력을 받는 UI 요소들이 들어있습니다.

from dash import dcc# Dropdown: 선택 메뉴dcc.Dropdown(    id="my-dropdown",    options=[        {"label": "서울", "value": "seoul"},        {"label": "부산", "value": "busan"},        {"label": "대구", "value": "daegu"},    ],    value="seoul",     # 기본 선택값    clearable=False    # 선택 해제 버튼 숨기기)# Slider: 슬라이더dcc.Slider(    id="my-slider",    min=0,    max=100,    step=10,    value=50,    marks={i: str(i) for i in range(0, 101, 10)})# RangeSlider: 범위 슬라이더dcc.RangeSlider(    id="my-range-slider",    min=2020,    max=2024,    step=1,    value=[2021, 2023])# Input: 텍스트 입력dcc.Input(    id="my-input",    type="text",    placeholder="검색어 입력",    debounce=True   # 입력 완료 후 콜백 실행)# Graph: Plotly 차트 컨테이너dcc.Graph(id="my-graph")

id 속성이 중요합니다. 콜백에서 이 id로 컴포넌트를 식별합니다. 같은 앱 안에서 id는 유일해야 합니다.

콜백 기본 구조

콜백은 @callback 데코레이터로 정의합니다. Dash 2.x부터는 from dash import callback으로 직접 import하여 사용하는 방식이 권장됩니다.

# 새 파일: chapter04_06_callback_basic.pyfrom dash import Dash, html, dcc, callback, Output, Inputimport plotly.express as pxapp = Dash(__name__)df = px.data.tips()app.layout = html.Div([    html.H1("콜백 기초 예제"),    dcc.Dropdown(        id="day-dropdown",        options=[{"label": d, "value": d} for d in df["day"].unique()],        value="Sat",        clearable=False    ),    dcc.Graph(id="tip-graph")])@callback(    Output("tip-graph", "figure"),   # 출력: tip-graph의 figure 속성    Input("day-dropdown", "value"),  # 입력: day-dropdown의 value 속성)def update_graph(selected_day):    filtered = df[df["day"] == selected_day]    fig = px.scatter(        filtered,        x="total_bill",        y="tip",        color="sex",        title=f"{selected_day} 식사 금액 vs 팁"    )    return figif __name__ == "__main__":    app.run(debug=True)

콜백의 실행 순서는 이렇습니다.

  1. 사용자가 드롭다운에서 요일을 선택합니다.
  2. day-dropdownvalue가 바뀝니다.
  3. Dash가 자동으로 update_graph() 함수를 호출합니다.
  4. 함수가 반환한 Figure 객체가 tip-graphfigure에 들어갑니다.
  5. 브라우저의 차트가 업데이트됩니다.

Output("tip-graph", "figure")에서 첫 번째 인자는 컴포넌트 id, 두 번째는 업데이트할 속성 이름입니다. Input("day-dropdown", "value")도 마찬가지입니다.

다중 Input 예제

입력이 여러 개일 때는 Input을 여러 개 나열합니다. 함수의 매개변수도 그에 맞게 늘어납니다.

# 수정: chapter04_06_callback_basic.pyfrom dash import Dash, html, dcc, callback, Output, Inputimport plotly.express as pxapp = Dash(__name__)df = px.data.tips()app.layout = html.Div([    html.H1("다중 Input 콜백"),    html.Div([        html.Label("요일 선택"),        dcc.Dropdown(            id="day-dropdown",            options=[{"label": d, "value": d} for d in ["Thur", "Fri", "Sat", "Sun"]],            value="Sat",            clearable=False,            style={"width": "200px"}        ),    ], style={"marginBottom": "10px"}),    html.Div([        html.Label("식사 시간"),        dcc.RadioItems(            id="time-radio",            options=[                {"label": "점심", "value": "Lunch"},                {"label": "저녁", "value": "Dinner"},            ],            value="Dinner",            inline=True        ),    ], style={"marginBottom": "20px"}),    dcc.Graph(id="tip-graph")])@callback(    Output("tip-graph", "figure"),    Input("day-dropdown", "value"),   # 첫 번째 입력    Input("time-radio", "value"),     # 두 번째 입력)def update_graph(selected_day, selected_time):  # 입력 순서와 같은 순서로 받음    filtered = df[(df["day"] == selected_day) & (df["time"] == selected_time)]    if filtered.empty:        fig = px.scatter(title=f"{selected_day} {selected_time}: 데이터 없음")    else:        fig = px.scatter(            filtered,            x="total_bill",            y="tip",            color="sex",            size="size",            title=f"{selected_day} {selected_time} — 식사 금액 vs 팁"        )    return figif __name__ == "__main__":    app.run(debug=True)

두 컴포넌트 중 어느 쪽이 바뀌어도 콜백이 실행됩니다. Input의 순서와 함수 매개변수의 순서가 일치해야 합니다.

State: 버튼을 눌렀을 때만 실행하기

Input은 값이 바뀌는 즉시 콜백을 실행합니다. 검색어를 타이핑할 때마다 실행된다면 서버에 부담이 됩니다. State는 값을 감시하되, 실행 트리거가 되지는 않습니다. 버튼 클릭 같은 별도의 트리거가 있을 때 함께 사용합니다.

# 수정: chapter04_06_callback_basic.pyfrom dash import Dash, html, dcc, callback, Output, Input, Stateimport plotly.express as pxapp = Dash(__name__)df = px.data.tips()app.layout = html.Div([    html.H1("다중 Input 콜백"),    html.Div([        html.Label("요일 선택"),        dcc.Dropdown(            id="day-dropdown",            options=[{"label": d, "value": d} for d in ["Thur", "Fri", "Sat", "Sun"]],            value="Sat",            clearable=False,            style={"width": "200px"}        ),    ], style={"marginBottom": "10px"}),    html.Div([        html.Label("식사 시간"),        dcc.RadioItems(            id="time-radio",            options=[                {"label": "점심", "value": "Lunch"},                {"label": "저녁", "value": "Dinner"},            ],            value="Dinner",            inline=True        ),    ], style={"marginBottom": "20px"}),    dcc.Graph(id="tip-graph"),    html.Hr(),    html.H2("State 예제: 버튼 클릭 시에만 실행"),    dcc.Input(id="search-input", type="text", placeholder="검색어 입력"),    html.Button("검색", id="search-button", n_clicks=0),    html.Div(id="search-result")])@callback(    Output("tip-graph", "figure"),    Input("day-dropdown", "value"),    Input("time-radio", "value"),)def update_graph(selected_day, selected_time):    filtered = df[(df["day"] == selected_day) & (df["time"] == selected_time)]    if filtered.empty:        fig = px.scatter(title=f"{selected_day} {selected_time}: 데이터 없음")    else:        fig = px.scatter(            filtered,            x="total_bill",            y="tip",            color="sex",            size="size",            title=f"{selected_day} {selected_time} — 식사 금액 vs 팁"        )    return fig@callback(    Output("search-result", "children"),    Input("search-button", "n_clicks"),   # 버튼 클릭만 트리거    State("search-input", "value"),       # 입력값은 상태로 읽기만    prevent_initial_call=True             # 앱 시작 시 자동 실행 방지)def search(n_clicks, search_value):    return f"검색어: {search_value} (클릭 횟수: {n_clicks})"if __name__ == "__main__":    app.run(debug=True)

State는 콜백의 트리거가 되지 않습니다. 버튼을 클릭할 때만 search_value가 함께 전달됩니다. prevent_initial_call=True는 앱이 처음 로딩될 때 콜백이 실행되지 않도록 합니다.

실전: 드롭다운으로 차트 종류를 선택하는 앱

# 새 파일: chapter04_06_chart_selector.pyfrom dash import Dash, html, dcc, callback, Output, Inputimport plotly.express as pxapp = Dash(__name__)df = px.data.tips()CHART_TYPES = {    "scatter": "산점도",    "bar": "막대 그래프",    "box": "박스 플롯",    "violin": "바이올린 플롯",    "histogram": "히스토그램"}app.layout = html.Div([    html.H1("차트 타입 선택기"),    dcc.Dropdown(        id="chart-type",        options=[{"label": v, "value": k} for k, v in CHART_TYPES.items()],        value="scatter",        clearable=False,        style={"width": "300px", "marginBottom": "20px"}    ),    dcc.Graph(id="output-graph")], style={"padding": "20px"})@callback(    Output("output-graph", "figure"),    Input("chart-type", "value"))def update_chart(chart_type):    if chart_type == "scatter":        return px.scatter(df, x="total_bill", y="tip", color="day",                          title="산점도: 식사 금액 vs 팁")    elif chart_type == "bar":        avg = df.groupby("day")["tip"].mean().reset_index()        return px.bar(avg, x="day", y="tip",                      category_orders={"day": ["Thur", "Fri", "Sat", "Sun"]},                      title="막대 그래프: 요일별 평균 팁")    elif chart_type == "box":        return px.box(df, x="day", y="tip", color="sex",                      category_orders={"day": ["Thur", "Fri", "Sat", "Sun"]},                      title="박스 플롯: 요일별 팁 분포")    elif chart_type == "violin":        return px.violin(df, x="day", y="tip", color="sex", box=True,                         category_orders={"day": ["Thur", "Fri", "Sat", "Sun"]},                         title="바이올린 플롯: 요일별 팁 분포")    else:        return px.histogram(df, x="tip", nbins=30, title="히스토그램: 팁 분포")if __name__ == "__main__":    app.run(debug=True)

드롭다운에서 차트 종류를 선택하면 즉시 그래프가 바뀝니다. 슬라이드 발표와 달리, 청중이 "히스토그램으로도 보여주세요"라고 하면 바로 전환할 수 있습니다.