Flask 웹 서버 실습 — 로그인 & Dashboard 구현

syeom·2026년 4월 27일

멀티캠퍼스 데이터분석 4월 27일 수업 내용 — Flask 라우팅, Jinja2 템플릿, Chart.js, Bootstrap Dashboard, DB 연동 로그인


📌 목차

  1. 프로젝트 구조
  2. querys/user.py — SQL 쿼리 모듈
  3. db.py — MyDB 클래스
  4. app.py — Flask 메인 애플리케이션
  5. templates/index.html — 로그인 페이지
  6. templates/main.html — 로그인 성공 페이지
  7. templates/dashboard.html — Bootstrap Dashboard
  8. static/css/dashboard.css — 커스텀 스타일
  9. static/js/color-modes.js — 다크모드 토글
  10. static/js/dashboard.js — Chart.js 그래프
  11. 서버 실행 & 동작 확인

1. 프로젝트 구조

20260427/
├── querys/
│   └── user.py          # SQL 쿼리문 모음
├── static/
│   ├── css/
│   │   ├── bootstrap.min.css   # Bootstrap 5.3.8 CSS
│   │   └── dashboard.css       # 커스텀 스타일
│   └── js/
│       ├── bootstrap.bundle.min.js   # Bootstrap JS
│       ├── color-modes.js            # 다크/라이트 모드 토글
│       └── dashboard.js              # Chart.js 그래프
├── templates/
│   ├── index.html       # 로그인 페이지
│   ├── main.html        # 로그인 성공 후 메인 페이지
│   └── dashboard.html   # Bootstrap Dashboard 예제
├── app.py               # Flask 메인 서버
└── db.py                # MyDB 클래스

💡 Flask 프로젝트 폴더 구조 규칙

  • templates/ → HTML 파일 저장 (Flask가 자동으로 인식)
  • static/ → CSS, JS, 이미지 등 정적 파일 저장
  • querys/ → SQL 쿼리문을 별도 모듈로 분리하여 관리

2. querys/user.py — SQL 쿼리 모듈

SQL 쿼리문들을 별도 모듈로 분리해서 관리합니다.
app.py에서 from querys import user 로 불러와 사용합니다.

# SQL query문들을 저장

# 회원 테이블 생성
create_query = """
    CREATE TABLE IF NOT EXISTS user (
        id       VARCHAR(32) PRIMARY KEY,
        password VARCHAR(32) NOT NULL,
        name     VARCHAR(32) NOT NULL
    )
"""

# 로그인 조회 — id, password 모두 일치하는 데이터 존재 여부 확인
login_query = """
    SELECT * FROM user
    WHERE id = %s AND password = %s
"""

# 아이디 중복 체크
id_check_query = """
    SELECT * FROM user
    WHERE id = %s
"""

# 회원 가입
signup_query = """
    INSERT INTO user VALUES (%s, %s, %s)
"""

💡 쿼리문을 별도 모듈로 분리하면 app.py가 깔끔해지고 유지보수가 편해집니다.


3. db.py — MyDB 클래스

생성자에 기본값이 설정되어 있어 MyDB() 로 바로 로컬 DB 연결이 가능합니다.

import pymysql

class MyDB:

	# 생성자 함수 (서버의 정보를 받아오기 위해 사용)
    def __init__(self,
                 host     = '127.0.0.1',
                 port     = 3306,
                 user     = 'root',
                 password = '1234',
                 db       = 'multicam'):
                 
        # 서버의 정보를 인자로 받아와서 객체 내부에서 독립적인 변수에 저장
        self.host     = host
        self.port     = port
        self.user     = user
        self.password = password
        self.db       = db
        
	# 데이터베이스에 변화를 주는 함수
    def commit(self):
        try:
            self.db_server.commit()
            self.db_server.close()
            del self.db_server
        except:
            print('DB 서버와 연결되어 있지 않습니다. sql_query()를 먼저 호출하세요.')

    def sql_query(self, query, *datas):
        try:
            self.db_server
            print('접속된 서버가 존재함')
        except:
            self.db_server = pymysql.connect(
                host     = self.host,
                port     = self.port,
                user     = self.user,
                password = self.password,
                db       = self.db
            )
            
		# cursor 생성
        cursor = self.db_server.cursor(pymysql.cursors.DictCursor)

        try:
            cursor.execute(query, datas)
            if query.lstrip().lower().startswith('select'):
                result = cursor.fetchall()
            else:
                result = 'Query OK!'
            return result
        except Exception as e:
            print('query 실행 중 에러:', e)

4. app.py — Flask 메인 애플리케이션

기본 설정

# flask 프레임워크 안에서 특정 기능을 로드
from flask import Flask, render_template, request, redirect, url_for
from db import MyDB
from querys import user
import pandas as pd

app = Flask(__name__)
db  = MyDB()

# 서버 시작 시 user 테이블 자동 생성
db.sql_query(user.create_query)
db.commit()

💡 Flask 주요 import

기능설명
Flask웹 서버 클래스
render_templatetemplates/ 폴더의 HTML 파일 불러오기
request유저가 보낸 데이터 접근
redirect특정 주소로 이동
url_for상대 경로 지정 (static 파일 경로 생성 등)

라우트 목록

# / → index.html 렌더링
@app.route('/')
def index():
    return render_template('index.html')
# Jinja2 템플릿 문법
# 1. {{ 변수명 }}: 파이썬에서 넘겨받은 데이터를 화면에 '출력'할 때 사용
# 2. {% 파이썬 코드 %}: code를 인식하여 조건에 맞는 경우만 html를 추가
#    - {% if ... %} : 조건에 맞을 때만 특정 HTML 부분을 보여줌
#    - {% for ... %} : 데이터를 반복하며 HTML 요소를 여러 개 생성

# /second → 단순 텍스트 반환
@app.route('/second')
def second():
    return 'Second Page'


# GET /login → URL 쿼리스트링으로 데이터 수신 (하드코딩 체크)
# /login 주소를 생성
@app.route('/login')
def login():
	# 유저가 보낸 데이터를 받아온다. (flask 프레임퉈크에서 기능을 불러와서 사용)
    # get 방식으로 보낸 데이터를 확인
    print(request.args)
    # requests.args가 dict형태 -> 각각의 key 값으로 데이터를 추출
    input_id   = request.args['input_id']
    input_pass = request.args['input_pass']

    if input_id == 'admin' and input_pass == '1234':
        return '로그인 성공'
    else:
        return 'ID, PASSWORD를 확인해주세요'


# POST /login2 → form 데이터로 수신, DB 조회 후 로그인 처리
# post 방식은 데이터를 숨겨서 보낸다 // 웹브라우저에서 주소창에 입력하여 확인이 불가능
@app.route('/login2', methods=['post'])
def login2():
    _id   = request.form['input_id']
    _pass = request.form['input_pass']

	# login_query에는 2개의 데이터가 필요(id, password)
    sql_result = db.sql_query(user.login_query, _id, _pass)
    # DB_server와의 연결을 종료
    db.commit()

    if sql_result:
    	# 리스트에서 특정 위치를 선택 -> sql_result[0] -> {'id': 'admin', 'password': '1234', 'name': 'kim'}
        # 딕셔너리에서 특정 키를 선택 -> sql_result[0]['name'] -> 'kim'
        sql_name = sql_result[0]['name']

        # aapl.csv 로드 → 최근 10개 데이터 추출
        df  = pd.read_csv('../csv/aapl.csv')
        df2 = df.tail(10)

        # Chart.js용 x, y 데이터
        # 데이터프레임에서 특정 컬럼을 하나 선택 -> Series(html에서 사용 불가능) -> list 형으로 변환
        x_data = df2['Date'].tolist()
        # y축 데이터 -> Adj Close 컬럼
        y_data = df2['Adj Close'].tolist()

        # 테이블용 컬럼명 & 데이터
        # 테이블의 컬럼의 이름들을 따로 추출해서 list형태로 변환
        th_data = df2.columns.tolist()
        # th_data -> ['Date','Open',...]
        td_data = df2.to_dict('records')

        return render_template(
            'main.html',
            name    = sql_name,
            x       = x_data,
            y       = y_data,
            th_data = th_data,
            td_data = td_data
        )
    else:
        return redirect('/')


# /dashboard → Bootstrap Dashboard 페이지
@app.route('/dashboard')
def dashboard():
    return render_template('dashboard.html')

# 웹 서버를 시작한다(구동한다) -> 함수의 return과 흡사 -> run() 함수 아래의 주소 값들은 적용되지 x
# run() 매개변수
    # host 매개변수 -> 허용 가능 주소 목록( 기본값은 로컬의 pc에서만 접근 가능,
    # 0.0.0.0으로 지정하면 모든 주소가 접근이 가능 )
    # port 매개변수 -> 해당 웹서버의 포트 번호를 지정(5000번 기본값)
    # debug 매개변수 -> 디버그 모드를 사용할 것인가? (기본값 : False)
    #                   True 변경하면 개발 모드 변경 -> 파일은 수정이 되었을때 서버가 재시작
app.run(debug=True)

GET vs POST 비교

GET (/login)POST (/login2)
데이터 위치URL 쿼리스트링request body
Flask 접근request.args['키']request.form['키']
보안URL에 노출됨숨겨서 전송
HTML 메서드method="get"method="post"

⚠️ app.run(debug=True) 아래 코드는 실행되지 않습니다.
debug=True → 파일 수정 시 서버 자동 재시작 (개발 모드)


5. templates/index.html — 로그인 페이지

GET 방식과 POST 방식 두 가지 로그인 폼을 구현합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1>Hello Flask</h1>
    <p>이 페이지는 메인 페이지 입니다.</p>

    <!-- GET 방식 → /login 으로 전송 -->
    <form action="/login" method="get">
        <label>ID</label>
        <input type="text" name="input_id">
        <br>
        <label>PASSWORD</label>
        <input type="password" name="input_pass">
        <br>
        <input type="submit">
    </form>

    <h3>post 방식으로 데이터 보내기</h3>

    <!-- POST 방식 → /login2 으로 전송 -->
    <form action="/login2" method="post">
        <label>ID</label>
        <input type="text" name="input_id">
        <br>
        <label>PASSWORD</label>
        <input type="password" name="input_pass">
        <br>
        <input type="submit">
    </form>
</body>
</html>

💡 form 태그 주의사항

  • action → 데이터를 보낼 주소
  • method → 전송 방식 (get / post)
  • inputname 속성 → 서버에서 데이터를 꺼낼 때 사용하는 key 값
  • from 으로 오타 내는 경우가 많으니 주의!

6. templates/main.html — 로그인 성공 페이지

Jinja2 템플릿 문법으로 Python 변수를 HTML에 대입합니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1>Main Page</h1>

    <!-- app.py에서 전달한 name 변수 대입 -->
    <p>{{name}} 님 안녕하세요!</p>

    <!-- Chart.js 그래프 영역 -->
    <div>
        <canvas id="myChart"></canvas>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  	
    <!-- 실제 그래프를 canvas에 그려주는 부분 -->
    <script>
        const ctx = document.getElementById('myChart');

        new Chart(ctx, {
            type: 'line',
            data: {
                labels:   {{x | tojson}},   // x축 — Date 컬럼
                datasets: [{
                    label:       '# of Votes',
                    data:        {{y | tojson}},   // y축 — Adj Close 컬럼
                    borderWidth: 1
                }]
            },
            options: {}
        });
    </script>

    <!-- 동적 테이블 -->
    <table border=1>
        <tr>
            <!--
                th_data -> ['Data','Open','High','Low','Close','Adj Close','Volumn']
                반복문을 이용해서 작업을 하면 짧고 유동성있게 사용이 가능
                python에서 for문은 들여쓰기를 통해 반복 실행 구문 선택
            -->
            <!-- 반복문의 시작 -->
            {% for col in th_data %}
                <th>{{col}}</th>
          	<!-- 반복문의 끝 -->
            {% endfor %}
        </tr>
        <!--
              td_data->
              [{'Date' : xxx, 'Open' : xxx, 'High' : xxx, 'Low' : xxx, 'Close' : xxx, 'Adj Close' : xxx, 'Volume' : xxx},{}]
          -->
        {% for row in td_data %}
        <tr>
            {% for col in th_data %}
                <td>{{row[col]}}</td>
            {% endfor %}
        </tr>
        {% endfor %}
    </table>
</body>
</html>

Jinja2 주요 문법

문법설명
{{변수명}}Python 변수를 HTML에 출력
{{변수 \| tojson}}Python 데이터를 JSON 형식으로 변환 (JS에서 사용)
{% for item in list %}반복문 시작
{% endfor %}반복문 끝
{% if 조건 %}조건문 시작
{% endif %}조건문 끝

💡 tolist() vs to_dict('records')

  • Series.tolist() → Python 리스트로 변환 (Jinja2에서 사용 가능)
  • df.to_dict('records')[{컬럼명: 값, ...}, ...] 형태로 변환 → 테이블 렌더링에 사용

7. templates/dashboard.html — Bootstrap Dashboard

Bootstrap 5.3 공식 예제 Dashboard 템플릿을 Flask에 적용합니다.
외부 CSS/JS 경로를 url_for('static', filename=...) 로 지정합니다.

<!-- Bootstrap 5.3.8 Examples의 dashboard 폴더에서 가져온 템플릿 -->

<!-- color-modes.js — head에서 먼저 로드 -->
<script src="{{url_for('static', filename='js/color-modes.js')}}"></script>

<!-- Bootstrap CSS -->
<link href="{{url_for('static', filename='css/bootstrap.min.css')}}" rel="stylesheet" />

<!-- 커스텀 CSS -->
<link href="{{url_for('static', filename='css/dashboard.css')}}" rel="stylesheet" />

<!-- Bootstrap JS (body 끝에) -->
<script src="{{url_for('static', filename='js/bootstrap.bundle.min.js')}}"></script>

<!-- Chart.js (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.3.2/dist/chart.umd.js"></script>

<!-- dashboard.js -->
<script src="{{url_for('static', filename='js/dashboard.js')}}"></script>

💡 url_for('static', filename='경로') 사용 이유
HTML 파일에서 static 파일을 직접 경로로 쓰면 오류가 발생할 수 있습니다.
Flask의 url_for() 를 사용하면 app.py 위치를 기준으로 올바른 경로를 자동 생성합니다.


8. static/css/dashboard.css — 커스텀 스타일

Bootstrap Dashboard의 사이드바, 네브바 스타일을 커스터마이징합니다.

/* 사이드바 nav-link 기본 스타일 */
.sidebar .nav-link {
    font-size: .875rem;
    font-weight: 500;
}

/* 활성화된 nav-link 색상 */
.sidebar .nav-link.active {
    color: #2470dc;
}

/* 사이드바 소제목 */
.sidebar-heading {
    font-size: .75rem;
}

/* 네브바 브랜드 영역 */
.navbar-brand {
    padding-top: .75rem;
    padding-bottom: .75rem;
    background-color: rgba(0, 0, 0, .25);
    box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}

/* 반응형 — md 이상에서 사이드바 sticky */
@media (min-width: 768px) {
    .sidebar .offcanvas-lg {
        position: sticky;
        top: 48px;
    }
}

9. static/js/color-modes.js — 다크모드 토글

Bootstrap Dashboard의 라이트/다크/자동 테마를 전환하는 JS입니다.
localStorage에 선택한 테마를 저장해 페이지 재방문 시에도 유지됩니다.

(() => {
    'use strict'

    // localStorage에서 저장된 테마 불러오기 / 저장
    const getStoredTheme  = () => localStorage.getItem('theme')
    const setStoredTheme  = theme => localStorage.setItem('theme', theme)

    // 시스템 설정 또는 저장값 기반으로 테마 결정
    const getPreferredTheme = () => {
        const storedTheme = getStoredTheme()
        if (storedTheme) return storedTheme
        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
    }

    // HTML에 data-bs-theme 속성 적용
    const setTheme = theme => {
        if (theme === 'auto') {
            document.documentElement.setAttribute(
                'data-bs-theme',
                window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
            )
        } else {
            document.documentElement.setAttribute('data-bs-theme', theme)
        }
    }

    setTheme(getPreferredTheme())

    // 시스템 다크모드 변경 감지
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
        const storedTheme = getStoredTheme()
        if (storedTheme !== 'light' && storedTheme !== 'dark') {
            setTheme(getPreferredTheme())
        }
    })

    // 버튼 클릭 시 테마 변경
    window.addEventListener('DOMContentLoaded', () => {
        showActiveTheme(getPreferredTheme())
        document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => {
            toggle.addEventListener('click', () => {
                const theme = toggle.getAttribute('data-bs-theme-value')
                setStoredTheme(theme)
                setTheme(theme)
                showActiveTheme(theme, true)
            })
        })
    })
})()

10. static/js/dashboard.js — Chart.js 그래프

Dashboard 페이지의 기본 라인 차트를 그립니다.

(() => {
    'use strict'

    const ctx = document.getElementById('myChart')

    new Chart(ctx, {
        type: 'line',
        data: {
            labels: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
            datasets: [{
                data:                [15339, 21345, 18483, 24003, 23489, 24092, 12034],
                lineTension:         0,
                backgroundColor:     'transparent',
                borderColor:         '#007bff',
                borderWidth:         4,
                pointBackgroundColor: '#007bff'
            }]
        },
        options: {
            plugins: {
                legend:  { display: false },
                tooltip: { boxPadding: 3 }
            }
        }
    })
})()

💡 main.html 의 Chart.js는 Python에서 전달한 실시간 데이터를 사용하고,
dashboard.js 의 Chart.js는 하드코딩된 샘플 데이터를 사용합니다.


11. 서버 실행 & 동작 확인

서버 실행

cd C:\study\multicampus_practice\20260427
python app.py

서버가 정상 실행되면 아래와 같은 메시지가 출력됩니다.

* Serving Flask app 'app'
* Debug mode: on
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Debugger is active!

동작 흐름

브라우저에서 127.0.0.1:5000 접속
    ↓
index.html 로그인 페이지 렌더링
    ↓
id/password 입력 후 제출
    ↓
GET 방식: 127.0.0.1:5000/login?input_id=syeom&input_pass=1234
POST 방식: /login2 (URL에 데이터 미노출)
    ↓
DB에서 user 테이블 조회
    ↓
로그인 성공 → main.html (그래프 + 테이블)
로그인 실패 → redirect('/') 로 돌아감



실행 화면

① 서버 실행 (터미널)

서버 실행

② 로그인 페이지 (127.0.0.1:5000)

로그인 폼에 id = syeom, password = 1234 입력 시
GET 방식이면 URL에 데이터가 노출됩니다.

127.0.0.1:5000/?input_id=syeom&input_pass=1234

③ DB 테이블 생성 확인 (MySQL Workbench)

서버 시작 시 user.create_query 가 자동 실행되어 user 테이블이 생성됩니다.

SELECT * FROM multicam.user;
-- admin / 1234 / kim
-- test  / 0000 / park

④ Dashboard 페이지 (127.0.0.1:5000/dashboard)

Bootstrap 5.3 Dashboard 예제 템플릿이 Flask에 적용됩니다.
url_for('static', filename=...) 으로 CSS/JS 파일이 정상 로드되는지 확인합니다.

GET /static/js/color-modes.js        200
GET /static/css/bootstrap.min.css    200
GET /static/css/dashboard.css        200
GET /static/js/bootstrap.bundle.min.js  200
GET /static/js/dashboard.js          200

⚠️ 자주 발생하는 에러

BuildError: Could not build url for endpoint 'static' with values ['filenamge']

url_for('static', filename=...) 에서 filenamefilenamge 로 오타 낸 경우입니다.
철자를 정확히 확인하세요!


📎 핵심 개념 요약

개념설명
Flask(__name__)Flask 앱 생성
@app.route('경로')URL과 함수 연결 (데코레이터)
render_template('파일.html', 변수=값)HTML 렌더링 + 변수 전달
request.args['키']GET 방식 데이터 수신
request.form['키']POST 방식 데이터 수신
redirect('/')특정 주소로 이동
url_for('static', filename='경로')static 파일 경로 생성
{{변수}}Jinja2 변수 출력
{{변수 \| tojson}}Python → JSON 변환 (JS 사용)
{% for %} ~ {% endfor %}Jinja2 반복문
df.tail(n)하위 n개 행 추출
Series.tolist()Series → Python 리스트
df.to_dict('records')DataFrame → 딕셔너리 리스트
app.run(debug=True)개발 서버 실행 (파일 수정 시 자동 재시작)
profile
공부 기록

0개의 댓글