[에러 해결] [Streamlit] Plotly 차트와 커스텀 HTML 카드 세로 정렬 불일치 원인과 해결 방법

안녕하세요! 구글 SEO와 사용자 경험(UX) 최적화에 정통한 기술 블로그 전문 에디터입니다. 오늘은 Streamlit 대시보드 개발 중 흔히 마주치는 레이아웃 문제, 특히 Plotly 차트와 커스텀 HTML 카드 간의 세로 정렬 불일치 문제에 대해 심층 분석하고 명확한 해결책을 제시해 드리고자 합니다. 이 문서는 개발자 여러분이 문제를 빠르게 해결하고, 더 나아가 효율적인 Streamlit 대시보드 레이아웃을 구성하는 데 필요한 지식을 얻는 데 초점을 맞추고 있습니다.

1. 에러 발생 상황

Streamlit을 사용하여 두 개의 컬럼을 나란히 배치하고, 한 컬럼에는 Plotly 차트를, 다른 컬럼에는 st.markdown()을 이용해 렌더링한 커스텀 HTML/CSS 카드를 표시하는 상황입니다. 개발자의 목표는 두 요소가 정확히 같은 세로 위치에서 시작하도록 정렬하는 것이었으나, 실제로는 HTML 카드가 Plotly 차트보다 시각적으로 아래로 밀려나 정렬이 맞지 않는 문제가 발생했습니다.

다음은 문제를 재현하는 데 사용된 코드 스니펫입니다:

import streamlit as st
import plotly.graph_objects as go # Plotly 예시를 위해 임포트

# 예시 Plotly 차트 생성 (실제 fig는 외부에서 생성되었다고 가정)
fig = go.Figure(
    data=[go.Bar(y=[2, 3, 1])],
    layout=go.Layout(
        height=320,
        margin=dict(l=10, r=30, t=10, b=10),
        paper_bgcolor="white",
        plot_bgcolor="white",
    )
)

st.set_page_config(layout="wide")

c3, c4 = st.columns([1.35, 1], gap="large")

with c3:
    st.markdown('<div class="cp-h2">Risk Factor Breakdown</div>', unsafe_allow_html=True)
    st.markdown(
        '<div class="cp-sub">Relative contribution of each clinical factor to the overall risk estimate.</div>',
        unsafe_allow_html=True,
    )

    st.plotly_chart(
        fig,
        use_container_width=True,
        config={"displayModeBar": False},
    )

with c4:
    st.markdown(
        """
        <div class="cp-card" style="height:345px;">
            <div class="cp-h2">What This Means</div>
            <div class="cp-sub">
                Some explanatory text...
            </div>
        </div>
        """,
        unsafe_allow_html=True,
    )

사용된 CSS와 Plotly 레이아웃 설정은 다음과 같습니다:

.cp-card {
    background: #ffffff;
    border: 1px solid #e6ecf3;
    border-radius: 16px;
    padding: 22px 24px;
    box-shadow: 0 1px 2px rgba(15,23,42,.04),
                0 8px 24px -12px rgba(15,23,42,.08);
    height: 100%;
}

이러한 설정에도 불구하고 Plotly 차트 패널과 HTML 카드가 같은 세로 위치에서 시작하지 않고, HTML 카드가 시각적으로 더 낮게 나타나는 문제가 발생했습니다. 다양한 시도(Plotly 높이, 마진 조정, 컬럼 너비 변경 등)에도 불구하고 정렬에는 변화가 없었습니다.

2. 명확한 발생 원인

Streamlit의 st.columns는 내부적으로 CSS Flexbox를 사용하여 컬럼들을 배치하며, 기본적으로 콘텐츠를 상단(align-items: flex-start)에 정렬합니다. 따라서 컬럼 자체의 정렬보다는, 각 컬럼 내부 요소들의 마진(margin) 및 패딩(padding) 불일치가 주된 원인입니다.

구체적인 원인은 다음과 같습니다:

  1. 왼쪽 컬럼의 불필요한 상단 마진: 왼쪽 컬럼(c3)에서는 Plotly 차트 이전에 두 개의 st.markdown() 호출을 통해 <div class="cp-h2"><div class="cp-sub">를 렌더링하고 있습니다. st.markdown()으로 렌더링된 블록 레벨(block-level) 요소들은 브라우저나 Streamlit의 기본 스타일에 의해 상단 및 하단에 일정량의 margin이 자동으로 부여됩니다. 이 마진들이 Plotly 차트를 아래로 밀어내는 효과를 발생시킵니다.
  2. 오른쪽 컬럼의 구조적 차이: 오른쪽 컬럼(c4)의 cp-card는 하나의 단일 st.markdown() 호출로 렌더링되며, cp-h2cp-sub 요소들이 cp-card 내부의 padding 영역에 포함되어 있습니다. 즉, cp-card 자체의 외부 마진은 최소화되어 있거나 없고, 내부 콘텐츠는 padding에 의해 시작 위치가 결정됩니다.
  3. st.plotly_chart의 내부 마진: st.plotly_chart() 컴포넌트 또한 Streamlit에 의해 래핑될 때 자체적으로 상단 마진을 가질 수 있습니다. 이러한 모든 요소들의 기본 마진이 중첩되면서, 왼쪽 컬럼의 전체적인 콘텐츠 블록이 오른쪽 컬럼의 콘텐츠 블록보다 더 낮게 시작하게 되는 것입니다.

결론적으로, 컬럼 간의 정렬 문제는 Streamlit의 유연한 레이아웃 시스템 내에서 각 컴포넌트가 차지하는 세로 공간의 불일치, 특히 의도치 않은 기본 마진들 때문에 발생합니다.

3. 해결 방법 및 코드 예시

이러한 세로 정렬 문제를 해결하는 가장 효과적인 방법은 컬럼 내부 요소들의 불필요한 마진을 제거하고, 양쪽 컬럼의 시작점을 일관되게 맞추는 것입니다. 두 가지 주요 해결 방법을 제시합니다.

3.1. 방법 1: 커스텀 CSS를 이용한 마진 초기화 및 조정 (권장)

가장 직접적이고 효과적인 방법은 Streamlit 앱에 커스텀 CSS를 주입하여 문제의 요소들이 가지는 기본 마진을 초기화하는 것입니다. 특히 cp-h2, cp-sub, 그리고 st.plotly_chart를 감싸는 Streamlit 내부 컨테이너의 마진을 조정해야 합니다.

import streamlit as st
import plotly.graph_objects as go

# 예시 Plotly 차트 생성 (실제 fig는 외부에서 생성되었다고 가정)
fig = go.Figure(
    data=[go.Bar(y=[2, 3, 1])],
    layout=go.Layout(
        height=320,
        margin=dict(l=10, r=30, t=10, b=10),
        paper_bgcolor="white",
        plot_bgcolor="white",
    )
)

st.set_page_config(layout="wide")

# --- CSS 주입 시작 ---
st.markdown(
    """
    <style>
        /* 커스텀 제목 요소들의 기본 마진 제거 */
        .cp-h2, .cp-sub {
            margin-top: 0px !important;
            margin-bottom: 0px !important;
            line-height: 1.2; /* 텍스트 라인 높이를 균일하게 유지하여 시각적 일관성 확보 */
        }

        /* Plotly 차트를 감싸는 Streamlit 내부 컨테이너의 상단 마진 제거 */
        /* 이 클래스명(data-testid)은 Streamlit 버전에 따라 변경될 수 있으므로,
           문제가 지속될 경우 개발자 도구(F12)로 정확한 클래스명을 확인해야 합니다. */
        div[data-testid="stPlotlyChart"] {
            margin-top: 0px !important;
            padding-top: 0px !important; /* 혹시 모를 내부 패딩도 제거 */
        }

        /* 만약 Streamlit 컬럼 내부의 첫 번째 요소가 자체적으로 마진을 가지고 있다면 조정 */
        /* 이 셀렉터는 Streamlit 내부 구조에 따라 변경될 수 있습니다. */
        .stColumn > div:first-child {
            margin-top: 0px !important;
        }

        /* cp-card의 패딩을 Plotly 컨테이너 상단과 맞추기 위해 필요시 조정 */
        .cp-card {
            padding-top: 22px; /* 기존 값 유지 */
            padding-bottom: 22px; /* 기존 값 유지 */
            /* 만약 Plotly 차트 상단 제목들의 전체 높이가 cp-card의 padding-top과 다르다면
               이 값을 조정하여 시각적인 시작점을 맞출 수 있습니다.
               예: padding-top: calc(22px + [추가될 높이]) */
        }
    </style>
    """,
    unsafe_allow_html=True,
)
# --- CSS 주입 끝 ---

c3, c4 = st.columns([1.35, 1], gap="large")

with c3:
    # 마진이 제거된 cp-h2와 cp-sub
    st.markdown('<div class="cp-h2">Risk Factor Breakdown</div>', unsafe_allow_html=True)
    st.markdown(
        '<div class="cp-sub">Relative contribution of each clinical factor to the overall risk estimate.</div>',
        unsafe_allow_html=True,
    )

    st.plotly_chart(
        fig,
        use_container_width=True,
        config={"displayModeBar": False},
    )

with c4:
    st.markdown(
        """
        <div class="cp-card" style="height:345px;">
            <div class="cp-h2">What This Means</div>
            <div class="cp-sub">
                Some explanatory text...
            </div>
        </div>
        """,
        unsafe_allow_html=True,
    )

이 CSS 코드는 .cp-h2, .cp-sub 요소와 st.plotly_chart를 감싸는 Streamlit 내부 divmargin-top0으로 강제 설정합니다. 이를 통해 왼쪽 컬럼 상단의 여백을 제거하고, 오른쪽 컬럼의 cp-card와 비슷한 시작 지점을 가질 수 있도록 돕습니다. !important 키워드는 Streamlit의 기본 스타일을 오버라이드하기 위해 사용됩니다.

3.2. 방법 2: st.container() 및 구조적 일관성 활용

보다 견고하고 유지보수하기 쉬운 방법은 각 컬럼의 콘텐츠를 st.container()로 감싸고, 이 컨테이너들에 일관된 스타일링을 적용하는 것입니다. st.container()는 자체적인 블록 레벨 컨테이너를 제공하며, 여기에 커스텀 CSS를 적용하기 용이합니다.

import streamlit as st
import plotly.graph_objects as go

# 예시 Plotly 차트 생성 (실제 fig는 외부에서 생성되었다고 가정)
fig = go.Figure(
    data=[go.Bar(y=[2, 3, 1])],
    layout=go.Layout(
        height=320,
        margin=dict(l=10, r=30, t=10, b=10),
        paper_bgcolor="white",
        plot_bgcolor="white",
    )
)

st.set_page_config(layout="wide")

# --- CSS 주입 (st.container에 적용할 스타일) ---
st.markdown(
    """
    <style>
        /* cp-h2, cp-sub 기본 마진 제거 (옵션) */
        .cp-h2, .cp-sub {
            margin-top: 0px !important;
            margin-bottom: 0px !important;
            line-height: 1.2;
        }

        /* st.container에 cp-card와 유사한 스타일 적용 */
        .st-emotion-cache-1g83g3d { /* st.container의 내부 div 클래스명, 버전별로 다를 수 있음 */
            background: #ffffff;
            border: 1px solid #e6ecf3;
            border-radius: 16px;
            padding: 22px 24px; /* cp-card와 동일한 패딩 적용 */
            box-shadow: 0 1px 2px rgba(15,23,42,.04),
                        0 8px 24px -12px rgba(15,23,42,.08);
            /* min-height를 설정하여 내용이 적어도 최소 높이를 유지하도록 함 */
            min-height: 345px; /* cp-card의 height와 유사하게 설정 */
            margin: 0 !important; /* 컨테이너 외부 마진 제거 */
        }
        /* st.container에 border=True를 사용했을 때 Streamlit이 추가하는 클래스명도 함께 고려 */
        .st-emotion-cache-1g83g3d.st-emotion-cache-16z1e1c > div:first-child {
            padding: 0px !important; /* Streamlit이 컨테이너에 추가하는 내부 패딩 제거 */
        }

        /* Plotly 차트 컨테이너 마진 제거 (여전히 필요할 수 있음) */
        div[data-testid="stPlotlyChart"] {
            margin-top: 0px !important;
            padding-top: 0px !important;
        }
    </style>
    """,
    unsafe_allow_html=True,
)
# --- CSS 주입 끝 ---

c3, c4 = st.columns([1.35, 1], gap="large")

with c3:
    with st.container(border=True): # Streamlit 1.29+에서 border=True는 외관을 향상시킵니다.
        # 내부 콘텐츠에 필요한 마진 조정
        st.markdown('
Risk Factor Breakdown
', unsafe_allow_html=True) st.markdown( '
Relative contribution of each clinical factor to the overall risk estimate.
', unsafe_allow_html=True, ) st.plotly_chart( fig, use_container_width=True, config={"displayModeBar": False}, ) with c4: # 기존 HTML 카드 유지 st.markdown( """ <div class="cp-card" style="height:345px;"> <div class="cp-h2">What This Means</div> <div class="cp-sub"> Some explanatory text... </div> </div> """, unsafe_allow_html=True, )

이 방법은 왼쪽 컬럼의 전체 영역을 st.container()로 감싸고, 이 컨테이너에 cp-card와 유사한 시각적 스타일(패딩, 그림자, 배경색 등)을 적용합니다. min-height를 설정하여 두 컨테이너의 전체 높이를 비슷하게 유지하고, 내부 텍스트 요소의 마진을 조절하여 정렬의 일관성을 높입니다.

참고: Streamlit이 생성하는 CSS 클래스명(예: .st-emotion-cache-1g83g3d)은 Streamlit 버전에 따라 변경될 수 있습니다. 만약 위 코드로 해결되지 않는다면, 브라우저의 개발자 도구(F12)를 사용하여 해당 요소의 현재 클래스명을 확인하고 CSS 셀렉터를 업데이트해야 합니다.

4. 향후 예방을 위한 팁

Streamlit 대시보드에서 이와 같은 레이아웃 정렬 문제를 예방하고 더 나은 사용자 경험을 제공하기 위한 몇 가지 팁입니다:

  1. 일관된 마진/패딩 전략: 모든 커스텀 HTML 요소와 Streamlit 컴포넌트 간에 마진 및 패딩을 일관되게 적용하거나 초기화하는 CSS 전략을 수립하세요. * { margin: 0; padding: 0; box-sizing: border-box; }와 같은 CSS Reset을 사용한 후 필요한 곳에만 마진/패딩을 추가하는 것도 좋은 방법입니다.
  2. st.container() 활용: 관련 있는 컴포넌트들을 st.container()로 묶어 논리적인 그룹을 만들고, 이 컨테이너에 직접 스타일을 적용하여 외부 요소와의 간섭을 최소화하세요.
  3. st.expander(), st.tabs() 등 활용: 복잡한 대시보드는 여러 컴포넌트가 중첩될 때 정렬 문제가 발생하기 쉽습니다. st.expander()st.tabs()와 같은 기능을 활용하여 콘텐츠를 구조화하고 필요한 정보만 노출하면 레이아웃 복잡성을 줄일 수 있습니다.
  4. 개발자 도구 적극 활용: 브라우저의 개발자 도구(F12)는 Streamlit이 렌더링하는 HTML 구조와 적용된 CSS 스타일을 이해하는 데 필수적입니다. 요소의 margin, padding, border를 시각적으로 확인하고 어떤 스타일이 문제를 일으키는지 파악하는 데 유용합니다.
  5. Streamlit 버전 관리: Streamlit은 활발하게 개발되는 프레임워크이므로, 새로운 버전에서 내부 CSS 클래스나 렌더링 방식이 변경될 수 있습니다. 특정 버전에 의존하는 커스텀 CSS는 업데이트 시 문제가 될 수 있으므로, 버전을 고정하거나 주기적으로 호환성을 확인하는 것이 좋습니다.

이러한 원인 분석과 해결 방법을 통해 Streamlit 대시보드의 세로 정렬 문제를 효과적으로 해결하고, 사용자에게 더 깔끔하고 전문적인 인터페이스를 제공할 수 있기를 바랍니다. 궁금한 점이 있다면 언제든지 질문해 주세요!

댓글 남기기