Flutter 팀 핸드북

원칙부터 실행까지, 우리 팀의 개발 표준 가이드

Chapter 1: 프로젝트 철학 및 관리

우리는 예측 불가능한 상황에 휘둘리지 않고, 약속한 것을 꾸준히 전달하며, 함께 성장하는 팀을 지향합니다.

1.1. 작업 분리의 원칙 (Jira)

모든 작업은 Jira 티켓으로 관리하며, 목적과 범위에 따라 명확하게 구조화합니다. 아래 항목을 클릭하여 설명을 확인하세요.

에픽 (Epic)
스토리 (Story)
작업 (Task)
하위 작업 (Sub-task)

1.2. 현실적인 계획 수립

우리는 '시간'이 아닌 '상대적 크기(스토리 포인트)'로 작업량을 예측하여 압박감과 부정확성을 줄입니다. 데이터 기반의 계획을 통해 팀의 지속 가능한 성장을 추구합니다.

속도 (Velocity)

한 스프린트에서 우리 팀이 '완료'한 스토리 포인트의 총합입니다. 이는 다음 스프린트 계획의 근거가 됩니다.

1.3. 스프린트 보호 및 예외 처리

스프린트는 팀의 약속이자 방패입니다. 예상치 못한 작업 발생 시, 아래의 절차에 따라 체계적으로 대응합니다.

1

발견 즉시 공유: 이슈를 발견하면 즉시 팀에 알리고 Jira 티켓을 생성합니다.

2

영향 분석: 긴급성, 중요도, 예상 소요 시간을 팀과 함께 분석합니다.

3

처리 결정: 분석 결과에 따라 '즉시 처리', '백로그 이동', 또는 '거래(Trade-off)'를 결정합니다.

Chapter 2: 개발 표준 작업 절차 (SOP)

하나의 Jira 티켓을 처리하는 전체 과정과 그 과정에서 지켜야 할 모든 기술 규칙입니다.

2.1. Git & 커밋 컨벤션

일관된 히스토리 관리를 위해 다음 규칙을 준수합니다.

브랜치 전략

우리는 Git-flow를 기반으로 한 다음 브랜치 전략을 사용합니다: maindevfeat/티켓번호

  • main 브랜치:

    기획한 동작들이 모두 구현되어 문제가 없는 경우 팀장의 승인 하에 PR이 이루어집니다. 해당 브랜치에 push 하는 경우 자동 스크린샷 촬영 및 앱스토어, 구글 플레이 배포가 일어납니다. 상의 없는 push는 금지합니다.

  • dev 브랜치:

    feat 브랜치의 개발이 끝난 경우 PR을 통해 병합될 통합 브랜치입니다. 상의 없는 merge 기능을 통한 병합은 금지하며, 반드시 코드 리뷰 후 통합되어야 합니다.

  • feat/티켓번호 브랜치:

    JIRA에서 관리하는 코드명을 사용하여 브랜치를 작성합니다. (예: feat/BA-09) 브랜치의 작업이 끝나고 머지될 준비가 된 경우 PR을 통해 dev 브랜치로 머지합니다.

커밋 규칙

  • 커밋 메시지 형식: [티켓번호] 타입: 설명
  • 커밋 메시지 수정: git commit --amend
  • 강제 옵션 금지: git push -f 등 절대 사용 금지
타입 설명
feat새로운 기능 추가
fix버그 수정
docs문서 수정
style코드 포맷팅 등 (기능 변경 없음)
refactor코드 리팩토링
test테스트 코드 추가/수정
chore빌드, 패키지 매니저 설정 등

2.2. 코드 아키텍처 (Clean Architecture)

유지보수와 확장이 용이한 구조를 위해 클린 아키텍처를 따릅니다.

디렉토리 구조

  • lib/common/: 전역 위젯, 모델, Provider
  • lib/features/: 기능 단위 그룹
    • data/: 데이터 소스
    • domain/: 비즈니스 로직 (Usecase)
    • presentation/: UI 및 상태 관리 (ViewModel)

Riverpod 상태 분리 원칙

  • 화면 단위 상태: features/.../view_models/에 위치. autoDispose가 기본.
  • 전역/공유 상태: common/presentation/providers/에 위치. keepAlive: true로 상태 유지.
// 예시: 전역 상태(Session)와 화면 ViewModel(LoginViewModel)의 상호작용
// 1. 전역 상태 정의
@Riverpod(keepAlive: true)
class Session extends _$Session { ... }

// 2. 화면 ViewModel에서 전역 상태 업데이트
@riverpod
class LoginViewModel extends _$LoginViewModel {
  Future login(...) async {
    // ... 로그인 성공 후 ...
    ref.read(sessionProvider.notifier).login(user);
  }
}

// 3. 다른 화면에서 전역 상태 참조
class ProfilePage extends ConsumerWidget {
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(sessionProvider);
    // ...
  }
}

2.3. 코딩 스타일 및 네이밍

  • 파일 이름: snake_case.dart
  • 비공개 멤버(Private Members): 이름 앞에 _를 붙여 해당 파일 외부에서의 접근을 막습니다. 캡슐화를 통해 위젯이나 클래스 내부의 상태와 로직을 보호합니다.
    // 예시: StatefulWidget에서 내부 상태 관리
    class MyCheckbox extends StatefulWidget { ... }
    
    class _MyCheckboxState extends State {
      // _isChecked는 이 State 클래스 외부에서 직접 접근 불가
      bool _isChecked = false; 
    
      // _toggleCheckbox 메서드 또한 외부에서 직접 호출 불가
      void _toggleCheckbox() {
        setState(() {
          _isChecked = !_isChecked;
        });
      }
      ...
    }
  • 재사용 가능한 위젯(Reusable Widgets):

    위젯은 독립적이고 재사용 가능하게 작성하여 중복을 줄이고 일관성을 높입니다.

    나쁜 예 1: UI 반복

    // ListView 안에서 동일한 구조의 위젯이 반복됨
    ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) {
        // 복잡한 위젯 구조가 여기에 그대로 노출됨
        return Card(
          child: ListTile(
            leading: CircleAvatar(child: Text(users[index].name[0])),
            title: Text(users[index].name),
            subtitle: Text(users[index].email),
          ),
        );
      },
    );

    좋은 예 1: 위젯 추출

    // 반복되는 UI를 별도의 위젯으로 추출
    ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) {
        // UserProfileCard 위젯으로 책임을 위임하여 코드가 간결해짐
        return UserProfileCard(user: users[index]);
      },
    );
    
    // 재사용 가능한 위젯
    class UserProfileCard extends StatelessWidget {
      final User user;
      const UserProfileCard({super.key, required this.user});
      // ... 위젯 구현
    }

    나쁜 예 2: 특정 상태에 의존

    // 위젯이 특정 ViewModel Provider를 직접 참조함
    class UserAvatar extends ConsumerWidget {
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // profileViewModelProvider에 강하게 결합되어 다른 곳에서 재사용 불가
        final user = ref.watch(profileViewModelProvider).user;
        return CircleAvatar(backgroundImage: NetworkImage(user.avatarUrl));
      }
    }

    좋은 예 2: 데이터 주입

    // 필요한 데이터(avatarUrl)를 생성자를 통해 주입받음
    class UserAvatar extends StatelessWidget {
      final String avatarUrl;
      const UserAvatar({super.key, required this.avatarUrl});
    
      @override
      Widget build(BuildContext context) {
        // 주입받은 데이터로 위젯을 그리므로 어디서든 재사용 가능
        return CircleAvatar(backgroundImage: NetworkImage(avatarUrl));
      }
    }
  • 공통 위젯: 여러 화면에서 사용될 공통 위젯은 팀장과 상의 후 /common/presentation/widgets 하위에 app_위젯이름.dart 형식으로 생성/수정합니다.

2.4. 자원 및 라우팅 관리

앱의 자원과 경로를 중앙에서 관리하여 일관성을 유지하고 추후 유지보수를 용이하게 합니다.

  • 이미지: 모든 이미지 경로는 /core/constants/app_images.dartstatic const 문자열로 정의 후 참조합니다.
  • 문자열: UI에 표시되는 모든 텍스트는 /core/constants/app_strings.dart에 정의 후 참조합니다. (국제화 대응)
  • 경로 이름 (GoRouter): 모든 라우트 경로는 /core/constants/app_routes.dart에 정의합니다.
  • 라우터 설정: 새로운 화면 추가 시, /core/router/router.dart 파일에 GoRoute()를 추가하여 경로를 등록합니다.

2.5. 표준 작업 절차 (Step-by-Step)

  1. 1

    작업 가져오기

    Jira 티켓을 'In Progress'로 이동하고 착수일을 기록합니다.

  2. 2

    사전 확인

    요구사항, Figma 디자인, 공통 위젯 존재 여부를 확인합니다.

  3. 3

    구현

    모든 개발 컨벤션을 준수하여 코드를 작성합니다.

  4. 4

    코드 제출 및 리뷰

    커밋 후 Pull Request(PR)를 생성하고 리뷰를 요청합니다.

  5. 5

    작업 완료

    리뷰 승인 및 Merge 후 Jira 티켓을 'Done'으로 이동합니다.

빠른 참조 (Quick Reference)

가장 자주 필요한 규칙과 도구를 빠르게 확인하세요.

커밋 메시지 빌더

아래 필드를 채워 표준 커밋 메시지를 생성하세요.

생성된 커밋 메시지:

신규 작업 체크리스트

새로운 작업을 시작할 때 아래 절차를 따르세요.

✨ AI 어시스턴트

Gemini AI를 활용하여 개발 생산성을 높여보세요.

API 키 설정

AI 어시스턴트 기능을 사용하려면 본인의 Google AI Studio API 키를 입력해주세요. 키는 브라우저에만 저장되며, 서버로 전송되지 않습니다.

Jira 스토리 자동 생성

구현할 기능에 대한 간단한 설명을 입력하면, 핸드북 규칙에 맞는 Jira 스토리 티켓 초안을 생성해줍니다.

생성된 Jira 티켓:

AI 코드 리뷰어

작성한 Dart 코드를 붙여넣으면, 우리 팀의 핸드북 규칙에 기반하여 개선점을 제안해줍니다.

AI 리뷰 코멘트: