React + Python FastAPI๋ก
์น ๊ฒ์ ๋ง๋ค๊ธฐ #4
์์ ํ๋ฉด ยท ๊ฒ์์ค๋ฒ ยท ๋๋ค์ ์ ์ฅ ยท FastAPI ์ฐ๋ ยท ๋ฆฌ๋๋ณด๋ ์์ฑ
1 ์์ฑ๋ ํ๋ฉด๋ค ๋จผ์ ๋ณด๊ธฐ
์ฝ๋ ์๊ธฐ ์ ์ ์ค์ ๋ก ์ด๋ป๊ฒ ์๊ฒผ๋์ง ๋จผ์ ๋ณด์. ์ด 4๊ฐ ํ๋ฉด์ด ์๋ค.
2 ์์ ํ๋ฉด ๋ง๋ค๊ธฐ
์ฒ์์ ๊ฒ์์ด ์ผ์ง์๋ง์ ๋ฐ๋ก ์์๋๋ค. ๊ทผ๋ฐ ๊ทธ๋ฌ๋ฉด ์กฐ์๋ฒ๋ ๋ชจ๋ฅด๊ณ ๋ฐ๋ก ์ฃฝ์ด๋ฒ๋ฆฌ๋๊น ์์ ํ๋ฉด์ ๋ฐ๋ก ๋ง๋ค์๋ค. React์ useState๋ก ํ์ฌ ์ด๋ค ํ๋ฉด์ธ์ง ๊ด๋ฆฌํ๋ ๋ฐฉ์์ด๋ค.
screen์ด๋ผ๋ ์ํ๊ฐ ํ๋๋ก ํ์ฌ ํ๋ฉด์ ๊ด๋ฆฌํ๋ค. ๊ฐ์ด 'start'๋ฉด ์์ํ๋ฉด, 'game'์ด๋ฉด ๊ฒ์ํ๋ฉด, 'gameover'๋ฉด ๊ฒ์์ค๋ฒ, 'leaderboard'๋ฉด ์์ํ๋ฅผ ๋ณด์ฌ์ค๋ค.
const [screen, setScreen] = useState('start') // ํ๋ฉด๋ณ๋ก ๋ค๋ฅธ UI ํ์ {screen === 'start' && <์์ํ๋ฉด />} {screen === 'gameover' && <๊ฒ์์ค๋ฒํ๋ฉด />} {screen === 'leaderboard' && <๋ฆฌ๋๋ณด๋ />} // ๋ฒํผ ํด๋ฆญ ์ ํ๋ฉด ์ ํ <button onClick={() => startGame()}>GAME START</button> <button onClick={() => setScreen('leaderboard')}>์์ํ</button>
ํ๋ฉด ์ ํํ ๋ canvas๋ฅผ
display: none์ผ๋ก ์จ๊ฒผ๋๋ ๊ฒ์์ด ์์ ํ ๋ง๊ฐ์ก๋ค. canvas๊ฐ DOM์์ ์ฌ๋ผ์ง๋ฉด canvasRef.current๊ฐ ๋๊ฒจ์ ๊ฒ์ ๋ฃจํ ์์ฒด๊ฐ ๋์์ ์ ํ๋ค. ๊ทธ๋์ canvas๋ ํญ์ ํ๋ฉด์ ๋๊ณ , ๊ทธ ์์ ์ค๋ฒ๋ ์ด๋ฅผ position: absolute๋ก ์์ฐ๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐํ๋ค.
3 ๊ฒ์์ค๋ฒ ํ๋ฉด + ๋๋ค์ ์ ๋ ฅ
์ฅ์ ๋ฌผ์ ๋ถ๋ชํ๋ฉด ๊ฒ์ ๋ฃจํ๋ฅผ ๋ฉ์ถ๊ณ ๊ฒ์์ค๋ฒ ์ค๋ฒ๋ ์ด๋ฅผ ๋์ด๋ค. ๊ฑฐ๊ธฐ์ ๋๋ค์ ์ ๋ ฅํ๊ณ ์ ์ ๋ฑ๋กํ๋ ํ๋ฆ์ด๋ค.
function loop() { ... if (checkCollision()) { gRunning = false // ๋ฃจํ ์ค๋จ removeListeners() // ํค ์ด๋ฒคํธ ์ ๊ฑฐ setFinalScore(score) // ์ต์ข ์ ์ ์ ์ฅ setFinalTime(elapsed.toFixed(1)) // ์ต์ข ์๊ฐ ์ ์ฅ setScreen('gameover') // ํ๋ฉด ์ ํ return } ... } // ๊ฒ์์ค๋ฒ ํ๋ฉด JSX {screen === 'gameover' && ( <div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.85)' }}> <div>๐ฅ GAME OVER</div> <div>SCORE: {finalScore}</div> <div>TIME: {finalTime}s</div> <input value={nickname} onChange={e => setNickname(e.target.value)} /> <button onClick={submitScore}>โ ์ ์ ๋ฑ๋ก</button> <button onClick={startGame}>โถ ๋ค์ํ๊ธฐ</button> </div> )}
4 FastAPI๋ก ์ ์ ์ ์ฅํ๊ธฐ
์ ์ ๋ฑ๋ก ๋ฒํผ์ ๋๋ฅด๋ฉด React๊ฐ Python ์๋ฒ์ ์ ์๋ฅผ ๋ณด๋ด๊ณ , ์๋ฒ๊ฐ scores.json ํ์ผ์ ์ ์ฅํ๋ค. ์ด๊ฒ ์๋ฒ์ ์ ์ฅ๋๊ธฐ ๋๋ฌธ์ ๋ธ๋ผ์ฐ์ ๋ฅผ ๊บผ๋ ๋ฐ์ดํฐ๊ฐ ๋จ์์๋ค.
| ์ญํ | ์ฃผ์ | ์ค๋ช |
|---|---|---|
| ์ ์ ์ ์ฅ | POST /score | ๋๋ค์ + ์ ์ + ์๊ฐ ๋ณด๋ด๋ฉด json์ ์ ์ฅ |
| ๋ฆฌ๋๋ณด๋ ์กฐํ | GET /leaderboard | ์ ์ฅ๋ ์ ์ ์์ 20๊ฐ ๊ฐ์ ธ์ค๊ธฐ |
async function submitScore() { if (!nickname.trim()) return // ๋๋ค์ ์์ผ๋ฉด ๋ฌด์ const res = await fetch('http://127.0.0.1:8000/score', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname: nickname, score: finalScore, time: finalTime, }) }) const data = await res.json() if (data.duplicate) { alert('์ด๋ฏธ ์กด์ฌํ๋ ๋๋ค์์ด์์!') return } setMyRank(data.rank) // ๋ด ์์ ์ ์ฅ await fetchLeaderboard() // ๋ฆฌ๋๋ณด๋ ์๋ก๊ณ ์นจ setScreen('leaderboard') // ๋ฆฌ๋๋ณด๋ ํ๋ฉด์ผ๋ก }
@app.post("/score") def post_score(entry: ScoreEntry): scores = load_scores() # ๋๋ค์ ์ค๋ณต ์ฒดํฌ existing = [s for s in scores if s["nickname"] == entry.nickname] if existing: return {"error": "์ค๋ณต ๋๋ค์", "duplicate": True} # ์ ์ ์ถ๊ฐ ํ ๋ด๋ฆผ์ฐจ์ ์ ๋ ฌ scores.append(entry.model_dump()) scores.sort(key=lambda x: x["score"], reverse=True) save_scores(scores) rank = next(i+1 for i, s in enumerate(scores) if s["nickname"] == entry.nickname) return {"rank": rank, "duplicate": False}
๋ธ๋ผ์ฐ์ ์์ ์๋ฒ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๊ฑฐ๋ ๋ฐ์์ค๋ JavaScript ํจ์๋ค.
POST๋ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ๋, GET์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ๋ ์ด๋ค. await๋ ์๋ต์ด ์ฌ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๋ ํค์๋๋ค.
๋ธ๋ผ์ฐ์ ์ localStorage๋ ์ฟ ํค๊ฐ ์๋๋ผ Python ์๋ฒ์
scores.json ํ์ผ์ ์ ์ฅํ๊ธฐ ๋๋ฌธ์ด๋ค. ๋ธ๋ผ์ฐ์ ๋ฅผ ๊ป๋ค ์ผ๋, ์บ์๋ฅผ ์ง์๋ ์๋ฒ ํ์ผ์ ๊ทธ๋๋ก ๋จ์์๋ค.
5 ๋ฆฌ๋๋ณด๋ ํ๋ฉด
์ ์ ๋ฑ๋ก์ด ๋๋๋ฉด ์๋์ผ๋ก ๋ฆฌ๋๋ณด๋๋ก ์ด๋ํ๋ค. ์๋ฒ์์ ๋ฐ์์จ ๋ฐ์ดํฐ๋ฅผ map()์ผ๋ก ํ๋ํ๋ ํ๋ฉด์ ๊ทธ๋ ค์ฃผ๋ ๋ฐฉ์์ด๋ค.
// ์๋ฒ์์ ๋ฆฌ๋๋ณด๋ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ async function fetchLeaderboard() { const res = await fetch('http://127.0.0.1:8000/leaderboard') const data = await res.json() setLeaderboard(data) // ์ํ์ ์ ์ฅ } // ํ๋ฉด์ ์์ ํ์ {leaderboard.slice(0, 10).map((entry, i) => ( <div key={i}> <span>{i === 0 ? '๐ฅ' : i === 1 ? '๐ฅ' : i === 2 ? '๐ฅ' : `${i+1}.`}</span> <span>{entry.nickname}</span> <span>{entry.score}pt</span> <span>{entry.time}s</span> </div> ))}
๋ฐฐ์ด์ ๊ฐ ํญ๋ชฉ์ ๋ณํํด์ ์ ๋ฐฐ์ด์ ๋ง๋๋ ํจ์๋ค. ์๋ฒ์์ ๋ฐ์ ์ ์ ๋ฐฐ์ด์
map()์ผ๋ก ์ํํ๋ฉด์ ๊ฐ๊ฐ์ HTML ์์๋ก ๋ณํํด์ ํ๋ฉด์ ํ์ํ๋ค.
6 ๋๋ค์ ์ค๋ณต ๋ฐฉ์ง
๊ฐ์ ๋๋ค์์ผ๋ก ์ฌ๋ฌ ๋ฒ ๋ฑ๋กํ๋ฉด ๋ฆฌ๋๋ณด๋๊ฐ ๋๋ฐฐ๋๋ ๋ฌธ์ ๊ฐ ์์๋ค. Python ์๋ฒ์์ ์ค๋ณต ์ฒดํฌ๋ฅผ ํด์ ์ด๋ฏธ ์๋ ๋๋ค์์ด๋ฉด ๋ฑ๋ก์ ๋ง๋๋ก ํ๋ค.
# ์ ์ฅ๋ ๋๋ค์ ์ค์ ๊ฐ์ ๊ฒ ์๋์ง ํ์ธ existing = [s for s in scores if s["nickname"] == entry.nickname.strip()] if existing: return {"error": "์ด๋ฏธ ์กด์ฌํ๋ ๋๋ค์", "duplicate": True} # ์ค๋ณต ์์ผ๋ฉด ์ ์ฅ ์งํ
if (data.duplicate) { alert('โ ์ด๋ฏธ ์กด์ฌํ๋ ๋๋ค์์ด์์!\n๋ค๋ฅธ ๋๋ค์์ ์ ๋ ฅํด์ฃผ์ธ์.') return // ๋ฑ๋ก ์ค๋จ }
7 ์ง์ง ํ๋ค์๋ ์ค๋ฅ๋ค
์ด๋ฒ ํํธ์์ ์ ์ผ ๊ณ ์ํ ๊ฒ ์ค๋ฅ๋ค์ด์๋ค. ํนํ ์ ํ ๊ด๋ จ ๋ฒ๊ทธ๋ ๋ฉฐ์น ์ ์ก์๋ค.
๐ด ์ค๋ฅ 1 โ ๋ค์ํ๊ธฐ ๋๋ฅด๋ฉด ์ ํ๊ฐ ์์ ์ ๋จ
๋ค์ํ๊ธฐ ๋ฒํผ์ ๋๋ฅผ ๋๋ง๋ค ๊ฒ์ ๋ฃจํ๊ฐ ์๋ก ์์๋๋๋ฐ, ์ด์ ๋ฃจํ๊ฐ ์์ ํ ์ ๋๋ ์ฑ๋ก ์ ๋ฃจํ๊ฐ ๋ ์์๋๋ ๊ฑฐ์๋ค. keydown ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ ๊ณ์ ์ค๋ณต์ผ๋ก ์์ฌ์ ๊ฒฐ๊ตญ ์ถฉ๋์ด ๋ฌ๋ค. ํด๊ฒฐ๋ฒ์ ๊ฒ์ ๋ฃจํ ID๋ฅผ ๋ชจ๋ ์ ์ญ ๋ณ์ gAnimId์ gRunning์ผ๋ก ๊ด๋ฆฌํด์, ์ ๊ฒ์ ์์ ์ ์ ์ด์ ๋ฃจํ๋ฅผ ์์ ํ ์ข
๋ฃํ๋ ๊ฑฐ์๋ค.
// ์ปดํฌ๋ํธ ๋ฐ๊นฅ์ ์ ์ธ โ ์ด๋์๋ ์ ๊ทผ ๊ฐ๋ฅ let gAnimId = null let gRunning = false function startGame() { // ์ ๊ฒ์ ์์ ์ ์ ์ด์ ๋ฃจํ ์์ ์ข ๋ฃ gRunning = false if (gAnimId) cancelAnimationFrame(gAnimId) gRunning = true function loop() { if (!gRunning) return // ์ด์ ๋ฃจํ๋ฉด ์ฆ์ ์ข ๋ฃ ... gAnimId = requestAnimationFrame(loop) } }
๐ด ์ค๋ฅ 2 โ ์ ํ๋ ๋๋๋ฐ ์บ๋ฆญํฐ๊ฐ ์ ์ฌ๋ผ๊ฐ
์ด๊ฑด ์ ๋ง ํฉ๋นํ ๋ฒ๊ทธ์๋ค. ๋ถ๋ช
์ ํ ๋ก์ง์ ๋ง๋๋ฐ ํ๋ฉด์์ ์บ๋ฆญํฐ๊ฐ ๊ผผ์ง๋ ์ ํ๋ ๊ฑฐ๋ค. ํ์ฐธ ๋ค์์ผ ์์ธ์ ์ฐพ์๋๋ฐ, drawPlayer ํจ์์์ ์บ๋ฆญํฐ Y ์์น๋ฅผ player.y๊ฐ ์๋ GROUND - ph๋ก ๊ณ ์ ํด์ ๊ทธ๋ฆฌ๊ณ ์์๋ค. ๋ฌผ๋ฆฌ ๊ณ์ฐ์ ๋๊ณ ์์๋๋ฐ ํ๋ฉด์ ๋ฐ์์ ์ ํ๊ณ ์์๋ ๊ฑฐ๋ค.
// โ ์๋ชป๋ ์ฝ๋ โ ํญ์ ๋ ์ ๋ถ์ด์ ๊ทธ๋ฆผ const y = GROUND - ph // โ ์์ โ ์ค์ ๋ฌผ๋ฆฌ ๊ณ์ฐ๋ player.y ์ฌ์ฉ const y = player.crouching ? GROUND - PH_CROUCH : player.y
๐ด ์ค๋ฅ 3 โ canvas ์จ๊ธฐ๋ฉด ๊ฒ์์ด ์์ ํ ๋ง๊ฐ์ง
์ฒ์์ ๊ฒ์์ค๋ฒ ํ๋ฉด์ผ๋ก ๋์ด๊ฐ ๋ canvas๋ฅผ display: none์ผ๋ก ์จ๊ฒผ๋ค. ๊ทธ๋ฌ๋๋ ๊ทธ ๋ค์์ ๋ค์ํ๊ธฐ๋ฅผ ๋๋ฌ๋ ์๋ฌด๊ฒ๋ ์ ๊ทธ๋ ค์ง๋ ๊ฑฐ๋ค. ์ด์ ๋ canvas๊ฐ DOM์์ ์ฌ๋ผ์ง๋ฉด useRef๋ก ์ก์ ์ฐธ์กฐ๊ฐ ๋๊ธฐ๊ธฐ ๋๋ฌธ์ด์๋ค. canvas๋ ํญ์ DOM์ ์์ด์ผ ํ๊ณ , ๊ทธ ์์ ์ค๋ฒ๋ ์ด๋ฅผ position: absolute๋ก ๋ฎ๋ ๋ฐฉ์์ผ๋ก ๋ฐ๊ฟจ๋ค.
๐ด ์ค๋ฅ 4 โ React StrictMode๊ฐ ๋ฃจํ๋ฅผ ๋ ๋ฒ ์คํ
๊ฐ๋ฐ ํ๊ฒฝ์์ React๋ StrictMode๋ผ๋ ๊ธฐ๋ฅ์ด ์ผ์ ธ์๋๋ฐ, ์ด๊ฒ useEffect๋ฅผ ์๋์ ์ผ๋ก ๋ ๋ฒ ์คํํ๋ค. ๊ทธ๋์ ๊ฒ์ ๋ฃจํ๊ฐ ๋ ๊ฐ ๋์๊ฐ๋ฉด์ ์ด๋ฒคํธ๊ฐ ์์ ํ ๊ผฌ์๋ค. main.jsx์์ StrictMode๋ฅผ ์ ๊ฑฐํด์ ํด๊ฒฐํ๋ค.
1. canvas๋ ์ ๋ DOM์์ ์จ๊ธฐ๊ฑฐ๋ ์ ๊ฑฐํ์ง ๋ง ๊ฒ
2. ๊ฒ์ ๋ฃจํ๋ ์ ์ญ ๋ณ์๋ก ๊ด๋ฆฌํด์ ์ค๋ณต ์คํ ๋ฐฉ์ง
3.
drawPlayer์์ ์ค์ player.y ์ฐ๋์ง ํ์ธ4. ๊ฒ์์์
StrictMode๋ ์ ๊ฑฐ
- ์๋ฃ ์์ ํ๋ฉด + ์กฐ์๋ฒ ์๋ด
- ์๋ฃ ๊ฒ์์ค๋ฒ ํ๋ฉด + ์ ์/์๊ฐ ํ์
- ์๋ฃ ๋๋ค์ ์ ๋ ฅ + FastAPI ์ ์ ์ ์ฅ
- ์๋ฃ ๋๋ค์ ์ค๋ณต ๋ฐฉ์ง
- ์๋ฃ ๋ฆฌ๋๋ณด๋ ํ๋ฉด + ๋ด ์์ ํ์
- ์๋ฃ ์ ํ/๋ค์ํ๊ธฐ ๋ฒ๊ทธ ์์
- ๋ค์ํธ Vercel + Render๋ก ๋ฐฐํฌํ๊ธฐ
๋ฒ๊ทธ ์ก๋ ๊ฒ ์ฝ๋ฉ์ ์ ๋ฐ์ด๋ค.
์ค๋ฅ๊ฐ ๋ฌ์ ๋ ํฌ๊ธฐํ์ง ์๊ณ ์์ธ์ ์ฐพ์๋๊ฐ๋ ๊ฒ ์ค๋ ฅ์ด ๋๋ ๋ฐฉ๋ฒ์ธ ๊ฒ ๊ฐ๋ค.
๋ค์ ํธ์์๋ ๋ง๋ ๊ฒ์์ ์ค์ ์๋ฒ์ ์ฌ๋ ค์ ๋๊ตฌ๋ ์ ์ํ ์ ์๊ฒ ๋ฐฐํฌํ๋ค.
'Python' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| Neon Runner ๋ชจ๋ฐ์ผ ๋ฒ์ ๋ง๋ค๊ธฐ #1 (0) | 2026.04.24 |
|---|---|
| React + FastAPI๋ก ์น ๊ฒ์ ๋ง๋ค๊ธฐ #4 - Render + Vercel ๋ฐฐํฌ (0) | 2026.04.24 |
| React + Python FastAPI๋ก์น ๊ฒ์ ๋ง๋ค๊ธฐ #2 (0) | 2026.04.23 |
| React + Python FastAPI๋ก์น ๊ฒ์ ๋ง๋ค๊ธฐ #1 (1) | 2026.04.22 |
| ๐ค๏ธ๋ ์จ ๋ฐ์ดํฐ(2) - ์ ์ฅํ๊ณ API ์๋ฒ๊น์ง ๋ง๋ค๊ธฐ (0) | 2026.03.25 |