Topic 2: 템플릿과 폼 - 데이터 주고받기 📝
🎯 학습 목표
Flask로 사용자와 상호작용하는 웹 애플리케이션을 만들어봐요!
- 템플릿(Templates)이 왜 필요한지 이해하기
- HTML 폼(Form)으로 사용자 입력 받는 방법
- 데이터가 어떻게 흘러가는지 파악하기
- GET과 POST 방식의 차이점 알아보기
🤔 왜 템플릿이 필요할까요?
Topic 1에서는 HTML을 파이썬 문자열로 직접 작성했어요. 간단한 페이지에는 괜찮지만, 복잡한 웹사이트를 만들려면 문제가 생겨요.
문자열로 HTML을 작성할 때의 문제점
# 이런 식으로 HTML을 작성하면...
@app.route('/')
def home():
return '''
<html>
<head><title>제목</title></head>
<body>
<h1>안녕하세요!</h1>
<p>오늘은 좋은 날이에요.</p>
<ul>
<li>항목 1</li>
<li>항목 2</li>
</ul>
</body>
</html>
'''
문제점들:
- 가독성: 파이썬 코드와 HTML이 섞여서 읽기 어려워요
- 재사용: 같은 디자인을 여러 페이지에서 쓰기 힘들어요
- 유지보수: HTML을 수정하려면 파이썬 코드를 건드려야 해요
- 협업: 디자이너와 개발자가 함께 작업하기 어려워요
템플릿이 해결해주는 것들
템플릿을 사용하면 HTML을 별도 파일로 분리할 수 있어요:
문제점 | 템플릿 해결책 |
---|---|
가독성 문제 | HTML과 Python 코드 완전 분리 |
재사용 어려움 | 공통 레이아웃을 여러 페이지에서 재사용 |
유지보수 복잡 | HTML 파일만 수정하면 됨 |
협업 어려움 | 디자이너는 HTML, 개발자는 Python에 집중 |
📁 프로젝트 구조 만들기
Flask 템플릿을 사용하려면 특별한 폴더 구조가 필요해요:
# 프로젝트 폴더 만들기
mkdir flask_templates_app
cd flask_templates_app
# templates 폴더는 반드시 이 이름이어야 해요!
mkdir templates
폴더 구조:
flask_templates_app/
├── app.py # 메인 Flask 애플리케이션
└── templates/ # HTML 템플릿 파일들
├── base.html # 기본 레이아웃
└── home.html # 홈페이지 템플릿
중요: templates
폴더는 반드시 이 이름이어야 해요. Flask가 자동으로 이 폴더에서 템플릿을 찾거든요!
🎨 첫 번째 템플릿 만들기
이제 실제로 템플릿을 만들어봐요!
단계 1: 기본 Flask 앱 만들기
먼저 app.py
파일을 만들어요:
# render_template 함수를 추가로 가져와요
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
# 문자열 대신 render_template 함수를 사용해요
return render_template('home.html')
if __name__ == '__main__':
app.run(debug=True)
코드 이해하기
from flask import Flask, render_template
render_template
는 HTML 템플릿 파일을 읽어서 완성된 HTML로 만들어주는 함수예요
return render_template('home.html')
templates/home.html
파일을 찾아서 HTML로 변환해요- 문자열을 직접 반환하는 대신 템플릿 파일을 사용해요
단계 2: HTML 템플릿 파일 만들기
이제 templates/home.html
파일을 만들어요:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flask 템플릿 실습</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
p {
color: #666;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container">
<h1>🎉 Flask 템플릿 성공!</h1>
<p>이 페이지는 별도의 HTML 파일에서 만들어졌습니다.</p>
<p>Python 코드와 HTML이 깔끔하게 분리되었어요!</p>
</div>
</body>
</html>
단계 3: 실행해보기
python app.py
브라우저에서 http://localhost:5000
에 접속해보세요!
🔄 데이터를 템플릿에 전달하기
템플릿의 진짜 힘은 동적 데이터를 표시할 수 있다는 거예요!
from flask import Flask, render_template, request
import random
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def name_game():
message = ""
color = "#4ECDC4"
if request.method == 'POST':
name = request.form.get('name', '').strip()
if name:
# 재미있는 랜덤 반응들
reactions = [
f"와! {name}님! 정말 멋진 이름이에요! ✨",
f"{name}님은 분명 특별한 사람이에요! 🌟",
f"{name}님의 이름에는 마법이 있어요! 🎭",
f"안녕하세요 {name}님! 오늘 기분이 어떠세요? 😊",
f"{name}님! 프로그래밍 천재가 되실 것 같아요! 🚀",
]
message = random.choice(reactions)
color = random.choice(["#FF6B6B", "#4ECDC4", "#45B7D1", "#96E6B3", "#F7DC6F"])
return render_template('name_game.html', message=message, color=color)
if __name__ == '__main__':
app.run(debug=True)
Step 3: 예쁜 HTML 템플릿 만들기
templates/name_game.html
파일을 만들어요:
<!DOCTYPE html>
<html>
<head>
<title>마법의 이름 게임 ✨</title>
<style>
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Arial', sans-serif;
display: flex;
justify-content: center;
align-items: center;
}
.game-container {
background: white;
padding: 50px;
border-radius: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
max-width: 500px;
animation: slideUp 0.8s ease;
}
@keyframes slideUp {
from { transform: translateY(50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
h1 {
color: #333;
margin-bottom: 30px;
font-size: 2.5em;
}
.magic-wand {
font-size: 3em;
animation: wiggle 2s ease-in-out infinite;
}
@keyframes wiggle {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(10deg); }
75% { transform: rotate(-10deg); }
}
.input-group {
margin: 30px 0;
}
input[type="text"] {
width: 100%;
padding: 15px;
font-size: 18px;
border: 3px solid #ddd;
border-radius: 15px;
text-align: center;
transition: all 0.3s;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
transform: scale(1.05);
}
.magic-button {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
padding: 15px 40px;
border: none;
border-radius: 25px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
margin-top: 20px;
}
.magic-button:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.response {
margin-top: 30px;
padding: 20px;
border-radius: 15px;
font-size: 20px;
font-weight: bold;
animation: bounce 0.6s ease;
}
@keyframes bounce {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.sparkles {
position: absolute;
font-size: 2em;
animation: sparkle 3s ease-in-out infinite;
}
@keyframes sparkle {
0%, 100% { opacity: 0; transform: scale(0); }
50% { opacity: 1; transform: scale(1); }
}
</style>
</head>
<body>
<div class="sparkles" style="top: 10%; left: 10%;">✨</div>
<div class="sparkles" style="top: 20%; right: 15%; animation-delay: 1s;">⭐</div>
<div class="sparkles" style="bottom: 15%; left: 20%; animation-delay: 2s;">💫</div>
<div class="sparkles" style="bottom: 25%; right: 10%; animation-delay: 0.5s;">🌟</div>
<div class="game-container">
<div class="magic-wand">🪄</div>
<h1>마법의 이름 게임</h1>
<p style="color: #666; font-size: 16px;">이름을 입력하면 마법이 일어나요!</p>
<form method="POST">
<div class="input-group">
<input type="text" name="name" placeholder="당신의 이름을 입력하세요..." required>
</div>
<button type="submit" class="magic-button">✨ 마법 시전하기! ✨</button>
</form>
{% if message %}
<div class="response" style="background-color: {{ color }}; color: white;">
{{ message }}
</div>
{% endif %}
</div>
</body>
</html>
🎮 두 번째 작품: 색깔 투표 시스템
이번엔 더 복잡한 걸 만들어봐요! 좋아하는 색깔에 투표하고 결과를 예쁘게 보여주는 시스템이에요.
전체 코드 (app.py에 추가)
# 기존 코드 아래에 이것들을 추가하세요!
# 투표 데이터 저장
votes = {
'❤️ 빨강': 0,
'💙 파랑': 0,
'💚 초록': 0,
'💛 노랑': 0,
'💜 보라': 0,
'🧡 주황': 0
}
@app.route('/vote', methods=['GET', 'POST'])
def color_vote():
if request.method == 'POST':
color = request.form.get('color')
if color in votes:
votes[color] += 1
# 총 투표 수 계산
total_votes = sum(votes.values())
# 퍼센트 계산
percentages = {}
for color, count in votes.items():
if total_votes > 0:
percentages[color] = (count / total_votes) * 100
else:
percentages[color] = 0
return render_template('vote.html',
votes=votes,
percentages=percentages,
total_votes=total_votes)
투표 템플릿 만들기
templates/vote.html
파일을 만들어요:
<!DOCTYPE html>
<html>
<head>
<title>색깔 투표 🌈</title>
<style>
body {
margin: 0;
padding: 20px;
background: linear-gradient(45deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%);
font-family: 'Arial', sans-serif;
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.vote-card {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 15px 35px rgba(0,0,0,0.1);
margin-bottom: 30px;
animation: fadeIn 0.8s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2.5em;
}
.vote-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
margin: 30px 0;
}
.vote-option {
text-align: center;
}
.vote-button {
width: 100%;
padding: 20px;
font-size: 18px;
border: 3px solid #ddd;
border-radius: 15px;
background: white;
cursor: pointer;
transition: all 0.3s;
}
.vote-button:hover {
transform: scale(1.1);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.submit-btn {
display: block;
margin: 30px auto;
padding: 15px 40px;
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 25px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
}
.submit-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.results {
margin-top: 40px;
}
.result-item {
margin: 15px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
position: relative;
overflow: hidden;
}
.result-bar {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: linear-gradient(45deg, #667eea, #764ba2);
opacity: 0.2;
transition: width 0.8s ease;
}
.result-text {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
}
.crown {
font-size: 1.5em;
animation: bounce 1s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
</style>
</head>
<body>
<div class="container">
<div class="vote-card">
<h1>🌈 좋아하는 색깔 투표 🌈</h1>
<p style="text-align: center; font-size: 18px; color: #666;">
어떤 색깔을 가장 좋아하나요?
</p>
<form method="POST">
<div class="vote-options">
{% for color in votes.keys() %}
<div class="vote-option">
<label>
<input type="radio" name="color" value="{{ color }}"
style="display: none;" required>
<div class="vote-button">{{ color }}</div>
</label>
</div>
{% endfor %}
</div>
<button type="submit" class="submit-btn">🗳️ 투표하기!</button>
</form>
</div>
{% if total_votes > 0 %}
<div class="vote-card results">
<h2 style="text-align: center; color: #333;">📊 실시간 투표 결과</h2>
<p style="text-align: center; color: #666;">총 {{ total_votes }}명이 투표했어요!</p>
{% for color, count in votes.items() %}
<div class="result-item">
<div class="result-bar" style="width: {{ percentages[color] }}%;"></div>
<div class="result-text">
<span>{{ color }}</span>
<span>
{{ count }}표 ({{ "%.1f"|format(percentages[color]) }}%)
{% if count == votes.values()|max and count > 0 %}
<span class="crown">👑</span>
{% endif %}
</span>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<script>
// 라디오 버튼 시각적 피드백
document.querySelectorAll('input[type="radio"]').forEach(radio => {
radio.addEventListener('change', function() {
document.querySelectorAll('.vote-button').forEach(btn => {
btn.style.background = 'white';
btn.style.borderColor = '#ddd';
});
this.nextElementSibling.style.background = '#667eea';
this.nextElementSibling.style.borderColor = '#667eea';
this.nextElementSibling.style.color = 'white';
});
});
</script>
</body>
</html>
💡 Flask 폼 처리 핵심 개념
Flask에서 폼 데이터를 처리하는 방법을 정리해보면:
기능 | Flask 구현 방법 |
---|---|
POST 요청 받기 | @app.route('/submit', methods=['POST']) |
폼 데이터 가져오기 | request.form.get('name') |
템플릿에 데이터 전달 | render_template('page.html', data=data) |
템플릿에서 변수 출력 | {{ title }} (Jinja2) |
이렇게 간단하고 직관적이에요! 😊
🎯 도전 과제: 더 재미있게!
1. 이름 게임에 새 반응 추가하기
reactions = [
f"와! {name}님! 정말 멋진 이름이에요! ✨",
# 여기에 더 재미있는 반응들을 추가해보세요!
f"{name}님은 미래의 개발자가 될 거예요! 💻",
f"{name}님의 이름을 보니 행복해져요! 😄"
]
2. 투표에 새로운 옵션 추가하기
votes = {
'❤️ 빨강': 0,
'💙 파랑': 0,
# 여기에 새로운 색깔 추가해보세요!
'🖤 검정': 0,
'🤍 하양': 0
}
3. 실시간 시계 만들기
Python의 datetime을 사용해서 페이지를 새로고침할 때마다 최신 시간을 보여주는 시계를 만들어보세요!
from datetime import datetime
@app.route('/time')
def show_time():
current_time = datetime.now().strftime('%Y년 %m월 %d일 %H:%M:%S')
return render_template('time.html', current_time=current_time)
💡 퀴즈: 폼 처리 이해도 체크
Q1. Flask에서 POST 데이터를 받으려면?
request.get('name')
request.form.get('name')
request.data['name']
💡 정답 확인
정답: 2번
request.form.get('name')
을 사용해서 폼 데이터를 받아요!
name = request.form.get('name')
Q2. 템플릿에서 조건문을 사용하려면?
{{ if condition }}
{% if condition %}
{# if condition #}
💡 정답 확인
정답: 2번
{% if condition %}
을 사용해서 조건문을 만들어요!
{% if message %}
<p>{{ message }}</p>
{% endif %}
✅ Topic 2 마스터 체크리스트
🚀 다음 단계
와! 이제 사용자와 대화할 수 있는 웹사이트를 만들었네요!
다음 Topic에서는:
- Unit 10에서 배운 웹 스크래핑을 웹에 연결하기
- 실시간 날씨 정보 보여주기
- 진짜 살아있는 데이터를 웹에 띄우기
준비되셨나요? Let’s go! 🚀