본문 바로가기

AI 콘텐츠 자동화

파이썬만으로 프론트엔드 만들기: 스트림릿(Streamlit) 도입 고군분투기

파이썬만으로 프론트엔드 만들기: 스트림릿(Streamlit) 도입 고군분투기


파이썬 스크립트를 웹으로 쉽게 변환할 수 있다는 스트림릿의 장점 이면에는 상태 관리라는 독특한 패러다임이 숨어 있었다. 이를 이해하지 못해 겪은 무한 새로고침의 늪과 해결 과정을 정리한다.

위 화면은 스트림릿으로 구현한 초기 웹 애플리케이션에서 상호작용 시 값이 초기화되는 현상을 캡처한 것이다. 버튼을 누를 때마다 화면이 초기화되는 이 오류는, 스트림릿의 렌더링 구조를 훌륭하게 이해하기 전까지 나를 끊임없이 괴롭혔다.

터미널의 벽을 허물기 위한 선택

최근 사내 업무 효율화를 위해 데이터 처리용 파이썬 스크립트를 여러 개 작성했다. 터미널 환경에 익숙한 개발자에게는 그저 명령어 한 줄이면 끝날 일이었지만, 비개발 직군 팀원들에게 까만 콘솔 창은 두려움의 대상이었다. 매번 "대표님, 이거 어떻게 실행하는 건가요?"라는 질문이 반복되자, 코드의 실행 방식을 터미널에서 '클릭' 기반의 직관적인 웹 화면으로 옮겨야겠다는 필요성을 절실히 느꼈다.

문제는 리소스였다. 사내용 내부 도구 하나를 만들자고 React나 Vue.js 같은 본격적인 프론트엔드 프레임워크를 도입하고, REST API로 백엔드를 연동하는 작업은 배보다 배꼽이 더 큰 격이었다. '어떻게 하면 파이썬으로 짠 기존 로직을 가장 빠르고 저렴하게 웹 UI로 포장할 수 있을까?' 이 고민의 끝에서 만난 구원투수가 바로 스트림릿(Streamlit)이었다.

"파이썬 몇 줄이면 끝납니다"라는 달콤한 환상

스트림릿 공식 문서가 보여주는 첫인상은 그야말로 마법 같았다. HTML, CSS, JavaScript 같은 프론트엔드 지식이 전혀 없어도, 오직 파이썬 코드 몇 줄이면 번듯한 웹 애플리케이션이 탄생한다고 소개되어 있었다. st.write()로 텍스트를 출력하고, st.button()으로 버튼을 만드는 직관적인 문법은 압도적인 개발 속도를 보장할 것 같았다.

기대감에 부풀어 즉시 화면 설계와 코딩에 돌입했다. 사용자가 검색어를 입력하고 '실행' 버튼을 누르면, 백엔드에서 데이터를 가공해 화면 하단에 표와 차트로 뿌려주는 단순한 구조였다. UI의 뼈대를 잡는 데는 과장 없이 한두 시간 남짓밖에 걸리지 않았다. '이 정도면 오늘 퇴근 전에 사내 배포까지 끝내겠는데?'라는 오만한 자신감이 차오르던 순간이었다.

Streamlit 화면과 웹앱 구조를 분석하는 모습

Streamlit 화면과 웹앱 구조를 분석하는 모습

단순히 도구의 문제로 치부하기엔 찝찝함이 남아 공식 문서로 돌아가 원인을 분석해 보기로 했다.

무한 새로고침의 늪에 빠지다

하지만 뼈대에 실제 데이터 처리 로직을 연결하자, 전혀 예상치 못한 기이한 현상들이 쏟아지기 시작했다.

검색어를 입력하고 버튼을 누르는 순간 화면이 깜빡이더니 기껏 입력한 검색어가 증발해 버렸다. 어찌어찌 데이터 조회 결과를 화면에 띄우는 데 성공해도, 결과를 다운로드하기 위해 하단의 다른 버튼을 누르면 조회된 데이터가 전부 날아가고 텅 빈 초기 화면으로 돌아가 버렸다.

마치 사용자가 화면과 상호작용할 때마다 전체 웹페이지가 무한히 새로고침되는 느낌이었다. A를 입력하면 B가 사라지고, B를 클릭하면 A가 초기화되는 뫼비우스의 띠 같았다. 코드를 뒤집어엎고 조건문을 기형적으로 엮어봐도 결과는 같았다. 껍데기는 그럴듯했지만, 상태(State)가 유지되지 않아 도저히 실무에 쓸 수 없는 예쁜 쓰레기가 만들어진 것이다.

도구를 탓하던 나, 공식 문서로 돌아가다

처음에는 스트림릿이라는 프레임워크 자체의 한계라고 치부했다. '역시 파이썬만으로 프론트엔드를 대체한다는 건 무리였어. 이건 그냥 장난감 수준의 차트나 그리는 도구인데 내가 너무 많은 걸 바란 게 아닐까?' 진지하게 모든 코드를 폐기하고 Flask와 바닐라 JavaScript로 처음부터 다시 짤까 고민했다.

하지만 깃허브와 개발자 커뮤니티를 둘러보니, 다른 사람들은 스트림릿으로 복잡한 대시보드는 물론 고도화된 AI 챗봇 화면까지 거뜬히 구현하고 있었다. 도구의 문제가 아니라면 원인은 하나, 내가 무언가 근본적인 개념을 놓치고 있다는 뜻이었다. 답답함을 억누르고 공식 문서의 첫 페이지로 돌아가, 이전에는 대충 넘겼던 '데이터 흐름(Data Flow)' 챕터를 정독하기 시작했다.

원인은 'Top-to-Bottom' 렌더링 스크립트 구조

문서를 읽어 내려가며, 내가 겪은 끔찍한 버그의 진짜 원인을 깨달았다. 스트림릿은 일반적인 웹 프레임워크와 완전히 다른 패러다임으로 동작하고 있었다.

일반적인 웹 애플리케이션은 최초 렌더링 후 필요한 DOM 요소만 JavaScript로 부분 업데이트(Re-rendering)한다. 하지만 스트림릿은 사용자가 버튼을 누르거나 값을 입력하는 등 '상호작용(Interaction)'이 발생할 때마다, 파이썬 스크립트 전체를 위에서부터 아래로(Top-to-Bottom) 처음부터 다시 실행(Rerun)하는 구조였다.

내가 버튼을 누르는 순간 파이썬 코드가 첫 줄부터 재실행되었고, 이 과정에서 일반 변수에 저장해 두었던 검색어나 데이터가 메모리에서 모조리 초기화되었던 것이다. 파이썬 스크립트가 매번 새롭게 실행된다는 이 핵심 메커니즘을 이해하지 못한 채, 데스크톱 앱이나 기존 웹의 변수 개념을 억지로 끼워 맞추려 했던 나의 무지가 빚어낸 촌극이었다.

Session State로 되찾은 평화

렌더링의 동작 원리를 파악하자 해결책은 명확해졌다. 스트림릿이 제공하는 st.session_state가 해답이었다. 이는 스크립트가 수백 번 재실행되더라도, 사용자가 브라우저에 접속해 있는 동안 데이터를 안전하게 보존해 주는 든든한 금고 역할을 했다.

즉각적인 코드 리팩토링에 들어갔다. 1. 기존에 일반 변수로 선언했던 데이터들(data = get_data())을 모두 세션 상태(st.session_state.data = get_data())에 저장하도록 구조를 바꿨다. 2. 단순히 if button_clicked:로 분기하던 순차적 로직을 버리고, 버튼 클릭 시 특정 함수를 호출해 세션 상태를 안전하게 업데이트하는 콜백(Callback) 방식(on_click)을 도입했다.

상태 관리(State Management)의 개념을 스트림릿의 룰에 맞게 적용하자, 거짓말처럼 모든 문제가 사라졌다. 텍스트를 입력해도 값이 날아가지 않았고, 다운로드 버튼을 눌러도 조회된 데이터는 화면에 굳건히 유지되었다.

쉬운 도구일수록, 그 이면의 철학을 읽어라

이틀간의 삽질 끝에 스트림릿 사내 도구를 무사히 배포했다. 팀원들은 더 이상 까만 터미널 창을 보며 두려워할 필요 없이, 웹 브라우저에서 버튼 클릭 몇 번으로 데이터를 추출하고 업무를 처리할 수 있게 되었다.

이번 스트림릿 도입기는 내게 중요한 개발자적 교훈을 남겼다. 겉보기에 사용하기 쉬운 도구일수록, 그 내면에는 복잡성을 감추기 위한 고도의 추상화와 독특한 설계 철학이 숨어있다는 사실이다. "단 몇 줄이면 됩니다!"라는 마케팅 문구에 취해 프레임워크의 근본적인 동작 원리를 무시한 대가는 가혹했다.

아무리 훌륭하고 직관적인 도구라도, 그것의 라이프사이클과 데이터 흐름을 정확히 이해하지 못하면 결코 제대로 다룰 수 없다. 개발에 마법 같은 지름길은 없으며, 기초적인 동작 원리(Under the hood)를 파악하는 것이 결국 가장 빠른 길이라는 기본기를 다시 한번 뼈저리게 새겼다.

배포는 성공적이었지만 아직 개선의 여지가 남아있다. 상호작용 시마다 스크립트가 재실행되다 보니, 무거운 연산이나 DB 조회가 불필요하게 반복되는 병목 구간이 존재한다. 나의 다음 스텝은 스트림릿의 강력한 무기인 @st.cache_data@st.cache_resource를 활용한 캐싱(Caching) 최적화다. 이번에 얻은 뼈아픈 교훈을 바탕으로, 공식 문서를 나침반 삼아 애플리케이션의 성능 한계를 돌파해 보려 한다.