Skip to content

Frontend Wiki

yummjin edited this page Apr 10, 2026 · 14 revisions

๐Ÿงญ Frontend Team Wiki

์ด ๋ฌธ์„œ๋Š” ํ”„๋ก ํŠธ์—”๋“œ ํŒ€์˜ ๊ธฐ์ˆ , ๊ทœ์น™, ๊ฒฐ์ •์‚ฌํ•ญ, ์šด์˜ ๋ฐฉ์‹์„ ๊ธฐ๋กํ•˜๊ณ  ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•œ ์œ„ํ‚ค์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ณ€๊ฒฝ ์‚ฌํ•ญ์€ ๋ฌธ์„œํ™” ๋ฐ ๋‚ ์งœ ๊ธฐ๋ก์„ ์›์น™์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

์ตœ์ดˆ ์ž‘์„ฑ: 2026-04-10


๐Ÿ“Œ 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

๐ŸŽฏ 1-1 ๋ชฉ์ 

  • CS ํ€ด์ฆˆ / ๋ฉด์ ‘ ์ค€๋น„ ํ”Œ๋žซํผ hellocs.site ์˜ ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ ๋ฐ ์œ ์ง€๋ณด์ˆ˜
  • ์›น๊ณผ ๋ชจ๋ฐ”์ผ(iOS/Android) ๋‘ ํ”Œ๋žซํผ ๋™์‹œ ์ง€์›์„ ๋‹จ์ผ ๋ชจ๋…ธ๋ ˆํฌ๋กœ ๊ด€๋ฆฌ

๐Ÿ‘ฅ 1-2 ๋Œ€์ƒ ์‚ฌ์šฉ์ž

๊ตฌ๋ถ„ ๋Œ€์ƒ
์ฃผ์š” ์‚ฌ์šฉ์ž CS ์ทจ์—… ์ค€๋น„ ์ค‘์ธ ๊ฐœ๋ฐœ์ž
๋ณด์กฐ ์‚ฌ์šฉ์ž ์ด๋ฏธ ์ทจ์—…ํ•œ ๋’ค ์ง€์‹ ์ ๊ฒ€์„ ์›ํ•˜๋Š” ๊ฐœ๋ฐœ์ž

โš™๏ธ 1-3 ์ฃผ์š” ๊ธฐ๋Šฅ

  • ํ€ด์ฆˆ ํ’€๊ธฐ: OX / ๊ฐ๊ด€์‹ / ๋‹จ๋‹ตํ˜• 3๊ฐ€์ง€ ์œ ํ˜•์˜ CS ํ€ด์ฆˆ (ํšŒ๋‹น 5๋ฌธํ•ญ)
  • ์Œ์„ฑ ๋‹ต๋ณ€ (Voice Quiz): ๋ชจ๋ฐ”์ผ ๋งˆ์ดํฌ ๋…น์Œ โ†’ STT ๋ณ€ํ™˜์œผ๋กœ ๋‹จ๋‹ตํ˜• ๋‹ต๋ณ€ ์ž…๋ ฅ
  • ์ฑ„์  ๋ฐ ํ”ผ๋“œ๋ฐฑ: ํ€ด์ฆˆ ์ œ์ถœ ํ›„ AI ์ฑ„์  + ์˜ค๋‹ต ์ƒ์„ธ ํ™•์ธ
  • ํ•™์Šต ์ŠคํŠธ๋ฆญ: ์—ฐ์† ํ•™์Šต์ผ ๋‹ฌ๋ ฅ ์‹œ๊ฐํ™”
  • ๋žญํ‚น: ์ „์ฒด ์‚ฌ์šฉ์ž ์ ์ˆ˜ ์ˆœ์œ„
  • ์˜จ๋ณด๋”ฉ: ์ด๋ฆ„ โ†’ ๊ด€์‹ฌ์‚ฌ โ†’ ํ€ด์ฆˆ ๋ ˆ๋ฒจ โ†’ ์™„๋ฃŒ 4๋‹จ๊ณ„
  • ์†Œ์…œ ๋กœ๊ทธ์ธ: ์นด์นด์˜ค OAuth

๐Ÿง‘โ€๐Ÿ’ป 2. ๊ธฐ์ˆ  ์Šคํƒ

๐Ÿงฉ 2-1 Web ๊ธฐ๋ณธ ์Šคํƒ

๊ตฌ๋ถ„ ๊ธฐ์ˆ  ๋ฒ„์ „
Framework React 19
Language TypeScript ~5.9
Build Tool Vite 8
Routing Stackflow (@stackflow/react) 1.x
Styling TailwindCSS + tailwind-merge + CVA 4.2
Client State Zustand 5
Server State TanStack React Query 5
HTTP Client Ky 1.x
Animation Motion (Framer Motion) 12
Package Manager pnpm 9.15

๐Ÿงฉ 2-2 Mobile ๊ธฐ๋ณธ ์Šคํƒ

๊ตฌ๋ถ„ ๊ธฐ์ˆ  ๋ฒ„์ „
Framework React Native (Expo) 54
Language TypeScript catalog ๊ณต์œ 
Navigation expo-router 6
WebView react-native-webview 13.15
Bridge @webview-bridge/react-native 1.7
์Œ์„ฑ ๋…น์Œ expo-av 16
Package Manager npm -

๐Ÿงฉ 2-3 ๊ณตํ†ต / ์ธํ”„๋ผ

๊ตฌ๋ถ„ ๊ธฐ์ˆ 
API ํƒ€์ž… ์ž๋™์ƒ์„ฑ openapi-typescript (OpenAPI โ†’ TypeScript)
CI/CD GitHub Actions (self-hosted runner)
์„œ๋น™ Docker + Nginx 1.27
๋ชจ๋‹ˆํ„ฐ๋ง Grafana (Nginx ํ”„๋ก์‹œ)
Lint ESLint 9 + typescript-eslint
Commit commitlint (์ปค์Šคํ…€ emoji ํƒ€์ž…)

๐Ÿ—๏ธ 3. ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๊ตฌ์กฐ

3-1 ๋ชจ๋…ธ๋ ˆํฌ ๊ตฌ์กฐ

Deadlock-Client/
โ”œโ”€โ”€ web/          # React 19 + Vite ์›น ์•ฑ (pnpm)
โ”œโ”€โ”€ mobile/       # React Native Expo ์•ฑ (npm)
โ””โ”€โ”€ CLAUDE.md

๋‘ ์•ฑ์€ ๋ณ„๋„ ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ €์™€ node_modules๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ํƒ€์ž… ๊ณต์œ ๋Š” ์—†๊ณ , ๋ธŒ๋ฆฟ์ง€ ์ธํ„ฐํŽ˜์ด์Šค(AppBridgeType)๋ฅผ ํ†ตํ•ด Web โ†” Native ๊ณ„์•ฝ์„ ๋งž์ถ˜๋‹ค.

3-2 Web ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ

web/src/
โ”œโ”€โ”€ app/            # Stackflow ์„ค์ •, ๋ผ์šฐํŠธ ์ •์˜, ์ธ์ฆ ์‰˜
โ”œโ”€โ”€ pages/          # ๊ฐ ๋ผ์šฐํŠธ Activity ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”œโ”€โ”€ home/
โ”‚   โ”œโ”€โ”€ login/
โ”‚   โ”œโ”€โ”€ onboarding/
โ”‚   โ”œโ”€โ”€ quiz/
โ”‚   โ”œโ”€โ”€ ranking/
โ”‚   โ”œโ”€โ”€ streak/
โ”‚   โ”œโ”€โ”€ user/
โ”‚   โ””โ”€โ”€ interview/
โ”œโ”€โ”€ components/     # UI ์ปดํฌ๋„ŒํŠธ (feature๋ณ„ + common/)
โ”œโ”€โ”€ model/          # Zustand ์Šคํ† ์–ด + ์ปค์Šคํ…€ ํ›…
โ”‚   โ”œโ”€โ”€ auth/       # useAuthStore
โ”‚   โ”œโ”€โ”€ quiz/       # useQuizStore, useQuizSolve, useVoiceQuiz
โ”‚   โ””โ”€โ”€ user/       # useUserStore
โ”œโ”€โ”€ api/            # API ๋ ˆ์ด์–ด
โ”‚   โ”œโ”€โ”€ config/     # ky ํด๋ผ์ด์–ธํŠธ, ์—”๋“œํฌ์ธํŠธ, ์ž๋™์ƒ์„ฑ ํƒ€์ž…
โ”‚   โ”œโ”€โ”€ auth/
โ”‚   โ”œโ”€โ”€ quiz/
โ”‚   โ”œโ”€โ”€ ranking/
โ”‚   โ”œโ”€โ”€ streak/
โ”‚   โ””โ”€โ”€ user/
โ””โ”€โ”€ assets/         # ์•„์ด์ฝ˜ SVG ์ปดํฌ๋„ŒํŠธ

3-3 Routing (Stackflow)

  • ๊ฐ ํŽ˜์ด์ง€ = Activity ๋‹จ์œ„. stackflow-route.tsx์—์„œ name / component / path ๋ฅผ ๋“ฑ๋ก.
  • URL ๋™๊ธฐํ™”: historySyncPlugin โ€” ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ์™€ Activity ์Šคํƒ ์ž๋™ ์—ฐ๋™.
  • ๊ธฐ๋ณธ fallback Activity: LoginPage
  • ์ง„์ž…์ : HomePage (์ธ์ฆ ์™„๋ฃŒ ํ›„)
  • ๋ชจ๋“  Activity๋Š” lazy() ๋กœ๋“œ๋กœ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์ ์šฉ.
// ์ƒˆ ํŽ˜์ด์ง€ ์ถ”๊ฐ€ ์˜ˆ์‹œ
{ name: "NewPage", component: lazy(() => import("@/pages/new/NewPage")), path: "/new" }

3-4 ์ƒํƒœ ๊ด€๋ฆฌ

์ƒํƒœ ์œ ํ˜• ๋„๊ตฌ ์œ„์น˜
์ธ์ฆ ํ† ํฐ Zustand (useAuthStore) model/auth/
ํ€ด์ฆˆ ํ’€์ด ํ๋ฆ„ (OXโ†’๊ฐ๊ด€์‹โ†’๋‹จ๋‹ตํ˜• ์ˆœ์„œ) Zustand (useQuizSolveStore) model/quiz/
์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ์บ์‹œ React Query api/*/api.query.ts
์œ ์ € ํ”„๋กœํ•„ Zustand (useUserStore) model/user/

3-5 API ํ†ต์‹  ๋ฐฉ์‹

api/config/
โ”œโ”€โ”€ api-client.ts         # ky ์ธ์Šคํ„ด์Šค (baseApiClient, authApiClient)
โ”œโ”€โ”€ api-client-handler.ts # ์‘๋‹ต ํŒŒ์‹ฑ ์œ ํ‹ธ
โ”œโ”€โ”€ api-client-method.ts  # GET/POST/PATCH/DELETE ๋ž˜ํผ
โ”œโ”€โ”€ api-endpoints.ts      # END_POINTS ์ƒ์ˆ˜
โ””โ”€โ”€ api-models.ts         # openapi-typescript ์ž๋™์ƒ์„ฑ ํƒ€์ž…
  • ์ธ์ฆ ํ๋ฆ„: beforeRequest ํ›…์—์„œ Authorization: Bearer <token> ์ฃผ์ž…
  • ํ† ํฐ ๊ฐฑ์‹ : 401 ์‘๋‹ต ์‹œ afterResponse ํ›…์—์„œ /auth/reissue ์ž๋™ ํ˜ธ์ถœ, ๋™์‹œ ์š”์ฒญ ์ค‘๋ณต ๋ฐฉ์ง€(Promise ๊ณต์œ )
  • ๊ธฐ๋Šฅ๋ณ„ ์ฟผ๋ฆฌ: api/*/api.query.ts ์—์„œ queryOptions / mutationOptions ๋กœ export

๐Ÿ”— 4. ๋ธŒ๋ฆฟ์ง€ / ์™ธ๋ถ€ ์—ฐ๋™ ๊ตฌ์กฐ

4-1 Web โ†” Native ๊ตฌ์กฐ

๋ชจ๋ฐ”์ผ ์•ฑ์€ WebView๋กœ ์›น ์•ฑ์„ ๋ž˜ํ•‘ํ•œ๋‹ค. ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ(๋งˆ์ดํฌ, ๋กœ๊ทธ์ธ ์ƒํƒœ)์€ @webview-bridge ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด Web์—์„œ ํ˜ธ์ถœํ•œ๋‹ค.

Web (React)                  Native (React Native)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
window.bridge.xxx()   โ”€โ”€โ”€โ”€โ”€โ–ถ  bridge/index.ts ๊ตฌํ˜„์ฒด ์‹คํ–‰
                      โ—€โ”€โ”€โ”€โ”€โ”€  Promise ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜

4-2 ํ˜„์žฌ ๋ธŒ๋ฆฟ์ง€ ์ธํ„ฐํŽ˜์ด์Šค

interface AppBridgeType extends Bridge {
  isLoggedIn: boolean;
  logout: () => Promise<void>;
  startRecording: () => Promise<{ status: "success" | "error"; errorMessage?: string }>;
  stopRecording: () => Promise<{ status: "success" | "error"; text?: string; errorMessage?: string }>;
}
๋ฉ”์„œ๋“œ ์„ค๋ช…
isLoggedIn ๋„ค์ดํ‹ฐ๋ธŒ์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๋กœ๊ทธ์ธ ์ƒํƒœ
logout() ๋„ค์ดํ‹ฐ๋ธŒ ์ธก ๋กœ๊ทธ์ธ ์ƒํƒœ ์ดˆ๊ธฐํ™”
startRecording() iOS/Android ๋งˆ์ดํฌ ๊ถŒํ•œ ์š”์ฒญ + ๋…น์Œ ์‹œ์ž‘ (expo-av)
stopRecording() ๋…น์Œ ์ข…๋ฃŒ + STT ๋ณ€ํ™˜ ํ›„ ํ…์ŠคํŠธ ๋ฐ˜ํ™˜

4-3 ์ƒˆ ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์ ˆ์ฐจ

  1. mobile/bridge/index.ts์˜ AppBridgeType ์ธํ„ฐํŽ˜์ด์Šค์— ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
  2. bridge() ๊ตฌํ˜„์ฒด ๋‚ด๋ถ€์— ๋กœ์ง ์ž‘์„ฑ
  3. ์›น์—์„œ window.bridge.newMethod() ํ˜ธ์ถœ

4-4 ๋”ฅ๋งํฌ / ๋„ค๋น„๊ฒŒ์ด์…˜ ์ฒ˜๋ฆฌ

  • webview/createHandleShouldStartLoad.ts: onShouldStartLoadWithRequest ํ•ธ๋“ค๋Ÿฌ
    • intent: ์Šคํ‚ด โ†’ browser_fallback_url ํŒŒ์‹ฑ ํ›„ window.location ์ด๋™
    • kakaotalk://, kakaokompass:// ๋“ฑ โ†’ Linking.openURL() ๋กœ ์•ฑ ์‹คํ–‰
    • kauth.kakao.com โ†’ WebView ๋‚ด ์ฒ˜๋ฆฌ (์นด์นด์˜ค OAuth ํ”Œ๋กœ์šฐ)
    • HTTP/HTTPS โ†’ WebView ์ฒ˜๋ฆฌ
    • ๊ทธ ์™ธ ์ปค์Šคํ…€ ์Šคํ‚ด โ†’ ๋„ค์ดํ‹ฐ๋ธŒ ์œ„์ž„

4-5 ์Œ์„ฑ ๋…น์Œ (STT) ํ๋ฆ„

Web: window.bridge.startRecording()
       โ†“
Native: ๋งˆ์ดํฌ ๊ถŒํ•œ ์š”์ฒญ โ†’ expo-av ๋…น์Œ ์‹œ์ž‘
       โ†“ (์‚ฌ์šฉ์ž๊ฐ€ ์™„๋ฃŒ ์‹œ)
Web: window.bridge.stopRecording()
       โ†“
Native: ๋…น์Œ ์ข…๋ฃŒ โ†’ STT ์„œ๋ฒ„ ์ „์†ก โ†’ ํ…์ŠคํŠธ ๋ฐ˜ํ™˜
       โ†“
Web: ๋ฐ˜ํ™˜๋œ ํ…์ŠคํŠธ๋ฅผ ๋‹จ๋‹ตํ˜• ์ž…๋ ฅ๊ฐ’์œผ๋กœ ์‚ฌ์šฉ

โš ๏ธ ํ˜„์žฌ STT ์„œ๋ฒ„ ์ „์†ก ๋กœ์ง์€ ๋ฏธ๊ตฌํ˜„ (mock ํ…์ŠคํŠธ ๋ฐ˜ํ™˜ ์ค‘). bridge/audioRecorder.ts์˜ sendAudioToServer ํ•จ์ˆ˜ ๊ตฌํ˜„ ํ•„์š”.


๐Ÿ“ 5. ์ฝ”๋“œ ๊ทœ์น™ ๋ฐ ์ปจ๋ฒค์…˜

5-1 ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€

emoji [type] ํ•œ๊ธ€ ์„ค๋ช…
type ์‚ฌ์šฉ ์ƒํ™ฉ
feat ์‹ ๊ทœ ๊ธฐ๋Šฅ
fix ๋ฒ„๊ทธ ์ˆ˜์ •
refactor ๋ฆฌํŒฉํ† ๋ง
chore ๋นŒ๋“œ/์„ค์ • ๋ณ€๊ฒฝ
docs ๋ฌธ์„œ
style ์Šคํƒ€์ผ(์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์Œ)
test ํ…Œ์ŠคํŠธ
perf ์„ฑ๋Šฅ ๊ฐœ์„ 
rename ํŒŒ์ผ/๋ณ€์ˆ˜๋ช… ๋ณ€๊ฒฝ
remove ์ฝ”๋“œ/ํŒŒ์ผ ์‚ญ์ œ

์˜ˆ์‹œ: โœจ [feat] ํ€ด์ฆˆ ํƒ€์ด๋จธ ์ถ”๊ฐ€

5-2 ํŒŒ์ผ / ์ปดํฌ๋„ŒํŠธ ๋„ค์ด๋ฐ

๋Œ€์ƒ ๊ทœ์น™ ์˜ˆ์‹œ
์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ PascalCase QuizSolvePage.tsx
ํ›… ํŒŒ์ผ camelCase, use ์ ‘๋‘์‚ฌ useQuizStore.ts
API ํŒŒ์ผ camelCase, ๋™์‚ฌ+๋ช…์‚ฌ postQuizList.ts
์ƒ์ˆ˜ UPPER_SNAKE_CASE QUIZ_SOLVE_TOTAL_COUNT
๊ฒฝ๋กœ alias @/* โ†’ src/* @/components/common/Button

5-3 ๋””๋ ‰ํ† ๋ฆฌ ๊ทœ์น™

  • pages: Stackflow Activity๋งŒ ๋ฐฐ์น˜. ํŽ˜์ด์ง€ ๋กœ์ง์€ model/ ๋˜๋Š” components/๋กœ ๋ถ„๋ฆฌ.
  • components: feature๋ช… ํด๋” ํ•˜์œ„์— ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜. ์žฌ์‚ฌ์šฉ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ๋Š” common/.
  • model: Zustand store + ๊ด€๋ จ ์ปค์Šคํ…€ ํ›…์„ feature๋ณ„๋กœ ๋ฌถ์Œ.
  • api: api.model.ts (์‘๋‹ต ํƒ€์ž…) / api.query.ts (queryOptions) / ๊ฐœ๋ณ„ fetch ํ•จ์ˆ˜ ํŒŒ์ผ๋กœ ๊ตฌ๋ถ„.

5-4 ์Šคํƒ€์ผ๋ง ๊ทœ์น™

  • ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค: TailwindCSS 4.2
  • ํด๋ž˜์Šค ๋ณ‘ํ•ฉ: cn() ์œ ํ‹ธ (tailwind-merge + clsx)
  • ์ปดํฌ๋„ŒํŠธ variants: CVA (class-variance-authority) ์‚ฌ์šฉ
// CVA ์˜ˆ์‹œ
const buttonVariant = cva("base-class", {
  variants: { size: { large: "...", medium: "..." } },
  defaultVariants: { size: "large" },
});

5-5 ESLint Import ์ˆœ์„œ (๊ฐ•์ œ)

@/app โ†’ @/pages โ†’ @/components โ†’ @/api โ†’ @/model โ†’ ๋‚˜๋จธ์ง€

5-6 ์ƒํƒœ ๊ด€๋ฆฌ ๊ทœ์น™

  • ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ(๋ชฉ๋ก, ์ƒ์„ธ): React Query
  • UI ํ๋ฆ„ ์ƒํƒœ(ํ€ด์ฆˆ ๋‹จ๊ณ„, ์„ ํƒ๊ฐ’): Zustand
  • ์ „์—ญ ์ธ์ฆ ํ† ํฐ: Zustand (useAuthStore)
  • Zustand store๋Š” model/<feature>/ ํ•˜์œ„์— ์œ„์น˜

โš ๏ธ 6. ๊ธฐ์ˆ ์  ์˜์‚ฌ๊ฒฐ์ • (ADR)

ADR-001: Stackflow ์ฑ„ํƒ (vs React Router / TanStack Router)

ํ•ญ๋ชฉ ๋‚ด์šฉ
๊ฒฐ์ • Stackflow ์‚ฌ์šฉ
์ด์œ  ๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐ™์€ Activity ์Šคํƒ ๊ธฐ๋ฐ˜ ๋„ค๋น„๊ฒŒ์ด์…˜ UX ๊ตฌํ˜„ (push/pop ์Šฌ๋ผ์ด๋“œ ํŠธ๋žœ์ง€์…˜), WebView ๋‚ด์—์„œ๋„ ๋„ค์ดํ‹ฐ๋ธŒ ๋А๋‚Œ์˜ ํ™”๋ฉด ์ „ํ™˜ ํ•„์š”
๋Œ€์•ˆ React Router v7, TanStack Router
ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ ์ƒํƒœ๊ณ„๊ฐ€ ์ž‘๊ณ  ๋ ˆํผ๋Ÿฐ์Šค ๋ถ€์กฑ. URL ๋™๊ธฐํ™”๋Š” historySyncPlugin์œผ๋กœ ๋ณ„๋„ ๊ตฌ์„ฑ ํ•„์š”

ADR-002: Ky HTTP ํด๋ผ์ด์–ธํŠธ ์ฑ„ํƒ (vs fetch / axios)

ํ•ญ๋ชฉ ๋‚ด์šฉ
๊ฒฐ์ • Ky ์‚ฌ์šฉ
์ด์œ  fetch ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์†Œํ™”, ํ›…(beforeRequest / afterResponse)์œผ๋กœ ํ† ํฐ ์ฃผ์ž…/๊ฐฑ์‹  ์ฒ˜๋ฆฌ๊ฐ€ ๊น”๋”ํ•จ
๋Œ€์•ˆ axios, ์ˆœ์ˆ˜ fetch
ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ axios ๋Œ€๋น„ ์ธํ„ฐ์…‰ํ„ฐ API๊ฐ€ ๋‹ฌ๋ผ ํŒ€ ๋Ÿฌ๋‹ ์ปค๋ธŒ ์กด์žฌ

ADR-003: @webview-bridge ์ฑ„ํƒ (vs postMessage ์ง์ ‘ ๊ตฌํ˜„)

ํ•ญ๋ชฉ ๋‚ด์šฉ
๊ฒฐ์ • @webview-bridge ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
์ด์œ  postMessage ๊ธฐ๋ฐ˜ ์ˆ˜๋™ ๊ตฌํ˜„ ๋Œ€๋น„ TypeScript ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด, Promise ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ํ˜ธ์ถœ ์ž๋™ ์ฒ˜๋ฆฌ
๋Œ€์•ˆ window.ReactNativeWebView.postMessage ์ง์ ‘ ๊ตฌํ˜„
ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์˜์กด์„ฑ ์ถ”๊ฐ€, ๋‚ด๋ถ€ ์ง๋ ฌํ™” ๋ฐฉ์‹์— ์ข…์†

ADR-004: OpenAPI TypeScript ์ž๋™ ์ƒ์„ฑ

ํ•ญ๋ชฉ ๋‚ด์šฉ
๊ฒฐ์ • openapi-typescript๋กœ ๋ฐฑ์—”๋“œ OpenAPI spec โ†’ TypeScript ํƒ€์ž… ์ž๋™ ์ƒ์„ฑ
์ด์œ  ์ˆ˜๋™ ํƒ€์ž… ๊ด€๋ฆฌ ์‹œ API ๋ณ€๊ฒฝ ๋•Œ๋งˆ๋‹ค ๋ˆ„๋ฝ ์œ„ํ—˜, ๋ฐฑ์—”๋“œ ์ŠคํŽ™๊ณผ ํ•ญ์ƒ ๋™๊ธฐํ™”
๋ช…๋ น์–ด pnpm generate:api-models
์ถœ๋ ฅ src/api/config/api-models.ts

๐Ÿ—‚๏ธ 7. ์ฐธ๊ณ  ์ž๋ฃŒ

๊ณต์‹ ๋ฌธ์„œ

๊ธฐ์ˆ  ๋ฌธ์„œ
Stackflow https://stackflow.so
TanStack Query https://tanstack.com/query/latest
Zustand https://zustand.docs.pmnd.rs
Ky https://github.com/sindresorhus/ky
TailwindCSS 4 https://tailwindcss.com/docs
CVA https://cva.style
@webview-bridge https://github.com/gronxb/webview-bridge
openapi-typescript https://openapi-ts.dev
Expo https://docs.expo.dev

์„œ๋น„์Šค URL

ํ™˜๊ฒฝ URL
ํ”„๋กœ๋•์…˜ ์›น https://hellocs.site
API https://api.hellocs.site
API Docs (Swagger) https://api.hellocs.site/v3/api-docs
Grafana https://hellocs.site/grafana/

โœจ 8. ์‚ฌ์šฉ ํŒ ๋ฐ ๊ทœ์น™

8-1 ๊ฐœ๋ฐœ ์‹œ์ž‘

# Web
cd web && pnpm install && pnpm dev        # http://localhost:5173

# Mobile
cd mobile && npm install && npm start     # Expo dev server

8-2 API ํƒ€์ž… ์žฌ์ƒ์„ฑ

๋ฐฑ์—”๋“œ API๊ฐ€ ๋ณ€๊ฒฝ๋์„ ๋•Œ ์‹คํ–‰:

cd web && pnpm generate:api-models

.env์— API_SWAGGER_URL ์„ค์ • ํ•„์š”.

8-3 ์ƒˆ ํŽ˜์ด์ง€ ์ถ”๊ฐ€ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • src/pages/<feature>/<PageName>.tsx ์ƒ์„ฑ
  • src/app/stackflow-route.tsx์— { name, component, path } ๋“ฑ๋ก
  • ํ•„์š” ์‹œ src/api/<feature>/, src/model/<feature>/ ์ถ”๊ฐ€

8-4 ์ƒˆ ๋ธŒ๋ฆฟ์ง€ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • mobile/bridge/index.ts์˜ AppBridgeType์— ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ถ”๊ฐ€
  • bridge() ๊ตฌํ˜„์ฒด์— ์‹ค์ œ ๋กœ์ง ์ž‘์„ฑ
  • ์›น์—์„œ window.bridge.xxx() ํ˜ธ์ถœ๋กœ ๊ฒ€์ฆ

8-5 ํ˜‘์—… ์ฃผ์˜์‚ฌํ•ญ

  • api-models.ts๋Š” ์ž๋™์ƒ์„ฑ ํŒŒ์ผ โ€” ์ง์ ‘ ์ˆ˜์ • ๊ธˆ์ง€, pnpm generate:api-models๋กœ๋งŒ ๊ฐฑ์‹ 
  • ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ํ˜•์‹ ๋ฏธ์ค€์ˆ˜ ์‹œ commitlint ํ›…์ด ๊ฑฐ๋ถ€ํ•จ
  • main ๋ธŒ๋žœ์น˜ push ์‹œ GitHub Actions๊ฐ€ ์ž๋™ ๋นŒ๋“œ + ๋ฐฐํฌ ํŠธ๋ฆฌ๊ฑฐ๋จ (web ๊ฒฝ๋กœ ๋ณ€๊ฒฝ ์‹œ)
  • ESLint import ์ˆœ์„œ ์œ„๋ฐ˜ ์‹œ CI์—์„œ ์‹คํŒจ โ€” ๋กœ์ปฌ์—์„œ pnpm lint ๋จผ์ € ํ™•์ธ

8-6 Vite ๊ฐœ๋ฐœ ํ”„๋ก์‹œ

๋กœ์ปฌ ๊ฐœ๋ฐœ ์‹œ ์•„๋ž˜ ๊ฒฝ๋กœ๋Š” https://hellocs.site๋กœ ํ”„๋ก์‹œ๋จ:

  • /api/
  • /v3/
  • /swagger-ui/

๐Ÿ“š 9. ๊ธฐ์ˆ  ์‹ฌํ™” / ๋…ผ๋ฌธ ์ฐธ๊ณ  ๋‚ด์šฉ

์ด ์„น์…˜์€ ํ”„๋กœ์ ํŠธ์—์„œ ์ ์šฉํ•œ ๊ธฐ์ˆ  ๊ฐœ๋…์„ ํ•™์ˆ ์  ๊ด€์ ์—์„œ ๊ธฐ์ˆ ํ•œ๋‹ค. ์‹ค์ œ ๊ตฌํ˜„ ํŒŒ์ผ๊ณผ ์—ฐ๊ฒฐํ•ด ์ด๋ก ๊ณผ ์‹ค์ฒœ์˜ ์—ฐ๊ฒฐ๊ณ ๋ฆฌ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.


9-1 WebView ๊ธฐ๋ฐ˜ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ ์•„ํ‚คํ…์ฒ˜

๊ฐœ๋…

๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ๊ณผ ์›น ์•ฑ์˜ ์ด๋ถ„๋ฒ• ๋Œ€์‹ , WebView๋ฅผ ์‰˜(Shell)๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์›น ์•ฑ์ด UI ์ „์ฒด๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•„ํ‚คํ…์ฒ˜. ๋‹จ์ผ ์ฝ”๋“œ๋ฒ ์ด์Šค๋กœ iOS/Android๋ฅผ ๋™์‹œ ์ง€์›ํ•˜๋ฉด์„œ, ์›น์˜ ๋น ๋ฅธ ๋ฐฐํฌ ์ฃผ๊ธฐ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌํ˜„

[React Native Shell]
  โ””โ”€ LinearGradient ๋ฐฐ๊ฒฝ
  โ””โ”€ SafeAreaView
  โ””โ”€ <WebView source={{ uri: WEBVIEW_URL }} />  โ† ์›น ์•ฑ ์ „์ฒด ๋ Œ๋”๋ง
       โ””โ”€ @webview-bridge๋กœ Native API ๋…ธ์ถœ
  • ๋„ค์ดํ‹ฐ๋ธŒ ์—ญํ• : ๋งˆ์ดํฌ, ๋กœ๊ทธ์ธ ์ƒํƒœ, ๋”ฅ๋งํฌ, ์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด
  • ์›น ์—ญํ• : ์ „์ฒด UI/UX, ๋ผ์šฐํŒ…, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง

ํ•™์ˆ ์  ๋งฅ๋ฝ

์ ‘๊ทผ ๋ฐฉ์‹ ์˜ˆ์‹œ ํŠน์ง•
๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ Swift, Kotlin ์ตœ๊ณ  ์„ฑ๋Šฅ, ํ”Œ๋žซํผ๋ณ„ ๊ฐœ๋ฐœ ๋น„์šฉ
ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋„ค์ดํ‹ฐ๋ธŒ React Native, Flutter ๋‹จ์ผ ์ฝ”๋“œ, ๋„ค์ดํ‹ฐ๋ธŒ ๋ Œ๋”๋ง
WebView ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ด ํ”„๋กœ์ ํŠธ ์›น ๋ฐฐํฌ ์†๋„ + ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ
PWA Service Worker ๊ธฐ๋ฐ˜ ์„ค์น˜ ์—†์ด ์•ฑ ์œ ์‚ฌ ๊ฒฝํ—˜

๊ด€๋ จ ํ‚ค์›Œ๋“œ: Hybrid Mobile Application Architecture, WebView Bridge Pattern, Cross-Platform Development


9-2 JavaScript Bridge ํ†ต์‹  ํŒจํ„ด๊ณผ ํƒ€์ž… ์•ˆ์ „์„ฑ

๋ฌธ์ œ

WebView์™€ Native ์‚ฌ์ด์˜ ํ†ต์‹ ์€ ์ „ํ†ต์ ์œผ๋กœ postMessage(string) โ€” ๋‹จ๋ฐฉํ–ฅ ๋ฌธ์ž์—ด ๋ฉ”์‹œ์ง€๋กœ ์ด๋ฃจ์–ด์กŒ๋‹ค. ์ด ๋ฐฉ์‹์€:

  • ์‘๋‹ต์„ ์ถ”์ ํ•  ์ˆ˜๋‹จ์ด ์—†์Œ (Fire-and-forget)
  • ํƒ€์ž… ์ •๋ณด ์†Œ์‹ค (์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”)
  • ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ถˆ๋ช…ํ™•

์ด ํ”„๋กœ์ ํŠธ์˜ ํ•ด๊ฒฐ์ฑ… (@webview-bridge)

์š”์ฒญ ํ๋ฆ„:
Web: window.bridge.startRecording()
  โ†’ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๊ณ ์œ  messageId ๋ถ€์—ฌ + JSON ์ง๋ ฌํ™” โ†’ postMessage
  โ†’ Native: messageId๋กœ ์‘๋‹ต ๋งคํ•‘ โ†’ Promise resolve

ํƒ€์ž… ๊ณ„์•ฝ:
interface AppBridgeType extends Bridge {
  startRecording: () => Promise<{ status: "success" | "error" }>
}
// Web๊ณผ Native๊ฐ€ ๋™์ผ ์ธํ„ฐํŽ˜์ด์Šค ํƒ€์ž…์„ ๊ณต์œ 

ํ•™์ˆ ์  ๋งฅ๋ฝ

  • RPC (Remote Procedure Call) ๊ฐœ๋…์˜ WebView ๋‚ด ์ ์šฉ: ๋กœ์ปฌ ํ•จ์ˆ˜ ํ˜ธ์ถœ์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ ์ง๋ ฌํ™” โ†’ ์ฑ„๋„ ์ „์†ก โ†’ ์—ญ์ง๋ ฌํ™” ๊ณผ์ •์„ ๊ฑฐ์นจ
  • ํƒ€์ž… ์•ˆ์ „ IPC(Inter-Process Communication): ์ปดํŒŒ์ผ ํƒ€์ž„์— ์ธํ„ฐํŽ˜์ด์Šค ๋ถˆ์ผ์น˜๋ฅผ ๊ฐ์ง€
  • Promise ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋กœ ์ฝœ๋ฐฑ ์ง€์˜ฅ(Callback Hell) ๋ฌธ์ œ ํ•ด๊ฒฐ

๊ด€๋ จ ํ‚ค์›Œ๋“œ: JavaScript Bridge, Type-Safe IPC, WebView Communication, RPC Pattern


9-3 ์œ ํ•œ ์ƒํƒœ ๋จธ์‹ (FSM)์œผ๋กœ ๋ชจ๋ธ๋งํ•œ ํ€ด์ฆˆ ํ’€์ด ํ๋ฆ„

๊ฐœ๋…

FSM(Finite State Machine): ์œ ํ•œํ•œ ์ƒํƒœ(State)์™€ ์ƒํƒœ ๊ฐ„ ์ „์ด(Transition)๋กœ ์‹œ์Šคํ…œ์„ ๋ชจ๋ธ๋งํ•˜๋Š” ๊ณ„์‚ฐ ์ด๋ก ์˜ ๊ธฐ๋ณธ ๊ฐœ๋….

์ด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌํ˜„

ํ€ด์ฆˆ ํ’€์ด ๋‹จ๊ณ„๋Š” ๋ช…์‹œ์  FSM์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค (useQuizStore.ts):

States: "ox" | "select" | "text"

Transitions:
  ์ดˆ๊ธฐํ™” โ†’ ox (OX ๋ฌธ์ œ ์žˆ์œผ๋ฉด)
         โ†’ select (๊ฐ๊ด€์‹๋งŒ ์žˆ์œผ๋ฉด)
         โ†’ text (๋‹จ๋‹ตํ˜•๋งŒ ์žˆ์œผ๋ฉด)

  ox โ†’ ox     (๋‹ค์Œ OX ๋ฌธ์ œ)
  ox โ†’ select (OX ์™„๋ฃŒ, ๊ฐ๊ด€์‹ ์žˆ์œผ๋ฉด)
  ox โ†’ text   (OX ์™„๋ฃŒ, ๊ฐ๊ด€์‹ ์—†๊ณ  ๋‹จ๋‹ตํ˜• ์žˆ์œผ๋ฉด)
  ox โ†’ [์™„๋ฃŒ] (OX ์™„๋ฃŒ, ์ดํ›„ ๋ฌธ์ œ ์—†์œผ๋ฉด completionPayload ์„ธํŒ…)

  select โ†’ select (๋‹ค์Œ ๊ฐ๊ด€์‹)
  select โ†’ text   (๊ฐ๊ด€์‹ ์™„๋ฃŒ, ๋‹จ๋‹ตํ˜• ์žˆ์œผ๋ฉด)
  select โ†’ [์™„๋ฃŒ]

  text โ†’ text   (๋‹ค์Œ ๋‹จ๋‹ตํ˜•)
  text โ†’ [์™„๋ฃŒ]
  • ์—ญ๋ฐฉํ–ฅ ์ „์ด(goToPreviousQuestion)๋„ ๊ตฌํ˜„: ์Šคํƒ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด์ „ ์ƒํƒœ ๋ณต์›
  • UI ์ปดํฌ๋„ŒํŠธ๋Š” ํ˜„์žฌ state๋งŒ ๋ฐ›์•„ ๋ Œ๋”๋ง โ†’ ์ƒํƒœ์™€ ๋ทฐ์˜ ๋ช…ํ™•ํ•œ ๋ถ„๋ฆฌ

ํ•™์ˆ ์  ๋งฅ๋ฝ

  • UI ๊ฐœ๋ฐœ์—์„œ FSM ๋ชจ๋ธ๋ง์€ XState ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ๋„ ๋„๋ฆฌ ์“ฐ์ž„. ์ด ํ”„๋กœ์ ํŠธ๋Š” Zustand ๋‚ด์— ์ง์ ‘ FSM์„ ๊ตฌํ˜„ํ•œ ์‚ฌ๋ก€
  • **๋ช…์‹œ์  ์ƒํƒœ(Explicit State)**๋Š” "impossible state"๋ฅผ ํƒ€์ž… ์‹œ์Šคํ…œ์œผ๋กœ ๋ฐฉ์ง€ (vs. ์—ฌ๋Ÿฌ boolean ์กฐํ•ฉ)
  • React์˜ useReducer๋„ FSM ๊ตฌํ˜„ ์ˆ˜๋‹จ์œผ๋กœ ์ž์ฃผ ์ธ์šฉ๋จ

๊ด€๋ จ ํ‚ค์›Œ๋“œ: Finite State Machine, UI State Modeling, Explicit State, XState, Impossible States


9-4 Result ํƒ€์ž… ํŒจํ„ด: ํƒ€์ž… ์•ˆ์ „ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

๋ฌธ์ œ

try/catch ๊ธฐ๋ฐ˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜์—์„œ ์‹คํŒจ ๊ฐ€๋Šฅ์„ฑ์ด ๋“œ๋Ÿฌ๋‚˜์ง€ ์•Š๋Š”๋‹ค. ํ˜ธ์ถœ์ž๊ฐ€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ์žŠ์–ด๋„ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๊ฐ์ง€ํ•˜์ง€ ๋ชปํ•œ๋‹ค.

์ด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌํ˜„ (api-client-handler.ts)

type Ok<T> = { ok: true;  data: T; status: number; headers: Headers }
type Err   = { ok: false; error: { message: string }; status: number }

export type ApiResult<T> = Ok<T> | Err

ํ˜ธ์ถœ๋ถ€์—์„œ ok ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ธฐ ์ „๊นŒ์ง€ data์— ์ ‘๊ทผ ๋ถˆ๊ฐ€:

const res = await postQuizList(params);
if (!res.ok) throw new Error("ํ€ด์ฆˆ ์กฐํšŒ ์‹คํŒจ");
return res.data; // ์—ฌ๊ธฐ์„œ๋งŒ data ํƒ€์ž… ๋ณด์žฅ

ํ•™์ˆ ์  ๋งฅ๋ฝ

  • Rust์˜ Result<T, E> ํƒ€์ž…, Haskell์˜ Either ๋ชจ๋‚˜๋“œ์—์„œ ์˜๊ฐ
  • TypeScript์˜ Discriminated Union(ํŒ๋ณ„ ์œ ๋‹ˆ์˜จ) ํŒจํ„ด์˜ ์‹ค๋ฌด ์ ์šฉ ์‚ฌ๋ก€
  • Railway-Oriented Programming: ์„ฑ๊ณต/์‹คํŒจ ๋‘ ๋ ˆ์ผ๋กœ ๋กœ์ง์„ ํ˜๋ ค๋ณด๋‚ด๋Š” ํ•จ์ˆ˜ํ˜• ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ธฐ๋ฒ•

๊ด€๋ จ ํ‚ค์›Œ๋“œ: Result Type, Discriminated Union, Railway-Oriented Programming, Type-Safe Error Handling


9-5 ์„ ์–ธ์  ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ์บ์‹œ ์ „๋žต

๋ฌธ์ œ

์ „ํ†ต์  ๋ฐฉ์‹(Redux + ์ˆ˜๋™ fetch)์—์„œ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉด:

  • ๋กœ๋”ฉ/์—๋Ÿฌ/์„ฑ๊ณต ์ƒํƒœ๋ฅผ ๋งค๋ฒˆ ์ง์ ‘ ๊ด€๋ฆฌ
  • ์บ์‹œ ๋ฌดํšจํ™” ํƒ€์ด๋ฐ ์ง์ ‘ ์ง€์ •
  • ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€ ๋กœ์ง ๋ฐ˜๋ณต ์ž‘์„ฑ

์ด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌํ˜„ (React Query)

// ์„ ์–ธ์  ์ฟผ๋ฆฌ ์ •์˜
export const quizQueries = {
  getQuizTopicQuery: () =>
    queryOptions({
      queryKey: ["quiz", "topic"],
      queryFn: async () => { /* fetch */ },
    }),
};

// ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ์บ์‹œ/๋กœ๋”ฉ/์—๋Ÿฌ๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ
const { data, isLoading, isError } = useQuery(quizQueries.getQuizTopicQuery());

์บ์‹œ ํ‚ค ๊ณ„์ธต ๊ตฌ์กฐ:

["quiz"]
  โ””โ”€ ["quiz", "topic"]
  โ””โ”€ ["quiz", "list", { topicId, count }]
  โ””โ”€ ["quiz", "grading-log", gradingLogId]

ํ•™์ˆ ์  ๋งฅ๋ฝ

  • Stale-While-Revalidate(SWR): ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•˜๋ฉด์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฐฑ์‹  โ€” HTTP RFC 5861์—์„œ ์ •์˜, React Query๊ฐ€ ์ด ์ „๋žต์„ UI์— ์ ์šฉ
  • ์„œ๋ฒ„ ์ƒํƒœ(Server State) vs ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ(Client State) ๋ถ„๋ฆฌ: ์„œ๋ฒ„ ์ƒํƒœ๋Š” ๋น„๋™๊ธฐ์ ์ด๊ณ  ๊ณต์œ ๋จ โ†’ ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๊ด€๋ฆฌ
  • React Query์˜ ์บ์‹œ๋Š” Normalized Cache ๋ฐฉ์‹์ด ์•„๋‹Œ Key-Value ๋ฐฉ์‹ (Apollo์™€ ์ฐจ์ด์ )

๊ด€๋ จ ํ‚ค์›Œ๋“œ: Server State Management, Stale-While-Revalidate, Cache Invalidation, Declarative Data Fetching


9-6 ์ธ์ฆ ํ† ํฐ ๊ฐฑ์‹ ๊ณผ ๊ฒฝ์Ÿ ์กฐ๊ฑด(Race Condition) ๋ฐฉ์ง€

๋ฌธ์ œ

SPA์—์„œ Access Token ๋งŒ๋ฃŒ ์‹œ ์—ฌ๋Ÿฌ API ์š”์ฒญ์ด ๋™์‹œ์— 401์„ ๋ฐ›์œผ๋ฉด, Refresh Token์œผ๋กœ ํ† ํฐ ๊ฐฑ์‹  ์š”์ฒญ์ด N๋ฒˆ ์ค‘๋ณต ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค โ†’ Refresh Token์ด ๋‹จ ํ•œ ๋ฒˆ๋งŒ ์œ ํšจํ•œ ๊ฒฝ์šฐ ๋‚˜๋จธ์ง€ ์š”์ฒญ ์‹คํŒจ.

์ด ํ”„๋กœ์ ํŠธ์˜ ํ•ด๊ฒฐ์ฑ… (api-client.ts)

// Promise๋ฅผ ๋ณ€์ˆ˜์— ๊ณต์œ  โ†’ ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€
let refreshAccessTokenPromise: Promise<string | null> | null = null;

async function refreshAccessToken() {
  if (!refreshAccessTokenPromise) {
    refreshAccessTokenPromise = (async () => {
      // ์‹ค์ œ ๊ฐฑ์‹  ์š”์ฒญ (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰)
      const result = await apiClientHandler(baseApiClient, END_POINTS.AUTH.REISSUE, { method: "POST" });
      // ...
    })().finally(() => {
      refreshAccessTokenPromise = null; // ์™„๋ฃŒ ํ›„ ์ดˆ๊ธฐํ™”
    });
  }
  return refreshAccessTokenPromise; // ์ง„ํ–‰ ์ค‘์ด๋ฉด ๊ฐ™์€ Promise ๋ฐ˜ํ™˜
}

๋™์‹œ์— 5๊ฐœ ์š”์ฒญ์ด 401์„ ๋ฐ›์•„๋„ ๊ฐฑ์‹  ์š”์ฒญ์€ ์ •ํ™•ํžˆ 1ํšŒ๋งŒ ๋ฐœ์ƒํ•˜๊ณ , ๋‚˜๋จธ์ง€ 4๊ฐœ๋Š” ๊ฐ™์€ Promise๋ฅผ await.

ํ•™์ˆ ์  ๋งฅ๋ฝ

  • Promise Coalescing / Request Deduplication: ๋™์ผ ๋น„๋™๊ธฐ ์ž‘์—…์„ ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๋Š” ํŒจํ„ด
  • Refresh Token Rotation: ๊ฐฑ์‹  ์‹œ๋งˆ๋‹ค ์ƒˆ Refresh Token ๋ฐœ๊ธ‰ โ†’ ํƒˆ์ทจ๋œ Refresh Token ์žฌ์‚ฌ์šฉ ๋ฐฉ์ง€ (OAuth 2.0 BCP)
  • Silent Authentication: ์‚ฌ์šฉ์ž ๊ฐœ์ž… ์—†์ด ์„ธ์…˜์„ ์ž๋™ ๋ณต๊ตฌํ•˜๋Š” UX ํŒจํ„ด

๊ด€๋ จ ํ‚ค์›Œ๋“œ: Token Refresh Race Condition, Promise Coalescing, Silent Authentication, OAuth 2.0 Token Rotation


9-7 ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…๊ณผ ์ง€์—ฐ ๋กœ๋”ฉ

๊ฐœ๋…

SPA(Single Page Application)์˜ ๋‹จ์ ์ธ ์ดˆ๊ธฐ ๋ฒˆ๋“ค ํฌ๊ธฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ธฐ๋ฒ•. ๋ผ์šฐํŠธ ๋‹จ์œ„๋กœ JavaScript๋ฅผ ๋ถ„๋ฆฌํ•ด ํ•„์š”ํ•  ๋•Œ๋งŒ ๋กœ๋“œ.

์ด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌํ˜„

// stackflow-route.tsx: ๋ชจ๋“  Activity๊ฐ€ lazy()๋กœ ์„ ์–ธ๋จ
const QuizSolvePage = lazy(() => import("@/pages/quiz/QuizSolvePage"));
const RankingPage   = lazy(() => import("@/pages/ranking/RankingPage"));
// ...

// stackflow-auth-shell.tsx: lazy๋œ AuthInitializer๋ฅผ Suspense๋กœ ๊ฐ์‹ธ
<Suspense fallback={null}>
  <AuthInitializer>{children}</AuthInitializer>
</Suspense>
  • ์ดˆ๊ธฐ ์ง„์ž… ์‹œ LoginPage ๋ฒˆ๋“ค๋งŒ ๋กœ๋“œ
  • ํ€ด์ฆˆ ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ์ ์— QuizSolvePage ๋ฒˆ๋“ค ๋กœ๋“œ
  • Vite๊ฐ€ ๊ฐ lazy import๋ฅผ ๋ณ„๋„ chunk ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ

ํ•™์ˆ ์  ๋งฅ๋ฝ

  • PRPL ํŒจํ„ด (Push, Render, Pre-cache, Lazy-load): Google์ด ์ œ์•ˆํ•œ Progressive Web App ์„ฑ๋Šฅ ์ „๋žต
  • Core Web Vitals ์ค‘ LCP(Largest Contentful Paint) ๊ฐœ์„ ์— ์ง์ ‘ ๊ธฐ์—ฌ
  • Dynamic Import (import()): ECMAScript 2020 ํ‘œ์ค€. Webpack/Vite๊ฐ€ ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฒญํฌ ๋ถ„๋ฆฌ

๊ด€๋ จ ํ‚ค์›Œ๋“œ: Code Splitting, Lazy Loading, Dynamic Import, PRPL Pattern, Core Web Vitals


9-8 ํ”ผ์ฒ˜ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ (Feature-Based Architecture)

๊ฐœ๋…

ํŒŒ์ผ์„ ์—ญํ•  ๊ธฐ์ค€(components/, services/, utils/)์ด ์•„๋‹Œ ๊ธฐ๋Šฅ(Feature) ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•˜๋Š” ๊ตฌ์กฐํ™” ๋ฐฉ์‹.

์ด ํ”„๋กœ์ ํŠธ์˜ ์ ์šฉ

src/
โ”œโ”€โ”€ api/
โ”‚   โ”œโ”€โ”€ quiz/         โ† quiz ๊ธฐ๋Šฅ์˜ API ์ „๋ถ€
โ”‚   โ”‚   โ”œโ”€โ”€ api.model.ts      (ํƒ€์ž…)
โ”‚   โ”‚   โ”œโ”€โ”€ api.query.ts      (React Query options)
โ”‚   โ”‚   โ””โ”€โ”€ postQuizList.ts   (fetch ํ•จ์ˆ˜)
โ”‚   โ””โ”€โ”€ ranking/      โ† ranking ๊ธฐ๋Šฅ์˜ API ์ „๋ถ€
โ”œโ”€โ”€ model/
โ”‚   โ”œโ”€โ”€ quiz/         โ† quiz ๊ธฐ๋Šฅ์˜ ์ƒํƒœ ์ „๋ถ€
โ”‚   โ””โ”€โ”€ auth/
โ””โ”€โ”€ components/
    โ”œโ”€โ”€ quiz/         โ† quiz ๊ธฐ๋Šฅ์˜ UI ์ „๋ถ€
    โ””โ”€โ”€ common/       โ† ๊ณต์œ  ์ปดํฌ๋„ŒํŠธ

ํ•œ ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•  ๋•Œ ๊ด€๋ จ ํŒŒ์ผ์ด ๊ฐ™์€ ํด๋”์— ๋ชจ์—ฌ ์žˆ์–ด ์‘์ง‘๋„(Cohesion) ๋†’์Œ, ๊ธฐ๋Šฅ ๊ฐ„ ์ง์ ‘ ์˜์กด์„ฑ ๋‚ฎ์•„ ๊ฒฐํ•ฉ๋„(Coupling) ๋‚ฎ์Œ.

ํ•™์ˆ ์  ๋งฅ๋ฝ

  • ๊ณ ์‘์ง‘ ์ €๊ฒฐํ•ฉ(High Cohesion, Low Coupling): ์†Œํ”„ํŠธ์›จ์–ด ๊ณตํ•™์˜ ๋ชจ๋“ˆ ์„ค๊ณ„ ์›์น™
  • Screaming Architecture (Robert C. Martin): ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด ์‹œ์Šคํ…œ์ด ๋ฌด์—‡์„ ํ•˜๋Š”์ง€ ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค
  • Vertical Slice Architecture: ๋ ˆ์ด์–ด(Controller-Service-Repository) ๋Œ€์‹  ๊ธฐ๋Šฅ ๋‹จ์œ„๋กœ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ง์œผ๋กœ ์ž๋ฅด๋Š” ํŒจํ„ด

๊ด€๋ จ ํ‚ค์›Œ๋“œ: Feature-Based Architecture, Screaming Architecture, Vertical Slice, High Cohesion Low Coupling


9-9 OpenAPI ๊ธฐ๋ฐ˜ ํƒ€์ž… ์ž๋™ ์ƒ์„ฑ (Contract-First API)

๊ฐœ๋…

๋ฐฑ์—”๋“œ๊ฐ€ OpenAPI(Swagger) ์ŠคํŽ™์„ ๊ธฐ์ค€(Contract)์œผ๋กœ ์ •์˜ํ•˜๊ณ , ํ”„๋ก ํŠธ์—”๋“œ๋Š” ๊ทธ ์ŠคํŽ™์—์„œ ํƒ€์ž…์„ ์ž๋™ ์ƒ์„ฑ. API ๋ณ€๊ฒฝ ์‹œ ์žฌ์ƒ์„ฑ๋งŒ ํ•˜๋ฉด ํƒ€์ž… ๋ถˆ์ผ์น˜๋ฅผ ์ปดํŒŒ์ผ ํƒ€์ž„์— ๊ฐ์ง€.

์ด ํ”„๋กœ์ ํŠธ์˜ ํ๋ฆ„

๋ฐฑ์—”๋“œ ์„œ๋ฒ„
  โ””โ”€ /v3/api-docs (OpenAPI JSON ์ŠคํŽ™ ๋…ธ์ถœ)
       โ†“ pnpm generate:api-models
web/src/api/config/api-models.ts (์ž๋™ ์ƒ์„ฑ)
  โ””โ”€ ๊ฐ api/*.model.ts์—์„œ ํƒ€์ž… ์ฐธ์กฐ
  โ””โ”€ ์ปดํฌ๋„ŒํŠธ์—์„œ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉ

ํ•™์ˆ ์  ๋งฅ๋ฝ

  • Schema-First / Contract-First Development: API ์ŠคํŽ™์„ ์ฝ”๋“œ๋ณด๋‹ค ๋จผ์ € ์ •์˜ โ†’ ํ”„๋ก ํŠธ/๋ฐฑ์—”๋“œ ๋ณ‘๋ ฌ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ
  • Code Generation: ๋ฐ˜๋ณต์ ์ธ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๋ฅผ ์ž๋™ํ™” โ€” ํœด๋จผ ์—๋Ÿฌ ๊ฐ์†Œ, ์ผ๊ด€์„ฑ ์œ ์ง€
  • OpenAPI 3.0์€ REST API์˜ ์‚ฌ์‹ค์ƒ ํ‘œ์ค€ ๋ช…์„ธ ์–ธ์–ด๋กœ, IDL(Interface Definition Language)์˜ ์—ญํ•  ์ˆ˜ํ–‰

๊ด€๋ จ ํ‚ค์›Œ๋“œ: Contract-First API Design, OpenAPI, Code Generation, Schema-Driven Development, IDL

Clone this wiki locally