diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 4035938..d6d511f 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,27 +1,13 @@ # ============================================================================= # CodeRabbit 설정 파일 -# macOS Swift 프로젝트 | Airbnb Swift Style Guide 기반 -# 마지막 업데이트: 2026/04/27 +# 마지막 업데이트: 2026/05/11 # ============================================================================= language: "ko-KR" early_access: false enable_free_tier: true -# ============================================================================= -# 리뷰어 톤 & 페르소나 설정 (최상위 레벨) -# 프로젝트 성격에 맞게 자유롭게 수정 -# ============================================================================= -tone_instructions: | - 당신은 경험 많은 Apple 플랫폼 개발자이자 코드 리뷰어입니다. - 목표는 팀원들이 더 나은 Swift 코드를 작성하며 함께 성장하도록 돕는 것입니다. - - 리뷰 원칙: - 1. 피드백은 명확하고 구체적이어야 하며, 문제의 원인과 개선 방법을 반드시 함께 제시하세요. - 2. 리뷰는 교육적이어야 하며, Apple Developer Documentation이나 Swift Evolution 제안 등 관련 자료를 함께 안내하세요. - 3. 비판보다는 개선 중심의 제안을 우선하세요. "이렇게 하면 안 돼요" 대신 "이렇게 하면 더 좋아요"로 표현하세요. - 4. 잘된 부분은 짧고 위트 있게 칭찬하세요. - 5. 스타일 지적은 SwiftLint/SwiftFormat으로 자동화할 수 있는 것인지 구분하고, 자동화 가능한 경우 툴 도입을 제안하세요. +tone_instructions: "경험 많은 Apple 플랫폼 개발자로서 리뷰하세요. 문제의 원인과 개선 방법을 함께 제시하고, 비판보다 개선 중심으로 피드백하세요. 잘된 부분은 짧게 칭찬하세요." # ============================================================================= # 리뷰 설정 @@ -37,72 +23,56 @@ reviews: collapse_walkthrough: false # --------------------------------------------------------------------------- - # 경로별 리뷰 집중 포인트 설정 - # 프로젝트 폴더 구조에 맞게 경로와 지침을 수정 + # 경로별 리뷰 체크리스트 + # 규칙의 배경·예시는 docs/CODING_GUIDELINES.md를 참고하세요. # --------------------------------------------------------------------------- path_instructions: - # 앱 핵심 로직 - path: "**/*.swift" instructions: | - - Airbnb Swift Style Guide(https://github.com/airbnb/swift)를 기준으로 리뷰하세요. - - 가독성, 단순성, 명확성을 저해하지 않는 범위 내에서만 간결함을 추구하는지 확인하세요. - - 각 줄의 최대 열 너비는 100자입니다. - - 불필요한 self 사용이 없는지 확인하세요. - - AnyObject 대신 class를 protocol 제약으로 쓰는 경우 지적하세요. - - guard let 구문에서 멀티라인인 경우 else가 새 줄에 위치하는지 확인하세요. - - 접근 제어자(access control)가 각 선언부에 명시적으로 기재되어 있는지 확인하세요. - - switch-case에서 불필요한 break 문이 없는지 확인하세요. - - 튜플을 반환할 때 필드가 3개 초과인 경우 struct 사용을 권장하세요. - - 콜론(`:`) 위치: 식별자 바로 뒤, 공백 없이, 이후 공백 1칸인지 확인하세요. - - NSObject 서브클래싱 대신 순수 Swift 클래스를 선호하는지 확인하세요. - - CGRectZero, CGRectGetWidth 등 구식 Objective-C 전역 함수 대신 Swift extension 메서드를 사용하는지 확인하세요. - - # SwiftUI 뷰 파일 - # - path: "**/Views/**/*.swift" - # instructions: | - # - 뷰 로직과 비즈니스 로직이 혼재하지 않도록 관심사 분리가 잘 되어 있는지 확인하세요. - # - EnvironmentValues에 프로퍼티 추가 시 @Entry 매크로를 사용하는지 확인하세요 (레거시 패턴 지양). - # - 재사용 가능한 뷰 컴포넌트로 분리할 여지가 있는지 확인하세요. - # - 디자인 시스템 컴포넌트 관련 리뷰는 디자인 시스템 도입 후 추가될 예정입니다. - - # ViewModel / 비즈니스 로직 - # - path: "**/ViewModels/**/*.swift" - # instructions: | - # - @MainActor 사용이 적절한지, UI 업데이트가 메인 스레드에서 이루어지는지 확인하세요. - # - Combine 또는 async/await 패턴이 일관되게 사용되는지 확인하세요. - # - 메모리 누수 가능성(순환 참조)이 없는지 확인하세요. [weak self] 캡처가 필요한 곳에 누락되지 않았는지 검토하세요. - - # 모델 / 데이터 레이어 - # - path: "**/Models/**/*.swift" - # instructions: | - # - 가변 상태(var)보다 불변(let)을 선호하는지 확인하세요. - # - 3개 초과 필드를 가진 튜플 대신 struct를 사용하는지 확인하세요. - # - Codable 구현 시 CodingKeys가 일관성 있게 관리되는지 확인하세요. - - # 네트워크 레이어 - # - path: "**/Network*/**/*.swift" - # instructions: | - # - 에러 핸들링이 누락된 곳은 없는지 확인하세요. - # - async/await 또는 Combine 패턴이 일관되게 적용되는지 확인하세요. - # - API 응답 모델에 명시적인 값이 정의되어 있는지 확인하세요 (외부 소스 매핑의 경우). - - # 테스트 파일 - # - path: "**/*Tests/**/*.swift" - # instructions: | - # - 테스트 케이스 이름이 테스트 의도를 명확히 설명하는지 확인하세요. - # - given/when/then 또는 Arrange/Act/Assert 패턴이 일관되게 사용되는지 확인하세요. - # - 테스트가 독립적으로 실행 가능한지(다른 테스트에 의존하지 않는지) 확인하세요. - - # 패키지 의존성 - # - path: "Package.swift" - # instructions: | - # - 새로운 의존성 추가 시 유지보수 상태(최근 커밋, 이슈 대응 등)를 함께 검토하도록 권장하세요. - # - 최소 지원 버전과의 호환성을 확인하세요. - - # Xcode 프로젝트 파일 (리뷰 제외) + 아래 항목을 체크리스트로 사용해 리뷰하세요. + + [안전성 — 최우선] + - 강제 언래핑(`!`) 사용 시 반드시 지적하고 `guard let` / `if let` / `??` 대안을 제시하세요. + - `unowned` 캡처 사용 시 `[weak self]` + `guard let self else { return }` 패턴으로 교체를 제안하세요. + - `init()`에서 네트워크 요청, DB 연결 등 시간이 오래 걸리는 작업이 없는지 확인하세요. + + [접근 제어] + - 접근 제어가 가능한 가장 엄격한 수준인지 확인하세요. (`private` > `fileprivate` > `internal`) + - `internal`을 명시적으로 쓴 경우 제거를 제안하세요. (기본값이므로 불필요) + - `public extension` 패턴 대신 각 선언에 직접 접근 제어가 명시되어 있는지 확인하세요. + - 클래스 프로퍼티는 원칙적으로 `private`인지 확인하세요. + + [클래스 설계] + - 더 이상 상속되지 않을 `class`에 `final`이 없으면 추가를 제안하세요. + - 프로토콜 정의에서 `class` 대신 `AnyObject`를 사용하는지 확인하세요. + - ViewController가 아닌 클래스 이름에 `*Controller`가 붙어 있으면 지적하세요. + + [SwiftUI 뷰 구조] + - `var body` 안에 중첩 레이아웃이 직접 구현되어 있으면 `extension`의 `private var/func`로 분리를 제안하세요. + - 하위뷰가 `extension` 밖에 선언되어 있으면 이동을 제안하세요. + - 동일한 뷰가 2곳 이상 사용되거나, 독립적인 State/로직을 갖거나, 50줄 이상이면 별도 View 파일 분리를 제안하세요. + - 파라미터 없는 하위뷰는 `private var`, 파라미터 있는 하위뷰는 `private func`를 사용하는지 확인하세요. + + [네이밍] + - 이벤트 핸들러 함수명이 과거형인지 확인하세요. (`didTapButton` ✅ / `handleButtonTap` ❌) + - 함수명 앞에 불필요한 `get`이 없는지 확인하세요. + - Bool 프로퍼티에 `is`, `has`, `can` 등의 접두사가 있는지 확인하세요. + - 이름이 애매한 경우 타입 힌트가 포함되어 있는지 확인하세요. (`cancelButton` ✅ / `cancel` ❌) + + [패턴] + - `if let _ = thing` 패턴은 `if thing != nil`로 교체를 제안하세요. + - 복잡한 `didSet` 옵저버는 별도 메서드로 추출을 제안하세요. + - 전역 상수 그룹화에 `struct` 대신 케이스 없는 `enum`을 사용하는지 확인하세요. + - 튜플 반환 시 필드가 3개를 초과하면 `struct` 사용을 제안하세요. + + [파일 구성] + - 코드 순서가 지켜지는지 확인하세요: 프로퍼티 → init → body → 메서드(public → private) + - `// MARK: -` 위아래에 빈 줄이 있는지 확인하세요. + - 모듈 임포트가 알파벳 순으로 정렬되어 있는지 확인하세요. (내장 프레임워크 먼저, 빈 줄로 서드파티 구분) + - path: "**/*.xcodeproj/**" instructions: | - - Xcode 프로젝트 파일은 자동 생성되는 경우가 많습니다. 리뷰는 스킵하되, 타겟/스킴 구조의 의도치 않은 변경이 있다면 코멘트하세요. + Xcode 프로젝트 파일은 리뷰를 스킵하되, 타겟/스킴 구조의 의도치 않은 변경이 있다면 코멘트하세요. auto_review: enabled: true @@ -130,13 +100,12 @@ knowledge_base: project_keys: [] # --------------------------------------------------------------------------- - # 코드 가이드라인 (knowledge_base 하위) - # CodeRabbit이 리포지토리 내 가이드라인 문서를 직접 읽고 리뷰에 반영합니다 - # CODING_GUIDELINES.md, DESIGN_SYSTEM_GUIDELINES,md 파일을 리포지토리 루트에 두기 + # 코드 가이드라인 + # CodeRabbit이 docs/ 폴더의 문서를 읽고 리뷰에 반영합니다. # --------------------------------------------------------------------------- - # code_guidelines: - # enabled: true - # filePatterns: - # - "**/CODING_GUIDELINES.md" - # TODO: 디자인 시스템 도입 후 아래 주석을 해제하세요 - # - "**/DESIGN_SYSTEM_GUIDELINES.md" + code_guidelines: + enabled: true + filePatterns: + - "docs/CODING_GUIDELINES.md" + # TODO: 디자인 시스템 도입 후 아래 주석을 해제하세요. + # - "docs/DESIGN_SYSTEM_GUIDELINES.md" diff --git a/docs/CODING_GUIDELINES.md b/docs/CODING_GUIDELINES.md new file mode 100644 index 0000000..c6cd3ca --- /dev/null +++ b/docs/CODING_GUIDELINES.md @@ -0,0 +1,337 @@ +# Coding Guidelines + +Airbnb Swift Style Guide 기반의 macOS SwiftUI 프로젝트 코딩 가이드라인입니다. + +--- + +## 핵심 원칙 + +- **성능**: 더 이상 상속되지 않을 `class`에는 반드시 `final` 키워드를 붙입니다. +- **안전성**: 강제 언래핑(`!`)을 사용하지 않습니다. +- **안전성**: `unowned` 캡처 대신 `[weak self]` + `guard let self else { return }` 패턴을 사용합니다. +- **명시성**: 약어와 생략을 지양합니다. (`VC` → `ViewController`) +- **명시성**: `extension`의 각 선언에 접근 제어를 개별적으로 명시합니다. +- **간결성**: 쉽게 추론 가능한 타입은 명시하지 않습니다. + +--- + +## 1. 네이밍 + +### 이벤트 핸들러 + +이벤트 처리 함수는 **과거형** 형태로 이름을 짓습니다. 주어가 명확하면 생략할 수 있습니다. + +```swift +// ✅ Good +private func didTapBookButton() { } +private func modelDidChange() { } + +// ❌ Bad +private func handleBookButtonTap() { } +private func bookButtonTapped() { } +``` + +### 타입 힌트 + +이름만으로 타입을 추론하기 어려운 경우 타입 힌트를 이름에 포함합니다. + +```swift +// ✅ Good +let titleText: String +let cancelButton: NSButton +let profileImageView: NSImageView + +// ❌ Bad +let title: String +let cancel: NSButton +``` + +### 이름 순서 + +이름은 **일반적인 것 → 구체적인 것** 순으로 작성합니다. + +```swift +// ✅ Good +let titleMarginLeft: CGFloat +let titleMarginRight: CGFloat +let bodyMarginLeft: CGFloat + +// ❌ Bad +let leftTitleMargin: CGFloat +let rightTitleMargin: CGFloat +``` + +### 약어 + +약어로 시작하면 소문자, 그 외에는 모두 대문자로 표기합니다. + +```swift +let userID: Int // 중간 → 대문자 +let urlString: String // 시작 → 소문자 +let websiteURL: URL // 끝 → 대문자 +let html: String // 시작 → 소문자 +``` + +--- + +## 2. 접근 제어 + +가능한 가장 엄격한 수준으로 설정합니다. `internal`은 기본값이므로 생략합니다. + +`public extension` 패턴은 내부 선언이 의도치 않게 공개될 수 있어 사용하지 않습니다. +각 선언에 직접 접근 제어를 명시합니다. + +```swift +// ✅ Good +extension Universe { + public func generateGalaxy() { } + internal func resetGalaxy() { } +} + +// ❌ Bad — generateGalaxy, resetGalaxy 모두 public이 되어버림 +public extension Universe { + func generateGalaxy() { } + func resetGalaxy() { } +} +``` + +--- + +## 3. SwiftUI 뷰 구조 + +### body 규칙 + +`var body`에는 **최상위 컨테이너 Stack 하나**만 직접 포함합니다. +세부 레이아웃은 하위뷰 프로퍼티로 분리합니다. + +```swift +// ✅ Good +var body: some View { + VStack(spacing: 16) { + headerSection + contentSection + footerSection + } +} + +// ❌ Bad +var body: some View { + VStack { + HStack { + Image(systemName: "person") + VStack(alignment: .leading) { + Text("이름") + Text("이메일") + } + } + Divider() + // ... 계속 + } +} +``` + +### 하위뷰 선언 규칙 + +하위뷰는 **`extension`에서 `private`으로** 선언합니다. + +| 경우 | 선언 방법 | +|---|---| +| 파라미터가 없는 정적인 뷰 | `private var someView: some View` | +| 파라미터가 필요한 동적인 뷰 | `private func someView(param: Type) -> some View` | +| 복잡하거나 재사용되는 뷰 | 별도 View 파일로 분리 | + +```swift +struct ContentView: View { + + // MARK: - Body + + var body: some View { + VStack(spacing: 16) { + headerSection + userList + emptyStateView(message: "데이터가 없습니다") + } + } +} + +// MARK: - Subviews + +extension ContentView { + + private var headerSection: some View { + HStack { + Text("타이틀") + .font(.title) + Spacer() + } + } + + private var userList: some View { + List(users) { user in + UserRowView(user: user) + } + } + + private func emptyStateView(message: String) -> some View { + Text(message) + .foregroundStyle(.secondary) + } +} +``` + +### 컴포넌트 분리 기준 + +다음 조건 중 하나라도 해당하면 별도 View 파일로 분리합니다. + +1. 동일한 뷰가 **2곳 이상**에서 사용될 때 +2. 하위뷰 자체가 **독립적인 State나 로직**을 갖고 있을 때 +3. 분리한 뷰가 **50줄 이상**이 될 것으로 예상될 때 + +--- + +## 4. 패턴 + +### 옵셔널 처리 + +강제 언래핑 대신 `guard let` / `if let` / `??`를 사용합니다. +동일한 이름으로 바인딩할 때는 단축 구문을 사용합니다. + +```swift +// ✅ Good +guard let user else { return } +let name = user.name ?? "Unknown" + +// ❌ Bad +let name = user!.name +guard let user = user else { return } +``` + +값을 사용하지 않을 경우 optional binding 대신 nil 체크를 사용합니다. + +```swift +// ✅ Good +if thing != nil { doThing() } + +// ❌ Bad +if let _ = thing { doThing() } +``` + +### 불변성 + +가능하면 `var` 대신 `let`을 사용하고, 컬렉션 변환 시 `map` / `compactMap` / `filter`를 선호합니다. + +```swift +// ✅ Good +let results = input.map { transform($0) } +let active = items.filter { $0.isActive } + +// ❌ Bad +var results = [SomeType]() +for element in input { + results.append(transform(element)) +} +``` + +### 프로퍼티 옵저버 + +복잡한 프로퍼티 옵저버는 별도 메서드로 추출합니다. + +```swift +// ✅ Good +var text: String? { + didSet { textDidUpdate(from: oldValue) } +} + +private func textDidUpdate(from oldValue: String?) { + guard oldValue != text else { return } + // side effects +} + +// ❌ Bad +var text: String? { + didSet { + guard oldValue != text else { return } + // 긴 side effects 로직... + } +} +``` + +### 네임스페이스 + +전역 상수·함수 그룹화에는 케이스 없는 `enum`을 사용합니다. +`struct`는 인스턴스화가 가능하므로 네임스페이스 용도에 적합하지 않습니다. + +```swift +// ✅ Good +enum Constants { + enum Layout { + static let padding: CGFloat = 16 + static let cornerRadius: CGFloat = 8 + } + enum Animation { + static let duration: CGFloat = 0.3 + } +} + +// ❌ Bad +struct Constants { + static let padding: CGFloat = 16 +} +``` + +--- + +## 5. 파일 구성 + +### 코드 순서 + +파일 내 코드는 아래 순서로 구성합니다. + +``` +1. 프로퍼티 (stored → computed 순) +2. init +3. body (SwiftUI View의 경우) +4. 메서드 (public → internal → private 순) +``` + +### MARK 구분 + +`// MARK: -` 로 각 섹션을 구분합니다. MARK 위아래에는 반드시 빈 줄을 넣습니다. + +```swift +final class ProfileViewModel: ObservableObject { + + // MARK: - Properties + + @Published private(set) var user: User? + private let userService: UserService + + // MARK: - Init + + init(userService: UserService) { + self.userService = userService + } + + // MARK: - Actions + + func didTapLoadButton() { + Task { await loadUser() } + } + + // MARK: - Private + + private func loadUser() async { } +} +``` + +### 임포트 순서 + +내장 프레임워크를 먼저, 빈 줄로 구분하여 서드파티 프레임워크를 임포트합니다. 각 그룹 내에서 알파벳 순으로 정렬합니다. + +```swift +import AppKit +import SwiftUI + +import Alamofire +import SnapKit +``` diff --git a/docs/DESIGN_SYSTEM_GUIDELINES.md b/docs/DESIGN_SYSTEM_GUIDELINES.md new file mode 100644 index 0000000..5fd7aca --- /dev/null +++ b/docs/DESIGN_SYSTEM_GUIDELINES.md @@ -0,0 +1,3 @@ +# Design System Guidelines + +> 🚧 디자인 시스템 도입 후 작성 예정입니다.