๐Ÿš€ FastAPI ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ ๊ธฐ์ดˆ ๊ฐ€์ด๋“œ

Hyeonio_oยท2025๋…„ 6์›” 14์ผ

BackEnd

๋ชฉ๋ก ๋ณด๊ธฐ
2/9
post-thumbnail

๐Ÿ‘‰ ๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋กœ ChatGPT API๋ฅผ ์‚ฌ์šฉํ•œ "๋ชจ๋‘์˜ ๋ ˆ์‹œํ”ผ"๋ฅผ ๋งŒ๋“ค๋ฉด์„œ ์–ด๋ ค์› ๋˜ ์ ์„ ์š”์•ฝํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.


๐Ÿ“š ๋ชฉ์ฐจ

  1. ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ Import ์ดํ•ดํ•˜๊ธฐ
  2. FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ธฐ๋ณธ ์„ค์ •
  3. HTTP ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (requests vs httpx)
  4. ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ๋ณด์•ˆ
  5. ๋งˆ๋ฌด๋ฆฌ

1. ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ Import ์ดํ•ดํ•˜๊ธฐ

๐Ÿ“ฆ ๋ชจ๋“ˆ๋ณ„ ์ƒ์„ธ ์„ค๋ช…

FastAPI ํ•ต์‹ฌ ๋ชจ๋“ˆ๋“ค

๋ชจ๋“ˆ์—ญํ• ์‹ค๋ฌด ํ™œ์šฉ๋„
FastAPI๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํด๋ž˜์Šคโญโญโญโญโญ
RequestHTTP ์š”์ฒญ ๊ฐ์ฒด ์ฒ˜๋ฆฌโญโญโญโญ
FormHTML ํผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌโญโญโญ
HTTPExceptionHTTP ์—๋Ÿฌ ์ฒ˜๋ฆฌโญโญโญโญโญ
Cookie์ฟ ํ‚ค ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌโญโญโญ
Depends์˜์กด์„ฑ ์ฃผ์ž…โญโญโญโญโญ

์‘๋‹ต ๊ด€๋ จ ๋ชจ๋“ˆ๋“ค

  • ๐ŸŒ HTMLResponse: HTML ํ˜•ํƒœ ์‘๋‹ต (์›นํŽ˜์ด์ง€ ๋ฐ˜ํ™˜)
  • ๐Ÿ”„ RedirectResponse: ํŽ˜์ด์ง€ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ (๋กœ๊ทธ์ธ ํ›„ ๋ฉ”์ธ์œผ๋กœ ์ด๋™)
  • ๐Ÿ“Š JSONResponse: JSON ํ˜•ํƒœ ์‘๋‹ต (API ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜)

ํ…œํ”Œ๋ฆฟ๊ณผ ์ •์  ํŒŒ์ผ

  • ๐ŸŽจ Jinja2Templates: HTML ํ…œํ”Œ๋ฆฟ ์—”์ง„ (Django ํ…œํ”Œ๋ฆฟ๊ณผ ์œ ์‚ฌ)
  • ๐Ÿ“ StaticFiles: CSS, JS, ์ด๋ฏธ์ง€ ๋“ฑ ์ •์  ํŒŒ์ผ ์„œ๋น™

CORS ๋ฏธ๋“ค์›จ์–ด

  • ๐Ÿ” CORSMiddleware: ๋‹ค๋ฅธ ๋„๋ฉ”์ธ ๊ฐ„ ํ†ต์‹  ํ—ˆ์šฉ ์„ค์ •

2. FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ธฐ๋ณธ ์„ค์ •

๐Ÿ› ๏ธ CORS ๋ฏธ๋“ค์›จ์–ด ์„ค์ •

# CORS ๋ฏธ๋“ค์›จ์–ด ์„ค์ •
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],           # ๋ชจ๋“  ๋„๋ฉ”์ธ ํ—ˆ์šฉ
    allow_credentials=True,        # ์ธ์ฆ ์ •๋ณด ํฌํ•จ ์š”์ฒญ ํ—ˆ์šฉ
    allow_methods=["*"],          # ๋ชจ๋“  HTTP ๋ฉ”์„œ๋“œ ํ—ˆ์šฉ
    allow_headers=["*"],          # ๋ชจ๋“  ํ—ค๋” ํ—ˆ์šฉ
)

๐Ÿ” CORS๋ž€?

Cross-Origin Resource Sharing์˜ ์ค„์ž„๋ง๋กœ, ์›น ๋ณด์•ˆ ์ •์ฑ…์ž…๋‹ˆ๋‹ค.

์„ค์ •์„ค๋ช…๊ฐœ๋ฐœํ™˜๊ฒฝ์šด์˜ํ™˜๊ฒฝ
allow_originsํ—ˆ์šฉํ•  ๋„๋ฉ”์ธ["*"]["https://myapp.com"]
allow_credentials์ธ์ฆ์ •๋ณด ํฌํ•จ ์š”์ฒญTrueTrue
allow_methodsํ—ˆ์šฉ HTTP ๋ฉ”์„œ๋“œ["*"]["GET", "POST"]
allow_headersํ—ˆ์šฉ ํ—ค๋”["*"]ํ•„์š”ํ•œ ํ—ค๋”๋งŒ

โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ: ์‹ค์ œ ๋ฐฐํฌ ์‹œ์—๋Š” allow_origins๋ฅผ ํŠน์ • ๋„๋ฉ”์ธ์œผ๋กœ ์ œํ•œํ•˜์„ธ์š”!

๐ŸŽฏ ์ •์  ํŒŒ์ผ ์„œ๋น™ ์„ค์ •

# ์ •์  ํŒŒ์ผ ์‚ฌ์šฉ ์„ค์ •
app.mount("/static", StaticFiles(directory="static"), name="static")

๐ŸŽจ HTML ํ…œํ”Œ๋ฆฟ ์—”์ง„ ์„ค์ •

# HTML ํ…œํ”Œ๋ฆฟ ๊ฒฝ๋กœ ์„ค์ •
templates = Jinja2Templates(directory="templates")

๐Ÿ”ง ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ๋ฒ•

@app.get("/")
async def read_root(request: Request):
    return templates.TemplateResponse("index.html", {
        "request": request,           # ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ
        "title": "ํ™ˆํŽ˜์ด์ง€",          # ํ…œํ”Œ๋ฆฟ ๋ณ€์ˆ˜
        "user": "๊น€๊ฐœ๋ฐœ์ž"           # ํ…œํ”Œ๋ฆฟ ๋ณ€์ˆ˜
    })

templates/index.html:

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>์•ˆ๋…•ํ•˜์„ธ์š”, {{ user }}๋‹˜!</h1>
</body>
</html>

3. HTTP ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (requests vs httpx)

๐Ÿ”„ requests vs httpx ์™„๋ฒฝ ๋น„๊ต

ํŠน์ง•requestshttpx์‹ค๋ฌด ์ถ”์ฒœ๋„
๋™๊ธฐ ์ง€์›โœ… ์™„๋ฒฝ ์ง€์›โœ… ์™„๋ฒฝ ์ง€์›โญโญโญโญโญ
๋น„๋™๊ธฐ ์ง€์›โŒ ๋ฏธ์ง€์›โœ… ์™„๋ฒฝ ์ง€์›โญโญโญโญโญ
HTTP/2 ์ง€์›โŒ ๋ฏธ์ง€์›โœ… ์™„๋ฒฝ ์ง€์›โญโญโญโญ
ํ˜„๋Œ€์ ์ธ APIโš ๏ธ ์˜ค๋ž˜๋œ ์„ค๊ณ„โœ… ์ตœ์‹  ์„ค๊ณ„โญโญโญโญโญ
์„ฑ๋Šฅ๋ณดํ†ต์šฐ์ˆ˜โญโญโญโญ
FastAPI ํ˜ธํ™˜์„ฑ์ œํ•œ์ ์™„๋ฒฝโญโญโญโญโญ

๐Ÿ“ ๋™๊ธฐ HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ๋ฒ•

requests ์‚ฌ์šฉ ์˜ˆ์‹œ

import requests

# ๊ธฐ๋ณธ GET ์š”์ฒญ
def get_user_info_sync(user_id: int):
    """๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ"""
    response = requests.get(f"https://api.example.com/users/{user_id}")
    
    if response.status_code == 200:
        return response.json()
    else:
        return {"error": f"์‚ฌ์šฉ์ž ์กฐํšŒ ์‹คํŒจ: {response.status_code}"}

# POST ์š”์ฒญ (JSON ๋ฐ์ดํ„ฐ)
def create_user_sync(user_data: dict):
    """๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ์ž ์ƒ์„ฑ"""
    response = requests.post(
        "https://api.example.com/users",
        json=user_data,
        headers={"Content-Type": "application/json"}
    )
    return response.json()

httpx ๋™๊ธฐ ์‚ฌ์šฉ ์˜ˆ์‹œ

import httpx

# httpx๋กœ ๋™๊ธฐ ์š”์ฒญ (requests์™€ ๊ฑฐ์˜ ๋™์ผํ•œ API)
def get_user_info_sync_httpx(user_id: int):
    """httpx๋กœ ๋™๊ธฐ ๋ฐฉ์‹ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ"""
    with httpx.Client() as client:
        response = client.get(f"https://api.example.com/users/{user_id}")
        
        if response.status_code == 200:
            return response.json()
        else:
            return {"error": f"์‚ฌ์šฉ์ž ์กฐํšŒ ์‹คํŒจ: {response.status_code}"}

โšก ๋น„๋™๊ธฐ HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ๋ฒ• (httpx๋งŒ ์ง€์›)

import httpx
import asyncio

# ๋น„๋™๊ธฐ GET ์š”์ฒญ
async def get_user_info_async(user_id: int):
    """๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ"""
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.example.com/users/{user_id}")
        
        if response.status_code == 200:
            return response.json()
        else:
            return {"error": f"์‚ฌ์šฉ์ž ์กฐํšŒ ์‹คํŒจ: {response.status_code}"}

# ๋น„๋™๊ธฐ POST ์š”์ฒญ
async def create_user_async(user_data: dict):
    """๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ์ž ์ƒ์„ฑ"""
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://api.example.com/users",
            json=user_data,
            headers={"Content-Type": "application/json"}
        )
        return response.json()

# ์—ฌ๋Ÿฌ ์š”์ฒญ ๋™์‹œ ์ฒ˜๋ฆฌ
async def get_multiple_users(user_ids: list):
    """์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋™์‹œ์— ์กฐํšŒ"""
    async with httpx.AsyncClient() as client:
        tasks = [
            client.get(f"https://api.example.com/users/{user_id}") 
            for user_id in user_ids
        ]
        
        # ๋ชจ๋“  ์š”์ฒญ์„ ๋™์‹œ์— ์‹คํ–‰
        responses = await asyncio.gather(*tasks)
        
        return [
            response.json() if response.status_code == 200 else None
            for response in responses
        ]

๐Ÿš€ FastAPI์—์„œ HTTP ํด๋ผ์ด์–ธํŠธ ํ™œ์šฉ

์™ธ๋ถ€ API ํ˜ธ์ถœํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ ์˜ˆ์‹œ

from fastapi import FastAPI
import httpx

app = FastAPI()

# ๋™๊ธฐ ๋ฐฉ์‹ (๊ฐ„๋‹จํ•œ ์ž‘์—…์šฉ)
@app.get("/user/{user_id}")
def get_external_user(user_id: int):
    """์™ธ๋ถ€ API์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ (๋™๊ธฐ)"""
    with httpx.Client() as client:
        response = client.get(f"https://jsonplaceholder.typicode.com/users/{user_id}")
        
        if response.status_code == 200:
            return {"success": True, "data": response.json()}
        else:
            return {"success": False, "error": "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}

# ๋น„๋™๊ธฐ ๋ฐฉ์‹ (๊ถŒ์žฅ)
@app.get("/user-async/{user_id}")
async def get_external_user_async(user_id: int):
    """์™ธ๋ถ€ API์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ (๋น„๋™๊ธฐ)"""
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://jsonplaceholder.typicode.com/users/{user_id}")
        
        if response.status_code == 200:
            return {"success": True, "data": response.json()}
        else:
            return {"success": False, "error": "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}

# ๋ณต์žกํ•œ ์™ธ๋ถ€ API ์—ฐ๋™ ์˜ˆ์‹œ
@app.get("/weather/{city}")
async def get_weather(city: str):
    """๋‚ ์”จ API ์—ฐ๋™ ์˜ˆ์‹œ"""
    async with httpx.AsyncClient() as client:
        # ์‹ค์ œ๋กœ๋Š” API ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค
        response = await client.get(
            f"https://api.openweathermap.org/data/2.5/weather",
            params={
                "q": city,
                "appid": "YOUR_API_KEY",
                "units": "metric",
                "lang": "kr"
            }
        )
        
        if response.status_code == 200:
            weather_data = response.json()
            return {
                "city": weather_data["name"],
                "temperature": weather_data["main"]["temp"],
                "description": weather_data["weather"][0]["description"]
            }
        else:
            return {"error": "๋‚ ์”จ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}

๐ŸŽฏ ์‹ค๋ฌด์—์„œ์˜ ์„ ํƒ ๊ธฐ์ค€

requests๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ

  • โœ… ๊ฐ„๋‹จํ•œ ์Šคํฌ๋ฆฝํŠธ๋‚˜ ๋ฐฐ์น˜ ์ž‘์—…
  • โœ… ๋™๊ธฐ ์ฒ˜๋ฆฌ๋งŒ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
  • โœ… ๊ธฐ์กด ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ์™€์˜ ํ˜ธํ™˜์„ฑ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ

httpx๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ (๊ถŒ์žฅ)

  • โœ… FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ
  • โœ… ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
  • โœ… HTTP/2 ์ง€์›์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
  • โœ… ์ตœ์‹  ๊ธฐ๋Šฅ๊ณผ ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ

4. ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ๋ณด์•ˆ

๐Ÿงน ํ…์ŠคํŠธ ์ •๋ฆฌ ํ•จ์ˆ˜

def clean_text(text: str) -> str:
    """ํ…์ŠคํŠธ์—์„œ HTML ํƒœ๊ทธ ์ œ๊ฑฐ ๋ฐ ํŠน์ˆ˜๋ฌธ์ž ์ฒ˜๋ฆฌ"""
    if not text:
        return ""
    
    # 1๏ธโƒฃ HTML ์—”ํ‹ฐํ‹ฐ ๋””์ฝ”๋”ฉ
    text = html.unescape(text)
    
    # 2๏ธโƒฃ HTML ํƒœ๊ทธ ์ œ๊ฑฐ
    text = re.sub(r'<[^>]+>', '', text)
    
    # 3๏ธโƒฃ ์—ฐ์†๋œ ๊ณต๋ฐฑ์„ ํ•˜๋‚˜๋กœ ๋ณ€๊ฒฝ
    text = re.sub(r'\s+', ' ', text)
    
    # 4๏ธโƒฃ ์•ž๋’ค ๊ณต๋ฐฑ ์ œ๊ฑฐ
    text = text.strip()
    
    return text

๐Ÿ›ก๏ธ XSS ๊ณต๊ฒฉ ๋ฐฉ์ง€ ๊ณผ์ •

๋‹จ๊ณ„์ฒ˜๋ฆฌ ๋‚ด์šฉ์˜ˆ์‹œ
HTML ์—”ํ‹ฐํ‹ฐ ๋””์ฝ”๋”ฉ&lt; โ†’ <&lt;script&gt; โ†’ <script>
HTML ํƒœ๊ทธ ์ œ๊ฑฐ๋ชจ๋“  ํƒœ๊ทธ ์‚ญ์ œ<script>alert()</script> โ†’ alert()
๊ณต๋ฐฑ ์ •๊ทœํ™”์—ฐ์† ๊ณต๋ฐฑ ํ†ตํ•ฉ์•ˆ๋…• ํ•˜์„ธ์š” โ†’ ์•ˆ๋…• ํ•˜์„ธ์š”
๊ณต๋ฐฑ ์ œ๊ฑฐ์•ž๋’ค ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ ์ œ๊ฑฐ ํ…์ŠคํŠธ โ†’ ํ…์ŠคํŠธ

โœ… ์ž…๋ ฅ ๊ฒ€์ฆ ํ•จ์ˆ˜

def validate_recipe_input(title: str, content: str) -> None:
    """๋ ˆ์‹œํ”ผ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ"""
    
    # ๐Ÿงน ํ…์ŠคํŠธ ์ •๋ฆฌ
    title = clean_text(title) if title else ""
    content = clean_text(content) if content else ""
    
    # ๐Ÿ“ ํ•„์ˆ˜๊ฐ’ ๊ฒ€์ฆ
    if not title:
        raise HTTPException(
            status_code=400,
            detail="โŒ ๋ ˆ์‹œํ”ผ ์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
        )
    
    if not content:
        raise HTTPException(
            status_code=400,
            detail="โŒ ๋ ˆ์‹œํ”ผ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
        )
    
    # ๐Ÿ“ ๊ธธ์ด ์ œํ•œ ๊ฒ€์ฆ
    if len(title) > 200:
        raise HTTPException(
            status_code=400,
            detail="โŒ ์ œ๋ชฉ์€ 200์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
        )
    
    if len(content) > 10000:
        raise HTTPException(
            status_code=400,
            detail="โŒ ๋‚ด์šฉ์€ 10,000์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
        )

๐Ÿ“Š ๊ฒ€์ฆ ๊ทœ์น™ ์š”์•ฝ

ํ•ญ๋ชฉ๊ทœ์น™์—๋Ÿฌ ์ฝ”๋“œ๋ฉ”์‹œ์ง€
์ œ๋ชฉ ํ•„์ˆ˜๋นˆ ๊ฐ’ ๋ถˆํ—ˆ400"๋ ˆ์‹œํ”ผ ์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
๋‚ด์šฉ ํ•„์ˆ˜๋นˆ ๊ฐ’ ๋ถˆํ—ˆ400"๋ ˆ์‹œํ”ผ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
์ œ๋ชฉ ๊ธธ์ด200์ž ์ด๋‚ด400"์ œ๋ชฉ์€ 200์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
๋‚ด์šฉ ๊ธธ์ด10,000์ž ์ด๋‚ด400"๋‚ด์šฉ์€ 10,000์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ

๐Ÿ’ก ํ•ต์‹ฌ ๊ฐœ๋… ์š”์•ฝ

๊ฐœ๋…์„ค๋ช…์‹ค๋ฌด ์ค‘์š”๋„
์ฒด๊ณ„์ ์ธ Import๋ชจ๋“ˆ์„ ์šฉ๋„๋ณ„๋กœ ์ •๋ฆฌํ•˜์—ฌ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒโญโญโญ
CORS ์„ค์ •ํ”„๋ก ํŠธ์—”๋“œ-๋ฐฑ์—”๋“œ ๊ฐ„ ์•ˆ์ „ํ•œ ํ†ต์‹ โญโญโญโญโญ
์ •์  ํŒŒ์ผ ์„œ๋น™CSS, JS ๋“ฑ ์ •์  ์ž์› ํšจ์œจ์  ์ œ๊ณตโญโญโญโญ
ํ…œํ”Œ๋ฆฟ ์—”์ง„๋™์  HTML ์ƒ์„ฑ์œผ๋กœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒโญโญโญ
HTTP ํด๋ผ์ด์–ธํŠธ์™ธ๋ถ€ API ์—ฐ๋™ (httpx ๊ถŒ์žฅ)โญโญโญโญโญ
์˜์กด์„ฑ ์ฃผ์ž…์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ๊ณผ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ ํ™•๋ณดโญโญโญโญโญ
๋ฐ์ดํ„ฐ ๊ฒ€์ฆXSS ๋ฐฉ์ง€์™€ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅโญโญโญโญโญ

๐Ÿ› ๏ธ ์‹ค๋ฌด ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด

  1. ๋‚ ์”จ ๋Œ€์‹œ๋ณด๋“œ

    • OpenWeatherMap API ์—ฐ๋™
    • ์—ฌ๋Ÿฌ ๋„์‹œ ๋‚ ์”จ ๋™์‹œ ์กฐํšŒ
    • ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ๊ธฐ๋Šฅ
  2. ์†Œ์…œ ๋ฏธ๋””์–ด ์ง‘๊ณ„๊ธฐ

    • ์—ฌ๋Ÿฌ SNS API ๋™์‹œ ํ˜ธ์ถœ
    • ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ๋ฐ ๋ถ„์„
    • ์บ์‹ฑ ์ „๋žต ๊ตฌํ˜„
  3. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ฒŒ์ดํŠธ์›จ์ด

    • ์—ฌ๋Ÿฌ ๋‚ด๋ถ€ ์„œ๋น„์Šค API ๋ผ์šฐํŒ…
    • ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ ๋ฐ ํ—ฌ์Šค์ฒดํฌ
    • API ์‘๋‹ต ์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง

0๊ฐœ์˜ ๋Œ“๊ธ€