Skip to content

Kusitms32th-KuCheck/KuCheck-Front

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

ํ-์ฒต (Ku-check): ํ์‹œ์ฆ˜์˜ ๋ชจ๋“  ์ผ์„ ํ•œํ์— ์ฒดํฌํ•˜๋‹ค, ํ์ฒต

๐Ÿ”— ์„œ๋น„์Šค ๋งํฌ: https://ku-check.vercel.app

แ„แ…ฒแ„Žแ…ฆแ†จ แ„‰แ…ฉแ„€แ…ข ๐ŸŽจ ์„œ๋น„์Šค ์„ค๋ช… ํ์ฒต์€ ํ์‹œ์ฆ˜ ์šด์˜์— ํฉ์–ด์ ธ ์žˆ๋˜ ์ถœ๊ฒฐ, ์ƒ๋ฒŒ์ , ๊ณต์ง€, ๋ถˆ์ฐธ์‚ฌ์œ ์„œ ์ œ์ถœ์„ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ•ด ํ•™ํšŒ ์šด์˜์„ ๋” ์ •ํ™•ํ•˜๊ณ  ๊ฐ„ํŽธํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ์ „์šฉ ๊ด€๋ฆฌ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. ์šด์˜์ง„์€ ๋ฐ˜๋ณต์ ์ธ ์ˆ˜๊ธฐ ํ–‰์ •์„ ์ค„์ผ ์ˆ˜ ์žˆ๊ณ , ํ•™ํšŒ์›์€ ๋‚ด ํ™œ๋™ ํ˜„ํ™ฉ๊ณผ ์ด๋ฒˆ ์ฃผ ํ•ต์‹ฌ ์ •๋ณด๋ฅผ ํ•œ ๊ณณ์—์„œ ํ™•์ธํ•˜๋ฉฐ ๋ถˆ์ฐธ์‚ฌ์œ ์„œ๊นŒ์ง€ ์•ฑ์—์„œ ๋ฐ”๋กœ ์ œ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค ํŒ€์› ์†Œ๊ฐœ

Frontend (FE)
ํ™ฉ์œ ๋ฆผ GitHub Avatar
ํ™ฉ์œ ๋ฆผ
Frontend Lead
์ง„์ฑ„์ • GitHub Avatar
์ง„์ฑ„์ •
Frontend

๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ

๊ธฐ์ˆ  / ๋„๊ตฌ ์„ ํƒ์ด์œ 
Next.js Next.js๋ฅผ ๋„์ž…ํ•œ ํ•ต์‹ฌ ๋ชฉํ‘œ๋Š” ์‚ฌ์šฉ์ž ์„ฑ๋Šฅ ํ–ฅ์ƒ์ž…๋‹ˆ๋‹ค. SSR/SSG๋ฅผ ํ†ตํ•œ ๋น ๋ฅธ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„ ํ™•๋ณด, ์ž๋™ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”, ์œ ์—ฐํ•œ ๋ ˆ์ด์•„์›ƒ ์‹œ์Šคํ…œ ๋“ฑ Next.js๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ฐ•๋ ฅํ•œ ์ตœ์ ํ™” ์ด์ ๋“ค์„ ์ ๊ทน ํ™œ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX)์„ ๊ทน๋Œ€ํ™”ํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.
TypeScript TypeScript๋ฅผ ๋„์ž…ํ•œ ๊ฐ€์žฅ ํฐ ์ด์œ ๋Š” ์ •์  ํƒ€์ž… ๊ฒ€์‚ฌ๋ฅผ ํ†ตํ•ด ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๋ฅผ ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ ๋ฏธ๋ฆฌ ์ฐจ๋‹จํ•˜์—ฌ ์„œ๋น„์Šค์˜ ์•ˆ์ •์„ฑ์„ ๊ทน๋Œ€ํ™”ํ•˜๊ธฐ ์œ„ํ•ด์„œ์ž…๋‹ˆ๋‹ค. ๋˜ํ•œ, ๋ช…ํ™•ํ•œ ํƒ€์ž… ์ถ”๋ก ๊ณผ ๊ฐ•๋ ฅํ•œ ์ž๋™ ์™„์„ฑ ๊ธฐ๋Šฅ์€ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ๋†’์—ฌ์ฃผ์—ˆ์œผ๋ฉฐ, ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ™•๋ณดํ•˜๋Š” ๋ฐ ๊ฒฐ์ •์ ์ธ ์—ญํ• ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
pnpm pnpm์„ ๋„์ž…ํ•œ ๊ฐ€์žฅ ํฐ ์ด์œ ๋Š” ์••๋„์ ์ธ ๋””์Šคํฌ ๊ณต๊ฐ„ ํšจ์œจ์„ฑ๊ณผ ๋น ๋ฅธ ์„ค์น˜ ์†๋„ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. npm/yarn๊ณผ ๋‹ฌ๋ฆฌ node_modules์— ํŒจํ‚ค์ง€๋ฅผ ๋ณต์ œํ•˜์ง€ ์•Š๊ณ  ์ „์—ญ ์Šคํ† ์–ด์—์„œ ํ•˜๋“œ ๋งํฌ(Hard Link) ๋ฐฉ์‹์œผ๋กœ ํŒŒ์ผ์„ ๊ฐ€์ ธ์™€, ๋””์Šคํฌ ์‚ฌ์šฉ๋Ÿ‰์„ ํš๊ธฐ์ ์œผ๋กœ ์ค„์ด๊ณ  ์ค‘๋ณต ์„ค์น˜๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, package.json์— ๋ช…์‹œ๋œ ์˜์กด์„ฑ์—๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ์—„๊ฒฉํ•œ node_modules ๊ตฌ์กฐ๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ, '์œ ๋ น ์˜์กด์„ฑ(Phantom Dependencies)' ๋ฌธ์ œ๋ฅผ ์›์ฒœ์ ์œผ๋กœ ์ฐจ๋‹จํ•˜๊ณ  ํ”„๋กœ์ ํŠธ์˜ ์•ˆ์ •์„ฑ์„ ๋†’์—ฌ์ค๋‹ˆ๋‹ค.
TailwindCSS TailwindCSS๋Š” '์œ ํ‹ธ๋ฆฌํ‹ฐ ํผ์ŠคํŠธ(Utility-First)' ์ ‘๊ทผ ๋ฐฉ์‹์€ ๋ฏธ๋ฆฌ ์ •์˜๋œ ํด๋ž˜์Šค๋ฅผ JSX/HTML ๋‚ด์—์„œ ์ง์ ‘ ์กฐํ•ฉํ•˜๊ฒŒ ํ•˜์—ฌ, CSS ํŒŒ์ผ๊ณผ ์ฝ”๋“œ๋ฅผ ์˜ค๊ฐ€๋Š” ์ปจํ…์ŠคํŠธ ์Šค์œ„์นญ ๋น„์šฉ์„ ์™„์ „ํžˆ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ง๊ด€์ ์ธ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ๊ณผ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ํ†ตํ•ด ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
Zustand Zustand๋Š” ๋งค์šฐ ๊ฐ„๊ฒฐํ•œ ๋ฌธ๋ฒ•๊ณผ ์ตœ์†Œํ•œ์˜ ์„ค์ •์œผ๋กœ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ๊ทน๋Œ€ํ™”ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, Provider๋กœ ๊ฐ์Œ€ ํ•„์š”๊ฐ€ ์—†๊ณ  ์ƒํƒœ์˜ ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ๊ตฌ๋…ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ์ž๋™์œผ๋กœ ๋ฐฉ์ง€ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ์ตœ์ ํ™”์— ์œ ๋ฆฌํ•˜์—ฌ ์„ ํƒํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
E2E E2E ํ…Œ์ŠคํŠธ๋ฅผ ๋„์ž…ํ•œ ์ด์œ ๋Š” '์‹ค์ œ ์‚ฌ์šฉ์ž' ๊ด€์ ์—์„œ ํ•ต์‹ฌ ๊ธฐ๋Šฅ(ํšŒ์›๊ฐ€์ž…, ํํ”ฝ, ๋ถˆ์ฐธ์‚ฌ์œ ์„œ ์ž‘์„ฑ)์˜ ์ „์ฒด ํ๋ฆ„์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜์—ฌ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด์„œ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์œ ๋‹›/ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๊ฐ€ ๋ฐœ๊ฒฌํ•˜๊ธฐ ์–ด๋ ค์šด ํ”„๋ก ํŠธ์—”๋“œ, ๋ฐฑ์—”๋“œ API, DB๋ฅผ ์•„์šฐ๋ฅด๋Š” ๋ณตํ•ฉ์ ์ธ ์ƒํ˜ธ์ž‘์šฉ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฒ„๊ทธ๋ฅผ ์žก์•„๋‚ด๋ฉฐ, ๋ฐฐํฌ ์ „ ๊ฐ€์žฅ ๋†’์€ ์ˆ˜์ค€์˜ ์„œ๋น„์Šค ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ•˜๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

๐Ÿ—‚๏ธ ํด๋” ๊ตฌ์กฐ

KuCheck-Front/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ app/                         # Next.js App Router - ํŽ˜์ด์ง€ ๋ฐ ๋ ˆ์ด์•„์›ƒ
โ”‚   โ”‚   โ”œโ”€โ”€ layout.tsx               # ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ
โ”‚   โ”‚   โ”œโ”€โ”€ page.tsx                 # ํ™ˆํŽ˜์ด์ง€
โ”‚   โ”‚   โ”œโ”€โ”€ (auth)/                  # ์ธ์ฆ ๊ด€๋ จ ๋ผ์šฐํŠธ ๊ทธ๋ฃน
โ”‚   โ”‚   โ”œโ”€โ”€ (manager)/               # ๋งค๋‹ˆ์ € ๊ธฐ๋Šฅ ๋ผ์šฐํŠธ ๊ทธ๋ฃน
โ”‚   โ”‚   โ””โ”€โ”€ (member)/                # ๋ฉค๋ฒ„ ๊ธฐ๋Šฅ ๋ผ์šฐํŠธ ๊ทธ๋ฃน
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ components/                  # ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ React ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”‚   โ”œโ”€โ”€ common/                  # ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ (Header, Footer, Sidebar ๋“ฑ)
โ”‚   โ”‚   โ”œโ”€โ”€ ui/                      # UI ์›์ž ์ปดํฌ๋„ŒํŠธ (Button, Input, Modal ๋“ฑ)
โ”‚   โ”‚   โ”œโ”€โ”€ manager/                 # ๋งค๋‹ˆ์ € ์ „์šฉ ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ QRCodeScanner/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ AttendanceList/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ”‚   โ””โ”€โ”€ member/                  # ๋ฉค๋ฒ„ ์ „์šฉ ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”‚       โ”œโ”€โ”€ CheckIn/
โ”‚   โ”‚       โ”œโ”€โ”€ AbsenceForm/
โ”‚   โ”‚       โ””โ”€โ”€ ...
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ hooks/                       # ์ปค์Šคํ…€ React Hooks
โ”‚   โ”‚   โ”œโ”€โ”€ useAuth.ts               # ์ธ์ฆ ๊ด€๋ จ ํ›…
โ”‚   โ”‚   โ”œโ”€โ”€ useAttendance.ts         # ์ถœ์„ ๊ด€๋ จ ํ›…
โ”‚   โ”‚   โ”œโ”€โ”€ useFetch.ts              # API ์š”์ฒญ ๊ด€๋ฆฌ ํ›…
โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ lib/                         # ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์ •
โ”‚   โ”‚   โ”œโ”€โ”€ api/                     # API ํด๋ผ์ด์–ธํŠธ
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ axiosInstance.ts     # Axios ์„ค์ •
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ endpoints.ts         # API ์—”๋“œํฌ์ธํŠธ ์ •์˜
โ”‚   โ”‚   โ”œโ”€โ”€ utils/                   # ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋“ค
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ imageOptimizer.ts    # ์ด๋ฏธ์ง€ ์ตœ์ ํ™”
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dateFormatter.ts     # ๋‚ ์งœ ํฌ๋งทํŒ…
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ store/                       # Zustand ์ƒํƒœ ๊ด€๋ฆฌ
โ”‚   โ”‚   โ”œโ”€โ”€ authStore.ts             # ์ธ์ฆ ์ƒํƒœ
โ”‚   โ”‚   โ”œโ”€โ”€ attendanceStore.ts       # ์ถœ์„ ์ƒํƒœ
โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ types/                       # TypeScript ํƒ€์ž… ์ •์˜
โ”‚   โ”‚   โ”œโ”€โ”€ attendance.ts            # ์ถœ์„ ๊ด€๋ จ ํƒ€์ž…
โ”‚   โ”‚   โ”œโ”€โ”€ user.ts                  # ์‚ฌ์šฉ์ž ๊ด€๋ จ ํƒ€์ž…
โ”‚   โ”‚   โ”œโ”€โ”€ api.ts                   # API ์‘๋‹ต ํƒ€์ž…
โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ constants/                   # ์ƒ์ˆ˜ ์ •์˜
โ”‚   โ”‚   โ”œโ”€โ”€ routes.ts                # ๋ผ์šฐํŠธ ๊ฒฝ๋กœ
โ”‚   โ”‚   โ”œโ”€โ”€ messages.ts              # ์—๋Ÿฌ/์„ฑ๊ณต ๋ฉ”์‹œ์ง€
โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ assets/                      # icons
โ”‚   โ”‚   โ”œโ”€โ”€ images/
โ”‚   โ”‚   โ”œโ”€โ”€ icons/
โ”‚   โ”‚   โ””โ”€โ”€ fonts/
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ utils/                       # ์ „์—ญ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
โ”‚   โ”‚   โ”œโ”€โ”€ validators.ts            # ์ž…๋ ฅ ๊ฒ€์ฆ ํ•จ์ˆ˜
โ”‚   โ”‚   โ”œโ”€โ”€ errorHandler.ts          # ์—๋Ÿฌ ์ฒ˜๋ฆฌ
โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ middleware.ts                # Next.js ๋ฏธ๋“ค์›จ์–ด (์ธ์ฆ ์ฒดํฌ ๋“ฑ)
โ”‚
โ”œโ”€โ”€ tests/                           # E2E ํ…Œ์ŠคํŠธ (Playwright)
โ”‚   โ”œโ”€โ”€ signup.spec.ts               # ํšŒ์›๊ฐ€์ž… ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ
โ”‚   โ”œโ”€โ”€ attendance.spec.ts           # ์ถœ์„ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
โ”‚   โ””โ”€โ”€ ...
โ”‚
โ”œโ”€โ”€ public/                          # ์ •์  ํŒŒ์ผ (์ด๋ฏธ์ง€, ํฐํŠธ ๋“ฑ)
โ”‚
โ”œโ”€โ”€ package.json                     # ํ”„๋กœ์ ํŠธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฐ ์˜์กด์„ฑ
โ”œโ”€โ”€ tsconfig.json                    # TypeScript ์„ค์ •
โ”œโ”€โ”€ next.config.ts                   # Next.js ์„ค์ •
โ”œโ”€โ”€ tailwind.config.ts               # Tailwind CSS ์„ค์ •
โ”œโ”€โ”€ eslint.config.mjs                # ESLint ์„ค์ •
โ””โ”€โ”€ .prettierrc                      # Prettier ์„ค์ •

๐Ÿ“ ํด๋”๋ณ„ ์ƒ์„ธ ์„ค๋ช…

src/app

Next.js์˜ App Router ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ… ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค. ํด๋” ๊ตฌ์กฐ๊ฐ€ ๊ณง URL ๊ฒฝ๋กœ๊ฐ€ ๋˜์–ด ์ง๊ด€์ ์ธ ํŽ˜์ด์ง€ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋ผ์šฐํŠธ ๊ทธ๋ฃน((auth), (manager), (member))์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ณตํ†ต ๋ ˆ์ด์•„์›ƒ์„ ๊ณต์œ ํ•˜๋ฉด์„œ๋„ URL ๊ฒฝ๋กœ์— ํฌํ•จ๋˜์ง€ ์•Š๊ฒŒ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

src/components

์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ React ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ธฐ๋Šฅ๊ณผ ์—ญํ• ์— ๋”ฐ๋ผ ๋ถ„๋ฅ˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • common: ๋ชจ๋“  ํŽ˜์ด์ง€์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” Header, Footer, Navigation ๋“ฑ
  • ui: shadcn/ui ๊ธฐ๋ณธ UI ์š”์†Œ๋“ค
  • manager/member: ๊ฐ ์‚ฌ์šฉ์ž ์—ญํ• ๋ณ„ ์ „์šฉ ์ปดํฌ๋„ŒํŠธ

src/hooks

React์˜ Hooks๋ฅผ ํ™œ์šฉํ•œ ๋กœ์ง ์žฌ์‚ฌ์šฉ์ž…๋‹ˆ๋‹ค. API ํ˜ธ์ถœ, ์ƒํƒœ ๊ด€๋ฆฌ, ํผ ์ฒ˜๋ฆฌ ๋“ฑ ๋ณต์žกํ•œ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ์˜ ์ˆœ์ˆ˜์„ฑ์„ ์œ ์ง€ํ•˜๊ณ  ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค.

src/lib

๊ณตํ†ต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฐ ๋„๊ตฌ๋“ค์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  • client: CSR API ์—”๋“œํฌ์ธํŠธ ๊ด€๋ฆฌ
  • server: SSR API ์—”๋“œํฌ์ธํŠธ ๊ด€๋ฆฌ

src/store

Zustand๋ฅผ ์‚ฌ์šฉํ•œ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ์ž…๋‹ˆ๋‹ค. ๊ฐ ๋„๋ฉ”์ธ๋ณ„๋กœ ์Šคํ† ์–ด๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ํ•„์š”ํ•œ ์ƒํƒœ๋งŒ ๊ตฌ๋…ํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

src/types

TypeScript์˜ ์ธํ„ฐํŽ˜์ด์Šค์™€ ํƒ€์ž…์„ ์ค‘์•™์ง‘์ค‘์‹์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. API ์‘๋‹ต, ์ปดํฌ๋„ŒํŠธ Props, ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋“ฑ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜ํ•˜์—ฌ ๊ฐœ๋ฐœ ์•ˆ์ •์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค.

src/constants

๋งค์ง ์ŠคํŠธ๋ง(magic string)๊ณผ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๊ฐ’๋“ค์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ๋ผ์šฐํŠธ ๊ฒฝ๋กœ, ๋ฉ”์‹œ์ง€, ์„ค์ •๊ฐ’ ๋“ฑ์„ ํ•œ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜์—ฌ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

src/utils

์ „์—ญ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋กœ, ์ž…๋ ฅ ๊ฒ€์ฆ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋“ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

tests

Playwright๋ฅผ ์‚ฌ์šฉํ•œ E2E ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. ์‹ค์ œ ์‚ฌ์šฉ์ž ๊ด€์ ์—์„œ ํšŒ์›๊ฐ€์ž…, ์ถœ์„ ์ฒดํฌ์ธ, ๋ถˆ์ฐธ์‚ฌ์œ ์„œ ์ œ์ถœ ๋“ฑ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ํ๋ฆ„์„ ์ž๋™ํ™”ํ•˜์—ฌ ๋ฐฐํฌ ์ „ ํ’ˆ์งˆ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.


์ฃผ์š” ์„ค๊ณ„ ์›์น™

  1. ๋„๋ฉ”์ธ ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ: ๊ธฐ๋Šฅ๋ณ„๋กœ ํด๋”๋ฅผ ๊ตฌ๋ถ„ํ•˜์—ฌ ํ™•์žฅ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ™•๋ณด
  2. ๋‹จ์ผ ์ฑ…์ž„ ์›์น™: ๊ฐ ํŒŒ์ผ/ํด๋”๋Š” ํ•˜๋‚˜์˜ ๋ช…ํ™•ํ•œ ์ฑ…์ž„์„ ๊ฐ€์ง
  3. ํƒ€์ž… ์•ˆ์ •์„ฑ: TypeScript๋ฅผ ์ฒ ์ €ํžˆ ํ™œ์šฉํ•˜์—ฌ ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ ์‚ฌ์ „ ๋ฐฉ์ง€
  4. ์„ฑ๋Šฅ ์ตœ์ ํ™”: Next.js์˜ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”, ๋™์  import, ์ฝ”๋“œ ๋ถ„ํ•  ํ™œ์šฉ
  5. ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ: ๋กœ์ง์„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ถ„๋ฆฌํ•˜์—ฌ E2E ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ทน๋Œ€ํ™”

๐Ÿ“ˆ ๊ฐœ์„  ๊ฒฝํ—˜

1๏ธโƒฃ ์ถœ์„์ฒดํฌ ํŽ˜์ด์ง€ ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์†๋„๊ฐ€ ๋А๋ฆฌ๋‹ค

๋ชฉ์ฐจ ์„ค๋ช…
๋ฌธ์ œ ์ƒํ™ฉ ์ €ํฌ ์„œ๋น„์Šค ๋ฉ”์ธ ๋กœ์ง์ธ ์ถœ์„์ฒดํฌ ํŽ˜์ด์ง€์—์„œ ๋‚˜์˜ ํ”„๋กœํ•„์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ด๋ฏธ์ง€ ๋ถ€๋ถ„์˜ ๋กœ๋”ฉ ์†๋„๊ฐ€ ํ˜„์ €ํ•˜๊ฒŒ ๋–จ์–ด์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.

LCP 29.6์ดˆ, ์‹ค์งˆ์ ์ธ ์ฒด๊ฐ์œผ๋กœ๋Š” 5-6์ดˆ ์ •๋„ ์ดํ›„ ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ ๋˜๋Š” ๋ฌธ์ œ์ ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ถฉ๋ถ„ํžˆ ์‚ฌ์šฉ์ž๊ฐ€ ๋А๋ผ๊ธฐ๋„ "๋А๋ฆฌ๋‹ค" ํŒ๋‹จ๋  ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•˜์˜€๊ณ , ์ด ๋•Œ๋ฌธ์— ์ „์ฒด์ ์ธ ์•ฑ์„œ๋น„์Šค ์™„์„ฑ๋„๊ฐ€ ๋–จ์–ด์ ธ๋ณด์ด๋Š” ๋ฌธ์ œ์ ์ด ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ๊ฐœ์„ ์„ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค. image
ํ•ด๊ฒฐ ๊ณผ์ • ์ฒ˜์Œ์—๋Š” Image ํƒœ๊ทธ ์†์„ฑ์˜ fetchPriority={'high'} ์™€ ์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ๋ฅผ ์ง€์ •ํ•˜์—ฌ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”๋ฅผ ์ง„ํ–‰ํ•ด๋ณด์•˜๊ณ , next.config.js ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ง• ์„ค์ •, ์บ์‹œ ์„ค์ •์„ ์ง„ํ–‰ํ•˜์—ฌ LCP ์ง€์ˆ˜๋ฅผ ๊ฐ์†Œ์‹œํ‚ค๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840]
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384]
minimumCacheTTL: 60 * 60 * 24 * 365

ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ๋™์ผํ•œ ์„ฑ๋Šฅ ์ง€์ˆ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๊ณ  ์žˆ์—ˆ๊ณ , Image ํƒœ๊ทธ ์†์„ฑ๋งŒ์œผ๋กœ๋Š” ์ด๋ฏธ์ง€ ์ตœ์ ํ™”๊ฐ€ ์–ด๋ ต๊ฒ ๋‹ค๋Š” ํŒ๋‹จ์„ ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Cloudinary, ImageKit, UploadCare์™€ ๊ฐ™์€ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ์„ ๊ณ ๋ คํ–ˆ์œผ๋‚˜, ๊ทผ๋ณธ์ ์ธ ๋ฌธ์ œ๋ฅผ ํฌ์žฅํ•˜๊ธฐ ์œ„ํ•ด ๋˜ ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ๊ณผ์—ฐ ์˜ฌ๋ฐ”๋ฅธ ํ•ด๊ฒฐ์ฑ…์ธ๊ฐ€ ํ•˜๋Š” ์˜๋ฌธ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์‚ฌ๊ณ ๋ฅผ ์ „ํ™˜ํ•˜์—ฌ "์•„์˜ˆ ์—…๋กœ๋“œ ๋‹จ๊ณ„์—์„œ ์ด๋ฏธ์ง€ ์šฉ๋Ÿ‰์„ ์ค„์—ฌ๋ณผ๊นŒ?" ํ•˜๋Š” ์ ‘๊ทผ์„ ์‹œ๋„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ €ํฌ ์„œ๋น„์Šค๋Š” ์ถœ์„์ฒดํฌ, ์ƒ๋ฒŒ์ , ์„ธ์…˜ ์žฅ์†Œ ๊ณต์ง€ ๋“ฑ์ด ์ฃผ์š” ๊ธฐ๋Šฅ์ด์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€๋Š” ์ค‘์š”๋„๊ฐ€ ๋‚ฎ์•˜์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์ถœ์„์ฒดํฌ ํŽ˜์ด์ง€์˜ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ๋ชจ๋ฐ”์ผ ํ™”๋ฉด์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋ฏ€๋กœ ํฐ ํฌ๊ธฐ์™€ ์šฉ๋Ÿ‰์ด ํ•„์š”ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

[1] ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ง•
์ข…ํšก๋น„ ์œ ์ง€ํ•˜๋ฉฐ ์ตœ๋Œ€ ํฌ๊ธฐ ์ œํ•œ:
let { width, height } = img
const aspectRatio = width / height
if (width > maxWidth) { width = maxWidth; height = width / aspectRatio }
if (height > maxHeight) { height = maxHeight; width = height * aspectRatio }
๐Ÿ“Š ํšจ๊ณผ: 4000ร—3000 โ†’ 1920ร—1080 ์ถ•์†Œ ์‹œ ํŒŒ์ผ ํฌ๊ธฐ ๋Œ€ํญ ๊ฐ์†Œ

[2] Canvas ํ™”์งˆ ์ตœ์ ํ™”
๋ฆฌ์‚ฌ์ด์ง• ์‹œ ์ด๋ฏธ์ง€ ํ’ˆ์งˆ ๋ณด์กด:
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = 'high'
๐Ÿ“Š ํšจ๊ณผ: ํ’ˆ์งˆ ์ €ํ•˜๋กœ ์ธํ•œ ์žฌ์••์ถ• ๋ฐฉ์ง€, ๋‹จ์ผ ์ตœ์ ํ™” ๋‹จ๊ณ„๋กœ ํšจ์œจํ™”

[3] Quality ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ์–ด
JPEG ์••์ถ•๋ฅ  ์ตœ์ ํ™”: quality = 0.8
๐Ÿ“Š ํšจ๊ณผ: ์ง€๊ฐํ•  ์ˆ˜ ์—†๋Š” ์ˆ˜์ค€์˜ ์†์‹ค ์••์ถ•์œผ๋กœ ํŒŒ์ผ ํฌ๊ธฐ ์ตœ์ ํ™”

[4] ์ •์ˆ˜ ๋ฐ˜์˜ฌ๋ฆผ์œผ๋กœ ๋ Œ๋”๋ง ์ตœ์ ํ™”
์†Œ์ˆ˜์  ์บ”๋ฒ„์Šค ํฌ๊ธฐ ์ œ๊ฑฐ:
canvas.width = Math.round(width)
canvas.height = Math.round(height)
๐Ÿ“Š ํšจ๊ณผ: ๋ Œ๋”๋ง ์„ฑ๋Šฅ ํ–ฅ์ƒ, ์บ”๋ฒ„์Šค ๊ทธ๋ฆฌ๊ธฐ ์‹œ๊ฐ„ ๋‹จ์ถ• (29์ดˆ โ†’ 18์ดˆ) image

[5] WebP ๋ณ€ํ™˜
ํ™•์žฅ์ž๋ฅผ JPEG/PNG์—์„œ WebP๋กœ ๋ณ€๊ฒฝ:
if (shouldConvertToWebP) { blob = await convertToWebP(canvas, quality) }
๐Ÿ“Š ํšจ๊ณผ: 20~30% ํฌ๊ธฐ ๊ฐ์†Œ๋กœ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ ์‹œ๊ฐ„ ๋‹จ์ถ•
๊ฒฐ๊ณผ LCP ์„ฑ๋Šฅ ์ง€์ˆ˜ ๊ฐœ์„ : 29.6์ดˆ โ†’ 18.8์ดˆ โ†’ 13.8์ดˆ

์—…๋กœ๋“œ ๋‹จ๊ณ„์—์„œ์˜ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ง•, Canvas ํ™”์งˆ ์ตœ์ ํ™”, Quality ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ์–ด, ์ •์ˆ˜ ๋ฐ˜์˜ฌ๋ฆผ, WebP ๋ณ€ํ™˜ ๋“ฑ ๋‹ค์–‘ํ•œ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์กฐํ•ฉํ•˜์—ฌ 74% ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ๋‹ฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๋ถ„๋“ค์˜ ์ ๊ทน์ ์ธ ํ˜‘๋ ฅ์œผ๋กœ WebP ํ™•์žฅ์ž ์ง€์›์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ ์‹œ๊ฐ„์„ ์ถ”๊ฐ€๋กœ ๋‹จ์ถ•ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ๋„ ํ•™ํšŒ์›๋ถ„๋“ค, ๋ฉ˜ํ† ๋ง, AI ๋“ฑ์„ ํ†ตํ•ด ์ถ”๊ฐ€์ ์ธ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๋ฐฉ์•ˆ์„ ์ง€์†์ ์œผ๋กœ ์—ฐ๊ตฌ ์ค‘์ž…๋‹ˆ๋‹ค. image

2๏ธโƒฃ ์ถœ์„์ฒดํฌ ํŽ˜์ด์ง€ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๊ฐ€ ๋А๋ฆฌ๋‹ค

๋ชฉ์ฐจ ์„ค๋ช…
๋ฌธ์ œ ์ƒํ™ฉ IT ๊ฒฝ์˜ํ•™ํšŒ ํ์‹œ์ฆ˜์˜ ์ถœ์„์ฒดํฌ ์•ฑ์„œ๋น„์Šค ๊ตฌํ˜„ ์ค‘, ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์ถœ์„์ฒดํฌ ํŽ˜์ด์ง€์˜ FCP(First Contentful Paint)๊ฐ€ ํ˜„์ €ํ•˜๊ฒŒ ์ €ํ•˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์‹ค์ œ ์‚ฌ์šฉ์ž๊ฐ€ QR์ฝ”๋“œ๋ฅผ ์ธ์‹ํ•˜๋ ค๋ฉด ์•„๋ฌด๊ฒƒ๋„ ๋ณด์ด์ง€ ์•Š์€ ์ƒํƒœ๋กœ 3-4์ดˆ ์ •๋„ ๋Œ€๊ธฐํ•ด์•ผ ํ•˜๊ณ , ํ™”๋ฉด์ด ๋กœ๋”ฉ๋˜๋ฉด QR์ฝ”๋“œ๋ฅผ ์ธ์‹ํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋Š” ์‚ฌ์šฉ์ž์˜ ๋ถˆํŽธ์„ ์•ผ๊ธฐํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, FCP ์„ฑ๋Šฅ ์ฆ๊ฐ€ ๋ฐ ๋กœ๋”ฉ ์ค‘ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ ์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค. image
ํ•ด๊ฒฐ ๊ณผ์ • [1] Dynamic Import - QRCode ๋น„๋™๊ธฐ ๋กœ๋“œ
qrcode.react ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์•ฝ 20-30KB ์šฉ๋Ÿ‰์„ ๊ฐ€์ง€๊ณ  ์žˆ์—ˆ๊ณ , ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ๋ฌด์กฐ๊ฑด ๋‹ค์šด๋กœ๋“œ๋˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ์„  ์ „: import { QRCodeSVG } from 'qrcode.react' (๋ฉ”์ธ ๋ฒˆ๋“ค์— ํฌํ•จ)

๊ฐœ์„  ํ›„: const QRCodeSVG = dynamic(() => import('qrcode.react').then(...), { ssr: false, loading: () => <์Šค์ผˆ๋ ˆํ†คUI /> })

ํšจ๊ณผ:
- ํŽ˜์ด์ง€ ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ QRCodeSVG ์ฝ”๋“œ ์ œ์™ธ
- ๋ Œ๋”๋ง ์‹œ ์Šค์ผˆ๋ ˆํ†ค UI ๋จผ์ € ํ‘œ์‹œ
- ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋น„๋™๊ธฐ ๋‹ค์šด๋กœ๋“œ
- ๋กœ๋“œ ์™„๋ฃŒ ํ›„ ์Šค์ผˆ๋ ˆํ†ค์ด ์‹ค์ œ QRCode๋กœ ๊ต์ฒด
๐Ÿ“Š ๋กœ๋”ฉ ์ค‘ ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€ ๋ณ€ํ™”๋ฅผ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์–ด ๋ถˆ์•ˆ๊ฐ ์ œ๊ฑฐ

[2] useCallback - ํ•จ์ˆ˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜
๊ฐœ์„  ์ „: const startTimer = (expAtValue: string) => { ... }
useEffect ๋‚ด expAt, token ๋ณ€๊ฒฝ ์‹œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ํ•จ์ˆ˜ ๊ฐ์ฒด ์ƒ์„ฑ

๊ฐœ์„  ํ›„: const startTimer = useCallback((expAtValue: string) => { ... }, [])
๐Ÿ“Š ํšจ๊ณผ: ํ•จ์ˆ˜ ์ฐธ์กฐ ์œ ์ง€, ์ž์‹ ์ปดํฌ๋„ŒํŠธ์˜ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€, ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ ์ฆ๊ฐ€

[3] useMemo - ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ์บ์‹ฑ
๊ฐœ์„  ์ „: ๋‚จ์€ ์ดˆ(remainingSeconds)๊ฐ€ ๋งค์ดˆ ์—…๋ฐ์ดํŠธ๋  ๋•Œ๋งˆ๋‹ค qrData = JSON.stringify({ token: tokenData.token }) ์žฌ๊ณ„์‚ฐ
10๋ถ„ ์ง„ํ–‰ ์‹œ 600๋ฒˆ์˜ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐœ์ƒ

๊ฐœ์„  ํ›„: const qrData = useMemo(() => (tokenData?.token ? JSON.stringify(...) : ''), [tokenData?.token])
๐Ÿ“Š ํšจ๊ณผ: token ๋ณ€๊ฒฝ ์‹œ๋งŒ qrData ์žฌ๊ณ„์‚ฐ, remainingSeconds ์—…๋ฐ์ดํŠธ ์‹œ ์บ์‹ฑ๋œ ๊ฐ’ ์‚ฌ์šฉ์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง ์ œ๊ฑฐ
๊ฒฐ๊ณผ FCP ์„ฑ๋Šฅ ํ–ฅ์ƒ: 5.6์ดˆ โ†’ 1.7์ดˆ

LightHouse ๊ธฐ์ค€ ์„ฑ๋Šฅ์ง€์ˆ˜: 13% ๊ฐœ์„ 

Dynamic Import, useCallback, useMemo๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๋ฅผ ํš๊ธฐ์ ์œผ๋กœ ๊ฐœ์„ ํ•˜๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๋”ฉ ์ค‘์ž„์„ ์ธ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ์Šค์ผˆ๋ ˆํ†ค UI๋กœ ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ถœ์„์ฒดํฌ ํ”„๋กœ์„ธ์Šค์˜ ์‚ฌ์šฉ์„ฑ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. image

3๏ธโƒฃ E2E ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€ ํƒ€์ž… ์•ˆ์ •ํ™”

๋ชฉ์ฐจ ์„ค๋ช…
๋ฌธ์ œ ์ƒํ™ฉ UT์„ธ์…˜ ์ค€๋น„๋ฅผ ์œ„ํ•ด QA๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๊ณผ์ •์—์„œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์‚ฌ์šฉ์ž ์ž…๋ ฅ๋“ค์ด ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฆ„ ํ•„๋“œ: ํ™ฉ์œ ๋ฆผ**><**, ใ…ใ…‡ใ…ใ„ด, ๋„ˆ๋ฌด ๊ธด ์ด๋ฆ„
์ „ํ™”๋ฒˆํ˜ธ ํ•„๋“œ: 101-421 (๋ถˆ์™„์ „ํ•œ ์ž…๋ ฅ)
์•…์˜์  ์ž…๋ ฅ: <script></script> (XSS ๊ณต๊ฒฉ ์‹œ๋„)

๋†๋‹ด์‹์œผ๋กœ ์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ๋ฅผ ๋„ฃ๊ธฐ๋„ ํ–ˆ์ง€๋งŒ, ์‹ค์ œ ์„œ๋น„์Šค ์‚ฌ์šฉ ์ค‘ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅด๊ฒŒ ์ž…๋ ฅํ•˜๋Š” ์‚ฌ์šฉ์ž๋Š” ๋ถ„๋ช…ํžˆ ๋ฐœ์ƒํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ณ€์ˆ˜๋“ค์„ ์ตœ๋Œ€ํ•œ ์ œ์–ดํ•˜๊ณ  ๋Œ€์‘ํ•˜๊ธฐ ์œ„ํ•ด E2E ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋„์ž…์˜ ํ•„์š”์„ฑ์„ ์ธ์‹ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
ํ•ด๊ฒฐ ๊ณผ์ • ๋‹ค์–‘ํ•œ ํ…Œ์ŠคํŠธ ๋„๊ตฌ ์ค‘ Playwright ๊ธฐ๋ฐ˜์˜ E2E ํ…Œ์ŠคํŠธ๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์œ ๋Š” ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ง์ ‘ ์„ค์ •ํ•˜์—ฌ ์‹ค์ œ ์‚ฌ์šฉ์ž ํ๋ฆ„๋Œ€๋กœ ์„œ๋น„์Šค๊ฐ€ ๋ฌธ์ œ์—†์ด ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

ํšŒ์›๊ฐ€์ž…์€ Step๋ณ„๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์—ˆ๊ณ  (Step 1: ์ด๋ฆ„ ์ž…๋ ฅ, Step 2: ํ•™๊ณผ ์ž…๋ ฅ ๋“ฑ), ๊ฐ Step์—์„œ ์š”๊ตฌ๋˜๋Š” ๊ธฐ๋Šฅ๋“ค์„ ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ณผํ•œ ํ…Œ์ŠคํŠธ๋ณด๋‹ค๋Š” ๋ชฉ์  ์ง€ํ–ฅ์ ์ธ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ: ๋‹ค๋ฅธ ๊ธฐ๋Šฅ ๊ตฌํ˜„์ด ๋‚จ์•„์žˆ์–ด ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๋ฅผ ํ”ผํ•˜๊ณ , ๋‹ค์–‘ํ•œ ์ž…๋ ฅ์„ ํ•˜๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์ œ์–ดํ•˜๊ธฐ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์— ์ง‘์ค‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ํ›„ ๋ฐœ๊ฒฌ๋œ ์ด์Šˆ: disable ์ฒ˜๋ฆฌ, error ๋กœ์ง ๋ฏธํก ๋“ฑ์ด ๋“œ๋Ÿฌ๋‚ฌ๊ณ , ํ…Œ์ŠคํŠธ์—์„œ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ ์ผ€์ด์Šค๋“ค์„ ์˜ˆ์™ธ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

[1] ์ด๋ฆ„ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
validateName(name): ๋นˆ ๊ฐ’ ์ฒดํฌ, ์ตœ์†Œ/์ตœ๋Œ€ ๊ธธ์ด(1-100์ž) ๊ฒ€์ฆ, ํ•œ๊ธ€/์˜๋ฌธ/๊ณต๋ฐฑ๋งŒ ํ—ˆ์šฉ
ํŠน์ˆ˜๋ฌธ์ž ์ œ๊ฑฐ: /^[๊ฐ€-ํžฃa-zA-Z\s]+$/ ์ •๊ทœ์‹์œผ๋กœ ํ•œ๊ธ€, ์˜๋ฌธ๋งŒ ํ—ˆ์šฉ
๐Ÿ“Š ํšจ๊ณผ: ์˜ˆ์ƒ ๋ฐ–์˜ ์ด๋ฆ„ ์ž…๋ ฅ ์ฐจ๋‹จ

[2] ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
isValidPhoneNumber(phoneNumber): 010, 02~064 ์ง€์—ญ๋ฒˆํ˜ธ ํฌํ•จ ๋ชจ๋“  ์œ ํšจํ•œ ํ˜•์‹ ๊ฒ€์ฆ
์ •๊ทœ์‹: `/^(010-\d{4}-\d{4}
๊ฒฐ๊ณผ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ์œจ: 64% โ†’ 100%

image image ์ฒด๊ณ„์ ์ธ ์ž…๋ ฅ ๊ฒ€์ฆ์œผ๋กœ ํšŒ์›๊ฐ€์ž… ํ”„๋กœ์„ธ์Šค์˜ ๋ชจ๋“  ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ์ž…๋ ฅ์„ ๋ฐ›๋Š” ๊ธฐ๋Šฅ 2๊ฐ€์ง€ ์ค‘ ํšŒ์›๊ฐ€์ž…์— ์ด์–ด ๋‹ค๋ฅธ ์ž…๋ ฅ ๊ธฐ๋Šฅ์—๋„ ๋™์ผํ•œ E2E ํ…Œ์ŠคํŠธ๋ฅผ ์ ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค.

โš’๏ธ ์‚ฌ์šฉ ์•„ํ‚คํ…์ณ

๐Ÿ“ฒ BBF ํŒจํ„ด (Backend-Based Fetching)

ํ•ต์‹ฌ

ํด๋ผ์ด์–ธํŠธ โ†’ Route Handler โ†’ ๋ฐฑ์—”๋“œ (ํ† ํฐ์€ ์„œ๋ฒ„์— ๋ณด๊ด€, XSS ๊ณต๊ฒฉ ๋ฐฉ์ง€)


Route Handler ์ž‘์„ฑ ํŒจํ„ด

GET

export async function GET() {
  const { data, error } = await apiCallServer('/v1/endpoint', {
    method: 'GET',
  })
  if (error) return Response.json({ error }, { status: 400 })
  return Response.json({ success: true, data })
}

POST with ๊ฒ€์ฆ

export async function POST(request: Request) {
  const body = await request.json()
  if (!body.id) return Response.json({ error: 'id required' }, { status: 400 })
  
  const { data, error } = await apiCallServer('/v1/endpoint', {
    method: 'POST',
    body: JSON.stringify(body),
  })
  if (error) return Response.json({ error }, { status: 400 })
  return Response.json({ success: true, data })
}

๋™์  ๋ผ์šฐํŠธ

export async function GET(
  { params }: { params: { id: string } }
) {
  if (!params.id) return Response.json({ error: 'id required' }, { status: 400 })
  
  const { data, error } = await apiCallServer(`/v1/endpoint/${params.id}`, {
    method: 'GET',
  })
  if (error) return Response.json({ error }, { status: 400 })
  return Response.json(data)
}

ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ

// โœ… ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ
const res = await fetch('/api/absence', { method: 'POST' })

// โŒ ์ ˆ๋Œ€ ๊ธˆ์ง€
import { apiCallServer } from '@/lib/api.server'  // ์—๋Ÿฌ!

ํด๋” ๊ตฌ์กฐ

src/app/api/
โ”œโ”€โ”€ absence/route.ts
โ”œโ”€โ”€ absence/manage/route.ts
โ”œโ”€โ”€ absence/manage/[sessionId]/route.ts
โ”œโ”€โ”€ attendance/...
โ”œโ”€โ”€ auth/cookies/route.ts
โ””โ”€โ”€ points/manage/...

src/lib/
โ”œโ”€โ”€ api.server.ts        # ํ•ต์‹ฌ ํ•จ์ˆ˜
โ””โ”€โ”€ auth.server.ts       # ํ† ํฐ ๊ด€๋ฆฌ

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •