Skip to content

๐ŸŽฏ ์‹ ์ž… ๊ฐœ๋ฐœ์ž ์˜จ๋ณด๋”ฉ ๊ฐ€์ด๋“œ โ€‹

ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค! ์ด ๋ฌธ์„œ๋Š” GMD Soft Platform Frontend ํ”„๋กœ์ ํŠธ์— ์ฒ˜์Œ ํ•ฉ๋ฅ˜ํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฒซ ์ฃผ ๋™์•ˆ ๋”ฐ๋ผ๊ฐˆ ์ˆ˜ ์žˆ๋Š” ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ โ€‹


๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ • โ€‹

1. ํ•„์ˆ˜ ๋„๊ตฌ ์„ค์น˜ โ€‹

Node.js ๋ฐ Yarn โ€‹

bash
# Node.js ๋ฒ„์ „ ํ™•์ธ (18 ์ด์ƒ ๊ถŒ์žฅ)
node -v

# Yarn ์„ค์น˜
npm install -g yarn

# Yarn ๋ฒ„์ „ ํ™•์ธ
yarn -v

Git ์„ค์ • โ€‹

bash
# 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. ํ”„๋กœ์ ํŠธ ํด๋ก  ๋ฐ ์„ค์ • โ€‹

bash
# ํ”„๋กœ์ ํŠธ ํด๋ก 
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 ์‹คํ–‰ โ€‹

bash
# 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/

2. Storybook์œผ๋กœ ๋””์ž์ธ ์‹œ์Šคํ…œ ํ•™์Šต โ€‹

Storybook์„ ์—ด์–ด์„œ ๋‹ค์Œ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์‚ดํŽด๋ณด์„ธ์š”:

Atoms (๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ):

  • Button: ๋‹ค์–‘ํ•œ variant (primary, secondary, danger ๋“ฑ)
  • Input: ์ž…๋ ฅ ํ•„๋“œ
  • Card: ์นด๋“œ ์ปจํ…Œ์ด๋„ˆ
  • Typography: ํ…์ŠคํŠธ ์Šคํƒ€์ผ

Molecules (์กฐํ•ฉ ์ปดํฌ๋„ŒํŠธ):

  • Modal: ๋ชจ๋‹ฌ ๋‹ค์ด์–ผ๋กœ๊ทธ
  • Table: ํ…Œ์ด๋ธ” (๊ฐ€์ƒํ™” ์ง€์›)
  • Select: ๋“œ๋กญ๋‹ค์šด ์„ ํƒ

Organisms (๋ณต์žกํ•œ ์ปดํฌ๋„ŒํŠธ):

  • VideoPlayer: ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด
  • Toast: ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€

3. ์ฃผ์š” ๊ธฐ์ˆ  ์Šคํƒ ํ•™์Šต โ€‹

ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ•™์Šตํ•˜์„ธ์š”:

ํ•„์ˆ˜ ํ•™์Šต:

์„ ํƒ ํ•™์Šต:

  • 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. ๋ธŒ๋žœ์น˜ ์ƒ์„ฑ โ€‹

bash
# ์ตœ์‹  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: ํƒ€์ž… ์ •์˜

typescript
// src/shared/ui/atoms/Button/Button.tsx
type ButtonSize = 'small' | 'medium' | 'large';

type ButtonProps = Readonly<{
  size?: ButtonSize;
  // ... ๊ธฐ์กด props
}>;

Step 2: ์Šคํƒ€์ผ ์ถ”๊ฐ€

typescript
// 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: ์ปดํฌ๋„ŒํŠธ์— ์ ์šฉ

typescript
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 ์Šคํ† ๋ฆฌ ์ถ”๊ฐ€

typescript
// 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. ํ…Œ์ŠคํŠธ ์‹คํ–‰ โ€‹

bash
# ๋ฆฐํŠธ ๊ฒ€์‚ฌ
npm run lint

# ํƒ€์ž… ์ฒดํฌ
npm run build

# ํ…Œ์ŠคํŠธ (ํ•„์š”์‹œ)
npm run test

# Storybook ํ™•์ธ
npm run storybook

5. ์ปค๋ฐ‹ ๋ฐ ํ‘ธ์‹œ โ€‹

bash
# ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ
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-variant

6. Pull Request ์ƒ์„ฑ โ€‹

GitHub์—์„œ Pull Request๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”.

7. ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฐ ๋จธ์ง€ โ€‹

  • ๋ฆฌ๋ทฐ์–ด์˜ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›๊ณ  ์ˆ˜์ •
  • Approve๋ฅผ ๋ฐ›์œผ๋ฉด ๋จธ์ง€

์ž์ฃผ ํ•˜๋Š” ์ž‘์—… ํŒจํ„ด โ€‹

1. ์ƒˆ๋กœ์šด ํŽ˜์ด์ง€ ์ถ”๊ฐ€ โ€‹

bash
# 1. ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
src/pages/UserProfile/UserProfile.tsx
src/pages/UserProfile/UserProfile.css.ts

# 2. ๋ผ์šฐํŠธ ์ถ”๊ฐ€
src/shared/config/routes.ts
src/app/routers/index.tsx

# 3. ๋ฉ”๋‰ด ์ถ”๊ฐ€ (ํ•„์š”์‹œ)

์˜ˆ์‹œ ์ฝ”๋“œ:

typescript
// 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 ์ •์˜

typescript
// entities/user/api/user.dto.ts
export type UserDto = {
  id: string;
  name: string;
  email: string;
};

Step 2: API ํ•จ์ˆ˜ ์ž‘์„ฑ

typescript
// 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 ์ถ”๊ฐ€

typescript
// 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 ์‚ฌ์šฉ

typescript
// features/user/model/useUser.ts
export function useUser(userId: string) {
  return useQuery({
    queryKey: queryKeys.user.detail(userId),
    queryFn: () => getUser(userId),
  });
}

Step 5: ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ

typescript
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. ํผ ๊ตฌํ˜„ํ•˜๊ธฐ โ€‹

typescript
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. ์—๋Ÿฌ ์ฒ˜๋ฆฌํ•˜๊ธฐ โ€‹

typescript
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. ์ƒˆ ์—๋Ÿฌ ์ฝ”๋“œ ์ถ”๊ฐ€ โ€‹

typescript
// 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]: () => '์˜ฌ๋ฐ”๋ฅธ ์ „ํ™”๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.',
};
typescript
// shared/api/error.ts
import { UserErrorCode } from '@entities/user/api/user.error';

export type ErrorCode =
  | CommonErrorCode
  | UserErrorCode
  // ๋‹ค๋ฅธ ๋„๋ฉ”์ธ ์—๋Ÿฌ ์ถ”๊ฐ€
  | VerificationErrorCode;

6. ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ โ€‹

typescript
// 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>;
}
typescript
// 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}`,
});
typescript
// 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 ์—๋Ÿฌ โ€‹

bash
# ์˜์กด์„ฑ ์žฌ์„ค์น˜
rm -rf node_modules
yarn install

2. ๋ฆฐํŠธ ์—๋Ÿฌ โ€‹

bash
# ์ž๋™ ์ˆ˜์ •
npm run lint:fix

3. ํƒ€์ž… ์—๋Ÿฌ โ€‹

typescript
// โŒ ์ž˜๋ชป๋œ ์˜ˆ
const user = data; // data์˜ ํƒ€์ž…์ด ๋ช…ํ™•ํ•˜์ง€ ์•Š์Œ

// โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ
const user: User = data;

4. Storybook์ด ์‹คํ–‰๋˜์ง€ ์•Š์Œ โ€‹

bash
# Storybook ์บ์‹œ ์‚ญ์ œ
rm -rf node_modules/.cache/storybook
npm run storybook

5. Git pre-commit hook ์‹คํŒจ โ€‹

bash
# ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ
git status

# ๋ฆฐํŠธ ์ˆ˜๋™ ์‹คํ–‰
npm run lint:fix

# ๋‹ค์‹œ ์ปค๋ฐ‹
git commit -m "..."

6. API ์—ฐ๋™ ์—๋Ÿฌ โ€‹

  • ๋„คํŠธ์›Œํฌ ํƒญ์—์„œ ์š”์ฒญ/์‘๋‹ต ํ™•์ธ
  • .env.local ํŒŒ์ผ์˜ VITE_BASE_URL ํ™•์ธ
  • ๋ฐฑ์—”๋“œ ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธ

ํ™”์ดํŒ…! ๐Ÿš€