Skip to content

기여 가이드

GMD Soft Platform Frontend 프로젝트에 기여해주셔서 감사합니다! 이 문서는 프로젝트에 기여하는 방법을 안내합니다.

📋 목차


시작하기 전에

필수 확인 사항

기여하기 전에 다음을 확인하세요:

  • [ ] onboarding.md를 읽고 개발 환경 설정 완료
  • [ ] CLAUDE.md를 읽고 개발 컨벤션 숙지
  • [ ] 작업할 이슈가 GitHub Issues에 등록되어 있는지 확인
  • [ ] 중복 작업이 없는지 확인 (다른 PR 검색)

작업 전 체크리스트

  • [ ] 최신 main 브랜치를 pull
  • [ ] 새로운 브랜치 생성
  • [ ] 의존성 최신 상태 확인 (yarn install)

개발 워크플로우

1. 이슈 확인 또는 생성

작업을 시작하기 전에 GitHub Issues에서 이슈를 확인하거나 생성하세요.

좋은 이슈 예시:

markdown
## 문제

Button 컴포넌트에 loading state가 없어서 비동기 작업 중 사용자 피드백을 제공할 수 없습니다.

## 제안

Button 컴포넌트에 `isLoading` prop을 추가하여 로딩 중 Spinner를 표시하도록 개선

## 작업 범위

- [ ] `isLoading` prop 추가
- [ ] Spinner 컴포넌트 통합
- [ ] Storybook 스토리 추가
- [ ] 테스트 작성

2. 브랜치 생성

bash
# 최신 main 브랜치 가져오기
git checkout main
git pull origin main

# 새 브랜치 생성
git checkout -b feature/add-button-loading-state

3. 개발

코드를 작성하고 자주 커밋하세요.

bash
# 작업 진행...

# 변경사항 확인
git status
git diff

# 스테이징 및 커밋
git add .
git commit -m "feat: Button에 isLoading prop 추가"

4. 테스트 및 검증

bash
# 린트 검사
npm run lint

# 타입 체크
npm run build

# 테스트 실행
npm run test

# Storybook 확인
npm run storybook

5. Push 및 PR 생성

bash
# 원격 브랜치에 푸시
git push origin feature/add-button-loading-state

# GitHub에서 Pull Request 생성

브랜치 전략

브랜치 네이밍 규칙

<type>/<description>

Type:

  • feature/ - 새로운 기능 추가
  • fix/ - 버그 수정
  • refactor/ - 코드 리팩토링
  • docs/ - 문서 수정
  • test/ - 테스트 코드 추가/수정
  • chore/ - 빌드 설정, 패키지 업데이트 등
  • style/ - 코드 포맷팅 (기능 변경 없음)

Description:

  • 소문자, kebab-case 사용
  • 명확하고 간결하게 작성
  • 영어 사용 권장

예시:

bash
feature/add-button-loading-state
fix/user-profile-rendering-error
refactor/extract-form-validation-logic
docs/update-onboarding-guide
test/add-button-component-tests
chore/upgrade-react-query
style/format-user-profile-component

브랜치 수명 주기

  1. 생성: main 브랜치에서 분기
  2. 개발: 작업 진행 및 커밋
  3. 리뷰: PR 생성 및 코드 리뷰
  4. 머지: main 브랜치에 머지
  5. 삭제: 로컬 및 원격 브랜치 삭제
bash
# 머지 후 로컬 브랜치 삭제
git checkout main
git pull origin main
git branch -d feature/add-button-loading-state

# 원격 브랜치 삭제 (GitHub UI에서 자동 삭제 설정 권장)
git push origin --delete feature/add-button-loading-state

커밋 컨벤션

Conventional Commits 사용

<type>: <subject>

<body> (선택)

<footer> (선택)

Type

  • feat: 새로운 기능 추가
  • fix: 버그 수정
  • refactor: 코드 리팩토링 (기능 변경 없음)
  • chore: 빌드 설정, 패키지 업데이트 등
  • docs: 문서 수정
  • test: 테스트 코드 추가/수정
  • style: 코드 포맷팅 (기능 변경 없음)
  • perf: 성능 개선

Subject

  • 50자 이내로 작성
  • 한글 또는 영어 사용
  • 마침표(.) 사용하지 않음
  • 현재형 동사 사용 ("추가", "수정", "제거")

예시

좋은 커밋 메시지:

bash
feat: Button 컴포넌트에 isLoading prop 추가

fix: 로그인 토큰 갱신 오류 수정

refactor: forEach를 for...of로 변경

docs: onboarding.md에 트러블슈팅 섹션 추가

test: useForm 테스트 추가

chore: TanStack Query 버전 업그레이드

피해야 할 커밋 메시지:

bash
# ❌ 너무 모호함
fix: 버그 수정

# ❌ 너무 김
feat: Button 컴포넌트에 isLoading prop을 추가하고 로딩 중에는 Spinner를 표시하도록 개선했으며...

# ❌ 과거형 사용
feat: Button에 isLoading을 추가했음

# ❌ Type 누락
Button에 isLoading 추가

커밋 단위

  • 하나의 커밋은 하나의 의도만 담기
  • 독립적으로 동작하는 단위로 커밋
  • 너무 작은 커밋보다는 의미 있는 단위로 묶기

예시:

bash
# ✅ 좋은 예 (의미 있는 단위)
feat: Button에 isLoading prop 추가
feat: Input에 icon prop 추가

# ❌ 나쁜 예 (너무 세분화)
feat: Button Props에 isLoading 타입 추가
feat: Button에 Spinner import 추가
feat: Button에 isLoading 조건부 렌더링 추가

Pull Request 작성

PR 제목

커밋 컨벤션과 동일한 형식 사용:

<type>: <subject>

예시:

feat: Button 컴포넌트에 loading state 추가
fix: 로그인 시 토큰 갱신 오류 수정
refactor: useForm 훅 구조 개선

PR 설명 템플릿

markdown
## 변경 사항

<!-- 무엇을 변경했는지 간략히 설명 -->

Button 컴포넌트에 `isLoading` prop을 추가하여 비동기 작업 중 로딩 상태를 표시할 수 있도록 개선했습니다.

## 주요 변경 내용

<!-- 구체적인 변경 사항을 나열 -->

- `Button.tsx``isLoading` prop 추가
- 로딩 중 Spinner 컴포넌트 렌더링
- Storybook 스토리 추가 (`Loading` 스토리)
- 타입 정의 업데이트

## 관련 이슈

<!-- 관련 이슈 번호 링크 -->

Closes #123

## 체크리스트

<!-- 완료한 항목에 체크 -->

- [x] 코드 작성 완료
- [x] Storybook 스토리 추가
- [x] 린트 검사 통과
- [x] 타입 체크 통과
- [x] 테스트 작성 (필요시)
- [x] 문서 업데이트 (필요시)
- [x] 코드 리뷰 요청

## 스크린샷

<!-- UI 변경이 있는 경우 스크린샷 첨부 -->

![Button Loading State](./screenshot.png)

## 테스트 방법

<!-- 리뷰어가 테스트할 수 있는 방법 설명 -->

1. Storybook 실행 (`npm run storybook`)
2. `Atoms/Button``Loading` 스토리 확인
3. 버튼 클릭 시 Spinner가 표시되는지 확인

## 추가 정보

<!-- 리뷰어가 알아야 할 추가 정보 -->

- Spinner 컴포넌트는 기존 `shared/ui/atoms/Spinner`를 재사용했습니다.
- `isLoading={true}`일 때 버튼은 disabled 상태가 됩니다.

PR 크기 가이드

작은 PR이 좋은 PR입니다!

  • 한 PR에는 하나의 기능만 포함
  • 변경된 파일 수: 10개 이하 권장
  • 변경된 라인 수: 300줄 이하 권장
  • 큰 작업은 여러 PR로 분할

Draft PR 활용

작업 진행 중에 피드백을 받고 싶다면 Draft PR을 활용하세요:

markdown
## [WIP] Button 컴포넌트 loading state 추가

**현재 상태**: 기본 구조만 구현, Spinner 통합 예정

**질문**:

- Spinner 크기를 Button size에 맞게 조정해야 할까요?
- isLoading일 때 children을 숨겨야 할까요?

코드 리뷰

리뷰 요청

  1. PR 생성 후 리뷰어 지정
  2. 슬랙 또는 팀 채널에 리뷰 요청 알림
  3. CI/CD 체크가 모두 통과되었는지 확인

리뷰어 체크리스트

리뷰어는 다음 사항을 확인합니다:

코드 품질

  • [ ] 코드가 CLAUDE.md의 컨벤션을 따르는가?
  • [ ] TypeScript 타입이 올바르게 정의되었는가?
  • [ ] Props는 Readonly<{}> 패턴을 사용하는가?
  • [ ] 반복문에서 for...of를 사용하는가?
  • [ ] 불필요한 코드나 주석이 없는가?

기능

  • [ ] 요구사항을 올바르게 구현했는가?
  • [ ] 엣지 케이스를 고려했는가?
  • [ ] 에러 처리가 적절한가?

테스트

  • [ ] 중요한 로직에 대한 테스트가 있는가?
  • [ ] 테스트가 모두 통과하는가?

문서

  • [ ] Storybook 스토리가 추가되었는가? (공통 컴포넌트의 경우)
  • [ ] 필요시 문서가 업데이트되었는가?

성능

  • [ ] 불필요한 리렌더링이 발생하지 않는가?
  • [ ] 큰 리스트의 경우 virtualization을 고려했는가?

보안

  • [ ] XSS, SQL Injection 등의 취약점이 없는가?
  • [ ] 사용자 입력 검증이 적절한가?

리뷰 피드백 작성

좋은 피드백:

markdown
**Question**: `isLoading``true`일 때 버튼이 disabled되는 이유가 있나요? 사용자가 로딩 중에도 취소 버튼을 눌러야 하는 경우도 있을 것 같은데요.

**Suggestion**: Spinner의 크기를 Button의 `size` prop에 따라 조정하면 더 일관성 있을 것 같습니다.

```typescript
const spinnerSize = size === 'small' ? 16 : size === 'large' ? 24 : 20;
```

Nit: 변수명 isLoading보다 loading이 더 간결할 것 같습니다. (선택사항)

Praise: Storybook 스토리를 잘 작성해주셨네요! 다양한 케이스를 잘 커버하고 있습니다. 👍


**피해야 할 피드백:**

```markdown
# ❌ 너무 모호함
코드를 개선해주세요.

# ❌ 비판적
이렇게 하면 안 됩니다.

# ❌ 설명 없음
여기를 수정하세요.

피드백 반영

  1. 피드백을 읽고 이해
  2. 불명확한 부분은 질문
  3. 수정 후 커밋 및 푸시
  4. 변경 사항을 PR 코멘트로 설명
markdown
@reviewer 피드백 감사합니다! 다음과 같이 수정했습니다:

- Spinner 크기를 Button size에 맞게 조정했습니다.
- `isLoading`일 때도 버튼을 클릭할 수 있도록 `disabled` 로직을 제거했습니다.

다시 한번 확인 부탁드립니다!

Approve 및 머지

  • 모든 피드백이 반영되고 리뷰어의 Approve를 받으면 머지
  • Squash Merge 사용 권장 (커밋 히스토리 정리)
  • 머지 후 브랜치 삭제

테스트

테스트 작성 기준

다음의 경우 테스트 작성을 권장합니다:

  • Pure Functions: 유틸리티 함수는 반드시 테스트 작성
  • Custom Hooks: 복잡한 로직이 있는 훅
  • 비즈니스 로직: 중요한 기능
  • 버그 수정: 재발 방지를 위한 regression test

테스트 생략 가능:

  • 단순 UI 컴포넌트 (Storybook으로 대체)
  • Getter/Setter 함수
  • 단순 타입 정의

테스트 작성 예시

typescript
// useForm.test.ts
import { renderHook, act } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { useForm } from './useForm';

describe('useForm', () => {
  it('초기값으로 formState가 설정된다', () => {
    const { result } = renderHook(() =>
      useForm({
        initialValues: { email: '', password: '' },
      }),
    );

    expect(result.current.formState.email).toBe('');
    expect(result.current.formState.password).toBe('');
  });

  it('handleInputChange로 값을 변경할 수 있다', () => {
    const { result } = renderHook(() =>
      useForm({
        initialValues: { email: '' },
      }),
    );

    act(() => {
      result.current.handleInputChange('email', 'test@example.com');
    });

    expect(result.current.formState.email).toBe('test@example.com');
  });

  it('validation 함수가 에러를 반환한다', () => {
    const { result } = renderHook(() =>
      useForm({
        initialValues: { email: '' },
        validation: {
          email: (value) => (value.includes('@') ? null : '올바른 이메일을 입력하세요'),
        },
      }),
    );

    act(() => {
      result.current.handleInputChange('email', 'invalid');
    });

    expect(result.current.formErrors.email).toBe('올바른 이메일을 입력하세요');
  });
});

Coverage 목표

  • Lines: 70% 이상
  • Functions: 70% 이상
  • Branches: 70% 이상
  • Statements: 70% 이상
bash
# Coverage 확인
npm run test:coverage

문서화

코드 문서화

JSDoc 주석 (선택):

복잡한 함수나 유틸리티의 경우 JSDoc 주석 추가:

typescript
/**
 * 파일 크기를 사람이 읽기 쉬운 형식으로 변환
 * @param bytes - 바이트 단위의 파일 크기
 * @returns 포맷된 문자열 (예: "1.5 MB")
 */
export function formatFileSize(bytes: number): string {
  // ...
}

Storybook 문서화

공통 컴포넌트(atoms, molecules, organisms)는 반드시 Storybook 스토리 작성:

typescript
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import Button from './Button';

const meta = {
  title: 'Atoms/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    color: {
      control: 'select',
      options: ['primary', 'secondary', 'danger'],
      description: '버튼 색상',
    },
    size: {
      control: 'select',
      options: ['small', 'medium', 'large'],
      description: '버튼 크기',
    },
    isLoading: {
      control: 'boolean',
      description: '로딩 상태',
    },
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    children: 'Primary Button',
    color: 'primary',
  },
};

export const Loading: Story = {
  args: {
    children: 'Loading',
    isLoading: true,
  },
};

export const AllSizes: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '8px' }}>
      <Button size="small">Small</Button>
      <Button size="medium">Medium</Button>
      <Button size="large">Large</Button>
    </div>
  ),
};

프로젝트 문서 업데이트

다음의 경우 프로젝트 문서 업데이트:

  • 새로운 컨벤션 추가: CLAUDE.md 업데이트
  • 새로운 패턴 발견: onboarding.md 또는 docs/ 업데이트
  • API 변경: 관련 문서 업데이트

추가 리소스

  • onboarding.md - 신입 개발자 온보딩
  • CLAUDE.md - 개발 컨벤션 상세 가이드
  • README.md - 프로젝트 개요

질문이 있나요?

  • 팀 슬랙 채널에 질문하기
  • PR 코멘트로 질문하기
  • 1:1 미팅 요청하기

함께 만들어가는 프로젝트에 감사드립니다! 🚀