내 프로젝트 2 : Flask Authentication System (Python + Flask 사용)

발행: (2025년 12월 14일 오전 02:30 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

My project 2 커버 이미지: Flask 인증 시스템 (Python + Flask 사용)

🔐 Flask 인증 시스템

나는 매일 사용하는 웹사이트가 실제로 어떻게 만들어지는지 항상 궁금했다.
우리는 회원가입, 로그인, 로그아웃 같은 기능을 끊임없이 사용하지만, 대부분 그 뒤에서 무슨 일이 일어나는지 잘 모른다.
나도 예외는 아니었다 — 이런 호기심이 점점 커졌고, 직접 구조를 경험해보고 싶었다.
그래서 처음부터 직접 만들어 보기로 결심했고, 손으로 직접 해보면서 이러한 시스템이 실제로 어떻게 동작하는지 이해할 수 있기를 바랐다.

📂 1. 프로젝트 구조

auth_blog/
│── main.py
│── users.db (auto-created)
└── templates/
    ├── home.html
    ├── signup.html
    └── login.html

🧠 2. 이 프로젝트가 하는 일

  • 사용자 회원가입 (해시된 비밀번호 저장)
  • 세션 관리가 포함된 사용자 로그인
  • login_required 데코레이터를 이용한 보호된 대시보드 페이지
  • 로그아웃 기능
  • 기본 미니 블로그 UI 레이아웃

이 구조는 거의 모든 현대 웹 애플리케이션의 기반이 된다.

🖥️ 3. 백엔드 코드 (main.py)

from flask import Flask, render_template, request, redirect, session
import sqlite3
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.secret_key = "super_secret_key"

# Login‑required decorator
def login_required(func):
    def wrapper(*args, **kwargs):
        if "user_id" not in session:
            return redirect("/login")
        return func(*args, **kwargs)
    wrapper.__name__ = func.__name__
    return wrapper

# Initialize database
def init_db():
    conn = sqlite3.connect("users.db")
    cur = conn.cursor()
    cur.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT UNIQUE NOT NULL,
        password TEXT NOT NULL
    )
    """)
    conn.commit()
    conn.close()

init_db()

# Signup
@app.route("/signup", methods=["GET", "POST"])
def signup():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]
        hashed_pw = generate_password_hash(password)

        conn = sqlite3.connect("users.db")
        cur = conn.cursor()
        try:
            cur.execute(
                "INSERT INTO users (username, password) VALUES (?, ?)",
                (username, hashed_pw)
            )
            conn.commit()
        except sqlite3.IntegrityError:
            return "Signup failed: Username already exists."
        finally:
            conn.close()

        return redirect("/login")

    return render_template("signup.html")

# Login
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]

        conn = sqlite3.connect("users.db")
        cur = conn.cursor()
        cur.execute(
            "SELECT id, username, password FROM users WHERE username = ?",
            (username,)
        )
        user = cur.fetchone()
        conn.close()

        if user and check_password_hash(user[2], password):
            session["user_id"] = user[0]
            session["username"] = user[1]
            return redirect("/dashboard")
        else:
            return "Login failed: Invalid username or password."

    return render_template("login.html")

# Dashboard (protected)
@app.route("/dashboard")
@login_required
def dashboard():
    return f"Welcome, {session['username']}! (Dashboard Page)"

# Logout
@app.route("/logout")
def logout():
    session.clear()
    return redirect("/login")

# Home
@app.route("/")
def home():
    return render_template("home.html")

if __name__ == "__main__":
    app.run(debug=True)

🖼️ 4. 템플릿

🏠 home.html

<!DOCTYPE html>
<html>
<head>
    <title>Mini Blog</title>
    <style>
        body { font-family: sans-serif; padding: 20px; background-color: #f4f7f9; }
        .post { 
            border: 1px solid #ddd; 
            padding: 15px; 
            margin-bottom: 15px; 
            border-radius: 8px; 
            background-color: white;
            box-shadow: 0 2px 4px rgba(0,0,0,0.05);
        }
        .top { 
            display: flex; justify-content: space-between; 
            align-items: center; border-bottom: 2px solid #007bff; 
            margin-bottom: 20px; padding-bottom: 10px;
        }
        a { text-decoration: none; color: #007bff; font-weight: bold; }
    </style>
</head>
<body>
    <div class="top">
        <h1>Mini Blog Posts</h1>
        <a href="/new">Write New Post →</a>
    </div>

    {% for post in posts %}
        <div class="post">
            <h3>{{ post[1] }}</h3>
            <p>{{ post[2] }}</p>
        </div>
    {% endfor %}

    {% if not posts %}
        <p>No posts yet. Start writing your first entry!</p>
    {% endif %}
</body>
</html>

🔑 login.html

<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
    <style>
        body { font-family: sans-serif; padding: 20px; }
        form { display: flex; flex-direction: column; width: 300px; gap: 10px; }
        input { padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
        button { padding: 10px; background: #28a745; color: white; 
                 border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #1e7e34; }
    </style>
</head>
<body>
    <h2>Log In</h2>
    <form method="POST" action="/login">
        <input type="text" name="username" placeholder="Username" required>
        <input type="password" name="password" placeholder="Password" required>
        <button type="submit">Log In</button>
    </form>
</body>
</html>

📝 signup.html

<!DOCTYPE html>
<html>
<head>
    <title>Sign Up</title>
    <style>
        body { font-family: sans-serif; padding: 20px; }
        form { display: flex; flex-direction: column; width: 300px; gap: 10px; }
        input { padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
        button { padding: 10px; background: #007bff; color: white; 
                 border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #0056b3; }
    </style>
</head>
<body>
    <h2>Sign Up</h2>
    <form method="POST" action="/signup">
        <input type="text" name="username" placeholder="Username" required>
        <input type="password" name="password" placeholder="Password" required>
        <button type="submit">Register</button>
    </form>
</body>
</html>

🔧 5. 직접 해보기 — 간단한 개선 사항

  • 인증된 대시보드에 게시물 작성 기능 추가
  • 사용자가 자신의 게시물을 수정/삭제할 수 있게 하기
  • 게시물에 타임스탬프 추가
  • 비밀번호 재설정 기능 구현
  • CSS 스타일시트를 이용해 UI 개선

이러한 작은 개선을 통해 웹 인증과 백엔드 구조를 더욱 깊이 이해할 수 있다. 하나 혹은 두 개 정도 변경해보고, 이 프로젝트가 실제 애플리케이션으로 빠르게 진화하는 모습을 확인해 보자!

Back to Blog

관련 글

더 보기 »

Part 2: 첫 번째 Django 프로젝트 설정

이것은 “Learn Backend Development by Building a Social Media App” 시리즈의 파트 2입니다. 다시 오신 것을 환영합니다! 이전 파트에서는 백엔드가 실제로 무엇인지 배웠습니다.