본문 바로가기

볼봇의 코딩 생활/웹 세계

[클라우드응용SW개발] Azure 와 Streamlit 으로 ToDo 앱 만들기

클라우드에서 웹 서비스를 올리는 방법은 생각보다 여러 가지가 있다. 과거에는 리눅스 서버 하나에 모든걸 직접 올리는 방식이 일반적이었지만, 요즘은 서버 관리에 신경쓰지 않고 코드만 올리면 알아서 돌아가는 PaaS 서비스가 대세이다.

이번 포스팅에서는 Azure 가 제공하는 여러 웹 서비스 플랫폼을 정리하고, 그 중 App Service 를 이용해 Streamlit 기반 ToDo 앱을 배포하는 과정을 다뤄본다.

Azure 에서 제공하는 웹 서비스 플랫폼

Azure 에서 웹 서비스를 구축하는 방법은 크게 네 가지가 있다. 각각 추상화 수준과 사용 목적이 다르므로 프로젝트 성격에 맞게 골라 쓰면 된다.

Azure App Service

가장 일반적인 완전 관리형 (PaaS) 서비스이다. 개발자는 코드만 작성해서 배포하면 되고, 웹 서버로서 필요한 작업들은 클라우드가 알아서 처리해준다.

.NET, Java, Node.js, Python, PHP 등 주요 언어와 프레임워크를 기본 지원하고, CI/CD, 배포 슬롯, HTTPS, 커스텀 도메인까지 전부 통합되어 있다. 직접 Certbot 으로 인증서 발급하고 crontab 으로 갱신 자동화할 일이 없다는게 좋다.

Azure Static Web Apps

웹 페이지를 정적인 영역과 동적인 영역으로 분리해서 처리하는 서비스이다.

HTML, CSS, JS, 이미지 같은 정적 요소는 글로벌 CDN 으로 배포되어서 컴퓨팅 파워를 쓰지 않고 그대로 사용자에게 전달된다. 실제 연산이 필요한 동적 기능은 Azure Functions (서버리스) 로 분리해서 처리한다.

전 세계에 분산된 노드에서 콘텐츠를 제공하기 때문에, 어디서든 빠르고 끊김 없이 접근할 수 있다는게 장점이다.

Azure Container Apps

컨테이너 기반 서비스이다. 가상 머신 (VM) 과 비교하면 차이가 확실하다.

VM 은 각 머신마다 OS 와 필요한 모든 요소를 다 올려야 해서 오버헤드가 크다. 반면 컨테이너는 소프트웨어와 실행에 필요한 환경 (라이브러리, 설정 등) 을 하나의 상자에 묶어서 배포하기 때문에 가볍다.

컨테이너 개수가 많아지면 관리가 복잡해지는데, 이걸 자동으로 관리해주는게 오케스트레이션이다. 표준 도구가 Kubernetes 이고, Azure Container Apps 는 이 Kubernetes 를 활용할 수 있게 해준다. 트래픽에 따른 자동 스케일링과 마이크로서비스 아키텍처에 적합하다.

Azure Virtual Machine

가장 기본적인 IaaS 방식이다. 리눅스 VM 을 직접 만들고, 그 위에 Python 이든 Node.js 든 필요한 환경을 전부 수동으로 세팅해야한다.

용량이 부족하면 같은 환경이 세팅된 VM 을 여러 대 추가해서 용량을 늘리는 스케일 아웃 방식을 쓴다. 물론 서버 대수만 늘린다고 되는건 아니고, 트래픽을 여러 대에 분산시켜주는 로드 밸런서도 같이 붙여야한다.

관리 포인트가 가장 많지만 그만큼 자유도도 높다.

App Service 생성해보기

실습으로 App Service 를 하나 만들어본다.

먼저 Azure 포털에 접속해서 리소스 그룹을 생성한다. 필자는 이름을 AppDev, 위치는 Korea Central 로 지정했다. 크레딧 절약을 위해 안 쓰는 리소스는 그때그때 정리하는게 좋다. 리소스 그룹 안에서 "웹앱" 을 검색한다.

설정 할 때 주의할 점이 있다.

  • 웹앱 이름은 곧 URL 이 되므로 유니크해야 한다.
  • 게시 옵션은 Python 코드를 바로 올릴거니까 '코드' 를 선택한다.
  • 런타임 스택은 Python 3.11 로 한다. 너무 최신이면 라이브러리 호환성 문제가 있고, 너무 오래된 버전은 보안 패치가 끊기므로 적절히 중간 버전을 고르는게 좋다.
  • 지역은 다시 Korea Central.

App Service Plan 이해하기

웹앱을 만들 때 App Service Plan 을 같이 지정해야한다. 이 친구는 쉽게 말해 웹 서버 그 자체이다. 스펙에 따라 비용이 결정되고, 하나의 Plan 안에서 여러 웹앱을 돌릴 수도 있다.

플랜은 Free, Basic, Premium 등으로 나뉘는데, Free 는 기능이 너무 제한적이라 권장하지 않는다. 실제 배포용이면 Basic, 이런저런 기능 테스트용이면 Premium (P0V3) 정도가 적당하다.

가격 정책은 사용 중에도 부족하면 그때그때 업그레이드 할 수 있으므로, 처음부터 고스펙으로 시작할 필요는 없다.

스케일링

트래픽이 늘면 웹서버를 여러개로 늘리는데, 이걸 스케일 아웃이라고 한다. App Service 에서는 서버 하나하나를 인스턴스라고 부른다.

수동으로 인스턴스 개수를 조절할 수도 있고 (최대 30 개), 자동으로 트래픽에 따라 늘리고 줄이는 Auto Scaling 도 설정 할 수 있다. 특정 조건 (CPU 70% 이상 등) 에 따라 크기를 조정하는 규칙 기반 설정도 가능하다.

단, 인스턴스 개수에 비례해서 비용도 늘어나므로 무작정 크게 잡으면 안된다.

로컬 개발 환경 세팅

이제 실제로 올릴 앱을 만들어본다. 예전에 리눅스 VM 에서 vi 로 코딩하던 것에 비하면 로컬 PC 에서 작업하는게 훨씬 편하다.

Python 3.11 설치

Python 공식 홈페이지에서 3.11 버전대의 설치 파일을 받는다. 윈도우 사용자는 Windows installer (64-bit) 를 받으면 된다.

설치할 때 한가지만 주의하면 되는데, 바로 "Add python.exe to PATH" 옵션을 반드시 체크해야 한다는 것. 이걸 빠뜨리면 커맨드라인에서 python 을 바로 못 부르기 때문에, 매번 절대경로로 호출하거나 환경변수를 수동으로 잡아줘야한다. 꽤 귀찮다.

VS Code 설치 및 확장

개발 도구는 VS Code 를 쓴다. 마이크로소프트에서 무료로 배포하는 편집기인데, 가볍고 확장성이 좋아서 거의 표준처럼 쓰이고 있다.

설치 후 작업 폴더 (예: WebApp) 를 만들고 Open Folder 로 연다. 그 안에 test.py 파일을 하나 만든다. 확장자를 .py 로 지정하면 자동으로 Python 아이콘이 붙는다.

왼쪽 Extensions 탭에서 Python 확장을 설치하면 IntelliSense (자동완성) 가 동작해서 개발이 훨씬 편해진다.

Streamlit 설치

Streamlit 은 Python 으로 데이터 기반 웹앱을 빠르게 만들 수 있는 라이브러리이다. 터미널에서 설치한다.

pip install streamlit
streamlit hello

브라우저에 "Welcome to Streamlit" 페이지가 뜨면 성공. 서버를 중단할 때는 Ctrl+C 를 누른다.

간단한 부가세 계산기 만들기

Streamlit 이 어떻게 동작하는지 감을 잡기 위해 간단한 부가세 계산기를 만들어본다.

import streamlit as st

st.title("Welcome to Streamlit")
st.header("부가세 계산기")
st.divider()

price = st.text_input("상품의 가격을 입력하세요")
btn_click = st.button("계산")

if btn_click:
    result = int(price) * 1.1
    st.text("부가세 포함 가격: " + str(result))

터미널에서 실행한다.

streamlit run test.py

localhost:8501 로 브라우저가 열린다. 우측 상단 메뉴에서 Always Rerun 을 켜두면 코드 저장할 때마다 자동으로 갱신되므로 편하다.

한가지 주의할 점은, st.text_input 으로 받은 값은 문자열이기 때문에 계산에 쓰려면 int() 나 float() 로 형변환을 해줘야 한다는 것. 안 그러면 TypeError 난다.

Azure 에 배포하기

로컬에서 잘 돌아가는걸 확인했으니 이제 클라우드에 올릴 차례이다.

가상 환경 구성

배포 전에 가상 환경부터 만든다. Python 은 프로젝트마다 요구하는 버전이나 패키지가 다른 경우가 많아서, 시스템 전역에 전부 설치하면 충돌이 난다. 각 프로젝트마다 독립된 환경을 만들어 쓰는게 정석이다.

python -m venv .venv
./.venv/Scripts/activate.ps1
pip install streamlit

방금 만든 가상 환경은 기존 PC 환경과 완전히 다른 독립 공간이라서, streamlit 을 다시 설치해줘야 한다.

VS Code 와 Azure 연결

Extensions 탭에서 Azure App Service 익스텐션을 찾아서 설치한다. 설치가 끝나면 좌측에 Azure 를 뜻하는 'A' 자 모양 아이콘이 생긴다.

Ctrl+Shift+P 로 명령 팔레트를 열고 "Azure: Sign in" 을 검색해 로그인한다. 로그인이 끝나면 본인의 구독 (Azure for Students 등) 이 목록에 나타나고, 그 아래로 앞서 만든 웹앱이 보인다.

배포용 스크립트 작성

여기서 중요한 점이 있다. Azure App Service 의 Python 환경에는 Streamlit 이 기본 설치되어 있지 않다. 그래서 웹앱이 시작될 때 streamlit 을 설치하고 실행하도록 셸 스크립트를 따로 만들어줘야 한다.

프로젝트 폴더에 streamlit.sh 파일을 만든다.

pip install streamlit
python -m streamlit run app.py --server.port 8000 --server.address 0.0.0.0

포트는 8000 번으로 지정한다. Azure App Service 는 기본적으로 이 포트를 통해 외부 요청을 받는다.

그리고 .deployment 파일도 추가한다.

[config]
SCM_DO_BUILD_DURING_DEPLOYMENT=false

Python 코드는 빌드 과정이 필요 없으니 이 옵션을 꺼서 배포 속도를 높인다.

배포 실행

VS Code 의 Azure 탭에서 웹앱을 우클릭하고 "Deploy to Web App" 을 선택한다. 현재 폴더를 선택하고, 기존 내용이 지워진다는 경고창에서 Deploy 를 누르면 배포가 시작된다.

최초 배포는 파일 패키징, Zip 압축, 업로드 과정까지 거쳐서 시간이 좀 걸린다.

Startup Command 설정

배포가 끝나도 웹앱이 어떻게 실행될지는 아직 모르는 상태이다. Azure 포털에서 해당 웹앱으로 가서 "설정 > 구성 (Configuration)" 메뉴를 연다.

"시작 명령" 항목에 아래 내용을 입력한다.

bash /home/site/wwwroot/streamlit.sh

저장하고 개요 페이지에서 "다시 시작" 버튼을 누른다. 초기 로딩에 꽤 시간이 걸리는데, pip install 이 동작하는 중이므로 인내심을 가지고 기다린다.

제공된 URL 로 접속해서 부가세 계산기가 동작하면 배포 성공이다.

MySQL 을 연동한 ToDo 앱 만들기

간단한 앱이 돌아가는걸 확인했으니, 이번에는 데이터베이스와 연동된 ToDo 앱을 만들어본다.

Azure Database for MySQL 생성

Azure 포털에서 리소스 그룹에 "Azure Database for MySQL" 을 추가한다. '단일 서버' 와 '유연한 서버' 중 유연한 서버 (Flexible Server) 를 선택한다.

서버 이름, 위치 (Korea Central), 관리자 ID/비밀번호를 설정한다. 워크로드는 테스트 용도이니 Dev/Test 로 선택한다. 이러면 비용이 훨씬 저렴하다.

여기서 중요한 작업 하나. 방화벽 설정을 안 해주면 외부에서 접속이 안된다. 네트워킹 메뉴로 가서 "현재 IP 주소 추가" 를 눌러 개발 PC 의 IP 주소를 허용 목록에 등록한다.

이전에 MariaDB 설정할 때 GRANT 쿼리로 IP 화이트리스트 걸던거랑 비슷한 개념이다. 클라우드에서는 이걸 방화벽 규칙으로 대체한다.

MySQL Workbench 로 테이블 설계

Azure 포털 개요 페이지에서 호스트 주소를 확인하고, Workbench 로 접속한다. 접속에 성공하면 ToDo 앱용 데이터베이스와 테이블을 생성한다.

CREATE DATABASE todo_app;
USE todo_app;

CREATE TABLE todos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    task VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    completed BOOLEAN DEFAULT FALSE
);

각 컬럼의 역할은 다음과 같다.

  • id: 자동 증가하는 고유 식별자 (Primary Key)
  • task: 할 일 내용 (필수 입력, NOT NULL)
  • created_at: 생성 시간, 값을 안 넣어도 현재 시간이 자동으로 들어감
  • completed: 완료 여부, 기본값은 false

Python 에서 DB 연결

가상 환경에 MySQL 커넥터를 설치한다.

pip install mysql-connector-python

app.py 에 DB 연결 함수를 작성한다.

import streamlit as st
import mysql.connector
from mysql.connector import Error

def get_db_connection():
    try:
        connection = mysql.connector.connect(
            host="YOUR_HOST.mysql.database.azure.com",
            user="YOUR_USER",
            password="YOUR_PASSWORD",
            database="todo_app"
        )
        return connection
    except Error as e:
        st.error(f"연결 실패: {e}")
        return None

CRUD 구현

할 일을 추가하는 add_task 함수이다.

def add_task(task):
    connection = get_db_connection()
    cursor = connection.cursor()
    query = "INSERT INTO todos(task) VALUES (%s)"
    cursor.execute(query, (task,))
    connection.commit()
    cursor.close()
    connection.close()

여기서 필자가 처음에 삽질했던 부분이 있다. cursor.execute(query, task) 이렇게 단일 값을 그냥 넘기면 ProgrammingError 가 난다. 반드시 (task,) 처럼 튜플로 감싸서 넘겨야 한다. 콤마 하나 빠뜨렸다고 안 돌아가니 주의하자.

할 일 목록을 가져오는 get_tasks 함수이다.

def get_tasks():
    try:
        connection = get_db_connection()
        cursor = connection.cursor()
        query = "SELECT * FROM todos WHERE completed = FALSE ORDER BY created_at DESC"
        cursor.execute(query)
        tasks = cursor.fetchall()
        cursor.close()
        connection.close()
        return tasks
    except Error as e:
        st.error(f"에러: {e}")
        return []

포인트는 두 가지이다.

  • WHERE completed = FALSE 로 완료되지 않은 항목만 필터링
  • ORDER BY created_at DESC 로 최근 생성 순 정렬

작업 완료 처리하는 mark_task_completed 함수이다.

def mark_task_completed(task_id):
    connection = get_db_connection()
    cursor = connection.cursor()
    query = "UPDATE todos SET completed = TRUE WHERE id = %s"
    cursor.execute(query, (task_id,))
    connection.commit()
    cursor.close()
    connection.close()

Complete 버튼을 누르면 해당 id 의 completed 값이 TRUE 로 바뀌고, 필터링 조건에 따라 목록에서 사라진다.

UI 조합

위 함수들을 Streamlit UI 와 엮으면 ToDo 앱이 완성된다.

st.header("Welcome to Super Todo App")
st.divider()

new_task = st.text_input("할 일을 입력하세요")
if st.button("Add Task") and new_task != "":
    add_task(new_task)
    st.success("할 일이 추가되었습니다!")

tasks = get_tasks()
for task in tasks:
    st.write(task)

Azure Static Web Apps 는 뭔가

마지막으로, App Service 와는 접근 방식이 좀 다른 Static Web Apps 도 정리해둔다.

핵심 아이디어는 정적 콘텐츠와 동적 API 를 분리한다는 것이다. HTML, CSS, JS 같은 정적 파일은 GitHub 에 푸시하면 GitHub Actions 가 자동으로 빌드하고 글로벌 CDN 에 배포해준다. 백엔드 기능은 서버리스인 Azure Functions 가 담당한다.

왜 이렇게 분리할까?

  • 정적 콘텐츠는 컴퓨팅 파워 없이 그대로 전달되므로 서버 부하가 없다.
  • JavaScript 는 클라이언트 브라우저에서 돌아가니까 서버 부담이 줄어든다.
  • CDN 덕분에 지구 반대편에서도 빠르게 로딩된다.

React, Angular, Vue, Next.js 같은 주요 JS 프레임워크를 전부 지원한다. 프론트엔드 중심 웹앱이라면 App Service 보다 Static Web Apps 쪽이 적합한 경우가 많다.

마무리

예전에 리눅스 서버에 직접 Nginx 올리고, Certbot 으로 인증서 발급받고, MariaDB 방화벽 뚫고 GRANT 쿼리 때려가며 세팅하던걸 생각하면 PaaS 의 편리함은 확실하다. 코드만 올리면 HTTPS, 도메인, 스케일링이 전부 자동으로 처리된다.

물론 그만큼 자유도는 떨어진다. 세밀하게 커스터마이징 하고 싶다면 여전히 VM 이 나을 수도 있다. 프로젝트 성격에 맞게 고르는게 답이다.

다음에는 Azure Functions 와 Static Web Apps 를 실제로 연동해보는걸 다뤄볼 예정이다.