포스트

[이제와서 시작하는 Python 마스터하기 #17] 웹 프레임워크 입문: Flask와 Django 완벽 가이드

[이제와서 시작하는 Python 마스터하기 #17] 웹 프레임워크 입문: Flask와 Django 완벽 가이드

💼 실무 예시: 토스뱅크 같은 핀테크 서비스 개발하기

웹 프레임워크를 배우기 전에, 실제 한국 기업들이 어떻게 Python 웹 프레임워크를 활용하고 있는지 살펴보겠습니다.

토스뱅크 스타일의 간단한 계좌 관리 API를 만들어보며 Flask와 Django의 차이점을 이해해보겠습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 토스뱅크 스타일 계좌 관리 시스템 개념
"""
요구사항:
1. 사용자 등록/로그인
2. 계좌 잔액 조회
3. 송금 기능
4. 거래 내역 조회
5. 보안 (2단계 인증)

Flask vs Django 선택 기준:
- Flask: 빠른 프로토타입, 마이크로서비스 (토스의 개별 서비스들)
- Django: 전체 백오피스 시스템, 관리자 페이지 필요시
"""

# 실제 핀테크 회사들의 선택
fintech_frameworks = {
    "토스": "Spring Boot (Java) + Python 마이크로서비스",
    "카카오페이": "Django + Flask 하이브리드",
    "네이버페이": "Spring + Python 데이터 분석",
    "배달의민족": "Django 기반 주문 시스템",
    "당근마켓": "Django + React 풀스택"
}

print("한국 핀테크/스타트업 프레임워크 사용 현황:")
for company, tech in fintech_frameworks.items():
    print(f"{company}: {tech}")

이제 이런 실제 서비스를 만들 수 있는 웹 프레임워크에 대해 알아보겠습니다.

🌐 웹 프레임워크란?

웹 프레임워크는 웹 애플리케이션을 효율적으로 개발할 수 있도록 도와주는 도구 모음입니다. Python에서는 Flask와 Django가 가장 인기있는 웹 프레임워크입니다.

graph TD
    A[웹 프레임워크] --> B[Flask]
    A --> C[Django]

    B --> B1[마이크로 프레임워크]
    B --> B2[유연성]
    B --> B3[최소한의 구성]
    B --> B4[확장 가능]

    C --> C1[풀스택 프레임워크]
    C --> C2[배터리 포함]
    C --> C3[ORM 내장]
    C --> C4[관리자 페이지]

[!TIP] Flask vs Django: 뭘 먼저 배울까요?

  • Flask: “내가 직접 하나씩 조립하고 싶다!” → 가볍고 자유도가 높아서 웹의 동작 원리를 배우기 좋습니다.
  • Django: “빨리 완성품을 만들고 싶다!” → 로그인, 관리자 페이지, DB 연동 등이 다 들어있어서 생산성이 엄청납니다.

초보자라면 Flask로 가볍게 시작해서 웹의 흐름을 익히고, Django로 넘어가는 것을 추천합니다!

🚀 Flask 시작하기

Flask 설치와 기본 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 설치
# pip install flask

from flask import Flask, render_template, request, jsonify

# Flask 앱 생성
app = Flask(__name__)

# 라우트 정의
@app.route('/')
def home():
    """홈페이지"""
    return '<h1>Welcome to Flask!</h1>'

@app.route('/hello/<name>')
def hello(name):
    """URL 매개변수 사용"""
    return f'<h1>Hello, {name}!</h1>'

@app.route('/about')
def about():
    """템플릿 렌더링"""
    return render_template('about.html', title='About Us')

# 앱 실행
if __name__ == '__main__':
    app.run(debug=True, port=5000)

[!WARNING] debug=True는 개발할 때만!

app.run(debug=True)를 쓰면 코드를 고칠 때마다 서버가 자동으로 재시작되고, 에러가 나면 웹페이지에 상세한 에러 정보를 보여줍니다. 개발할 땐 정말 편하지만, 실제 서비스(배포)할 때 켜두면 해커에게 서버 내부 정보를 다 보여주는 꼴이 됩니다. 배포 시에는 반드시 꺼야 합니다!

Flask REST API 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from flask import Flask, jsonify, request
from flask_cors import CORS
import sqlite3

app = Flask(__name__)
CORS(app)  # Cross-Origin 허용

# 데이터베이스 초기화
def init_db():
    conn = sqlite3.connect('users.db')
    c = conn.cursor()
    c.execute('''CREATE TABLE IF NOT EXISTS users
                 (id INTEGER PRIMARY KEY, name TEXT, email TEXT)''')
    conn.commit()
    conn.close()

# GET - 모든 사용자 조회
@app.route('/api/users', methods=['GET'])
def get_users():
    conn = sqlite3.connect('users.db')
    c = conn.cursor()
    c.execute("SELECT * FROM users")
    users = []
    for row in c.fetchall():
        users.append({
            'id': row[0],
            'name': row[1],
            'email': row[2]
        })
    conn.close()
    return jsonify(users)

# POST - 새 사용자 생성
@app.route('/api/users', methods=['POST'])
def create_user():
    data = request.json
    conn = sqlite3.connect('users.db')
    c = conn.cursor()
    c.execute("INSERT INTO users (name, email) VALUES (?, ?)",
              (data['name'], data['email']))
    conn.commit()
    user_id = c.lastrowid
    conn.close()

    return jsonify({
        'id': user_id,
        'name': data['name'],
        'email': data['email']
    }), 201

# PUT - 사용자 정보 수정
@app.route('/api/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    data = request.json
    conn = sqlite3.connect('users.db')
    c = conn.cursor()
    c.execute("UPDATE users SET name=?, email=? WHERE id=?",
              (data['name'], data['email'], user_id))
    conn.commit()
    conn.close()

    return jsonify({'message': 'User updated successfully'})

# DELETE - 사용자 삭제
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    conn = sqlite3.connect('users.db')
    c = conn.cursor()
    c.execute("DELETE FROM users WHERE id=?", (user_id,))
    conn.commit()
    conn.close()

    return jsonify({'message': 'User deleted successfully'})

# 에러 핸들링
@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not found'}), 404

@app.errorhandler(500)
def internal_error(error):
    return jsonify({'error': 'Internal server error'}), 500

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

Flask 블루프린트와 프로젝트 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# project_structure/
# ├── app/
# │   ├── __init__.py
# │   ├── models.py
# │   ├── views/
# │   │   ├── __init__.py
# │   │   ├── auth.py
# │   │   └── blog.py
# │   └── templates/
# ├── config.py
# └── run.py

# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    app.config.from_object('config.Config')

    db.init_app(app)

    # 블루프린트 등록
    from app.views.auth import auth_bp
    from app.views.blog import blog_bp

    app.register_blueprint(auth_bp, url_prefix='/auth')
    app.register_blueprint(blog_bp, url_prefix='/blog')

    return app

# app/views/auth.py
from flask import Blueprint, render_template, request, redirect, url_for
from werkzeug.security import generate_password_hash, check_password_hash

auth_bp = Blueprint('auth', __name__)

@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        # 로그인 로직
        return redirect(url_for('blog.index'))
    return render_template('login.html')

@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        hashed_password = generate_password_hash(password)
        # 사용자 등록 로직
        return redirect(url_for('auth.login'))
    return render_template('register.html')

🎯 Django 시작하기

Django 프로젝트 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Django 설치
pip install django

# 프로젝트 생성
django-admin startproject myproject

# 앱 생성
python manage.py startapp myapp

# 마이그레이션
python manage.py makemigrations
python manage.py migrate

# 서버 실행
python manage.py runserver

Django 모델과 ORM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# myapp/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone

class Post(models.Model):
    """블로그 포스트 모델"""
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    content = models.TextField()
    created_at = models.DateTimeField(default=timezone.now)
    updated_at = models.DateTimeField(auto_now=True)
    published = models.BooleanField(default=False)

    class Meta:
        ordering = ['-created_at']
        verbose_name = '포스트'
        verbose_name_plural = '포스트들'

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        from django.urls import reverse
        return reverse('post_detail', kwargs={'slug': self.slug})

class Comment(models.Model):
    """댓글 모델"""
    post = models.ForeignKey(Post, on_delete=models.CASCADE,
                           related_name='comments')
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['created_at']

    def __str__(self):
        return f'Comment by {self.author.username} on {self.post}'

# ORM 사용 예제
# 생성
post = Post.objects.create(
    title="Django Tutorial",
    slug="django-tutorial",
    author=user,
    content="Django is awesome!"
)

# 조회
all_posts = Post.objects.all()
published_posts = Post.objects.filter(published=True)
recent_posts = Post.objects.filter(
    created_at__gte=timezone.now() - timezone.timedelta(days=7)
)

# 수정
post = Post.objects.get(id=1)
post.title = "Updated Title"
post.save()

# 삭제
Post.objects.filter(id=1).delete()

# 집계
from django.db.models import Count, Avg
post_count = Post.objects.count()
posts_with_comments = Post.objects.annotate(
    comment_count=Count('comments')
)

Django Views와 Templates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# myapp/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, DetailView, CreateView
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Post, Comment
from .forms import PostForm, CommentForm

# 함수 기반 뷰
def post_list(request):
    """포스트 목록"""
    posts = Post.objects.filter(published=True)
    return render(request, 'blog/post_list.html', {'posts': posts})

def post_detail(request, slug):
    """포스트 상세"""
    post = get_object_or_404(Post, slug=slug, published=True)
    comments = post.comments.all()

    if request.method == 'POST':
        comment_form = CommentForm(request.POST)
        if comment_form.is_valid():
            comment = comment_form.save(commit=False)
            comment.post = post
            comment.author = request.user
            comment.save()
            messages.success(request, '댓글이 작성되었습니다.')
            return redirect('post_detail', slug=post.slug)
    else:
        comment_form = CommentForm()

    return render(request, 'blog/post_detail.html', {
        'post': post,
        'comments': comments,
        'comment_form': comment_form
    })

@login_required
def post_create(request):
    """포스트 작성"""
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            messages.success(request, '포스트가 작성되었습니다.')
            return redirect('post_detail', slug=post.slug)
    else:
        form = PostForm()

    return render(request, 'blog/post_form.html', {'form': form})

# 클래스 기반 뷰
class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 10

    def get_queryset(self):
        return Post.objects.filter(published=True)

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['comments'] = self.object.comments.all()
        context['comment_form'] = CommentForm()
        return context

class PostCreateView(CreateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

Django REST Framework

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# 설치
# pip install djangorestframework

# settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework',
    'myapp',
]

# myapp/serializers.py
from rest_framework import serializers
from .models import Post, Comment

class CommentSerializer(serializers.ModelSerializer):
    author = serializers.ReadOnlyField(source='author.username')

    class Meta:
        model = Comment
        fields = ['id', 'author', 'content', 'created_at']

class PostSerializer(serializers.ModelSerializer):
    author = serializers.ReadOnlyField(source='author.username')
    comments = CommentSerializer(many=True, read_only=True)
    comment_count = serializers.SerializerMethodField()

    class Meta:
        model = Post
        fields = ['id', 'title', 'slug', 'author', 'content',
                 'created_at', 'updated_at', 'published',
                 'comments', 'comment_count']

    def get_comment_count(self, obj):
        return obj.comments.count()

# myapp/api_views.py
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Post, Comment
from .serializers import PostSerializer, CommentSerializer

class PostViewSet(viewsets.ModelViewSet):
    """
    포스트 API ViewSet
    """
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    lookup_field = 'slug'

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

    @action(detail=True, methods=['post'])
    def add_comment(self, request, slug=None):
        """포스트에 댓글 추가"""
        post = self.get_object()
        serializer = CommentSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(author=request.user, post=post)
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

    @action(detail=False)
    def published(self, request):
        """게시된 포스트만 조회"""
        published_posts = Post.objects.filter(published=True)
        serializer = self.get_serializer(published_posts, many=True)
        return Response(serializer.data)

# myapp/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .api_views import PostViewSet

router = DefaultRouter()
router.register('posts', PostViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

🔍 Flask vs Django: 언제 무엇을 사용할까?

Flask를 선택해야 할 때

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"""
Flask 장점:
1. 가벼움과 유연성
2. 마이크로서비스에 적합
3. 학습 곡선이 낮음
4. 최소한의 구성으로 시작 가능
"""

# Flask 프로젝트 예: 마이크로서비스
from flask import Flask, jsonify
import redis

app = Flask(__name__)
cache = redis.Redis(host='localhost', port=6379)

@app.route('/api/cache/<key>')
def get_cache(key):
    value = cache.get(key)
    if value:
        return jsonify({'key': key, 'value': value.decode()})
    return jsonify({'error': 'Key not found'}), 404

@app.route('/api/cache/<key>/<value>', methods=['POST'])
def set_cache(key, value):
    cache.set(key, value)
    return jsonify({'message': 'Cache set successfully'})

if __name__ == '__main__':
    app.run(port=5001)

Django를 선택해야 할 때

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"""
Django 장점:
1. 풀스택 기능 내장
2. 강력한 ORM
3. 관리자 페이지 자동 생성
4. 대규모 프로젝트에 적합
5. 보안 기능 내장
"""

# Django 프로젝트 예: 전자상거래 사이트
# models.py
class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.IntegerField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    products = models.ManyToManyField(Product, through='OrderItem')
    total = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)

# admin.py
from django.contrib import admin

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ['name', 'price', 'stock', 'category']
    list_filter = ['category', 'price']
    search_fields = ['name', 'description']

🚀 실전 프로젝트: Todo 애플리케이션

Flask 버전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# flask_todo.py
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
db = SQLAlchemy(app)

class Todo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    description = db.Column(db.Text)
    completed = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def __repr__(self):
        return f'<Todo {self.title}>'

@app.route('/')
def index():
    todos = Todo.query.order_by(Todo.created_at.desc()).all()
    return render_template('index.html', todos=todos)

@app.route('/add', methods=['POST'])
def add_todo():
    title = request.form.get('title')
    description = request.form.get('description')

    new_todo = Todo(title=title, description=description)
    db.session.add(new_todo)
    db.session.commit()

    return redirect(url_for('index'))

@app.route('/complete/<int:id>')
def complete_todo(id):
    todo = Todo.query.get_or_404(id)
    todo.completed = not todo.completed
    db.session.commit()
    return redirect(url_for('index'))

@app.route('/delete/<int:id>')
def delete_todo(id):
    todo = Todo.query.get_or_404(id)
    db.session.delete(todo)
    db.session.commit()
    return redirect(url_for('index'))

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True)

Django 버전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# Django Todo 앱
# models.py
from django.db import models
from django.contrib.auth.models import User

class Todo(models.Model):
    PRIORITY_CHOICES = [
        ('L', 'Low'),
        ('M', 'Medium'),
        ('H', 'High'),
    ]

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    priority = models.CharField(max_length=1, choices=PRIORITY_CHOICES,
                              default='M')
    completed = models.BooleanField(default=False)
    due_date = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-priority', '-created_at']

    def __str__(self):
        return self.title

# views.py
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.views.generic import ListView
from .models import Todo
from .forms import TodoForm

@login_required
def todo_list(request):
    todos = Todo.objects.filter(user=request.user)
    return render(request, 'todo/list.html', {'todos': todos})

@login_required
def todo_create(request):
    if request.method == 'POST':
        form = TodoForm(request.POST)
        if form.is_valid():
            todo = form.save(commit=False)
            todo.user = request.user
            todo.save()
            return redirect('todo_list')
    else:
        form = TodoForm()
    return render(request, 'todo/form.html', {'form': form})

class TodoListView(ListView):
    model = Todo
    template_name = 'todo/list.html'
    context_object_name = 'todos'

    def get_queryset(self):
        return Todo.objects.filter(user=self.request.user)

💡 웹 프레임워크 Best Practices

보안

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Flask 보안
from flask import Flask
from flask_cors import CORS
from werkzeug.security import generate_password_hash
import secrets

app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex(16)
CORS(app, origins=['http://localhost:3000'])

# Django 보안 설정
# settings.py
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com']
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

[!IMPORTANT] SECRET_KEY는 절대 노출 금지!

Flask나 Django 설정에 있는 SECRET_KEY는 암호화 서명에 쓰이는 매우 중요한 열쇠입니다. 이 키가 유출되면 해커가 사용자 세션을 위조할 수 있습니다. 깃허브(GitHub)에 코드를 올릴 때 절대 이 키를 코드에 직접 적어서 올리지 마세요! 환경 변수(os.environ)나 별도의 설정 파일로 관리해야 합니다.

성능 최적화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 캐싱
from flask_caching import Cache

cache = Cache(app, config={'CACHE_TYPE': 'simple'})

@app.route('/expensive')
@cache.cached(timeout=300)
def expensive_operation():
    # 비용이 많이 드는 작업
    return result

# Django 캐싱
from django.core.cache import cache

def get_popular_posts():
    posts = cache.get('popular_posts')
    if posts is None:
        posts = Post.objects.filter(
            published=True
        ).order_by('-views')[:10]
        cache.set('popular_posts', posts, 3600)
    return posts

테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Flask 테스트
import unittest
from app import app

class FlaskTestCase(unittest.TestCase):
    def setUp(self):
        self.app = app.test_client()

    def test_home_page(self):
        response = self.app.get('/')
        self.assertEqual(response.status_code, 200)

    def test_api_endpoint(self):
        response = self.app.get('/api/users')
        self.assertEqual(response.status_code, 200)
        data = response.get_json()
        self.assertIsInstance(data, list)

# Django 테스트
from django.test import TestCase, Client
from django.contrib.auth.models import User
from .models import Post

class PostTestCase(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass'
        )

    def test_post_creation(self):
        post = Post.objects.create(
            title='Test Post',
            author=self.user,
            content='Test content'
        )
        self.assertEqual(post.title, 'Test Post')

    def test_post_list_view(self):
        response = self.client.get('/posts/')
        self.assertEqual(response.status_code, 200)

🎯 배포하기

Flask 배포 (Gunicorn + Nginx)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# requirements.txt
Flask==2.3.0
gunicorn==21.2.0

# Gunicorn 실행
gunicorn -w 4 -b 0.0.0.0:8000 app:app

# Nginx 설정
server {
    listen 80;
    server_name yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Django 배포 (uWSGI + Nginx)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 정적 파일 수집
python manage.py collectstatic

# uWSGI 실행
uwsgi --http :8000 --module myproject.wsgi

# Docker 배포
FROM python:3.11
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]

⚠️ 초보자들이 자주 하는 실수

웹 프레임워크를 처음 배울 때 자주 발생하는 실수들을 정리했습니다. 이런 실수들을 미리 알고 피해가세요!

1. Flask에서 app.run(debug=True)를 프로덕션에서 사용

1
2
3
4
5
6
7
8
9
# ❌ 잘못된 예: 프로덕션에서 디버그 모드
if __name__ == '__main__':
    app.run(debug=True)  # 보안 취약점!

# ✅ 올바른 예: 환경별 설정 분리
import os
if __name__ == '__main__':
    debug_mode = os.environ.get('FLASK_ENV') == 'development'
    app.run(debug=debug_mode, host='0.0.0.0', port=5000)

2. Django에서 settings.py에 SECRET_KEY 하드코딩

1
2
3
4
5
6
# ❌ 잘못된 예: 민감한 정보 노출
SECRET_KEY = 'django-insecure-actual-secret-key-here'

# ✅ 올바른 예: 환경 변수 사용
import os
SECRET_KEY = os.environ.get('SECRET_KEY', 'default-for-dev-only')

3. SQL 쿼리에서 사용자 입력 직접 삽입

1
2
3
4
5
6
7
8
# ❌ 잘못된 예: SQL Injection 취약점
query = f"SELECT * FROM users WHERE username = '{username}'"

# ✅ 올바른 예: 파라미터화된 쿼리
# Flask-SQLAlchemy
users = User.query.filter_by(username=username).all()
# Django ORM
users = User.objects.filter(username=username)

4. CORS 설정을 ‘*‘로 열어두기

1
2
3
4
5
6
# ❌ 잘못된 예: 모든 도메인 허용
from flask_cors import CORS
CORS(app, origins='*')  # 보안 위험!

# ✅ 올바른 예: 특정 도메인만 허용
CORS(app, origins=['https://yourdomain.com', 'https://app.yourdomain.com'])

5. 에러 처리 없이 API 응답 반환

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 잘못된 예: 에러 처리 누락
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
    user = User.query.get(user_id)  # None일 수 있음
    return jsonify(user.to_dict())  # AttributeError 발생 가능

# ✅ 올바른 예: 적절한 에러 처리
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
    user = User.query.get(user_id)
    if not user:
        return jsonify({'error': 'User not found'}), 404
    return jsonify(user.to_dict())

6. 데이터베이스 연결을 매번 새로 생성

1
2
3
4
5
6
7
8
9
# ❌ 잘못된 예: 매번 새 연결
def get_users():
    conn = sqlite3.connect('database.db')  # 비효율적
    # ... 쿼리 실행
    conn.close()

# ✅ 올바른 예: 연결 풀 사용
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)  # 연결 풀 자동 관리

7. 환경별 설정 파일 분리하지 않기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ❌ 잘못된 예: 하나의 설정 파일
DEBUG = True
DATABASE_URL = 'postgresql://localhost/myapp'

# ✅ 올바른 예: 환경별 설정
# config.py
class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY')

class DevelopmentConfig(Config):
    DEBUG = True
    DATABASE_URL = 'sqlite:///dev.db'

class ProductionConfig(Config):
    DEBUG = False
    DATABASE_URL = os.environ.get('DATABASE_URL')

8. 프론트엔드와 백엔드 세션/인증 동기화 실패

1
2
3
4
5
6
7
8
9
10
11
12
# ❌ 잘못된 예: 세션 불일치
# 프론트엔드는 JWT를 사용하는데 백엔드는 세션 쿠키 사용

# ✅ 올바른 예: 일관된 인증 방식
from flask_jwt_extended import JWTManager, create_access_token
jwt = JWTManager(app)

@app.route('/api/login', methods=['POST'])
def login():
    # 로그인 검증 후
    access_token = create_access_token(identity=user.id)
    return jsonify({'access_token': access_token})

이런 실수들을 피하면 더 안전하고 효율적인 웹 애플리케이션을 만들 수 있습니다!

🎓 파이썬 마스터하기 시리즈

📚 기초편 (1-7)

  1. Python 소개와 개발 환경 설정 완벽 가이드
  2. 변수, 자료형, 연산자 완벽 정리
  3. 조건문과 반복문 마스터하기
  4. 함수와 람다 완벽 가이드
  5. 리스트, 튜플, 딕셔너리 정복하기
  6. 문자열 처리와 정규표현식
  7. 파일 입출력과 예외 처리

🚀 중급편 (8-12)

  1. 클래스와 객체지향 프로그래밍
  2. 모듈과 패키지 관리
  3. 데코레이터와 제너레이터
  4. 비동기 프로그래밍 (async/await)
  5. 데이터베이스 연동하기

💼 고급편 (13-16)

  1. 웹 스크래핑과 API 활용
  2. 테스트와 디버깅 전략
  3. 성능 최적화 기법
  4. 멀티프로세싱과 병렬 처리

🏆 실전편 (17-20)

  1. 웹 프레임워크 입문 (Flask/Django) ← 현재 글
  2. 데이터 분석 도구 활용 (Pandas/NumPy)
  3. 머신러닝 기초 (scikit-learn)
  4. 실전 프로젝트와 베스트 프랙티스

이전글: 멀티프로세싱과 병렬 처리 ⬅️ 현재글: 웹 프레임워크 입문 (Flask/Django) 다음글: 데이터 분석 도구 활용 (Pandas/NumPy) ➡️


이번 포스트에서는 Python의 대표적인 웹 프레임워크인 Flask와 Django를 완벽히 마스터했습니다. 다음 포스트에서는 데이터 분석 도구인 Pandas와 NumPy에 대해 자세히 알아보겠습니다. Happy Coding! 🐍✨

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.