๐ฏ ์ ์ ๊ฐ๋ฐ์ ์จ๋ณด๋ฉ ๊ฐ์ด๋ โ
ํ์ํฉ๋๋ค! ์ด ๋ฌธ์๋ GMD Soft Platform Frontend ํ๋ก์ ํธ์ ์ฒ์ ํฉ๋ฅํ ๊ฐ๋ฐ์๊ฐ ์ฒซ ์ฃผ ๋์ ๋ฐ๋ผ๊ฐ ์ ์๋ ๋จ๊ณ๋ณ ๊ฐ์ด๋์ ๋๋ค.
๐ ๋ชฉ์ฐจ โ
- ๊ฐ๋ฐ ํ๊ฒฝ ์ค์
- ํ๋ก์ ํธ ์ดํดํ๊ธฐ
- ์์ ์์
- ์์ฃผ ํ๋ ์์ ํจํด
- ํธ๋ฌ๋ธ์ํ
- ๋์ ๋ฐ๊ธฐ
๊ฐ๋ฐ ํ๊ฒฝ ์ค์ โ
1. ํ์ ๋๊ตฌ ์ค์น โ
Node.js ๋ฐ Yarn โ
# Node.js ๋ฒ์ ํ์ธ (18 ์ด์ ๊ถ์ฅ)
node -v
# Yarn ์ค์น
npm install -g yarn
# Yarn ๋ฒ์ ํ์ธ
yarn -vGit ์ค์ โ
# Git ์ฌ์ฉ์ ์ ๋ณด ์ค์
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"VS Code ์ค์น โ
ํ์ VS Code ํ์ฅ ํ๋ก๊ทธ๋จ:
- ESLint - ์ฝ๋ ํ์ง ๊ฒ์ฌ
- Prettier - ์ฝ๋ ํฌ๋งทํ
- Vanilla Extract - CSS-in-JS ๋ฌธ๋ฒ ๊ฐ์กฐ
- SonarQube for IDE - ์ฝ๋ ๋ถ์
2. ํ๋ก์ ํธ ํด๋ก ๋ฐ ์ค์ โ
# ํ๋ก์ ํธ ํด๋ก
git clone https://github.com/gmdsoft/md.platform.frontend
cd frontend
# ์์กด์ฑ ์ค์น
yarn install
# ํ๊ฒฝ ๋ณ์ ์ค์
cp .env.example .env
# .env ํ์ผ์ ์ด์ด์ ํ์ํ ๊ฐ ์ค์ (ํ ๋ด ํ๋ก ํธ์๋ ๊ฐ๋ฐ์์๊ฒ ๋ฌธ์)
# ๋ก์ปฌ ๊ฐ๋ฐ ์๋ฒ ์คํ
npm run local๋ธ๋ผ์ฐ์ ์์ http://localhost:5173 ์ ์ํ์ฌ ์ฑ์ด ์ ์์ ์ผ๋ก ์คํ๋๋์ง ํ์ธํ์ธ์.
3. Storybook ์คํ โ
# Storybook ์คํ
npm run storybook๋ธ๋ผ์ฐ์ ์์ http://localhost:6006 ์ ์ํ์ฌ ๋์์ธ ์์คํ
์ ํ์ธํ์ธ์.
4. VS Code ์ค์ โ
.vscode/settings.json ํ์ผ์ด ํ๋ก์ ํธ์ ํฌํจ๋์ด ์์ด ์๋์ผ๋ก ์ ์ฉ๋ฉ๋๋ค.
๊ถ์ฅ ์ค์ :
- Format On Save: ํ์ฑํ
- Auto Save: onFocusChange
- Tab Size: 2
ํ๋ก์ ํธ ์ดํดํ๊ธฐ โ
1. ๋ฌธ์ ์ฝ๊ธฐ โ
https://d1u1qy5bpe3a3.cloudfront.net/
- ํ๋ก ํธ์๋ ๊ฐ๋ฐ ๋ฌธ์
- TypeScript ์ปจ๋ฒค์
- React ์ปจ๋ฒค์
- ํ์ผ ๋ค์ด๋ฐ ๊ท์น
- ์๋ฌ ์ฒ๋ฆฌ
- ์ํ ๊ด๋ฆฌ
2. Storybook์ผ๋ก ๋์์ธ ์์คํ ํ์ต โ
Storybook์ ์ด์ด์ ๋ค์ ์ปดํฌ๋ํธ๋ค์ ์ดํด๋ณด์ธ์:
Atoms (๊ธฐ๋ณธ ์ปดํฌ๋ํธ):
- Button: ๋ค์ํ variant (primary, secondary, danger ๋ฑ)
- Input: ์ ๋ ฅ ํ๋
- Card: ์นด๋ ์ปจํ ์ด๋
- Typography: ํ ์คํธ ์คํ์ผ
Molecules (์กฐํฉ ์ปดํฌ๋ํธ):
- Modal: ๋ชจ๋ฌ ๋ค์ด์ผ๋ก๊ทธ
- Table: ํ ์ด๋ธ (๊ฐ์ํ ์ง์)
- Select: ๋๋กญ๋ค์ด ์ ํ
Organisms (๋ณต์กํ ์ปดํฌ๋ํธ):
- VideoPlayer: ๋น๋์ค ํ๋ ์ด์ด
- Toast: ์๋ฆผ ๋ฉ์์ง
3. ์ฃผ์ ๊ธฐ์ ์คํ ํ์ต โ
ํ๋ก์ ํธ์์ ์ฌ์ฉํ๋ ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ตํ์ธ์:
ํ์ ํ์ต:
- React 18 - ๊ธฐ๋ณธ React ํ (useState, useEffect, useMemo, useCallback)
- TypeScript - ํ์ ์์คํ
- TanStack Query - ์๋ฒ ์ํ ๊ด๋ฆฌ (useQuery, useMutation)
- Vanilla Extract - CSS-in-JS
- React Router - ๋ผ์ฐํ
์ ํ ํ์ต:
- Vitest - ํ ์คํธ ํ๋ ์์ํฌ
- Storybook - ์ปดํฌ๋ํธ ๋ฌธ์ํ
- Feature-Sliced Design - ์ํคํ ์ฒ ํจํด
5. ์ฝ๋ ํ์ โ
๋ค์ ํ์ผ๋ค์ ์ง์ ์ด์ด์ ์ฝ๋๋ฅผ ์ฝ์ด๋ณด์ธ์:
1. ๊ฐ๋จํ ์ปดํฌ๋ํธ:
src/shared/ui/atoms/Button/Button.tsx- ๊ธฐ๋ณธ ๋ฒํผ ์ปดํฌ๋ํธsrc/shared/ui/atoms/Button/Button.css.ts- Vanilla Extract ์คํ์ผ
2. ์ปค์คํ ํ :
src/shared/hooks/useToast.ts- Toast ์๋ฆผ ํsrc/features/user/model/useForm.ts- ํผ ๊ด๋ฆฌ ํ
3. API ํธ์ถ:
src/entities/user/api/user.api.ts- User API ํจ์src/entities/user/api/user.dto.ts- DTO ํ์src/shared/api/base.ts- Axios ์ธํฐ์ ํฐ
4. ํ์ด์ง:
src/pages/SignIn/SignIn.tsx- ๋ก๊ทธ์ธ ํ์ด์งsrc/pages/Home/Home.tsx- ํ ํ์ด์ง
์์ ์์ โ
1. ๊ฐ๋จํ ์ด์ ์ ํ โ
์ฒ์์๋ ๋ค์๊ณผ ๊ฐ์ ๊ฐ๋จํ ์์ ์ผ๋ก ์์ํ์ธ์:
- UI ์ปดํฌ๋ํธ์ ์๋ก์ด variant ์ถ๊ฐ
- ๊ธฐ์กด ์ปดํฌ๋ํธ์ props ์ถ๊ฐ
- ๊ฐ๋จํ ๋ฒ๊ทธ ์์
- ๋ฌธ์ ์์
์ถ์ฒ ์ฒซ ์ด์:
- Button ์ปดํฌ๋ํธ์ ์๋ก์ด size variant ์ถ๊ฐ
- Input ์ปดํฌ๋ํธ์ icon props ์ถ๊ฐ
- ์์ ์คํ์ผ ๊ฐ์
2. ๋ธ๋์น ์์ฑ โ
# ์ต์ main ๋ธ๋์น๋ก ์ฒดํฌ์์
git checkout main
git pull origin main
# ์ ๋ธ๋์น ์์ฑ
git checkout -b feature/add-button-size-variant
# ๋ธ๋์น ๋ค์ด๋ฐ ๊ท์น:
# - feature/* : ์๋ก์ด ๊ธฐ๋ฅ
# - fix/* : ๋ฒ๊ทธ ์์
# - refactor/*: ๋ฆฌํฉํ ๋ง
# - test/* : ํ
์คํธ ์ถ๊ฐ/์์
# - chore/* : ๊ธฐํ ์์
# - docs/* : ๋ฌธ์ ์์
# - style/* : ์คํ์ผ/ํฌ๋งทํ
# - build/* : ๋น๋ ๊ด๋ จ ์์
3. ์ฝ๋ ์์ฑ โ
์์: Button ์ปดํฌ๋ํธ์ size variant ์ถ๊ฐ
Step 1: ํ์ ์ ์
// src/shared/ui/atoms/Button/Button.tsx
type ButtonSize = 'small' | 'medium' | 'large';
type ButtonProps = Readonly<{
size?: ButtonSize;
// ... ๊ธฐ์กด props
}>;Step 2: ์คํ์ผ ์ถ๊ฐ
// src/shared/ui/atoms/Button/Button.css.ts
export const small = style({
padding: `${token.layout.spacing.size80} ${token.layout.spacing.size120}`,
fontSize: '14px',
});
export const medium = style({
padding: `${token.layout.spacing.size120} ${token.layout.spacing.size160}`,
fontSize: '16px',
});
export const large = style({
padding: `${token.layout.spacing.size160} ${token.layout.spacing.size200}`,
fontSize: '18px',
});Step 3: ์ปดํฌ๋ํธ์ ์ ์ฉ
export default function Button({ size = 'medium', ...props }: ButtonProps) {
const sizeClass = size === 'small' ? styles.small : size === 'large' ? styles.large : styles.medium;
return (
<button className={`${styles.base} ${sizeClass}`}>
{props.children}
</button>
);
}Step 4: Storybook ์คํ ๋ฆฌ ์ถ๊ฐ
// src/shared/ui/atoms/Button/Button.stories.tsx
export const AllSizes: Story = {
render: () => (
<Flex gap={8}>
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
<Button size="large">Large</Button>
</Flex>
),
};4. ํ ์คํธ ์คํ โ
# ๋ฆฐํธ ๊ฒ์ฌ
npm run lint
# ํ์
์ฒดํฌ
npm run build
# ํ
์คํธ (ํ์์)
npm run test
# Storybook ํ์ธ
npm run storybook5. ์ปค๋ฐ ๋ฐ ํธ์ โ
# ๋ณ๊ฒฝ์ฌํญ ํ์ธ
git status
# ํ์ผ ์ถ๊ฐ
git add src/shared/ui/atoms/Button/
# ์ปค๋ฐ (Husky๊ฐ ์๋์ผ๋ก lint ๋ฐ format ์คํ)
git commit -m "feat: Button ์ปดํฌ๋ํธ์ size variant ์ถ๊ฐ"
# ํธ์
git push origin feature/add-button-size-variant6. Pull Request ์์ฑ โ
GitHub์์ Pull Request๋ฅผ ์์ฑํ์ธ์.
7. ์ฝ๋ ๋ฆฌ๋ทฐ ๋ฐ ๋จธ์ง โ
- ๋ฆฌ๋ทฐ์ด์ ํผ๋๋ฐฑ์ ๋ฐ๊ณ ์์
- Approve๋ฅผ ๋ฐ์ผ๋ฉด ๋จธ์ง
์์ฃผ ํ๋ ์์ ํจํด โ
1. ์๋ก์ด ํ์ด์ง ์ถ๊ฐ โ
# 1. ํ์ด์ง ์ปดํฌ๋ํธ ์์ฑ
src/pages/UserProfile/UserProfile.tsx
src/pages/UserProfile/UserProfile.css.ts
# 2. ๋ผ์ฐํธ ์ถ๊ฐ
src/shared/config/routes.ts
src/app/routers/index.tsx
# 3. ๋ฉ๋ด ์ถ๊ฐ (ํ์์)์์ ์ฝ๋:
// src/pages/UserProfile/UserProfile.tsx
export default function UserProfile() {
const { data: user } = useQuery({
queryKey: queryKeys.user.current(),
queryFn: getCurrentUser,
});
return (
<div>
<h1>{user?.name}</h1>
</div>
);
}
// src/shared/config/routes.ts
export const ROUTES = {
USER_PROFILE: '/user/profile',
// ...
};
// src/app/routers/index.tsx
<Route path={ROUTES.USER_PROFILE} element={<UserProfile />} />2. API ์ฐ๋ํ๊ธฐ โ
Step 1: DTO ์ ์
// entities/user/api/user.dto.ts
export type UserDto = {
id: string;
name: string;
email: string;
};Step 2: API ํจ์ ์์ฑ
// entities/user/api/user.api.ts
export async function getUser(userId: string): Promise<UserDto> {
const { data } = await api.get<UserDto>(`/api/users/${userId}`);
return data;
}Step 3: Query Key ์ถ๊ฐ
// shared/config/queryKeys.ts
export const queryKeys = {
user: {
all: ['user'] as const,
detail: (id: string) => [...queryKeys.user.all, 'detail', id] as const,
},
};Step 4: useQuery ์ฌ์ฉ
// features/user/model/useUser.ts
export function useUser(userId: string) {
return useQuery({
queryKey: queryKeys.user.detail(userId),
queryFn: () => getUser(userId),
});
}Step 5: ์ปดํฌ๋ํธ์์ ์ฌ์ฉ
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useUser(userId);
if (isLoading) return <div>๋ก๋ฉ ์ค...</div>;
if (error) return <div>์๋ฌ ๋ฐ์</div>;
return <div>{data.name}</div>;
}3. ํผ ๊ตฌํํ๊ธฐ โ
import { useForm } from '@shared/hooks';
function SignUpForm() {
const { formState, formErrors, handleInputChange, isFormValid } = useForm({
initialValues: {
email: '',
password: '',
},
validation: {
email: (value) => {
if (!value.includes('@')) return '์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ์ ์
๋ ฅํ์ธ์';
return null;
},
password: (value) => {
if (value.length < 8) return '๋น๋ฐ๋ฒํธ๋ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค';
return null;
},
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!isFormValid) return;
// API ํธ์ถ
};
return (
<form onSubmit={handleSubmit}>
<Input
value={formState.email}
error={formErrors.email}
onChange={(e) => handleInputChange('email', e.target.value)}
/>
<Input
type="password"
value={formState.password}
error={formErrors.password}
onChange={(e) => handleInputChange('password', e.target.value)}
/>
<Button type="submit">์ ์ถ</Button>
</form>
);
}4. ์๋ฌ ์ฒ๋ฆฌํ๊ธฐ โ
import { useMutation } from '@tanstack/react-query';
import { isApiError } from '@shared/api/error-handler';
import { useToast } from '@shared/hooks';
function Component() {
const toast = useToast();
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
toast.success('์ฑ๊ณต์ ์ผ๋ก ์ ์ฅ๋์์ต๋๋ค.');
},
onError: (error) => {
if (isApiError(error)) {
toast.error(error.message);
} else {
toast.error('์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
}
},
});
return <button onClick={() => mutation.mutate(data)}>์ ์ฅ</button>;
}5. ์ ์๋ฌ ์ฝ๋ ์ถ๊ฐ โ
// entities/user/api/user.error.ts
export enum UserErrorCode {
USER_NOT_FOUND = 'USER-1000',
DUPLICATED_EMAIL = 'USER-1001',
// ์๋ก์ด ์๋ฌ ์ฝ๋ ์ถ๊ฐ
INVALID_PHONE_NUMBER = 'USER-1007',
}
export const userErrorMessages: Record<UserErrorCode, () => string> = {
// ...
[UserErrorCode.INVALID_PHONE_NUMBER]: () => '์ฌ๋ฐ๋ฅธ ์ ํ๋ฒํธ๋ฅผ ์
๋ ฅํ์ธ์.',
};// shared/api/error.ts
import { UserErrorCode } from '@entities/user/api/user.error';
export type ErrorCode =
| CommonErrorCode
| UserErrorCode
// ๋ค๋ฅธ ๋๋ฉ์ธ ์๋ฌ ์ถ๊ฐ
| VerificationErrorCode;6. ๊ณตํต ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ โ
// shared/ui/atoms/Badge/Badge.tsx
import * as styles from './Badge.css';
type BadgeProps = Readonly<{
children: React.ReactNode;
color?: 'primary' | 'secondary' | 'danger';
}>;
export default function Badge({ children, color = 'primary' }: BadgeProps) {
return <span className={`${styles.base} ${styles[color]}`}>{children}</span>;
}// shared/ui/atoms/Badge/Badge.css.ts
import { style } from '@vanilla-extract/css';
import { token } from '@shared/styles';
export const base = style({
padding: `${token.layout.spacing.size40} ${token.layout.spacing.size80}`,
borderRadius: `${token.layout.radius.sm}`,
fontSize: '12px',
fontWeight: 'bold',
});
export const primary = style({
backgroundColor: `${token.foundation.primary.base}`,
color: `${token.text.onPrimary}`,
});// shared/ui/atoms/Badge/Badge.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import Badge from './Badge';
const meta = {
title: 'Atoms/Badge',
component: Badge,
tags: ['autodocs'],
} satisfies Meta<typeof Badge>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: 'Primary',
color: 'primary',
},
};ํธ๋ฌ๋ธ์ํ โ
์์ฃผ ๋ฐ์ํ๋ ์๋ฌ โ
1. Cannot find module ์๋ฌ โ
# ์์กด์ฑ ์ฌ์ค์น
rm -rf node_modules
yarn install2. ๋ฆฐํธ ์๋ฌ โ
# ์๋ ์์
npm run lint:fix3. ํ์ ์๋ฌ โ
// โ ์๋ชป๋ ์
const user = data; // data์ ํ์
์ด ๋ช
ํํ์ง ์์
// โ
์ฌ๋ฐ๋ฅธ ์
const user: User = data;4. Storybook์ด ์คํ๋์ง ์์ โ
# Storybook ์บ์ ์ญ์
rm -rf node_modules/.cache/storybook
npm run storybook5. Git pre-commit hook ์คํจ โ
# ๋ณ๊ฒฝ์ฌํญ ํ์ธ
git status
# ๋ฆฐํธ ์๋ ์คํ
npm run lint:fix
# ๋ค์ ์ปค๋ฐ
git commit -m "..."6. API ์ฐ๋ ์๋ฌ โ
- ๋คํธ์ํฌ ํญ์์ ์์ฒญ/์๋ต ํ์ธ
.env.localํ์ผ์VITE_BASE_URLํ์ธ- ๋ฐฑ์๋ ์๋ฒ๊ฐ ์คํ ์ค์ธ์ง ํ์ธ
ํ์ดํ ! ๐