diff --git a/sidebar.config.mjs b/sidebar.config.mjs index 3960d24..9796872 100644 --- a/sidebar.config.mjs +++ b/sidebar.config.mjs @@ -94,7 +94,6 @@ export const sidebars = [ { label: "🧭 Part 9. ν”„λ‘œμ νŠΈ ꡬ쑰 & μ•„ν‚€ν…μ²˜", items: [ - { label: "클린 μ•„ν‚€ν…μ²˜", slug: "part9/clean-architecture" }, { label: "κΈ°λŠ₯별 vs 계측별 폴더 ꡬ쑰", slug: "part9/folder-structure" }, { label: "λ©€ν‹° λͺ¨λ“ˆ μ•„ν‚€ν…μ²˜", slug: "part9/multi-module" }, ], @@ -107,20 +106,20 @@ export const sidebars = [ { label: "μ• λ‹ˆλ©”μ΄μ…˜", slug: "part10/animations" }, { label: "μ ‘κ·Όμ„±", slug: "part10/accessibility" }, { label: "λ‹€κ΅­μ–΄ 처리", slug: "part10/internationalization" }, - { label: "퍼포먼슀 νŠœλ‹", slug: "part10/performance" }, - { label: "μΆ”μ²œ νŒ¨ν‚€μ§€", slug: "part10/recommended-packages" }, + { label: "μ„±λŠ₯ μ΅œμ ν™”", slug: "part10/performance" }, + // { label: "μΆ”μ²œ νŒ¨ν‚€μ§€", slug: "part10/recommended-packages" }, ], }, { label: "πŸ“š 뢀둝", items: [ - { label: "개발 도ꡬ와 링크", slug: "appendix/tools" }, + // { label: "개발 도ꡬ와 링크", slug: "appendix/tools" }, { label: "Flutter 였λ₯˜ λŒ€μ‘λ²•", slug: "appendix/error-handling" }, { label: "μ½”λ“œ ν…œν”Œλ¦Ώ", slug: "appendix/code-templates" }, - { label: "FAQ", slug: "appendix/faq" }, { label: "μ†Œμ…œ 둜그인", slug: "appendix/social-login" }, { label: "iOS 라이브 μ•‘ν‹°λΉ„ν‹°", slug: "appendix/live-activities" }, { label: "WidgetBook", slug: "appendix/widgetbook" }, + { label: "FAQ", slug: "appendix/faq" }, ], }, ]; diff --git a/src/content/docs/appendix/social-login.md b/src/content/docs/appendix/social-login.mdx similarity index 83% rename from src/content/docs/appendix/social-login.md rename to src/content/docs/appendix/social-login.mdx index 243fe53..b83fb71 100644 --- a/src/content/docs/appendix/social-login.md +++ b/src/content/docs/appendix/social-login.mdx @@ -75,7 +75,7 @@ abstract class SocialLoginProvider { ```yaml dependencies: - kakao_flutter_sdk_user: ^1.6.0 + kakao_flutter_sdk_user: ^1.9.7+3 ``` ### 2. ν”Œλž«νΌλ³„ μ„€μ • @@ -222,149 +222,11 @@ void main() { ```yaml dependencies: - flutter_naver_login: ^1.8.0 + naver_login_sdk: ^3.0.0 ``` -### 2. ν”Œλž«νΌλ³„ μ„€μ • +> μ€€λΉ„μ€‘μž…λ‹ˆλ‹€. -#### Android μ„€μ • - -1. `android/app/src/main/res/values/strings.xml` 파일 생성 및 μ„€μ •: - -```xml - - - ${YOUR_CLIENT_ID} - ${YOUR_CLIENT_SECRET} - ${YOUR_APP_NAME} - -``` - -2. `android/app/build.gradle` 파일의 `defaultConfig` μ„Ήμ…˜μ— λ§€λ‹ˆνŽ˜μŠ€νŠΈ ν”Œλ ˆμ΄μŠ€ν™€λ” μΆ”κ°€: - -```gradle -defaultConfig { - // ... - manifestPlaceholders += [ - 'naverClientId': '${YOUR_CLIENT_ID}', - 'naverClientSecret': '${YOUR_CLIENT_SECRET}', - 'naverClientName': '${YOUR_APP_NAME}' - ] -} -``` - -3. `android/app/src/main/AndroidManifest.xml` νŒŒμΌμ— 넀이버 둜그인 μ„€μ • μΆ”κ°€: - -```xml - - - - - - - - - - - - - - - - - - - - - -``` - -#### iOS μ„€μ • - -1. `ios/Runner/Info.plist` νŒŒμΌμ— 넀이버 μ„€μ • μΆ”κ°€: - -```xml -CFBundleURLTypes - - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - naver${YOUR_CLIENT_ID} - - - - -LSApplicationQueriesSchemes - - - naversearchapp - naversearchthirdlogin - - -naverClientId -${YOUR_CLIENT_ID} -naverClientSecret -${YOUR_CLIENT_SECRET} -naverServiceAppName -${YOUR_APP_NAME} -``` - -### 3. 넀이버 둜그인 κ΅¬ν˜„ - -```dart -import 'package:flutter_naver_login/flutter_naver_login.dart'; - -class NaverLoginProvider implements SocialLoginProvider { - @override - Future login() async { - try { - // 넀이버 둜그인 μ‹€ν–‰ - NaverLoginResult result = await FlutterNaverLogin.logIn(); - - // 둜그인 κ²°κ³Ό 확인 - if (result.status == NaverLoginStatus.success) { - // μ‚¬μš©μž 정보 κ°€μ Έμ˜€κΈ° - NaverAccessToken token = await FlutterNaverLogin.currentAccessToken; - NaverAccountResult account = await FlutterNaverLogin.currentAccount(); - - return SocialLoginResult.success( - accessToken: token.accessToken, - provider: 'naver', - email: account.email, - name: account.name, - profileImage: account.profileImage, - ); - } else if (result.status == NaverLoginStatus.cancelledByUser) { - return const SocialLoginResult.cancelled(provider: 'naver'); - } else { - return SocialLoginResult.error( - message: '넀이버 둜그인 μ‹€νŒ¨: ${result.errorMessage}', - provider: 'naver', - ); - } - } catch (error) { - return SocialLoginResult.error( - message: error.toString(), - provider: 'naver', - ); - } - } - - @override - Future logout() async { - await FlutterNaverLogin.logOut(); - } -} -``` ## μ• ν”Œ 둜그인 κ΅¬ν˜„ diff --git a/src/content/docs/appendix/tools.md b/src/content/docs/appendix/tools.md index 99e6b4c..53c400e 100644 --- a/src/content/docs/appendix/tools.md +++ b/src/content/docs/appendix/tools.md @@ -22,7 +22,6 @@ Flutter κ°œλ°œμ„ 효과적으둜 μˆ˜ν–‰ν•˜κΈ° μœ„ν•΄μ„œλŠ” μ μ ˆν•œ 도ꡬλ₯Ό | **Awesome Flutter Snippets** | 자주 μ‚¬μš©λ˜λŠ” Flutter μ½”λ“œ 쑰각 제곡 | | **Flutter Widget Snippets** | μœ„μ ― μ½”λ“œ μŠ€λ‹ˆνŽ« 제곡 | | **Pubspec Assist** | pubspec.yaml 파일 관리 λ„μš°λ―Έ | -| **bloc** | Bloc νŒ¨ν„΄ 개발 지원 | | **Git History** | Git 이λ ₯ 관리 μ‹œκ°ν™” | | **Error Lens** | 인라인 였λ₯˜ ν•˜μ΄λΌμ΄νŒ… | @@ -35,14 +34,6 @@ Flutter κ°œλ°œμ„ 효과적으둜 μˆ˜ν–‰ν•˜κΈ° μœ„ν•΄μ„œλŠ” μ μ ˆν•œ 도ꡬλ₯Ό | **Flipper** | Facebook의 λͺ¨λ°”일 μ•± 디버깅 ν”Œλž«νΌ | [μ›Ήμ‚¬μ΄νŠΈ](https://fbflipper.com/) | | **Sentry** | μ‹€μ‹œκ°„ μ—λŸ¬ 좔적 | [μ›Ήμ‚¬μ΄νŠΈ](https://sentry.io/) | -### UI λ””μžμΈ 도ꡬ - -| 도ꡬ | μ„€λͺ… | 링크 | -| ------------------------- | ------------------------ | ---------------------------------------------------------------- | -| **Figma** | ν˜‘μ—… 기반 UI λ””μžμΈ 도ꡬ | [μ›Ήμ‚¬μ΄νŠΈ](https://www.figma.com/) | -| **Adobe XD** | UI/UX λ””μžμΈ 도ꡬ | [μ›Ήμ‚¬μ΄νŠΈ](https://www.adobe.com/products/xd.html) | -| **Flutter UI Challenges** | UI κ΅¬ν˜„ μ—°μŠ΅ ν”„λ‘œμ νŠΈ | [GitHub](https://github.com/lohanidamodar/flutter_ui_challenges) | - ### CI/CD 도ꡬ | 도ꡬ | μ„€λͺ… | 링크 | diff --git a/src/content/docs/part10/performance.md b/src/content/docs/part10/performance.md index baf406b..14d73ab 100644 --- a/src/content/docs/part10/performance.md +++ b/src/content/docs/part10/performance.md @@ -1,5 +1,5 @@ --- -title: 퍼포먼슀 νŠœλ‹ +title: μ„±λŠ₯ μ΅œμ ν™” --- Flutter μ•±μ˜ μ„±λŠ₯은 μ‚¬μš©μž κ²½ν—˜μ— 직접적인 영ν–₯을 λ―ΈμΉ˜λŠ” μ€‘μš”ν•œ μš”μ†Œμž…λ‹ˆλ‹€. 앱이 λΆ€λ“œλŸ½κ²Œ μž‘λ™ν•˜κ³ , λ°˜μ‘μ΄ λΉ λ₯΄λ©°, μžμ›μ„ 효율적으둜 μ‚¬μš©ν•  λ•Œ μ‚¬μš©μž λ§Œμ‘±λ„κ°€ λ†’μ•„μ§‘λ‹ˆλ‹€. 이 μž₯μ—μ„œλŠ” Flutter μ•±μ˜ μ„±λŠ₯을 μ΅œμ ν™”ν•˜κΈ° μœ„ν•œ λ‹€μ–‘ν•œ μ „λž΅κ³Ό 기법을 μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€. @@ -319,7 +319,7 @@ class ImageCache { } ``` -### 2. λ””μŠ€ν¬μ¦ˆ νŒ¨ν„΄ +### 2. ν•΄μ œ νŒ¨ν„΄ `StatefulWidget`μ—μ„œ λ¦¬μ†ŒμŠ€λ₯Ό 적절히 ν•΄μ œν•©λ‹ˆλ‹€: diff --git a/src/content/docs/part5/go-router.md b/src/content/docs/part5/go-router.md index d05f351..f60aeff 100644 --- a/src/content/docs/part5/go-router.md +++ b/src/content/docs/part5/go-router.md @@ -775,11 +775,6 @@ go_routerλŠ” λ‹€λ₯Έ Flutter λΌμš°νŒ… λΌμ΄λΈŒλŸ¬λ¦¬μ— λΉ„ν•΄ λͺ‡ κ°€μ§€ μž₯ - **go_router**: 곡식 지원, κ°„λ‹¨ν•œ μ„€μ •, μ½”λ“œ 생성 λΆˆν•„μš” - **auto_route**: μ½”λ“œ 생성 기반, νƒ€μž… μ•ˆμ „μ„±, 더 λ§Žμ€ μ„€μ • ν•„μš” -### 3. go_router vs get - -- **go_router**: 곡식 지원, Navigator 2.0 기반, URL 동기화 지원 κ°•λ ₯ -- **get**: 더 넓은 κΈ°λŠ₯ μ„ΈνŠΈ (μƒνƒœ 관리, 쒅속성 μ£Όμž… λ“±), 더 λ‹¨μˆœν•œ API - ## μš”μ•½ - **go_router**λŠ” Flutter νŒ€μ΄ 곡식 μ§€μ›ν•˜λŠ” λ„€λΉ„κ²Œμ΄μ…˜ 라이브러리둜, Navigator 2.0의 κΈ°λŠ₯을 더 μ‰½κ²Œ μ‚¬μš©ν•  수 있게 ν•΄μ€λ‹ˆλ‹€. diff --git a/src/content/docs/part8/error-tracking.md b/src/content/docs/part8/error-tracking.md index 4f6b4cc..98962ce 100644 --- a/src/content/docs/part8/error-tracking.md +++ b/src/content/docs/part8/error-tracking.md @@ -55,55 +55,7 @@ dependencies: flutter pub get ``` -#### 3. Android μ„€μ • - -Androidμ—μ„œ Crashlyticsλ₯Ό μ„€μ •ν•˜λ €λ©΄ `android/app/build.gradle` νŒŒμΌμ— λ‹€μŒ ν”ŒλŸ¬κ·ΈμΈμ„ μΆ”κ°€ν•΄μ•Ό ν•©λ‹ˆλ‹€: - -```gradle -// app/build.gradle 파일 -dependencies { - // ... κΈ°μ‘΄ 쒅속성 - implementation 'com.google.firebase:firebase-crashlytics:18.4.0' - implementation 'com.google.firebase:firebase-analytics:21.3.0' -} - -apply plugin: 'com.google.firebase.crashlytics' -``` - -그리고 ν”„λ‘œμ νŠΈ μˆ˜μ€€μ˜ `android/build.gradle` νŒŒμΌμ— λ‹€μŒ λ‚΄μš©μ„ μΆ”κ°€ν•©λ‹ˆλ‹€: - -```gradle -// project/build.gradle 파일 -buildscript { - repositories { - // ... κΈ°μ‘΄ μ €μž₯μ†Œ - google() - } - dependencies { - // ... κΈ°μ‘΄ 쒅속성 - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' - } -} -``` - -#### 4. iOS μ„€μ • - -iOS의 경우 `ios/Podfile`에 λ‹€μŒ λ‚΄μš©μ„ μΆ”κ°€ν•©λ‹ˆλ‹€: - -```ruby -target 'Runner' do - // ... κΈ°μ‘΄ λ‚΄μš© - pod 'FirebaseCrashlytics' -end -``` - -그리고 λ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€: - -```bash -cd ios && pod install --repo-update -``` - -#### 5. Flutter μ•±μ—μ„œ Crashlytics μ΄ˆκΈ°ν™” +#### 3. Flutter μ•±μ—μ„œ Crashlytics μ΄ˆκΈ°ν™” μ•±μ˜ 메인 νŒŒμΌμ—μ„œ Crashlyticsλ₯Ό μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€: @@ -259,7 +211,7 @@ pubspec.yaml에 λ‹€μŒ νŒ¨ν‚€μ§€λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€: ```yaml dependencies: - sentry_flutter: ^7.9.0 + sentry_flutter: ^8.14.2 ``` νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ν•©λ‹ˆλ‹€: diff --git a/src/content/docs/part9/clean-architecture.md b/src/content/docs/part9/clean-architecture.md deleted file mode 100644 index a8138dc..0000000 --- a/src/content/docs/part9/clean-architecture.md +++ /dev/null @@ -1,813 +0,0 @@ ---- -title: 클린 μ•„ν‚€ν…μ²˜ λ„μž…ν•˜κΈ° ---- - - -Flutter μ•±μ˜ 규λͺ¨κ°€ 컀지고 λ³΅μž‘ν•΄μ§ˆμˆ˜λ‘ μ½”λ“œμ˜ μœ μ§€λ³΄μˆ˜μ„±, ν™•μž₯μ„±, ν…ŒμŠ€νŠΈ μš©μ΄μ„±μ„ ν™•λ³΄ν•˜λŠ” 것이 μ€‘μš”ν•΄μ§‘λ‹ˆλ‹€. 클린 μ•„ν‚€ν…μ²˜λŠ” μ΄λŸ¬ν•œ 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•œ μ†Œν”„νŠΈμ›¨μ–΄ 섀계 λ°©λ²•λ‘ μœΌλ‘œ, Flutter 앱에도 효과적으둜 μ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 λ¬Έμ„œμ—μ„œλŠ” Flutter에 클린 μ•„ν‚€ν…μ²˜λ₯Ό λ„μž…ν•˜λŠ” 방법을 μ‹€μš©μ μΈ κ΄€μ μ—μ„œ μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€. - -## 클린 μ•„ν‚€ν…μ²˜λž€? - -클린 μ•„ν‚€ν…μ²˜λŠ” λ‘œλ²„νŠΈ C. λ§ˆν‹΄(Robert C. Martin, 일λͺ… Uncle Bob)이 μ œμ•ˆν•œ μ†Œν”„νŠΈμ›¨μ–΄ μ•„ν‚€ν…μ²˜ νŒ¨ν„΄μœΌλ‘œ, λ‹€μŒκ³Ό 같은 핡심 원칙을 λ”°λ¦…λ‹ˆλ‹€: - -### 클린 μ•„ν‚€ν…μ²˜μ˜ μ£Όμš” 원칙 - -1. **관심사 뢄리(Separation of Concerns)**: μ„œλ‘œ λ‹€λ₯Έ μ±…μž„μ„ κ°€μ§„ μ½”λ“œλ₯Ό λΆ„λ¦¬ν•©λ‹ˆλ‹€. -2. **μ˜μ‘΄μ„± κ·œμΉ™(Dependency Rule)**: λͺ¨λ“  μ˜μ‘΄μ„±μ€ μ™ΈλΆ€ κ³„μΈ΅μ—μ„œ λ‚΄λΆ€ κ³„μΈ΅μœΌλ‘œ ν–₯ν•΄μ•Ό ν•©λ‹ˆλ‹€. λ‚΄λΆ€ 계측은 μ™ΈλΆ€ 계측에 λŒ€ν•΄ μ•Œμ§€ λͺ»ν•©λ‹ˆλ‹€. -3. **λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ 독립성**: λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™μ€ UI, λ°μ΄ν„°λ² μ΄μŠ€, ν”„λ ˆμž„μ›Œν¬, μ™ΈλΆ€ λΌμ΄λΈŒλŸ¬λ¦¬μ™€ 독립적이어야 ν•©λ‹ˆλ‹€. - -### 클린 μ•„ν‚€ν…μ²˜μ˜ 계측 - -클린 μ•„ν‚€ν…μ²˜λŠ” 일반적으둜 λ‹€μŒ μ„Έ κ°€μ§€ μ£Όμš” κ³„μΈ΅μœΌλ‘œ κ΅¬μ„±λ©λ‹ˆλ‹€: - -1. **도메인 계측(Domain Layer)**: λΉ„μ¦ˆλ‹ˆμŠ€ 둜직과 μ—”ν‹°ν‹°λ₯Ό ν¬ν•¨ν•˜λŠ” 핡심 κ³„μΈ΅μž…λ‹ˆλ‹€. -2. **데이터 계측(Data Layer)**: 데이터 μ†ŒμŠ€μ™€μ˜ 톡신을 λ‹΄λ‹Ήν•˜λŠ” κ³„μΈ΅μž…λ‹ˆλ‹€. -3. **ν”„λ ˆμ  ν…Œμ΄μ…˜ 계측(Presentation Layer)**: UI와 μ‚¬μš©μž μƒν˜Έμž‘μš©μ„ μ²˜λ¦¬ν•˜λŠ” κ³„μΈ΅μž…λ‹ˆλ‹€. - -## Flutterμ—μ„œμ˜ 클린 μ•„ν‚€ν…μ²˜ κ΅¬ν˜„ - -Flutterμ—μ„œ 클린 μ•„ν‚€ν…μ²˜λ₯Ό κ΅¬ν˜„ν•˜κΈ° μœ„ν•œ ꡬ체적인 μ ‘κ·Ό 방식을 μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€. - -### 1. ν”„λ‘œμ νŠΈ ꡬ쑰 μ„€μ • - -클린 μ•„ν‚€ν…μ²˜λ₯Ό μ μš©ν•œ Flutter ν”„λ‘œμ νŠΈμ˜ κΈ°λ³Έ κ΅¬μ‘°λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€: - -``` -lib/ -β”œβ”€β”€ core/ # 곡톡 κΈ°λŠ₯ 및 μœ ν‹Έλ¦¬ν‹° -β”‚ β”œβ”€β”€ error/ # 였λ₯˜ 처리 -β”‚ β”œβ”€β”€ network/ # λ„€νŠΈμ›Œν¬ κ΄€λ ¨ -β”‚ └── utils/ # μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ -β”‚ -β”œβ”€β”€ data/ # 데이터 계측 -β”‚ β”œβ”€β”€ datasources/ # 데이터 μ†ŒμŠ€ κ΅¬ν˜„ -β”‚ β”‚ β”œβ”€β”€ local/ # 둜컬 데이터 μ†ŒμŠ€ -β”‚ β”‚ └── remote/ # 원격 데이터 μ†ŒμŠ€ -β”‚ β”œβ”€β”€ models/ # 데이터 λͺ¨λΈ(DTO) -β”‚ └── repositories/ # 리포지토리 κ΅¬ν˜„ -β”‚ -β”œβ”€β”€ domain/ # 도메인 계측 -β”‚ β”œβ”€β”€ entities/ # λΉ„μ¦ˆλ‹ˆμŠ€ μ—”ν‹°ν‹° -β”‚ β”œβ”€β”€ repositories/ # 리포지토리 μΈν„°νŽ˜μ΄μŠ€ -β”‚ └── usecases/ # μœ μŠ€μΌ€μ΄μŠ€(λΉ„μ¦ˆλ‹ˆμŠ€ 둜직) -β”‚ -β”œβ”€β”€ presentation/ # ν”„λ ˆμ  ν…Œμ΄μ…˜ 계측 -β”‚ β”œβ”€β”€ pages/ # ν™”λ©΄ μœ„μ ― -β”‚ β”œβ”€β”€ providers/ # μƒνƒœ 관리 -β”‚ └── widgets/ # μž¬μ‚¬μš© μœ„μ ― -β”‚ -└── main.dart # μ•± μ§„μž…μ  -``` - -이 κ΅¬μ‘°λŠ” κΈ°λŠ₯별 ꡬ쑰와 κ²°ν•©ν•˜μ—¬ μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€: - -``` -lib/ -β”œβ”€β”€ core/ # 곡톡 κΈ°λŠ₯ 및 μœ ν‹Έλ¦¬ν‹° -β”‚ -β”œβ”€β”€ features/ # κΈ°λŠ₯별 ꡬ성 -β”‚ β”œβ”€β”€ auth/ # 인증 κΈ°λŠ₯ -β”‚ β”‚ β”œβ”€β”€ data/ -β”‚ β”‚ β”œβ”€β”€ domain/ -β”‚ β”‚ └── presentation/ -β”‚ β”‚ -β”‚ β”œβ”€β”€ products/ # μƒν’ˆ κΈ°λŠ₯ -β”‚ β”‚ β”œβ”€β”€ data/ -β”‚ β”‚ β”œβ”€β”€ domain/ -β”‚ β”‚ └── presentation/ -β”‚ β”‚ -β”‚ └── cart/ # μž₯λ°”κ΅¬λ‹ˆ κΈ°λŠ₯ -β”‚ β”œβ”€β”€ data/ -β”‚ β”œβ”€β”€ domain/ -β”‚ └── presentation/ -β”‚ -└── main.dart # μ•± μ§„μž…μ  -``` - -### 2. 계측별 κ΅¬ν˜„ 방법 - -#### 도메인 계측(Domain Layer) - -도메인 계측은 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ λ‹΄λ‹Ήν•˜λŠ” 핡심 κ³„μΈ΅μœΌλ‘œ, λ‹€μŒκ³Ό 같은 μš”μ†Œλ‘œ κ΅¬μ„±λ©λ‹ˆλ‹€: - -1. **μ—”ν‹°ν‹°(Entities)**: λΉ„μ¦ˆλ‹ˆμŠ€ 객체 λͺ¨λΈ -2. **리포지토리 μΈν„°νŽ˜μ΄μŠ€(Repository Interfaces)**: 데이터 μ•‘μ„ΈμŠ€ 좔상화 -3. **μœ μŠ€μΌ€μ΄μŠ€(Use Cases)**: λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μΊ‘μŠν™” - -```dart -// 1. μ—”ν‹°ν‹° μ •μ˜ -// domain/entities/product.dart -class Product { - final String id; - final String name; - final double price; - final String description; - - const Product({ - required this.id, - required this.name, - required this.price, - required this.description, - }); -} - -// 2. 리포지토리 μΈν„°νŽ˜μ΄μŠ€ -// domain/repositories/product_repository.dart -abstract class ProductRepository { - Future> getProducts(); - Future getProductById(String id); - Future saveProduct(Product product); - Future deleteProduct(String id); -} - -// 3. μœ μŠ€μΌ€μ΄μŠ€ -// domain/usecases/get_products.dart -class GetProducts { - final ProductRepository repository; - - GetProducts(this.repository); - - Future> execute() { - return repository.getProducts(); - } -} - -// domain/usecases/get_product_by_id.dart -class GetProductById { - final ProductRepository repository; - - GetProductById(this.repository); - - Future execute(String id) { - return repository.getProductById(id); - } -} -``` - -#### 데이터 계측(Data Layer) - -데이터 계측은 데이터 μ €μž₯μ†Œ(API, 둜컬 DB λ“±)μ™€μ˜ 톡신을 λ‹΄λ‹Ήν•˜κ³ , 도메인 계측에 μ •μ˜λœ 리포지토리 μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•©λ‹ˆλ‹€: - -1. **데이터 λͺ¨λΈ(Data Models)**: APIλ‚˜ DB와 ν†΅μ‹ ν•˜κΈ° μœ„ν•œ DTO(Data Transfer Object) -2. **데이터 μ†ŒμŠ€(Data Sources)**: μ‹€μ œ 데이터 μ•‘μ„ΈμŠ€ 둜직 -3. **리포지토리 κ΅¬ν˜„(Repository Implementations)**: 도메인 계측에 μ •μ˜λœ μΈν„°νŽ˜μ΄μŠ€ κ΅¬ν˜„ - -```dart -// 1. 데이터 λͺ¨λΈ -// data/models/product_model.dart -class ProductModel { - final String id; - final String name; - final double price; - final String description; - - const ProductModel({ - required this.id, - required this.name, - required this.price, - required this.description, - }); - - // JSON 직렬화/역직렬화 - factory ProductModel.fromJson(Map json) { - return ProductModel( - id: json['id'], - name: json['name'], - price: json['price'].toDouble(), - description: json['description'], - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'price': price, - 'description': description, - }; - } - - // 도메인 μ—”ν‹°ν‹°λ‘œ λ³€ν™˜ - Product toEntity() { - return Product( - id: id, - name: name, - price: price, - description: description, - ); - } - - // 도메인 μ—”ν‹°ν‹°μ—μ„œ λ³€ν™˜ - factory ProductModel.fromEntity(Product product) { - return ProductModel( - id: product.id, - name: product.name, - price: product.price, - description: product.description, - ); - } -} - -// 2. 데이터 μ†ŒμŠ€ -// data/datasources/remote/product_remote_data_source.dart -abstract class ProductRemoteDataSource { - Future> getProducts(); - Future getProductById(String id); - Future saveProduct(ProductModel product); - Future deleteProduct(String id); -} - -// data/datasources/remote/product_remote_data_source_impl.dart -class ProductRemoteDataSourceImpl implements ProductRemoteDataSource { - final http.Client client; - final String baseUrl; - - ProductRemoteDataSourceImpl({ - required this.client, - required this.baseUrl, - }); - - @override - Future> getProducts() async { - try { - final response = await client.get( - Uri.parse('$baseUrl/products'), - headers: {'Content-Type': 'application/json'}, - ); - - if (response.statusCode == 200) { - final List jsonList = json.decode(response.body); - return jsonList.map((json) => ProductModel.fromJson(json)).toList(); - } else { - throw ServerException(); - } - } catch (e) { - throw ServerException(); - } - } - - // λ‹€λ₯Έ λ©”μ„œλ“œ κ΅¬ν˜„... -} - -// 3. 리포지토리 κ΅¬ν˜„ -// data/repositories/product_repository_impl.dart -class ProductRepositoryImpl implements ProductRepository { - final ProductRemoteDataSource remoteDataSource; - final NetworkInfo networkInfo; - - ProductRepositoryImpl({ - required this.remoteDataSource, - required this.networkInfo, - }); - - @override - Future> getProducts() async { - if (await networkInfo.isConnected) { - try { - final remoteProducts = await remoteDataSource.getProducts(); - return remoteProducts.map((model) => model.toEntity()).toList(); - } on ServerException { - throw ServerFailure(); - } - } else { - throw NetworkFailure(); - } - } - - // λ‹€λ₯Έ λ©”μ„œλ“œ κ΅¬ν˜„... -} -``` - -#### ν”„λ ˆμ  ν…Œμ΄μ…˜ 계측(Presentation Layer) - -ν”„λ ˆμ  ν…Œμ΄μ…˜ 계측은 UI와 μ‚¬μš©μž μƒν˜Έμž‘μš©μ„ λ‹΄λ‹Ήν•˜λ©°, Riverpod을 μ‚¬μš©ν•˜μ—¬ μƒνƒœλ₯Ό κ΄€λ¦¬ν•©λ‹ˆλ‹€: - -1. **μƒνƒœ 관리(State Management)**: Riverpod ν”„λ‘œλ°”μ΄λ” -2. **UI μœ„μ ―(UI Widgets)**: ν™”λ©΄ 및 μ»΄ν¬λ„ŒνŠΈ - -```dart -// 1. μƒνƒœ 관리 (Riverpod) -// presentation/providers/product_providers.dart -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -// μ˜μ‘΄μ„± μ£Όμž…μ„ μœ„ν•œ ν”„λ‘œλ°”μ΄λ” -final productRepositoryProvider = Provider((ref) { - final remoteDataSource = ref.read(productRemoteDataSourceProvider); - final networkInfo = ref.read(networkInfoProvider); - - return ProductRepositoryImpl( - remoteDataSource: remoteDataSource, - networkInfo: networkInfo, - ); -}); - -final getProductsUseCaseProvider = Provider((ref) { - final repository = ref.read(productRepositoryProvider); - return GetProducts(repository); -}); - -// μƒνƒœ ν”„λ‘œλ°”μ΄λ” -final productsProvider = FutureProvider>((ref) async { - final getProductsUseCase = ref.read(getProductsUseCaseProvider); - return getProductsUseCase.execute(); -}); - -final productDetailsProvider = FutureProvider.family((ref, id) async { - final getProductByIdUseCase = ref.read(getProductByIdUseCaseProvider); - return getProductByIdUseCase.execute(id); -}); - -// 2. UI μœ„μ ― -// presentation/pages/product_list_page.dart -class ProductListPage extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final productsAsync = ref.watch(productsProvider); - - return Scaffold( - appBar: AppBar(title: Text('μƒν’ˆ λͺ©λ‘')), - body: productsAsync.when( - data: (products) => ListView.builder( - itemCount: products.length, - itemBuilder: (context, index) { - final product = products[index]; - return ListTile( - title: Text(product.name), - subtitle: Text('\$${product.price}'), - onTap: () => Navigator.of(context).pushNamed( - '/product/${product.id}', - ), - ); - }, - ), - loading: () => Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center( - child: Text('였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: $error'), - ), - ), - ); - } -} -``` - -### 3. μ˜μ‘΄μ„± μ£Όμž…(Dependency Injection) - -클린 μ•„ν‚€ν…μ²˜μ—μ„œ μ˜μ‘΄μ„± μ£Όμž…μ€ 맀우 μ€‘μš”ν•©λ‹ˆλ‹€. Riverpod을 μ‚¬μš©ν•˜λ©΄ μ˜μ‘΄μ„± μ£Όμž…μ„ 효과적으둜 관리할 수 μžˆμŠ΅λ‹ˆλ‹€: - -```dart -// core/di/injection_container.dart -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:http/http.dart' as http; - -// 인프라 계측 ν”„λ‘œλ°”μ΄λ” -final httpClientProvider = Provider((ref) { - return http.Client(); -}); - -final networkInfoProvider = Provider((ref) { - final connectivity = ref.read(connectivityProvider); - return NetworkInfoImpl(connectivity); -}); - -// 데이터 μ†ŒμŠ€ ν”„λ‘œλ°”μ΄λ” -final productRemoteDataSourceProvider = Provider((ref) { - final client = ref.read(httpClientProvider); - return ProductRemoteDataSourceImpl( - client: client, - baseUrl: 'https://api.example.com', - ); -}); - -// 리포지토리 ν”„λ‘œλ°”μ΄λ” -final productRepositoryProvider = Provider((ref) { - final remoteDataSource = ref.read(productRemoteDataSourceProvider); - final networkInfo = ref.read(networkInfoProvider); - - return ProductRepositoryImpl( - remoteDataSource: remoteDataSource, - networkInfo: networkInfo, - ); -}); - -// μœ μŠ€μΌ€μ΄μŠ€ ν”„λ‘œλ°”μ΄λ” -final getProductsUseCaseProvider = Provider((ref) { - final repository = ref.read(productRepositoryProvider); - return GetProducts(repository); -}); - -final getProductByIdUseCaseProvider = Provider((ref) { - final repository = ref.read(productRepositoryProvider); - return GetProductById(repository); -}); -``` - -### 4. μ—λŸ¬ 처리 - -클린 μ•„ν‚€ν…μ²˜μ—μ„œ μ—λŸ¬ μ²˜λ¦¬λŠ” μΌκ΄€λ˜κ³  μ²΄κ³„μ μœΌλ‘œ 이루어져야 ν•©λ‹ˆλ‹€: - -```dart -// core/error/exceptions.dart -class ServerException implements Exception {} -class CacheException implements Exception {} -class NetworkException implements Exception {} - -// core/error/failures.dart -abstract class Failure {} - -class ServerFailure extends Failure {} -class CacheFailure extends Failure {} -class NetworkFailure extends Failure {} - -// μ—λŸ¬ 처리 μ˜ˆμ‹œ -// data/repositories/product_repository_impl.dart -@override -Future> getProducts() async { - if (await networkInfo.isConnected) { - try { - final remoteProducts = await remoteDataSource.getProducts(); - return remoteProducts.map((model) => model.toEntity()).toList(); - } on ServerException { - throw ServerFailure(); - } - } else { - throw NetworkFailure(); - } -} -``` - -## μ‹€μ œ Flutter 예제: Todo μ•± - -μ‹€μ œ Todo μ•± 예제λ₯Ό 톡해 클린 μ•„ν‚€ν…μ²˜λ₯Ό μ μš©ν•˜λŠ” 방법을 μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€. - -### 도메인 계측 κ΅¬ν˜„ - -```dart -// domain/entities/todo.dart -class Todo { - final String id; - final String title; - final String description; - final bool completed; - - const Todo({ - required this.id, - required this.title, - required this.description, - required this.completed, - }); -} - -// domain/repositories/todo_repository.dart -abstract class TodoRepository { - Future> getTodos(); - Future getTodoById(String id); - Future addTodo(Todo todo); - Future updateTodo(Todo todo); - Future deleteTodo(String id); -} - -// domain/usecases/get_todos.dart -class GetTodos { - final TodoRepository repository; - - GetTodos(this.repository); - - Future> execute() { - return repository.getTodos(); - } -} - -// domain/usecases/add_todo.dart -class AddTodo { - final TodoRepository repository; - - AddTodo(this.repository); - - Future execute(Todo todo) { - return repository.addTodo(todo); - } -} -``` - -### 데이터 계측 κ΅¬ν˜„ - -```dart -// data/models/todo_model.dart -import 'package:json_annotation/json_annotation.dart'; - -part 'todo_model.g.dart'; - -@JsonSerializable() -class TodoModel { - final String id; - final String title; - final String description; - final bool completed; - - const TodoModel({ - required this.id, - required this.title, - required this.description, - required this.completed, - }); - - factory TodoModel.fromJson(Map json) => - _$TodoModelFromJson(json); - - Map toJson() => _$TodoModelToJson(this); - - // λ³€ν™˜ λ©”μ„œλ“œ - Todo toEntity() => Todo( - id: id, - title: title, - description: description, - completed: completed, - ); - - factory TodoModel.fromEntity(Todo todo) => TodoModel( - id: todo.id, - title: todo.title, - description: todo.description, - completed: todo.completed, - ); -} - -// data/datasources/todo_remote_data_source.dart -abstract class TodoRemoteDataSource { - Future> getTodos(); - Future getTodoById(String id); - Future addTodo(TodoModel todo); - Future updateTodo(TodoModel todo); - Future deleteTodo(String id); -} - -// data/datasources/todo_remote_data_source_impl.dart -class TodoRemoteDataSourceImpl implements TodoRemoteDataSource { - final http.Client client; - final String baseUrl; - - TodoRemoteDataSourceImpl({ - required this.client, - required this.baseUrl, - }); - - @override - Future> getTodos() async { - try { - final response = await client.get( - Uri.parse('$baseUrl/todos'), - headers: {'Content-Type': 'application/json'}, - ); - - if (response.statusCode == 200) { - final List jsonList = json.decode(response.body); - return jsonList.map((json) => TodoModel.fromJson(json)).toList(); - } else { - throw ServerException(); - } - } catch (e) { - throw ServerException(); - } - } - - // λ‹€λ₯Έ λ©”μ„œλ“œ κ΅¬ν˜„... -} - -// data/repositories/todo_repository_impl.dart -class TodoRepositoryImpl implements TodoRepository { - final TodoRemoteDataSource remoteDataSource; - final NetworkInfo networkInfo; - - TodoRepositoryImpl({ - required this.remoteDataSource, - required this.networkInfo, - }); - - @override - Future> getTodos() async { - if (await networkInfo.isConnected) { - try { - final remoteTodos = await remoteDataSource.getTodos(); - return remoteTodos.map((model) => model.toEntity()).toList(); - } on ServerException { - throw ServerFailure(); - } - } else { - throw NetworkFailure(); - } - } - - // λ‹€λ₯Έ λ©”μ„œλ“œ κ΅¬ν˜„... -} -``` - -### ν”„λ ˆμ  ν…Œμ΄μ…˜ 계측 κ΅¬ν˜„ - -```dart -// presentation/providers/todo_providers.dart -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -// 리포지토리 ν”„λ‘œλ°”μ΄λ” -final todoRepositoryProvider = Provider((ref) { - final remoteDataSource = ref.read(todoRemoteDataSourceProvider); - final networkInfo = ref.read(networkInfoProvider); - - return TodoRepositoryImpl( - remoteDataSource: remoteDataSource, - networkInfo: networkInfo, - ); -}); - -// μœ μŠ€μΌ€μ΄μŠ€ ν”„λ‘œλ°”μ΄λ” -final getTodosUseCaseProvider = Provider((ref) { - final repository = ref.read(todoRepositoryProvider); - return GetTodos(repository); -}); - -final addTodoUseCaseProvider = Provider((ref) { - final repository = ref.read(todoRepositoryProvider); - return AddTodo(repository); -}); - -// μƒνƒœ ν”„λ‘œλ°”μ΄λ” -final todosProvider = FutureProvider>((ref) async { - final getTodosUseCase = ref.read(getTodosUseCaseProvider); - return getTodosUseCase.execute(); -}); - -// μƒνƒœ 관리 λ…Έν‹°νŒŒμ΄μ–΄ -final todoListNotifierProvider = StateNotifierProvider>>((ref) { - final getTodosUseCase = ref.read(getTodosUseCaseProvider); - final addTodoUseCase = ref.read(addTodoUseCaseProvider); - - return TodoListNotifier( - getTodosUseCase: getTodosUseCase, - addTodoUseCase: addTodoUseCase, - ); -}); - -class TodoListNotifier extends StateNotifier>> { - final GetTodos getTodosUseCase; - final AddTodo addTodoUseCase; - - TodoListNotifier({ - required this.getTodosUseCase, - required this.addTodoUseCase, - }) : super(const AsyncValue.loading()) { - _loadTodos(); - } - - Future _loadTodos() async { - state = const AsyncValue.loading(); - try { - final todos = await getTodosUseCase.execute(); - state = AsyncValue.data(todos); - } catch (e, stackTrace) { - state = AsyncValue.error(e, stackTrace); - } - } - - Future addTodo(Todo todo) async { - try { - await addTodoUseCase.execute(todo); - _loadTodos(); // λͺ©λ‘ λ‹€μ‹œ λ‘œλ“œ - } catch (e) { - // 였λ₯˜ 처리 - } - } -} - -// presentation/pages/todo_list_page.dart -class TodoListPage extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final todosAsync = ref.watch(todoListNotifierProvider); - - return Scaffold( - appBar: AppBar(title: Text('Todo λͺ©λ‘')), - body: todosAsync.when( - data: (todos) => ListView.builder( - itemCount: todos.length, - itemBuilder: (context, index) { - final todo = todos[index]; - return ListTile( - title: Text(todo.title), - subtitle: Text(todo.description), - trailing: Checkbox( - value: todo.completed, - onChanged: (value) { - // μ²΄ν¬λ°•μŠ€ μƒνƒœ λ³€κ²½ 처리 - }, - ), - ); - }, - ), - loading: () => Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center( - child: Text('였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: $error'), - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - // μƒˆ Todo μΆ”κ°€ ν™”λ©΄μœΌλ‘œ 이동 - Navigator.of(context).pushNamed('/add-todo'); - }, - child: Icon(Icons.add), - ), - ); - } -} -``` - -## 클린 μ•„ν‚€ν…μ²˜μ˜ μž₯점과 단점 - -### μž₯점 - -1. **관심사 뢄리**: λΉ„μ¦ˆλ‹ˆμŠ€ 둜직, 데이터 μ•‘μ„ΈμŠ€, UIκ°€ λͺ…ν™•νžˆ λΆ„λ¦¬λ©λ‹ˆλ‹€. -2. **ν…ŒμŠ€νŠΈ μš©μ΄μ„±**: 각 계측이 λ…λ¦½μ μ΄λ―€λ‘œ λ‹¨μœ„ ν…ŒμŠ€νŠΈκ°€ μš©μ΄ν•©λ‹ˆλ‹€. -3. **μœ μ§€λ³΄μˆ˜μ„±**: μ½”λ“œκ°€ κ΅¬μ‘°ν™”λ˜μ–΄ μžˆμ–΄ 변경이 ν•„μš”ν•  λ•Œ 영ν–₯ λ²”μœ„κ°€ μ œν•œμ μž…λ‹ˆλ‹€. -4. **ν™•μž₯μ„±**: μƒˆλ‘œμš΄ κΈ°λŠ₯을 μΆ”κ°€ν•˜κ±°λ‚˜ κΈ°μ‘΄ κ΅¬ν˜„μ„ κ΅μ²΄ν•˜κΈ° μ‰½μŠ΅λ‹ˆλ‹€. -5. **ν”„λ ˆμž„μ›Œν¬ 독립성**: 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 Flutterλ‚˜ μ™ΈλΆ€ λΌμ΄λΈŒλŸ¬λ¦¬μ— μ˜μ‘΄ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. - -### 단점 - -1. **초기 μ„€μ • λ³΅μž‘μ„±**: μž‘μ€ ν”„λ‘œμ νŠΈμ—μ„œλŠ” κ³Όλ„ν•œ λ³΄μΌλŸ¬ν”Œλ ˆμ΄νŠΈ μ½”λ“œκ°€ 생길 수 μžˆμŠ΅λ‹ˆλ‹€. -2. **ν•™μŠ΅ 곑선**: νŒ€μ›λ“€μ΄ μ•„ν‚€ν…μ²˜λ₯Ό μ΄ν•΄ν•˜κ³  μ μš©ν•˜λŠ” 데 μ‹œκ°„μ΄ ν•„μš”ν•©λ‹ˆλ‹€. -3. **개발 속도**: μ΄ˆκΈ°μ—λŠ” 개발 속도가 느렀질 수 μžˆμŠ΅λ‹ˆλ‹€. -4. **μ½”λ“œλŸ‰ 증가**: μΈν„°νŽ˜μ΄μŠ€, κ΅¬ν˜„μ²΄, λͺ¨λΈ λ³€ν™˜ λ“±μœΌλ‘œ 인해 μ½”λ“œλŸ‰μ΄ μ¦κ°€ν•©λ‹ˆλ‹€. - -## μ‹€μš©μ μΈ μ ‘κ·Ό 방식 - -λͺ¨λ“  ν”„λ‘œμ νŠΈμ— μ™„μ „ν•œ 클린 μ•„ν‚€ν…μ²˜κ°€ ν•„μš”ν•œ 것은 μ•„λ‹™λ‹ˆλ‹€. ν”„λ‘œμ νŠΈ 규λͺ¨μ™€ νŒ€ νŠΉμ„±μ— -따라 λ‹€μŒκ³Ό 같이 μ ‘κ·Όν•  수 μžˆμŠ΅λ‹ˆλ‹€: - -### μ†Œκ·œλͺ¨ ν”„λ‘œμ νŠΈ - -μ†Œκ·œλͺ¨ ν”„λ‘œμ νŠΈμ—μ„œλŠ” κ°„μ†Œν™”λœ μ•„ν‚€ν…μ²˜λ₯Ό μ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€: - -``` -lib/ -β”œβ”€β”€ data/ # 데이터 μ•‘μ„ΈμŠ€ -β”‚ β”œβ”€β”€ models/ -β”‚ └── repositories/ -β”œβ”€β”€ screens/ # UI ν™”λ©΄ -β”‚ β”œβ”€β”€ home/ -β”‚ └── details/ -β”œβ”€β”€ providers/ # μƒνƒœ 관리 -└── main.dart -``` - -### μ€‘κ·œλͺ¨ ν”„λ‘œμ νŠΈ - -μ€‘κ·œλͺ¨ ν”„λ‘œμ νŠΈμ—μ„œλŠ” 도메인 κ³„μΈ΅μ˜ 일뢀 κ°œλ…μ„ λ„μž…ν•  수 μžˆμŠ΅λ‹ˆλ‹€: - -``` -lib/ -β”œβ”€β”€ data/ # 데이터 계측 -β”‚ β”œβ”€β”€ models/ -β”‚ └── repositories/ -β”œβ”€β”€ domain/ # 도메인 계측 (κ°„μ†Œν™”) -β”‚ └── usecases/ -β”œβ”€β”€ presentation/ # ν”„λ ˆμ  ν…Œμ΄μ…˜ 계측 -β”‚ β”œβ”€β”€ pages/ -β”‚ └── providers/ -β”œβ”€β”€ core/ # μœ ν‹Έλ¦¬ν‹° -└── main.dart -``` - -### λŒ€κ·œλͺ¨ ν”„λ‘œμ νŠΈ - -λŒ€κ·œλͺ¨ ν”„λ‘œμ νŠΈμ—μ„œλŠ” μ™„μ „ν•œ 클린 μ•„ν‚€ν…μ²˜μ™€ κΈ°λŠ₯별 ꡬ쑰λ₯Ό κ²°ν•©ν•  수 μžˆμŠ΅λ‹ˆλ‹€: - -``` -lib/ -β”œβ”€β”€ core/ -β”œβ”€β”€ features/ -β”‚ β”œβ”€β”€ auth/ -β”‚ β”‚ β”œβ”€β”€ data/ -β”‚ β”‚ β”œβ”€β”€ domain/ -β”‚ β”‚ └── presentation/ -β”‚ β”œβ”€β”€ home/ -β”‚ β”‚ β”œβ”€β”€ data/ -β”‚ β”‚ β”œβ”€β”€ domain/ -β”‚ β”‚ └── presentation/ -β”‚ └── settings/ -β”‚ β”œβ”€β”€ data/ -β”‚ β”œβ”€β”€ domain/ -β”‚ └── presentation/ -└── main.dart -``` - -## 점진적 λ„μž… μ „λž΅ - -κΈ°μ‘΄ ν”„λ‘œμ νŠΈμ— 클린 μ•„ν‚€ν…μ²˜λ₯Ό λ„μž…ν•  λ•ŒλŠ” 점진적인 접근이 ν•„μš”ν•©λ‹ˆλ‹€: - -1. **계측 뢄리뢀터 μ‹œμž‘**: λ¨Όμ € UI, λΉ„μ¦ˆλ‹ˆμŠ€ 둜직, 데이터 μ•‘μ„ΈμŠ€ μ½”λ“œλ₯Ό λΆ„λ¦¬ν•©λ‹ˆλ‹€. -2. **ν•œ κΈ°λŠ₯μ”© λ¦¬νŒ©ν† λ§**: μƒˆλ‘œμš΄ κΈ°λŠ₯μ΄λ‚˜ μ€‘μš”ν•œ κΈ°λŠ₯λΆ€ν„° 클린 μ•„ν‚€ν…μ²˜λ‘œ λ¦¬νŒ©ν† λ§ν•©λ‹ˆλ‹€. -3. **ν…ŒμŠ€νŠΈ μž‘μ„±**: λ¦¬νŒ©ν† λ§κ³Ό ν•¨κ»˜ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜μ—¬ μ•ˆμ •μ„±μ„ ν™•λ³΄ν•©λ‹ˆλ‹€. -4. **μΈν„°νŽ˜μ΄μŠ€ λ„μž…**: μ μ§„μ μœΌλ‘œ μΈν„°νŽ˜μ΄μŠ€μ™€ μ˜μ‘΄μ„± μ£Όμž…μ„ λ„μž…ν•©λ‹ˆλ‹€. - -## κ²°λ‘  - -클린 μ•„ν‚€ν…μ²˜λŠ” Flutter μ•±μ˜ ν™•μž₯μ„±, μœ μ§€λ³΄μˆ˜μ„±, ν…ŒμŠ€νŠΈ μš©μ΄μ„±μ„ 크게 ν–₯μƒμ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ λͺ¨λ“  ν”„λ‘œμ νŠΈμ— λ™μΌν•œ μˆ˜μ€€μœΌλ‘œ μ μš©ν•  ν•„μš”λŠ” μ—†μœΌλ©°, ν”„λ‘œμ νŠΈμ˜ 규λͺ¨μ™€ νŠΉμ„±μ— 맞게 적절히 μ‘°μ •ν•˜λŠ” 것이 μ€‘μš”ν•©λ‹ˆλ‹€. - -μ²˜μŒμ—λŠ” λ³΅μž‘ν•΄ 보일 수 μžˆμ§€λ§Œ, 핡심 원칙을 μ΄ν•΄ν•˜κ³  μ μ§„μ μœΌλ‘œ λ„μž…ν•œλ‹€λ©΄ μž₯기적으둜 더 μ•ˆμ •μ μ΄κ³  μœ μ§€λ³΄μˆ˜ν•˜κΈ° μ‰¬μš΄ μ½”λ“œλ² μ΄μŠ€λ₯Ό ꡬ좕할 수 μžˆμŠ΅λ‹ˆλ‹€. 특히 νŒ€ 규λͺ¨κ°€ ν¬κ±°λ‚˜ 앱이 계속 μ„±μž₯ν•  κ²ƒμœΌλ‘œ μ˜ˆμƒλ˜λŠ” 경우 클린 μ•„ν‚€ν…μ²˜μ˜ λ„μž…μ„ κ³ λ €ν•΄λ³Ό κ°€μΉ˜κ°€ μžˆμŠ΅λ‹ˆλ‹€.