-
Notifications
You must be signed in to change notification settings - Fork 0
Frontend Wiki
์ด ๋ฌธ์๋ ํ๋ก ํธ์๋ ํ์ ๊ธฐ์ , ๊ท์น, ๊ฒฐ์ ์ฌํญ, ์ด์ ๋ฐฉ์์ ๊ธฐ๋กํ๊ณ ๊ณต์ ํ๊ธฐ ์ํ ์ํค์ ๋๋ค. ๋ชจ๋ ๋ณ๊ฒฝ ์ฌํญ์ ๋ฌธ์ํ ๋ฐ ๋ ์ง ๊ธฐ๋ก์ ์์น์ผ๋ก ํฉ๋๋ค.
์ต์ด ์์ฑ: 2026-04-10
- CS ํด์ฆ / ๋ฉด์ ์ค๋น ํ๋ซํผ hellocs.site ์ ํด๋ผ์ด์ธํธ ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ ๋ฐ ์ ์ง๋ณด์
- ์น๊ณผ ๋ชจ๋ฐ์ผ(iOS/Android) ๋ ํ๋ซํผ ๋์ ์ง์์ ๋จ์ผ ๋ชจ๋ ธ๋ ํฌ๋ก ๊ด๋ฆฌ
| ๊ตฌ๋ถ | ๋์ |
|---|---|
| ์ฃผ์ ์ฌ์ฉ์ | CS ์ทจ์ ์ค๋น ์ค์ธ ๊ฐ๋ฐ์ |
| ๋ณด์กฐ ์ฌ์ฉ์ | ์ด๋ฏธ ์ทจ์ ํ ๋ค ์ง์ ์ ๊ฒ์ ์ํ๋ ๊ฐ๋ฐ์ |
- ํด์ฆ ํ๊ธฐ: OX / ๊ฐ๊ด์ / ๋จ๋ตํ 3๊ฐ์ง ์ ํ์ CS ํด์ฆ (ํ๋น 5๋ฌธํญ)
- ์์ฑ ๋ต๋ณ (Voice Quiz): ๋ชจ๋ฐ์ผ ๋ง์ดํฌ ๋ น์ โ STT ๋ณํ์ผ๋ก ๋จ๋ตํ ๋ต๋ณ ์ ๋ ฅ
- ์ฑ์ ๋ฐ ํผ๋๋ฐฑ: ํด์ฆ ์ ์ถ ํ AI ์ฑ์ + ์ค๋ต ์์ธ ํ์ธ
- ํ์ต ์คํธ๋ฆญ: ์ฐ์ ํ์ต์ผ ๋ฌ๋ ฅ ์๊ฐํ
- ๋ญํน: ์ ์ฒด ์ฌ์ฉ์ ์ ์ ์์
- ์จ๋ณด๋ฉ: ์ด๋ฆ โ ๊ด์ฌ์ฌ โ ํด์ฆ ๋ ๋ฒจ โ ์๋ฃ 4๋จ๊ณ
- ์์ ๋ก๊ทธ์ธ: ์นด์นด์ค OAuth
| ๊ตฌ๋ถ | ๊ธฐ์ | ๋ฒ์ |
|---|---|---|
| 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 |
| ๊ตฌ๋ถ | ๊ธฐ์ | ๋ฒ์ |
|---|---|---|
| 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 | - |
| ๊ตฌ๋ถ | ๊ธฐ์ |
|---|---|
| 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 ํ์ ) |
Deadlock-Client/
โโโ web/ # React 19 + Vite ์น ์ฑ (pnpm)
โโโ mobile/ # React Native Expo ์ฑ (npm)
โโโ CLAUDE.md
๋ ์ฑ์ ๋ณ๋ ํจํค์ง ๋งค๋์ ์ node_modules๋ฅผ ์ฌ์ฉํ๋ค. ํ์
๊ณต์ ๋ ์๊ณ , ๋ธ๋ฆฟ์ง ์ธํฐํ์ด์ค(AppBridgeType)๋ฅผ ํตํด Web โ Native ๊ณ์ฝ์ ๋ง์ถ๋ค.
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 ์ปดํฌ๋ํธ
- ๊ฐ ํ์ด์ง = 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" }| ์ํ ์ ํ | ๋๊ตฌ | ์์น |
|---|---|---|
| ์ธ์ฆ ํ ํฐ | Zustand (useAuthStore) |
model/auth/ |
| ํด์ฆ ํ์ด ํ๋ฆ (OXโ๊ฐ๊ด์โ๋จ๋ตํ ์์) | Zustand (useQuizSolveStore) |
model/quiz/ |
| ์๋ฒ ๋ฐ์ดํฐ ์บ์ | React Query | api/*/api.query.ts |
| ์ ์ ํ๋กํ | Zustand (useUserStore) |
model/user/ |
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
๋ชจ๋ฐ์ผ ์ฑ์ WebView๋ก ์น ์ฑ์ ๋ํํ๋ค. ๋ค์ดํฐ๋ธ ๊ธฐ๋ฅ(๋ง์ดํฌ, ๋ก๊ทธ์ธ ์ํ)์ @webview-bridge ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด Web์์ ํธ์ถํ๋ค.
Web (React) Native (React Native)
โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
window.bridge.xxx() โโโโโโถ bridge/index.ts ๊ตฌํ์ฒด ์คํ
โโโโโโ Promise ๊ฒฐ๊ณผ ๋ฐํ
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 ๋ณํ ํ ํ ์คํธ ๋ฐํ |
-
mobile/bridge/index.ts์AppBridgeType์ธํฐํ์ด์ค์ ๋ฉ์๋ ์ถ๊ฐ -
bridge()๊ตฌํ์ฒด ๋ด๋ถ์ ๋ก์ง ์์ฑ - ์น์์
window.bridge.newMethod()ํธ์ถ
-
webview/createHandleShouldStartLoad.ts:onShouldStartLoadWithRequestํธ๋ค๋ฌ-
intent:์คํด โbrowser_fallback_urlํ์ฑ ํwindow.location์ด๋ -
kakaotalk://,kakaokompass://๋ฑ โLinking.openURL()๋ก ์ฑ ์คํ -
kauth.kakao.comโ WebView ๋ด ์ฒ๋ฆฌ (์นด์นด์ค OAuth ํ๋ก์ฐ) - HTTP/HTTPS โ WebView ์ฒ๋ฆฌ
- ๊ทธ ์ธ ์ปค์คํ ์คํด โ ๋ค์ดํฐ๋ธ ์์
-
Web: window.bridge.startRecording()
โ
Native: ๋ง์ดํฌ ๊ถํ ์์ฒญ โ expo-av ๋
น์ ์์
โ (์ฌ์ฉ์๊ฐ ์๋ฃ ์)
Web: window.bridge.stopRecording()
โ
Native: ๋
น์ ์ข
๋ฃ โ STT ์๋ฒ ์ ์ก โ ํ
์คํธ ๋ฐํ
โ
Web: ๋ฐํ๋ ํ
์คํธ๋ฅผ ๋จ๋ตํ ์
๋ ฅ๊ฐ์ผ๋ก ์ฌ์ฉ
โ ๏ธ ํ์ฌ STT ์๋ฒ ์ ์ก ๋ก์ง์ ๋ฏธ๊ตฌํ (mock ํ ์คํธ ๋ฐํ ์ค).bridge/audioRecorder.ts์sendAudioToServerํจ์ ๊ตฌํ ํ์.
emoji [type] ํ๊ธ ์ค๋ช
| type | ์ฌ์ฉ ์ํฉ |
|---|---|
feat |
์ ๊ท ๊ธฐ๋ฅ |
fix |
๋ฒ๊ทธ ์์ |
refactor |
๋ฆฌํฉํ ๋ง |
chore |
๋น๋/์ค์ ๋ณ๊ฒฝ |
docs |
๋ฌธ์ |
style |
์คํ์ผ(์ฝ๋ ๋ณ๊ฒฝ ์์) |
test |
ํ ์คํธ |
perf |
์ฑ๋ฅ ๊ฐ์ |
rename |
ํ์ผ/๋ณ์๋ช ๋ณ๊ฒฝ |
remove |
์ฝ๋/ํ์ผ ์ญ์ |
์์: โจ [feat] ํด์ฆ ํ์ด๋จธ ์ถ๊ฐ
| ๋์ | ๊ท์น | ์์ |
|---|---|---|
| ์ปดํฌ๋ํธ ํ์ผ | PascalCase | QuizSolvePage.tsx |
| ํ ํ์ผ | camelCase, use ์ ๋์ฌ |
useQuizStore.ts |
| API ํ์ผ | camelCase, ๋์ฌ+๋ช ์ฌ | postQuizList.ts |
| ์์ | UPPER_SNAKE_CASE | QUIZ_SOLVE_TOTAL_COUNT |
| ๊ฒฝ๋ก alias |
@/* โ src/*
|
@/components/common/Button |
- pages: Stackflow Activity๋ง ๋ฐฐ์น. ํ์ด์ง ๋ก์ง์ model/ ๋๋ components/๋ก ๋ถ๋ฆฌ.
-
components: feature๋ช
ํด๋ ํ์์ ๊ด๋ จ ์ปดํฌ๋ํธ ๋ฐฐ์น. ์ฌ์ฌ์ฉ ๊ณตํต ์ปดํฌ๋ํธ๋
common/. - model: Zustand store + ๊ด๋ จ ์ปค์คํ ํ ์ feature๋ณ๋ก ๋ฌถ์.
-
api:
api.model.ts(์๋ต ํ์ ) /api.query.ts(queryOptions) / ๊ฐ๋ณ fetch ํจ์ ํ์ผ๋ก ๊ตฌ๋ถ.
- ์ ํธ๋ฆฌํฐ ํด๋์ค: 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" },
});@/app โ @/pages โ @/components โ @/api โ @/model โ ๋๋จธ์ง
- ์๋ฒ ๋ฐ์ดํฐ(๋ชฉ๋ก, ์์ธ): React Query
- UI ํ๋ฆ ์ํ(ํด์ฆ ๋จ๊ณ, ์ ํ๊ฐ): Zustand
- ์ ์ญ ์ธ์ฆ ํ ํฐ: Zustand (
useAuthStore) - Zustand store๋
model/<feature>/ํ์์ ์์น
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๊ฒฐ์ | Stackflow ์ฌ์ฉ |
| ์ด์ | ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ์ Activity ์คํ ๊ธฐ๋ฐ ๋ค๋น๊ฒ์ด์ UX ๊ตฌํ (push/pop ์ฌ๋ผ์ด๋ ํธ๋์ง์ ), WebView ๋ด์์๋ ๋ค์ดํฐ๋ธ ๋๋์ ํ๋ฉด ์ ํ ํ์ |
| ๋์ | React Router v7, TanStack Router |
| ํธ๋ ์ด๋์คํ | ์ํ๊ณ๊ฐ ์๊ณ ๋ ํผ๋ฐ์ค ๋ถ์กฑ. URL ๋๊ธฐํ๋ historySyncPlugin์ผ๋ก ๋ณ๋ ๊ตฌ์ฑ ํ์ |
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๊ฒฐ์ | Ky ์ฌ์ฉ |
| ์ด์ | fetch ๊ธฐ๋ฐ์ผ๋ก ๋ฒ๋ค ํฌ๊ธฐ ์ต์ํ, ํ (beforeRequest / afterResponse)์ผ๋ก ํ ํฐ ์ฃผ์ /๊ฐฑ์ ์ฒ๋ฆฌ๊ฐ ๊น๋ํจ |
| ๋์ | axios, ์์ fetch |
| ํธ๋ ์ด๋์คํ | axios ๋๋น ์ธํฐ์ ํฐ API๊ฐ ๋ฌ๋ผ ํ ๋ฌ๋ ์ปค๋ธ ์กด์ฌ |
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๊ฒฐ์ | @webview-bridge ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ |
| ์ด์ | postMessage ๊ธฐ๋ฐ ์๋ ๊ตฌํ ๋๋น TypeScript ํ์ ์์ ์ฑ ํ๋ณด, Promise ๊ธฐ๋ฐ ๋น๋๊ธฐ ํธ์ถ ์๋ ์ฒ๋ฆฌ |
| ๋์ |
window.ReactNativeWebView.postMessage ์ง์ ๊ตฌํ |
| ํธ๋ ์ด๋์คํ | ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์กด์ฑ ์ถ๊ฐ, ๋ด๋ถ ์ง๋ ฌํ ๋ฐฉ์์ ์ข ์ |
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๊ฒฐ์ |
openapi-typescript๋ก ๋ฐฑ์๋ OpenAPI spec โ TypeScript ํ์
์๋ ์์ฑ |
| ์ด์ | ์๋ ํ์ ๊ด๋ฆฌ ์ API ๋ณ๊ฒฝ ๋๋ง๋ค ๋๋ฝ ์ํ, ๋ฐฑ์๋ ์คํ๊ณผ ํญ์ ๋๊ธฐํ |
| ๋ช ๋ น์ด | pnpm generate:api-models |
| ์ถ๋ ฅ | src/api/config/api-models.ts |
| ๊ธฐ์ | ๋ฌธ์ |
|---|---|
| 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 |
|---|---|
| ํ๋ก๋์ ์น | https://hellocs.site |
| API | https://api.hellocs.site |
| API Docs (Swagger) | https://api.hellocs.site/v3/api-docs |
| Grafana | https://hellocs.site/grafana/ |
# Web
cd web && pnpm install && pnpm dev # http://localhost:5173
# Mobile
cd mobile && npm install && npm start # Expo dev server๋ฐฑ์๋ API๊ฐ ๋ณ๊ฒฝ๋์ ๋ ์คํ:
cd web && pnpm generate:api-models.env์ API_SWAGGER_URL ์ค์ ํ์.
-
src/pages/<feature>/<PageName>.tsx์์ฑ -
src/app/stackflow-route.tsx์{ name, component, path }๋ฑ๋ก - ํ์ ์
src/api/<feature>/,src/model/<feature>/์ถ๊ฐ
-
mobile/bridge/index.ts์AppBridgeType์ ๋ฉ์๋ ์๊ทธ๋์ฒ ์ถ๊ฐ -
bridge()๊ตฌํ์ฒด์ ์ค์ ๋ก์ง ์์ฑ - ์น์์
window.bridge.xxx()ํธ์ถ๋ก ๊ฒ์ฆ
-
api-models.ts๋ ์๋์์ฑ ํ์ผ โ ์ง์ ์์ ๊ธ์ง,pnpm generate:api-models๋ก๋ง ๊ฐฑ์ - ์ปค๋ฐ ๋ฉ์์ง ํ์ ๋ฏธ์ค์ ์
commitlintํ ์ด ๊ฑฐ๋ถํจ -
main๋ธ๋์น push ์ GitHub Actions๊ฐ ์๋ ๋น๋ + ๋ฐฐํฌ ํธ๋ฆฌ๊ฑฐ๋จ (web ๊ฒฝ๋ก ๋ณ๊ฒฝ ์) - ESLint import ์์ ์๋ฐ ์ CI์์ ์คํจ โ ๋ก์ปฌ์์
pnpm lint๋จผ์ ํ์ธ
๋ก์ปฌ ๊ฐ๋ฐ ์ ์๋ ๊ฒฝ๋ก๋ https://hellocs.site๋ก ํ๋ก์๋จ:
/api//v3//swagger-ui/
์ด ์น์ ์ ํ๋ก์ ํธ์์ ์ ์ฉํ ๊ธฐ์ ๊ฐ๋ ์ ํ์ ์ ๊ด์ ์์ ๊ธฐ์ ํ๋ค. ์ค์ ๊ตฌํ ํ์ผ๊ณผ ์ฐ๊ฒฐํด ์ด๋ก ๊ณผ ์ค์ฒ์ ์ฐ๊ฒฐ๊ณ ๋ฆฌ๋ฅผ ํ์ธํ ์ ์๋ค.
๋ค์ดํฐ๋ธ ์ฑ๊ณผ ์น ์ฑ์ ์ด๋ถ๋ฒ ๋์ , 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
WebView์ Native ์ฌ์ด์ ํต์ ์ ์ ํต์ ์ผ๋ก postMessage(string) โ ๋จ๋ฐฉํฅ ๋ฌธ์์ด ๋ฉ์์ง๋ก ์ด๋ฃจ์ด์ก๋ค. ์ด ๋ฐฉ์์:
- ์๋ต์ ์ถ์ ํ ์๋จ์ด ์์ (Fire-and-forget)
- ํ์ ์ ๋ณด ์์ค (์ง๋ ฌํ/์ญ์ง๋ ฌํ)
- ์๋ฌ ์ฒ๋ฆฌ ๋ถ๋ช ํ
์์ฒญ ํ๋ฆ:
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
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
try/catch ๊ธฐ๋ฐ ์๋ฌ ์ฒ๋ฆฌ๋ ํจ์ ์๊ทธ๋์ฒ์์ ์คํจ ๊ฐ๋ฅ์ฑ์ด ๋๋ฌ๋์ง ์๋๋ค. ํธ์ถ์๊ฐ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์์ด๋ ์ปดํ์ผ๋ฌ๊ฐ ๊ฐ์งํ์ง ๋ชปํ๋ค.
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
์ ํต์ ๋ฐฉ์(Redux + ์๋ fetch)์์ ์๋ฒ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ฉด:
- ๋ก๋ฉ/์๋ฌ/์ฑ๊ณต ์ํ๋ฅผ ๋งค๋ฒ ์ง์ ๊ด๋ฆฌ
- ์บ์ ๋ฌดํจํ ํ์ด๋ฐ ์ง์ ์ง์
- ์ค๋ณต ์์ฒญ ๋ฐฉ์ง ๋ก์ง ๋ฐ๋ณต ์์ฑ
// ์ ์ธ์ ์ฟผ๋ฆฌ ์ ์
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
SPA์์ Access Token ๋ง๋ฃ ์ ์ฌ๋ฌ API ์์ฒญ์ด ๋์์ 401์ ๋ฐ์ผ๋ฉด, Refresh Token์ผ๋ก ํ ํฐ ๊ฐฑ์ ์์ฒญ์ด N๋ฒ ์ค๋ณต ๋ฐ์ํ ์ ์๋ค โ Refresh Token์ด ๋จ ํ ๋ฒ๋ง ์ ํจํ ๊ฒฝ์ฐ ๋๋จธ์ง ์์ฒญ ์คํจ.
// 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
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
ํ์ผ์ ์ญํ ๊ธฐ์ค(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
๋ฐฑ์๋๊ฐ 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
- ๐ฃ๏ธ Roadmap ------------------------------
- ๐ Sprint Planning
- ๐ Sprint Backlog