Skip to content

Frontend Wiki

yummjin edited this page Apr 10, 2026 · 14 revisions

๐Ÿงญ Frontend Team Wiki

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


1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

2. ๊ธฐ์ˆ  ์Šคํƒ ๋ฐ ์„ ์ • ์ด์œ 

3. ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๊ตฌ์กฐ

4. WebView ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์กฐ

5. ํ˜‘์—…์šฉ ์ปจ๋ฒค์…˜ ์ •์˜


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

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

๊ธฐ์ˆ  ๋ฌธ์„œ
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