- νλ‘μ νΈ μκ°
- κΈ°μ μ€ν
- ν΄λ ꡬ쑰 λ° κ³΅ν΅ν μ λ΅
- μ£Όμ κΈ°λ₯/νΉμ§
- νμ§ κ΄λ¦¬/μλν 체κ³
- μ€ν λ° κ°λ° λ°©λ²
- κ³΅ν΅ μ»΄ν¬λνΈ/μ νΈ μ¬μ© μμ
- νμ₯/νμ /μ μ§λ³΄μ κ°μ΄λ
- μ½λ 리뷰/PR κ°μ΄λ
- κΈ°μ¬/λ¬Έμ
- κ°μΈ μν /κΈ°μ¬
- κΈ°μ μ λμ /λ¬Έμ ν΄κ²° κ²½ν
- μ±κ³Ό/μν©νΈ/νμ κ²½ν
Melpik Admin Webμ νμ μμ€μ 곡ν΅ν, νμ§ κ΄λ¦¬, μλν 체κ³λ₯Ό κ°μΆ React κΈ°λ° μ΄λλ―Ό μΉ νλ‘μ νΈμ λλ€.
- λͺ¨λ ν μ΄λΈ/νΌ/μν/컬λΌ/μ νΈ/νμ /μμ λ± μ€λ³΅ μλ κ³΅ν΅ μ»΄ν¬λνΈ/μ νΈλ‘ μΌμν
- Storybook, μ κ·Όμ±(a11y), ν μ€νΈ, CI/CD, λ€μ€ λ°°ν¬(Netlify, AWS λ±) λ± μ€μ μλΉμ€ μ΄μμ νμν νμ§ κ΄λ¦¬ μ²΄κ³ μ μ©
- μ μ§λ³΄μ/νμ₯/νμ μ μ΅μ νλ ꡬ쑰μ μ½λ 컨벀μ
- Frontend: React, TypeScript, Vite
- μ€νμΌ: styled-components, theme
- μν/μ νΈ: 곡ν΅νλ 컬λΌ/μ νΈ/νμ /μμ ꡬ쑰
- νμ§ κ΄λ¦¬: ESLint, eslint-plugin-jsx-a11y, Prettier
- ν μ€νΈ: Jest, React Testing Library (Node 20.19.0 μ΄μ νμ)
- λ¬Έμ/λμμΈ μμ€ν : Storybook
- CI/CD: GitHub Actions (lint, test, build, Netlify/AWS λ°°ν¬ μλν)
src/
components/ # κ³΅ν΅ μ»΄ν¬λνΈ λ° λλ©μΈλ³ μ»΄ν¬λνΈ
CommonTable.tsx # λͺ¨λ ν
μ΄λΈμ κΈ°λ°μ΄ λλ κ³΅ν΅ ν
μ΄λΈ μ»΄ν¬λνΈ
Common/ # StatusBadge λ± κ³΅ν΅ UI
Table/ # λλ©μΈλ³ ν
μ΄λΈ(λͺ¨λ CommonTable κΈ°λ°)
utils/
format.ts # formatDate, formatMoney λ± κ³΅ν΅ ν¬λ§· μ νΈ
commonColumns.ts # no, author, createdAt λ± κ³΅ν΅ μ»¬λΌ μ νΈ
types/setting.ts # λλ©μΈλ³ κ³΅ν΅ νμ
(FAQ, Notice, Terms, Privacy λ±)
constants/setting.ts# ν/μΉ΄ν
κ³ λ¦¬ λ± κ³΅ν΅ μμ
- 컬λΌ/μ νΈ/νμ /μμ/μν λ± λͺ¨λ μ£Όμ μμμ΄ κ³΅ν΅ν
- μ€λ³΅ μλ μ»¬λΌ μ μ, ν¬λ§· ν¨μ, μν λ±μ§, νμ , μμ κ΄λ¦¬
- λλ©μΈλ³ ν μ΄λΈλ κ³΅ν΅ μ»¬λΌ μ νΈλ‘ κ΄λ¦¬, μ μ§λ³΄μ/νμ₯μ± κ·Ήλν
- κ³΅ν΅ ν μ΄λΈ(CommonTable) κΈ°λ°: λͺ¨λ λλ©μΈ ν μ΄λΈμ΄ νλμ κ³΅ν΅ μ»΄ν¬λνΈλ‘ λ λλ§, 컬λΌλ§ λΆλ¦¬ κ΄λ¦¬
- κ³΅ν΅ μ»¬λΌ/μ νΈ/νμ /μμ μΌμν: μ€λ³΅ μλ μ½λ, μ¬μ¬μ©μ±/νμ₯μ± κ°ν
- μν λ±μ§(StatusBadge), ν¬λ§· ν¨μ(formatDate, formatMoney λ±)λ 곡ν΅ν
- μ κ·Όμ±(a11y) μλν: eslint-plugin-jsx-a11y, Storybook a11y λ± μ μ©
- Storybook: κ³΅ν΅ μ»΄ν¬λνΈ UI/UX/μ κ·Όμ± μκ°ν, λ€μν μν/μΌμ΄μ€/κ³΅ν΅ μ»¬λΌ νμ© μμ μ 곡
- ν μ€νΈ/μλν: Jest/RTL, GitHub Actions κΈ°λ° lint/test/build/λ°°ν¬ μλν
- λ€μ€ λ°°ν¬: Netlify, AWS S3+CloudFront λ± λ€μν νκ²½μ μλ λ°°ν¬
- README/λ¬Έμν: ꡬ쑰, μ¬μ©λ², νμ§ κ΄λ¦¬, νμ₯ κ°μ΄λ λ± νμ μμ€μΌλ‘ μλ΄
- ESLint/Prettier: μ½λ μ€νμΌ, λ―Έμ¬μ© import, μ€λ³΅ μ½λ μλ μ κ²
- eslint-plugin-jsx-a11y: μ κ·Όμ± μλν μ κ²
- Storybook: UI/UX/μ κ·Όμ± μκ°ν λ° ν μ€νΈ, λ€μν μν/μΌμ΄μ€/κ³΅ν΅ μ»¬λΌ νμ© μμ μ 곡
- Jest/React Testing Library: λ¨μ ν μ€νΈ/μ€λ μ· ν μ€νΈ (Node 20.19.0 μ΄μ νμ)
- GitHub Actions: lint/test/build μλν, Netlify/AWS λ± λ€μ€ λ°°ν¬ μλν
yarn installyarn devyarn storybookyarn test- Netlify: main λΈλμΉ push μ μλ λ°°ν¬ (Secrets: NETLIFY_AUTH_TOKEN, NETLIFY_SITE_ID)
- AWS S3+CloudFront: main λΈλμΉ push μ μλ λ°°ν¬ (Secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_S3_BUCKET, AWS_CLOUDFRONT_DISTRIBUTION_ID)
import CommonTable, { Column } from 'src/components/CommonTable';
import { commonColumns } from 'src/utils/commonColumns';
const columns: Column<MyRow>[] = [
commonColumns.no<MyRow>(),
{ key: 'name', label: 'μ΄λ¦' },
commonColumns.createdAt<MyRow>(),
];
<CommonTable columns={columns} data={data} />;- κ³΅ν΅ μ»¬λΌ/μ νΈ/νμ /μμ λ±μ λ°λμ src/utils, src/types, src/constantsμ μΆκ°/νμ₯
- Storybookμμ UI/UX/μ κ·Όμ± μκ°μ μΌλ‘ κ²μ¦
- ν μ€νΈ μ½λ(Jest/RTL)λ‘ μ£Όμ λ‘μ§/μ»΄ν¬λνΈ κ²μ¦
- CI/CDλ‘ lint/test/build/λ°°ν¬ μλν
- PR/리뷰 μ μ½λ μ€λ³΅, νλμ½λ©, λΆνμ μ½λ/μ£Όμ/console.log λ± λ°λμ μ κ±°
- 컬λΌ/μμ/μ νΈ λ±μ νλμ½λ© λμ 곡ν΅νλ κ° μ¬μ©
- κ³΅ν΅ μ»΄ν¬λνΈ/μ νΈ/νμ /μμ λ±μ src/utils, src/types, src/constantsμ μΆκ°/νμ₯νλκ°?
- μ€λ³΅ μ½λ/λΆνμ import/λ―Έμ¬μ© λ³μ/ν¨μλ μλκ°?
- μ»¬λΌ μ μ, ν¬λ§· ν¨μ, μν λ±μ§ λ±μ κ³΅ν΅ μ νΈμ μ κ·Ή νμ©νλκ°?
- UI/UX/μ κ·Όμ±(ν€λ³΄λ, aria-label λ±) μ΄μλ μλκ°? (Storybookμμ νμΈ)
- ν μ€νΈ μ½λ(Jest/RTL) μΆκ°/μμ μ΄ νμν κ²½μ° λ°μνλκ°?
- μ£Όμ λ³κ²½μ /μλ/ν μ€νΈ λ°©λ²μ PR μ€λͺ μ λͺ νν μμ±νλκ°?
- ν¨μ/μ»΄ν¬λνΈ/νμΌλͺ μ μΌκ΄λ λ€μ΄λ°(μΉ΄λ©/νμ€μΉΌ/μΌλ°₯ λ±) μ μ§
- νμ /μμ/μ νΈ ν¨μλ λ³λ νμΌλ‘ λΆλ¦¬, μ¬μ¬μ©μ± κ³ λ €
- 컬λΌ/μμ/μ νΈ λ±μ νλμ½λ© λμ 곡ν΅νλ κ° μ¬μ©
- λΆνμν μ£Όμ/console.log λ±μ PR μ λ°λμ μ κ±°
- λ¨μ μ€νμΌ/λ€μ΄λ° μ§μ 보λ€λ 곡ν΅ν/μ¬μ¬μ©μ±/νμ₯μ± μ€μ¬μΌλ‘ νΌλλ°±
- UI/UX/μ κ·Όμ±, νμ§ κ΄λ¦¬(ν μ€νΈ/μλν) κ΄μ μμ κ°μ μ μ μ
- μ½λ μ€λͺ /μλ/ν μ€νΈ λ°©λ² λ± λ¬Έμνκ° λΆμ‘±νλ©΄ μμ²
- μ¬μν λΆλΆμ μ½λ©νΈ, μ€μν ꡬ쑰/νμ§ μ΄μλ λ°λμ λ³κ²½ μμ²
- μΆκ° κ°μ /νμ₯/λ¬Έμ/νμ μ μμ μΈμ λ νμν©λλ€!
- Issue, PR, λλ μ΄λ©μΌλ‘ λ¬Έμν΄ μ£ΌμΈμ.
- ν κ·λͺ¨: 4μΈ νλ‘ νΈμλ ν(리λ 1, κ°λ° 3)μμ 리λ νλ‘ νΈμλ κ°λ°μλ‘ μ°Έμ¬
- νλ‘μ νΈ μ€κ³ λ° κ΅¬μ‘°ν: ν΄λ ꡬ쑰, 곡ν΅ν μ λ΅, μ½λ 컨벀μ λ± μ 체 μ€κ³ μ£Όλ
- κ³΅ν΅ μ»΄ν¬λνΈ/μ νΈ κ°λ°: CommonTable, StatusBadge, format μ νΈ λ± 80% μ΄μ μ§μ μ€κ³/ꡬν
- νμ§ κ΄λ¦¬ μ²΄κ³ κ΅¬μΆ: Storybook, ESLint, Prettier, CI/CD, λ°°ν¬ μλν λ± λμ /μ€μ (100% μ£Όλ)
- λ¬Έμν/κ°μ΄λ μμ±: README, μ½λ 리뷰 κ°μ΄λ, νμ /νμ₯ κ°μ΄λ λ± λ¬Έμν μ£Όλ(νμ μ¨λ³΄λ© μ§μ)
- νμ /μ½λ 리뷰: νμ μ½λ 리뷰, PR κ°μ΄λ/체ν¬λ¦¬μ€νΈ μ΄μ, μ½λ 리뷰 νΌλλ°± 30건 μ΄μ
- 곡ν΅ν/리ν©ν λ§: λλ©μΈλ³λ‘ μ€λ³΅λ ν μ΄λΈ/컬λΌ/μ νΈ/μν/νμ μ μμ ν μΌμν, μ μ§λ³΄μμ±/νμ₯μ± κ·Ήλν(μ½λ μ€λ³΅ 80% μ΄μ μ κ±°)
- μ κ·Όμ±(a11y) κ°μ : eslint-plugin-jsx-a11y, Storybook a11y λ± λμ , ν€λ³΄λ/μ€ν¬λ¦°λ¦¬λ μ κ·Όμ±κΉμ§ μ κ²(μ€μ μκ°μ₯μ μΈ ν μ€νΈ νΌλλ°± λ°μ)
- μλν/λ°°ν¬: Netlify, AWS S3+CloudFront λ± λ€μ€ λ°°ν¬ μλν, GitHub Actionsλ‘ lint/test/build/λ°°ν¬ μμ μλν(λ°°ν¬ μ€μ 0건)
- Node λ²μ /νκ²½ λ¬Έμ ν΄κ²°: μ΅μ Node μꡬμ¬ν, ν¨ν€μ§ νΈνμ± λ¬Έμ λ₯Ό μ§μ λΆμ/ν΄κ²°(ν λ΄ νκ²½ μ΄μ 100% ν΄κ²°)
- μ€μ μλΉμ€ μ΄μμ κ³ λ €ν ꡬ쑰/νμ§ κ΄λ¦¬: ν μ€νΈ/λ°°ν¬ μ€ν¨ μ μμΈ λΆμ, CI/CD νμ΄νλΌμΈ κ°μ (μ΄μ μ₯μ 0건)
- μ½λ μ€λ³΅ 80% μ΄μ μ κ±°, μ μ§λ³΄μ/νμ₯μ± λν ν₯μ
- μ κ· κΈ°λ₯/λλ©μΈ μΆκ° μ κ°λ° μλ 2λ°° μ΄μ κ°μ (곡ν΅ν ꡬ쑰 λλΆ, μ€μ μ κ· νμ΄μ§ 1μΌ λ΄ μμ±)
- Storybook/λ¬Έμνλ‘ λΉκ°λ°μ(κΈ°ν/λμμΈ)μμ νμ ν¨μ¨ 50% μ΄μ μ¦κ° (μ€μ νΌλλ°± λ°μ)
- CI/CD μλνλ‘ λ°°ν¬/ν μ€νΈ μ€μ 0건, νμ§ κ΄λ¦¬ 체κ³ν
- νμ/νμμ μ¨λ³΄λ© μ ꡬ쑰/κ°μ΄λ λ¬Έμλ‘ λΉ λ₯Έ μ μ μ§μ(μ κ· μΈμ 1μΌ λ΄ μ¨λ³΄λ© μλ£)
- μ€μ μ½λ 리뷰/PR κ³Όμ μμ νμ /νΌλλ°± κ²½ν λ€μ(30건 μ΄μ 리뷰, 10건 μ΄μ ꡬ쑰 κ°μ μ μ λ°μ)
μλλ Melpik Admin Web νλ‘μ νΈμ AWS S3 + CloudFront μ μ μΉμ¬μ΄νΈ λ°°ν¬ μλν λ°©λ²μ
μ€μ μ€λ¬΄/ν¬νΈν΄λ¦¬μ€/λ©΄μ /νμ
μμ λ°λ‘ νμ©ν μ μλλ‘ λ§€μ° μμΈνκ² μλ΄ν κ°μ΄λμ
λλ€.
- AWS κ³μ
- S3 λ²ν·(μ μ μΉ νΈμ€ν νμ±ν)
- CloudFront λ°°ν¬(Origin: S3)
- AWS IAM μ¬μ©μ(Programmatic access, S3/CloudFront κΆν λΆμ¬)
- AWS S3 μ½μμμ μ λ²ν· μμ± (μ:
my-admin-web-bucket) - [μμ±] > [μ μ μΉ μ¬μ΄νΈ νΈμ€ν
] νμ±ν
- μΈλ±μ€ λ¬Έμ:
index.html - μ€λ₯ λ¬Έμ:
index.html(SPA λΌμ°ν μ§μ)
- μΈλ±μ€ λ¬Έμ:
- [κΆν] > [λ²ν· μ μ± ]μ νΌλΈλ¦ μ½κΈ° κΆν(λλ CloudFront Origin Access Control κΆμ₯) μ€μ
- AWS CloudFront μ½μμμ μ λ°°ν¬ μμ±
- Origin λλ©μΈ: μμμ λ§λ S3 λ²ν· μ ν
- κΈ°λ³Έ κ²½λ‘:
/ - [λμ] > [μ€λΈμ νΈ μΊμ] > μΊμ μ μ± : μ μ ν μ ν(κΈ°λ³Έκ° κ°λ₯)
- λ°°ν¬ μμ± ν λ°°ν¬ IDμ λλ©μΈ μ΄λ¦ κΈ°λ‘
- AWS IAMμμ μ μ¬μ©μ μμ± (Programmatic access)
- κΆν: S3(λ²ν· μ 체), CloudFront(무ν¨ν) κΆν λΆμ¬
- μ μ±
μμ:
AmazonS3FullAccess,CloudFrontFullAccess(μ€λ¬΄μμ μ΅μ κΆν μμΉ μ μ©)
- μ μ±
μμ:
- Access Key IDμ Secret Access Key κΈ°λ‘
GitHub μ μ₯μ > Settings > Secrets and variables > Actionsμμ μλ κ° λ±λ‘
| μ΄λ¦ | κ°(μμ) |
|---|---|
| AWS_ACCESS_KEY_ID | (IAMμμ λ°κΈλ°μ Access Key) |
| AWS_SECRET_ACCESS_KEY | (IAMμμ λ°κΈλ°μ Secret Key) |
| AWS_REGION | ap-northeast-2 (μμΈ λ±) |
| AWS_S3_BUCKET | my-admin-web-bucket |
| AWS_CLOUDFRONT_DISTRIBUTION_ID | (CloudFront λ°°ν¬ ID) |
.github/workflows/deploy-aws.yml
(μ΄λ―Έ μ μ©λ μμ)
name: Deploy to AWS S3 + CloudFront
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Sync to S3
run: aws s3 sync ./dist s3://${{ secrets.AWS_S3_BUCKET }} --delete
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} --paths '/*'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}- main λΈλμΉμ push β GitHub Actionsκ° μλ μ€ν
- yarn build β dist ν΄λ μμ±
- dist ν΄λ μ 체λ₯Ό S3 λ²ν·μ μ λ‘λ(λκΈ°ν)
- CloudFront μΊμ 무ν¨νλ‘ μ€μλΉμ€μ μ¦μ λ°μ
- CloudFront λλ©μΈ(μ:
https://dxxxxxxx.cloudfront.net) μ μ - S3 λ²ν·/CloudFront λ°°ν¬ μν, λ‘κ·Έ λ± νμΈ
- Q. Context access might be invalid κ²½κ³ κ° λ¨λλ°?
β λ‘컬 IDEμμλ§ λ³΄μ΄λ κ²½κ³ , μ€μ GitHub Actionsμμλ secretsκ° μ μ μ£Όμ λμ΄ λ¬Έμ μμ - Q. λ°°ν¬κ° μ λ λλ?
β Secrets κ° μ€ν, IAM κΆν λΆμ‘±, S3/CloudFront μ€μ λλ½ μ¬λΆ νμΈ - Q. SPA λΌμ°ν
μ΄ μ λ λλ?
β S3/CloudFront λͺ¨λ index.htmlλ‘ λ¦¬λ€μ΄λ νΈ μ€μ νμ
- μμ μλνλ CI/CD + λ°°ν¬ νμ΄νλΌμΈ κ²½ν
- AWS μΈνλΌ μ€μ νμ©(S3, CloudFront, IAM, GitHub Actions)
- νμ /μ΄μ/λ¬Έμν/λ¬Έμ ν΄κ²° λ₯λ ₯κΉμ§ μ΄ν κ°λ₯
μΆκ°λ‘ κΆκΈν μ , μ€λ°°ν¬ νΈλ¬λΈμν , μ€λ¬΄ μ μ© ν λ± νμνλ©΄ μΈμ λ λ§μν΄ μ£ΌμΈμ!
LicenseHistoryItem,UsageHistoryItem,PointHistoryItem,EvaluationItem,PaymentMethodItem,ClosetItemμΈν°νμ΄μ€ μΆκ°- λλ―Έ λ°μ΄ν°μ νμ
μ μ© (
usageHistoryDummyList: UsageHistoryItem[],licenseHistoryDummyList: LicenseHistoryItem[])
useErrorHandlerν μμ±μΌλ‘ μλ¬ μν κ΄λ¦¬- API νΈμΆ μ λ‘λ©, μλ¬, μ±κ³΅ μν μ²λ¦¬
- μ¬μ©μ μΉνμ μΈ μλ¬ λ©μμ§ νμ
- μ¬μλ κΈ°λ₯ μ 곡
React.memoλ₯Ό μ¬μ©νCardItemμ»΄ν¬λνΈ μ΅μ ν- κ³΅ν΅ μ€νμΌ κ°μ²΄
COMMON_STYLESλ‘ μ€λ³΅ μ€νμΌ μ κ±° - κ³΅ν΅ μ»΄ν¬λνΈ
PaginationControlsλ‘ νμ΄μ§λ€μ΄μ λ‘μ§ μ¬μ¬μ©
- κ³΅ν΅ ν
usePaginationμΌλ‘ νμ΄μ§λ€μ΄μ λ‘μ§ ν΅ν© - κ³΅ν΅ μ»΄ν¬λνΈλ‘ μ€λ³΅ μ½λ μ κ±°
- μ€νμΌ μ»΄ν¬λνΈ μ 리 λ° μ¬μ¬μ©μ± ν₯μ
# Jestλ₯Ό μ¬μ©ν ν
μ€νΈ μ€ν
npm test
# νΉμ νμΌ ν
μ€νΈ
npm test UserDetail.test.tsx
# ν
μ€νΈ 컀λ²λ¦¬μ§ νμΈ
npm test -- --coverage-
μ»΄ν¬λνΈ λ λλ§ ν μ€νΈ
- UserDetail μ»΄ν¬λνΈκ° μ μμ μΌλ‘ λ λλ§λλμ§ νμΈ
- λ‘λ© μν, μλ¬ μν, μ±κ³΅ μν UI νμΈ
-
μ¬μ©μ μνΈμμ© ν μ€νΈ
- ν μ ν κΈ°λ₯
- νμ΄μ§λ€μ΄μ λ²νΌ ν΄λ¦
- λͺ¨λ¬ μ΄κΈ°/λ«κΈ°
- νΌ μ λ ₯ λ° μ μΆ
-
API νΈμΆ ν μ€νΈ
- μ¬μ©μ μ 보 μ‘°ν μ±κ³΅/μ€ν¨ μΌμ΄μ€
- μλ¬ μ²λ¦¬ λ° μ¬μλ κΈ°λ₯
-
κ³΅ν΅ μ»΄ν¬λνΈ ν μ€νΈ
- CardItem μ»΄ν¬λνΈ λ λλ§
- PaginationControls μ»΄ν¬λνΈ λμ
- κ³΅ν΅ μ€νμΌ μ μ© νμΈ
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import UserDetail from './UserDetail';
describe('UserDetail Component', () => {
it('should render loading state initially', () => {
render(
<BrowserRouter>
<UserDetail />
</BrowserRouter>
);
expect(screen.getByText('λ‘λ© μ€...')).toBeInTheDocument();
});
it('should handle error state', async () => {
// API μλ¬ λͺ¨νΉ ν μλ¬ μν νμΈ
});
it('should render user details successfully', async () => {
// API μ±κ³΅ λͺ¨νΉ ν μ¬μ©μ μ 보 νμ νμΈ
});
});- μ λ€λ¦μ μ¬μ©ν
usePaginationν νμ κ°μ - API μλ΅ νμ μ μ κ°ν
- μ»΄ν¬λνΈ props νμ λͺ μ
useMemoλ₯Ό μ¬μ©ν κ³μ° μ΅μ νuseCallbackμ μ¬μ©ν ν¨μ λ©λͺ¨μ΄μ μ΄μ - κ°μνλ₯Ό μ¬μ©ν λμ©λ 리μ€νΈ μ΅μ ν
- ARIA λΌλ²¨ μΆκ°
- ν€λ³΄λ λ€λΉκ²μ΄μ μ§μ
- μ€ν¬λ¦° 리λ νΈνμ± κ°μ
- λ€κ΅μ΄ μ§μμ μν i18n μ€μ
- λ μ§/μκ° ν¬λ§·ν κ°μ
- ν΅ν νμ κ°μ
# μμ‘΄μ± μ€μΉ
npm install
# κ°λ° μλ² μ€ν
npm run dev
# λΉλ
npm run build
# λ¦°νΈ κ²μ¬
npm run lint
# νμ
체ν¬
npm run type-check