React + FastAPI๋กœ ์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ #3 - ์™„์„ฑ!

React + Python FastAPI๋กœ
์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ #4

์‹œ์ž‘ ํ™”๋ฉด ยท ๊ฒŒ์ž„์˜ค๋ฒ„ ยท ๋‹‰๋„ค์ž„ ์ €์žฅ ยท FastAPI ์—ฐ๋™ ยท ๋ฆฌ๋”๋ณด๋“œ ์™„์„ฑ

1 ์™„์„ฑ๋œ ํ™”๋ฉด๋“ค ๋จผ์ € ๋ณด๊ธฐ

์ฝ”๋“œ ์–˜๊ธฐ ์ „์— ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ์ƒ๊ฒผ๋Š”์ง€ ๋จผ์ € ๋ณด์ž. ์ด 4๊ฐœ ํ™”๋ฉด์ด ์žˆ๋‹ค.

๐Ÿ“บ ์‹œ์ž‘ ํ™”๋ฉด
NEON RUNNER
SURVIVE ยท SCORE ยท DOMINATE
โฌ† / SPACE โ€” ์ ํ”„ (๋”๋ธ”์ ํ”„ ๊ฐ€๋Šฅ)
โฌ‡ / S โ€” ์—Ž๋“œ๋ฆฌ๊ธฐ
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
๐ŸŸฅ ๋นจ๊ฐ„ ๋ธ”๋ก โ€” ์ ํ”„๋กœ ํ”ผํ•˜๊ธฐ
๐ŸŸก ์ƒˆ / ๐ŸŸฃ ๋Œ๋งน์ด โ€” ์—Ž๋“œ๋ ค์„œ ํ”ผํ•˜๊ธฐ
โ–ถ GAME START
โ˜… ์ˆœ์œ„ํ‘œ
๐Ÿ“บ ๊ฒŒ์ž„ ํ™”๋ฉด
โฑ 12.4s โ˜… 124 โšก 4.2x
๐Ÿ“บ ๊ฒŒ์ž„์˜ค๋ฒ„ ํ™”๋ฉด
๐Ÿ’ฅ GAME OVER
SCORE
247
TIME
24.7s
๋‹‰๋„ค์ž„ ์ž…๋ ฅ ํ›„ ์ ์ˆ˜ ๋“ฑ๋ก
NICKNAME
โ˜… ์ ์ˆ˜ ๋“ฑ๋ก
โ–ถ ๋‹ค์‹œํ•˜๊ธฐ
๐Ÿ“บ ๋ฆฌ๋”๋ณด๋“œ ํ™”๋ฉด
โ˜… LEADERBOARD โ˜…
๐Ÿ† ๋‚ด ์ˆœ์œ„: #2
๐Ÿฅ‡
JISUN
580pt
58.0s
๐Ÿฅˆ
EUNKYU
247pt
24.7s
๐Ÿฅ‰
PLAYER3
180pt
18.0s
โ–ถ ๋‹ค์‹œํ•˜๊ธฐ
๐Ÿ”„ ์ƒˆ๋กœ๊ณ ์นจ

2 ์‹œ์ž‘ ํ™”๋ฉด ๋งŒ๋“ค๊ธฐ

์ฒ˜์Œ์—” ๊ฒŒ์ž„์ด ์ผœ์ง€์ž๋งˆ์ž ๋ฐ”๋กœ ์‹œ์ž‘๋๋‹ค. ๊ทผ๋ฐ ๊ทธ๋Ÿฌ๋ฉด ์กฐ์ž‘๋ฒ•๋„ ๋ชจ๋ฅด๊ณ  ๋ฐ”๋กœ ์ฃฝ์–ด๋ฒ„๋ฆฌ๋‹ˆ๊นŒ ์‹œ์ž‘ ํ™”๋ฉด์„ ๋”ฐ๋กœ ๋งŒ๋“ค์—ˆ๋‹ค. React์˜ useState๋กœ ํ˜„์žฌ ์–ด๋–ค ํ™”๋ฉด์ธ์ง€ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

๐Ÿ’ก
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๋Š” ์ ˆ๋Œ€ ์ˆจ๊ธฐ๋ฉด ์•ˆ ๋œ๋‹ค
ํ™”๋ฉด ์ „ํ™˜ํ•  ๋•Œ 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๊ฐœ ๊ฐ€์ ธ์˜ค๊ธฐ
React โ†’ FastAPI ์ ์ˆ˜ ์ „์†ก (fetch)
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')  // ๋ฆฌ๋”๋ณด๋“œ ํ™”๋ฉด์œผ๋กœ
}
FastAPI ์„œ๋ฒ„ โ€” ์ ์ˆ˜ ์ €์žฅ API (Python)
@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}
๐Ÿ’ก
fetch๊ฐ€ ๋ญ”๊ฐ€?
๋ธŒ๋ผ์šฐ์ €์—์„œ ์„œ๋ฒ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด๊ฑฐ๋‚˜ ๋ฐ›์•„์˜ค๋Š” 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()์ด ๋ญ”๊ฐ€?
๋ฐฐ์—ด์˜ ๊ฐ ํ•ญ๋ชฉ์„ ๋ณ€ํ™˜ํ•ด์„œ ์ƒˆ ๋ฐฐ์—ด์„ ๋งŒ๋“œ๋Š” ํ•จ์ˆ˜๋‹ค. ์„œ๋ฒ„์—์„œ ๋ฐ›์€ ์ ์ˆ˜ ๋ฐฐ์—ด์„ map()์œผ๋กœ ์ˆœํšŒํ•˜๋ฉด์„œ ๊ฐ๊ฐ์„ HTML ์š”์†Œ๋กœ ๋ณ€ํ™˜ํ•ด์„œ ํ™”๋ฉด์— ํ‘œ์‹œํ•œ๋‹ค.

6 ๋‹‰๋„ค์ž„ ์ค‘๋ณต ๋ฐฉ์ง€

๊ฐ™์€ ๋‹‰๋„ค์ž„์œผ๋กœ ์—ฌ๋Ÿฌ ๋ฒˆ ๋“ฑ๋กํ•˜๋ฉด ๋ฆฌ๋”๋ณด๋“œ๊ฐ€ ๋„๋ฐฐ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค. Python ์„œ๋ฒ„์—์„œ ์ค‘๋ณต ์ฒดํฌ๋ฅผ ํ•ด์„œ ์ด๋ฏธ ์žˆ๋Š” ๋‹‰๋„ค์ž„์ด๋ฉด ๋“ฑ๋ก์„ ๋ง‰๋„๋ก ํ–ˆ๋‹ค.

Python โ€” ๋‹‰๋„ค์ž„ ์ค‘๋ณต ์ฒดํฌ
# ์ €์žฅ๋œ ๋‹‰๋„ค์ž„ ์ค‘์— ๊ฐ™์€ ๊ฒŒ ์žˆ๋Š”์ง€ ํ™•์ธ
existing = [s for s in scores if s["nickname"] == entry.nickname.strip()]

if existing:
    return {"error": "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋‹‰๋„ค์ž„", "duplicate": True}

# ์ค‘๋ณต ์—†์œผ๋ฉด ์ €์žฅ ์ง„ํ–‰
React โ€” ์ค‘๋ณต์ด๋ฉด ์•Œ๋ฆผ ํ‘œ์‹œ
if (data.duplicate) {
  alert('โŒ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋‹‰๋„ค์ž„์ด์—์š”!\n๋‹ค๋ฅธ ๋‹‰๋„ค์ž„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.')
  return  // ๋“ฑ๋ก ์ค‘๋‹จ
}

7 ์ง„์งœ ํž˜๋“ค์—ˆ๋˜ ์˜ค๋ฅ˜๋“ค

์ด๋ฒˆ ํŒŒํŠธ์—์„œ ์ œ์ผ ๊ณ ์ƒํ•œ ๊ฒŒ ์˜ค๋ฅ˜๋“ค์ด์—ˆ๋‹ค. ํŠนํžˆ ์ ํ”„ ๊ด€๋ จ ๋ฒ„๊ทธ๋Š” ๋ฉฐ์น ์„ ์žก์•˜๋‹ค.

๐Ÿ”ด ์˜ค๋ฅ˜ 1 โ€” ๋‹ค์‹œํ•˜๊ธฐ ๋ˆ„๋ฅด๋ฉด ์ ํ”„๊ฐ€ ์•„์˜ˆ ์•ˆ ๋จ

๋‹ค์‹œํ•˜๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ๋งˆ๋‹ค ๊ฒŒ์ž„ ๋ฃจํ”„๊ฐ€ ์ƒˆ๋กœ ์‹œ์ž‘๋๋Š”๋ฐ, ์ด์ „ ๋ฃจํ”„๊ฐ€ ์™„์ „ํžˆ ์•ˆ ๋๋‚œ ์ฑ„๋กœ ์ƒˆ ๋ฃจํ”„๊ฐ€ ๋˜ ์‹œ์ž‘๋˜๋Š” ๊ฑฐ์˜€๋‹ค. keydown ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋„ ๊ณ„์† ์ค‘๋ณต์œผ๋กœ ์Œ“์—ฌ์„œ ๊ฒฐ๊ตญ ์ถฉ๋Œ์ด ๋‚ฌ๋‹ค. ํ•ด๊ฒฐ๋ฒ•์€ ๊ฒŒ์ž„ ๋ฃจํ”„ ID๋ฅผ ๋ชจ๋“ˆ ์ „์—ญ ๋ณ€์ˆ˜ gAnimId์™€ gRunning์œผ๋กœ ๊ด€๋ฆฌํ•ด์„œ, ์ƒˆ ๊ฒŒ์ž„ ์‹œ์ž‘ ์ „์— ์ด์ „ ๋ฃจํ”„๋ฅผ ์™„์ „ํžˆ ์ข…๋ฃŒํ•˜๋Š” ๊ฑฐ์˜€๋‹ค.

์˜ค๋ฅ˜ 1 ํ•ด๊ฒฐ โ€” ์ „์—ญ ๋ณ€์ˆ˜๋กœ ๋ฃจํ”„ ๊ด€๋ฆฌ
// ์ปดํฌ๋„ŒํŠธ ๋ฐ”๊นฅ์— ์„ ์–ธ โ€” ์–ด๋””์„œ๋“  ์ ‘๊ทผ ๊ฐ€๋Šฅ
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๋กœ ๊ณ ์ •ํ•ด์„œ ๊ทธ๋ฆฌ๊ณ  ์žˆ์—ˆ๋‹ค. ๋ฌผ๋ฆฌ ๊ณ„์‚ฐ์€ ๋˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ํ™”๋ฉด์— ๋ฐ˜์˜์„ ์•ˆ ํ•˜๊ณ  ์žˆ์—ˆ๋˜ ๊ฑฐ๋‹ค.

์˜ค๋ฅ˜ 2 ํ•ด๊ฒฐ
// โŒ ์ž˜๋ชป๋œ ์ฝ”๋“œ โ€” ํ•ญ์ƒ ๋•…์— ๋ถ™์–ด์„œ ๊ทธ๋ฆผ
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๋กœ ๋ฐฐํฌํ•˜๊ธฐ

๋ฒ„๊ทธ ์žก๋Š” ๊ฒŒ ์ฝ”๋”ฉ์˜ ์ ˆ๋ฐ˜์ด๋‹ค.

์˜ค๋ฅ˜๊ฐ€ ๋‚ฌ์„ ๋•Œ ํฌ๊ธฐํ•˜์ง€ ์•Š๊ณ  ์›์ธ์„ ์ฐพ์•„๋‚˜๊ฐ€๋Š” ๊ฒŒ ์‹ค๋ ฅ์ด ๋А๋Š” ๋ฐฉ๋ฒ•์ธ ๊ฒƒ ๊ฐ™๋‹ค.

๋‹ค์Œ ํŽธ์—์„œ๋Š” ๋งŒ๋“  ๊ฒŒ์ž„์„ ์‹ค์ œ ์„œ๋ฒ„์— ์˜ฌ๋ ค์„œ ๋ˆ„๊ตฌ๋‚˜ ์ ‘์†ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฐฐํฌํ•œ๋‹ค.

โ–ถ ๋‹ค์ŒํŽธ: Vercel + Render๋กœ ๋ฐฐํฌํ•˜๊ธฐ