포스트

[Python 100일 챌린지] Day 82 - 라우팅과 URL 처리

[Python 100일 챌린지] Day 82 - 라우팅과 URL 처리

어제 우리는 Flask의 기초를 배웠습니다. 단순한 @app.route('/') 하나로요. 🤔 ──하지만 실제 웹사이트는 수백, 수천 개의 URL을 가지고 있습니다!

/user/123, /products/electronics, /search?q=python 이런 복잡한 URL들을 어떻게 처리할까요?

오늘은 Flask의 라우팅 시스템을 마스터합니다. URL 패턴 매칭, 동적 라우트, HTTP 메서드, 리다이렉트까지! 웹 개발의 핵심 개념들을 모두 배워봅시다! 💡

(30분 완독 ⭐⭐⭐⭐)

🎯 오늘의 학습 목표

  1. 동적 라우트와 URL 변수 처리하기
  2. HTTP 메서드 (GET, POST) 이해하기
  3. 리다이렉트와 URL 빌더 사용하기
  4. 요청 데이터와 쿼리 파라미터 다루기

📚 사전 지식


🎯 학습 목표 1: 동적 라우트와 URL 변수 처리하기

한 줄 설명

동적 라우트 = URL에 변수를 넣어서 하나의 함수로 여러 페이지를 처리하는 방법

실생활 비유

1
2
3
4
5
6
7
8
9
10
11
12
13
🏠 아파트 호실 시스템:

정적 라우트 = 각 호실마다 별도의 문
- 101호 전용 문, 102호 전용 문, 103호 전용 문... (비효율적!)

동적 라우트 = 호실 번호판이 있는 하나의 문 양식
- "/아파트/<동>/<호수>" 하나로 모든 집을 찾아갈 수 있음
- /아파트/1동/101호, /아파트/1동/102호, /아파트/2동/201호
- URL 패턴 하나로 무한대의 페이지 처리!

코드 비교:
❌ @app.route('/user/alice')  # 사용자마다 라우트 추가 (불가능!)
✅ @app.route('/user/<username>')  # 모든 사용자 처리 (효율적!)

정적 라우트 vs 동적 라우트

1
2
3
4
5
6
7
# 정적 라우트 (Static Route)
@app.route('/about')       # 항상 같은 URL
@app.route('/contact')     # 항상 같은 URL

# 동적 라우트 (Dynamic Route)
@app.route('/user/<username>')     # username이 변함
@app.route('/post/<int:post_id>')  # post_id가 변함

왜 동적 라우트가 필요한가?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ 나쁜 방법: 모든 사용자마다 라우트를 만든다면?
@app.route('/user/alice')
def user_alice():
    return "Alice의 프로필"

@app.route('/user/bob')
def user_bob():
    return "Bob의 프로필"

# ... 사용자가 1만명이면? 😱

# ✅ 좋은 방법: 동적 라우트 하나로 해결!
@app.route('/user/<username>')
def user_profile(username):
    return f"{username}의 프로필"

기본 동적 라우트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app.py
from flask import Flask

app = Flask(__name__)

# 문자열 변수 (기본값)
@app.route('/hello/<name>')
def hello(name):
    return f"""
    <h1>안녕하세요, {name}님!</h1>
    <p>환영합니다.</p>
    """

# 테스트:
# http://localhost:5000/hello/철수 → "안녕하세요, 철수님!"
# http://localhost:5000/hello/영희 → "안녕하세요, 영희님!"

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

URL 변수 타입 지정

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
# app.py
from flask import Flask

app = Flask(__name__)

# 1. string (기본값) - 슬래시(/)를 제외한 모든 텍스트
@app.route('/user/<string:username>')
def show_user(username):
    return f"사용자: {username}"

# 2. int - 정수만 허용
@app.route('/post/<int:post_id>')
def show_post(post_id):
    return f"게시글 번호: {post_id} (타입: {type(post_id)})"

# 3. float - 실수 허용
@app.route('/price/<float:amount>')
def show_price(amount):
    return f"가격: {amount:.2f}원 (타입: {type(amount)})"

# 4. path - 슬래시(/)를 포함한 모든 텍스트
@app.route('/files/<path:filepath>')
def show_file(filepath):
    return f"파일 경로: {filepath}"

# 5. uuid - UUID 형식만 허용
@app.route('/object/<uuid:id>')
def show_object(id):
    return f"객체 ID: {id}"

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

테스트:

1
2
3
4
5
6
7
8
✅ /post/123         → OK (int)
❌ /post/abc         → 404 (문자열은 불가)

✅ /price/19.99      → OK (float)
❌ /price/free       → 404

✅ /files/docs/report.pdf  → OK (path - 슬래시 포함)
❌ /user/admin/root        → 404 (string - 슬래시 불가)

여러 변수 사용하기

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
# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return """
    <h1>URL 패턴 테스트</h1>
    <ul>
        <li><a href="/user/alice/25">사용자: alice, 나이: 25</a></li>
        <li><a href="/blog/2024/03/15">블로그: 2024년 3월 15일</a></li>
    </ul>
    """

# 여러 변수를 URL에 포함
@app.route('/user/<username>/<int:age>')
def user_info(username, age):
    return f"""
    <h1>사용자 정보</h1>
    <ul>
        <li>이름: {username}</li>
        <li>나이: {age}살</li>
        <li>성인 여부: {'' if age >= 19 else '아니오'}</li>
    </ul>
    """

# 날짜 형식의 URL
@app.route('/blog/<int:year>/<int:month>/<int:day>')
def show_blog_post(year, month, day):
    return f"""
    <h1>블로그 포스트</h1>
    <p>날짜: {year}{month}{day}일</p>
    """

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

🎯 학습 목표 2: HTTP 메서드 (GET, POST) 이해하기

한 줄 설명

HTTP 메서드 = 서버에게 “무엇을 할지” 알려주는 동작 명령어 (GET은 조회, POST는 전송)

실생활 비유

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
📬 우체국에서 소포 보내기:

GET (조회) = 우체국 창구에서 "우편물 조회해주세요"
- URL에 모든 정보가 보임: "123번 우편물 어디있나요?"
- 누구나 URL만 보면 무엇을 조회하는지 알 수 있음
- 예: /track?id=123&type=package

POST (전송) = 우체국 창구에 봉투 안에 넣어서 제출
- 봉투 안의 내용은 밖에서 안 보임 (보안!)
- URL에는 안 보임: /send-package
- 로그인, 회원가입처럼 민감한 정보는 POST 사용

비교:
GET: /search?password=1234  ← 비밀번호가 URL에 노출! 위험! ❌
POST: /login (비밀번호는 본문에 숨겨서 전송) ✅

HTTP 메서드란?

1
2
3
4
5
6
7
HTTP 메서드: 클라이언트가 서버에게 "어떤 동작"을 요청하는지 알려주는 방법

🔍 GET    - 데이터 조회 (읽기)
📝 POST   - 데이터 전송 (쓰기/생성)
✏️ PUT    - 데이터 수정 (전체 업데이트)
🔧 PATCH  - 데이터 일부 수정
🗑️ DELETE - 데이터 삭제

GET vs POST

1
2
3
4
5
6
7
8
9
10
11
12
13
# GET 메서드 (기본값)
# - URL에 데이터가 노출됨
# - 북마크 가능
# - 캐시 가능
# - 데이터 길이 제한 있음
: /search?q=python&page=1

# POST 메서드
# - URL에 데이터가 안 보임 (본문에 포함)
# - 북마크 불가
# - 캐시 불가
# - 데이터 길이 제한 없음
: 로그인, 회원가입, 파일 업로드

GET 메서드 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# app.py
from flask import Flask, request

app = Flask(__name__)

# 기본값은 GET 메서드만 허용
@app.route('/search')
def search():
    # URL 쿼리 파라미터 읽기
    query = request.args.get('q', '')  # ?q=python
    page = request.args.get('page', 1, type=int)  # ?page=2

    return f"""
    <h1>검색 결과</h1>
    <p>검색어: {query}</p>
    <p>페이지: {page}</p>
    """

# 테스트:
# /search?q=python → 검색어: python, 페이지: 1
# /search?q=flask&page=2 → 검색어: flask, 페이지: 2

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

POST 메서드 처리

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
# app.py
from flask import Flask, request

app = Flask(__name__)

# GET과 POST 둘 다 허용
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        # GET: 로그인 폼 표시
        return """
        <h1>로그인</h1>
        <form method="POST">
            <input type="text" name="username" placeholder="아이디" required>
            <input type="password" name="password" placeholder="비밀번호" required>
            <button type="submit">로그인</button>
        </form>
        """
    else:  # POST
        # POST: 폼 데이터 처리
        username = request.form.get('username')
        password = request.form.get('password')

        # 간단한 인증 로직 (실제로는 데이터베이스 사용)
        if username == 'admin' and password == '1234':
            return f"<h1>환영합니다, {username}님!</h1>"
        else:
            return "<h1>로그인 실패</h1><a href='/login'>다시 시도</a>"

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

여러 메서드 처리하기

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
# app.py
from flask import Flask, request, jsonify

app = Flask(__name__)

# 간단한 데이터 저장소 (실제로는 데이터베이스 사용)
todos = []

@app.route('/api/todos', methods=['GET', 'POST', 'DELETE'])
def handle_todos():
    if request.method == 'GET':
        # 할 일 목록 조회
        return jsonify(todos)

    elif request.method == 'POST':
        # 새 할 일 추가
        todo = request.json.get('todo')
        todos.append(todo)
        return jsonify({'message': '추가됨', 'todo': todo}), 201

    elif request.method == 'DELETE':
        # 모든 할 일 삭제
        todos.clear()
        return jsonify({'message': '모두 삭제됨'}), 200

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

API 테스트 (curl 사용):

1
2
3
4
5
6
7
8
9
10
# GET - 목록 조회
curl http://localhost:5000/api/todos

# POST - 추가
curl -X POST http://localhost:5000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"todo": "Flask 공부하기"}'

# DELETE - 삭제
curl -X DELETE http://localhost:5000/api/todos

🎯 학습 목표 3: 리다이렉트와 URL 빌더 사용하기

한 줄 설명

리다이렉트 = 사용자를 다른 페이지로 자동으로 보내는 것 (로그인 후 메인 페이지로 이동하기)

실생활 비유

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🏢 병원 접수 시스템:

redirect() = 다른 창구로 안내하기
- 3번 창구에 가려고 했는데 "1번 창구로 가세요~" 하고 안내받음
- 자동으로 올바른 위치로 이동시켜줌

예시:
1. 로그인 안 한 사람이 "마이페이지" 접근 시도
2. 서버: "로그인부터 하세요!" → /login으로 리다이렉트
3. 로그인 완료 후 → /mypage로 다시 리다이렉트

url_for() = 창구 번호 대신 "담당자 이름"으로 찾기
❌ redirect('/user/profile')  ← 주소가 바뀌면 모든 코드 수정 필요!
✅ redirect(url_for('user_profile'))  ← 함수 이름만 알면 됨!

건물 주소가 바뀌어도, "홍길동 담당자" 이름만 알면 찾아갈 수 있는 것과 같음!

redirect() 함수

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
# app.py
from flask import Flask, redirect, url_for

app = Flask(__name__)

@app.route('/')
def home():
    return "홈 페이지"

@app.route('/admin')
def admin():
    # 인증 체크 (실제로는 세션 사용)
    is_logged_in = False

    if not is_logged_in:
        # 로그인 페이지로 리다이렉트
        return redirect('/login')

    return "관리자 페이지"

@app.route('/login')
def login():
    return """
    <h1>로그인이 필요합니다</h1>
    <form>
        <button>로그인</button>
    </form>
    """

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

url_for() 함수

왜 url_for()를 사용해야 할까?

1
2
3
4
5
6
7
8
9
# ❌ 나쁜 방법: 하드코딩된 URL
return redirect('/user/profile')

# URL이 변경되면? /users/profile → 모든 코드를 수정해야 함!

# ✅ 좋은 방법: url_for() 사용
return redirect(url_for('user_profile'))

# 함수 이름만 알면 됨! URL이 변경되어도 코드 수정 불필요
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
# app.py
from flask import Flask, redirect, url_for

app = Flask(__name__)

@app.route('/')
def home():
    return """
    <h1>홈 페이지</h1>
    <ul>
        <li><a href="/user/alice">Alice 프로필 (하드코딩)</a></li>
        <li><a href="{{ url_for('user_profile', username='bob') }}">Bob 프로필 (url_for)</a></li>
    </ul>
    """

@app.route('/user/<username>')
def user_profile(username):
    return f"<h1>{username}의 프로필</h1>"

@app.route('/go-to-profile/<name>')
def go_to_profile(name):
    # url_for()로 다른 라우트의 URL 생성
    profile_url = url_for('user_profile', username=name)
    return redirect(profile_url)

# 테스트:
# /go-to-profile/charlie → /user/charlie로 리다이렉트

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

실전 예제: 로그인 후 리다이렉트

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
# app.py
from flask import Flask, redirect, url_for, request

app = Flask(__name__)

@app.route('/')
def home():
    return """
    <h1>환영합니다</h1>
    <a href="/login">로그인</a>
    """

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

        if username == 'admin' and password == '1234':
            # 로그인 성공 → 대시보드로 리다이렉트
            return redirect(url_for('dashboard', username=username))
        else:
            return "로그인 실패 <a href='/login'>다시 시도</a>"

    return """
    <h1>로그인</h1>
    <form method="POST">
        <input type="text" name="username" placeholder="아이디"><br>
        <input type="password" name="password" placeholder="비밀번호"><br>
        <button>로그인</button>
    </form>
    """

@app.route('/dashboard/<username>')
def dashboard(username):
    return f"""
    <h1>{username}님의 대시보드</h1>
    <p>로그인에 성공했습니다!</p>
    <a href="/">홈으로</a>
    """

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

🎯 학습 목표 4: 요청 데이터와 쿼리 파라미터 다루기

한 줄 설명

request 객체 = 사용자가 보낸 모든 정보를 담고 있는 택배 상자

실생활 비유

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
📦 택배 상자 (request 객체):

request.args = 상자 겉면에 적힌 메모 (URL 뒤의 ?key=value)
- 누구나 볼 수 있음
- 예: /search?q=파이썬&page=2
- request.args.get('q') → "파이썬"

request.form = 상자 안에 든 물건 (폼 데이터)
- 상자를 열어야 보임 (POST 방식)
- 예: 로그인 폼의 아이디, 비밀번호
- request.form.get('password') → 안전하게 전달됨

request.files = 상자 안의 큰 물건 (파일)
- 이미지, 문서 파일 등
- request.files.get('photo') → 업로드된 파일

request.json = 상자 안의 정리된 목록 (API 데이터)
- JSON 형식의 구조화된 데이터
- {"name": "홍길동", "age": 25}

request 객체

Flask의 request 객체로 클라이언트의 모든 요청 데이터에 접근할 수 있습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import request

# 쿼리 파라미터: ?key=value
request.args.get('key')

# 폼 데이터: <form method="POST">
request.form.get('key')

# JSON 데이터: Content-Type: application/json
request.json.get('key')

# 파일 업로드
request.files.get('file')

# HTTP 헤더
request.headers.get('User-Agent')

# 요청 메서드
request.method  # GET, POST, etc.

# 요청 URL
request.url
request.path

쿼리 파라미터 처리

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
# app.py
from flask import Flask, request

app = Flask(__name__)

@app.route('/search')
def search():
    # 쿼리 파라미터 읽기
    keyword = request.args.get('q', '')           # 기본값: 빈 문자열
    page = request.args.get('page', 1, type=int)  # 기본값: 1, 타입: int
    sort = request.args.get('sort', 'relevance')  # 기본값: relevance

    # 모든 쿼리 파라미터
    all_params = request.args.to_dict()

    return f"""
    <h1>검색 결과</h1>
    <ul>
        <li>검색어: {keyword}</li>
        <li>페이지: {page}</li>
        <li>정렬: {sort}</li>
        <li>모든 파라미터: {all_params}</li>
    </ul>
    """

# 테스트:
# /search?q=flask&page=2&sort=date
# → 검색어: flask, 페이지: 2, 정렬: date

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

폼 데이터 처리

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
# app.py
from flask import Flask, request

app = Flask(__name__)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'GET':
        return """
        <h1>회원가입</h1>
        <form method="POST">
            <input type="text" name="username" placeholder="아이디" required><br>
            <input type="email" name="email" placeholder="이메일" required><br>
            <input type="password" name="password" placeholder="비밀번호" required><br>
            <label>
                <input type="checkbox" name="agree" value="yes"> 약관 동의
            </label><br>
            <button>가입하기</button>
        </form>
        """

    # POST 요청 처리
    username = request.form.get('username')
    email = request.form.get('email')
    password = request.form.get('password')
    agree = request.form.get('agree')  # 체크박스

    # 유효성 검사
    if not agree:
        return "약관에 동의해야 합니다 <a href='/register'>돌아가기</a>"

    return f"""
    <h1>회원가입 완료!</h1>
    <ul>
        <li>아이디: {username}</li>
        <li>이메일: {email}</li>
        <li>약관 동의: {agree}</li>
    </ul>
    """

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

JSON 데이터 처리 (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
# app.py
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/user', methods=['POST'])
def create_user():
    # JSON 데이터 파싱
    data = request.get_json()

    # 필수 필드 체크
    if not data or 'username' not in data:
        return jsonify({'error': 'username은 필수입니다'}), 400

    username = data.get('username')
    email = data.get('email')
    age = data.get('age')

    # 데이터 처리 (실제로는 데이터베이스 저장)
    user = {
        'id': 1,
        'username': username,
        'email': email,
        'age': age,
        'created_at': '2025-06-20'
    }

    return jsonify(user), 201  # 201 Created

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

API 테스트:

1
2
3
4
5
6
7
curl -X POST http://localhost:5000/api/user \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "email": "[email protected]",
    "age": 25
  }'

💻 실전 예제

예제 1: 제품 카탈로그

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
# app.py
from flask import Flask, request, redirect, url_for

app = Flask(__name__)

# 가상의 제품 데이터베이스
products = {
    1: {'id': 1, 'name': '노트북', 'price': 1500000, 'category': 'electronics'},
    2: {'id': 2, 'name': '마우스', 'price': 30000, 'category': 'electronics'},
    3: {'id': 3, 'name': '키보드', 'price': 120000, 'category': 'electronics'},
    4: {'id': 4, 'name': '책상', 'price': 250000, 'category': 'furniture'},
}

@app.route('/')
def home():
    return """
    <h1>제품 카탈로그</h1>
    <ul>
        <li><a href="/products">모든 제품</a></li>
        <li><a href="/products?category=electronics">전자제품</a></li>
        <li><a href="/products?category=furniture">가구</a></li>
        <li><a href="/products?sort=price">가격순 정렬</a></li>
    </ul>
    """

@app.route('/products')
def list_products():
    # 쿼리 파라미터
    category = request.args.get('category')
    sort_by = request.args.get('sort')

    # 필터링
    filtered = products.values()
    if category:
        filtered = [p for p in filtered if p['category'] == category]

    # 정렬
    if sort_by == 'price':
        filtered = sorted(filtered, key=lambda x: x['price'])

    # HTML 생성
    html = "<h1>제품 목록</h1><ul>"
    for product in filtered:
        html += f"""
        <li>
            <a href="/products/{product['id']}">
                {product['name']} - {product['price']:,}원
            </a>
        </li>
        """
    html += "</ul><a href='/'>홈으로</a>"

    return html

@app.route('/products/<int:product_id>')
def product_detail(product_id):
    product = products.get(product_id)

    if not product:
        return "<h1>제품을 찾을 수 없습니다</h1><a href='/products'>목록으로</a>", 404

    return f"""
    <h1>{product['name']}</h1>
    <ul>
        <li>가격: {product['price']:,}원</li>
        <li>카테고리: {product['category']}</li>
    </ul>
    <a href="/products">목록으로</a>
    """

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

예제 2: 간단한 블로그

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
# app.py
from flask import Flask, request, redirect, url_for
from datetime import datetime

app = Flask(__name__)

# 블로그 포스트 저장소
posts = []
post_id_counter = 1

@app.route('/')
def home():
    html = "<h1>블로그</h1>"
    html += "<a href='/write'>새 글 쓰기</a><br><br>"

    if not posts:
        html += "<p>아직 게시글이 없습니다.</p>"
    else:
        html += "<ul>"
        for post in reversed(posts):  # 최신글이 위로
            html += f"""
            <li>
                <a href="/post/{post['id']}">
                    {post['title']}
                </a>
                <small>({post['created_at']})</small>
            </li>
            """
        html += "</ul>"

    return html

@app.route('/write', methods=['GET', 'POST'])
def write():
    if request.method == 'POST':
        global post_id_counter

        title = request.form.get('title')
        content = request.form.get('content')

        if not title or not content:
            return "제목과 내용을 모두 입력해주세요 <a href='/write'>돌아가기</a>"

        # 새 포스트 생성
        new_post = {
            'id': post_id_counter,
            'title': title,
            'content': content,
            'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        }
        posts.append(new_post)
        post_id_counter += 1

        return redirect(url_for('home'))

    return """
    <h1>새 글 쓰기</h1>
    <form method="POST">
        <input type="text" name="title" placeholder="제목" required><br><br>
        <textarea name="content" rows="10" cols="50" placeholder="내용" required></textarea><br><br>
        <button>게시</button>
        <a href="/">취소</a>
    </form>
    """

@app.route('/post/<int:post_id>')
def view_post(post_id):
    post = next((p for p in posts if p['id'] == post_id), None)

    if not post:
        return "<h1>게시글을 찾을 수 없습니다</h1><a href='/'>홈으로</a>", 404

    return f"""
    <h1>{post['title']}</h1>
    <p><small>작성일: {post['created_at']}</small></p>
    <hr>
    <p>{post['content']}</p>
    <br>
    <a href="/">목록으로</a>
    """

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

⚠️ 주의사항

1. request 객체는 요청 컨텍스트에서만 사용

1
2
3
4
5
6
7
8
9
10
from flask import request

# ❌ 잘못됨: 라우트 밖에서 사용
username = request.args.get('username')  # 에러!

@app.route('/user')
def user():
    # ✅ 올바름: 라우트 안에서 사용
    username = request.args.get('username')
    return f"User: {username}"

2. 사용자 입력은 항상 검증하기

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 위험: 검증 없이 사용
@app.route('/eval')
def evaluate():
    code = request.args.get('code')
    return eval(code)  # SQL Injection, XSS 등 취약!

# ✅ 안전: 입력 검증
@app.route('/calc')
def calculate():
    num = request.args.get('num', type=int)
    if num is None or num < 0:
        return "잘못된 입력입니다", 400
    return f"Result: {num * 2}"

3. 리다이렉트 시 상태 코드 명시

1
2
3
4
5
6
7
8
# 301: 영구 이동 (Permanent Redirect)
return redirect(url_for('new_page'), code=301)

# 302: 임시 이동 (Temporary Redirect, 기본값)
return redirect(url_for('temp_page'), code=302)

# 307: POST 메서드 유지
return redirect(url_for('target'), code=307)

🧪 연습 문제

문제 1: 온도 변환기

섭씨를 화씨로, 화씨를 섭씨로 변환하는 Flask 앱을 만드세요.

  • /convert/c2f/25 → “25°C = 77.0°F”
  • /convert/f2c/77 → “77°F = 25.0°C”
✅ 정답
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
from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return """
    <h1>온도 변환기</h1>
    <p>예시:</p>
    <ul>
        <li><a href="/convert/c2f/25">/convert/c2f/25</a> - 섭씨 → 화씨</li>
        <li><a href="/convert/f2c/77">/convert/f2c/77</a> - 화씨 → 섭씨</li>
    </ul>
    """

@app.route('/convert/<string:mode>/<float:temp>')
def convert_temp(mode, temp):
    if mode == 'c2f':
        result = (temp * 9/5) + 32
        return f"{temp}°C = {result:.1f}°F"
    elif mode == 'f2c':
        result = (temp - 32) * 5/9
        return f"{temp}°F = {result:.1f}°C"
    else:
        return "잘못된 모드입니다. c2f 또는 f2c를 사용하세요.", 400

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

문제 2: 방명록

방문자가 메시지를 남길 수 있는 방명록을 만드세요.

  • GET /guestbook: 모든 메시지 표시 + 작성 폼
  • POST /guestbook: 새 메시지 추가 후 목록으로 리다이렉트
✅ 정답
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
from flask import Flask, request, redirect, url_for
from datetime import datetime

app = Flask(__name__)

messages = []

@app.route('/guestbook', methods=['GET', 'POST'])
def guestbook():
    if request.method == 'POST':
        name = request.form.get('name')
        message = request.form.get('message')

        if name and message:
            messages.append({
                'name': name,
                'message': message,
                'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            })

        return redirect(url_for('guestbook'))

    # GET: 방명록 표시
    html = "<h1>방명록</h1>"

    # 메시지 목록
    if messages:
        html += "<ul>"
        for msg in reversed(messages):
            html += f"""
            <li>
                <strong>{msg['name']}</strong>: {msg['message']}
                <br><small>{msg['time']}</small>
            </li>
            """
        html += "</ul>"
    else:
        html += "<p>아직 메시지가 없습니다.</p>"

    # 작성 폼
    html += """
    <h2>메시지 남기기</h2>
    <form method="POST">
        <input type="text" name="name" placeholder="이름" required><br>
        <textarea name="message" placeholder="메시지" required></textarea><br>
        <button>남기기</button>
    </form>
    """

    return html

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

📝 요약

이번 Day 82에서 학습한 내용:

  1. 동적 라우트: URL 변수, 타입 지정 (<int:id>, <path:filepath>)
  2. HTTP 메서드: GET, POST 처리, methods 파라미터
  3. 리다이렉트: redirect(), url_for() 함수
  4. 요청 데이터: request.args, request.form, request.json

📚 다음 학습

Day 83: 템플릿 엔진 (Jinja2) ⭐⭐⭐⭐

HTML을 Python 코드에 문자열로 작성하는 건 너무 불편합니다! 내일은 Jinja2 템플릿 엔진으로 HTML을 깔끔하게 관리하는 법을 배웁니다!


“좋은 라우팅 설계는 좋은 웹사이트의 시작입니다!” 🚀

Day 82/100 Phase 9: 웹 개발 입문 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.