├── README.md ├── src ├── content │ └── docs │ │ ├── part9 │ │ ├── monorepo.md │ │ └── folder-structure.mdx │ │ ├── part1 │ │ ├── changelog.md │ │ ├── introduction.md │ │ ├── llms.mdx │ │ ├── setup.md │ │ ├── first-project.mdx │ │ └── project-structure.mdx │ │ ├── index.mdx │ │ ├── part8 │ │ ├── build-modes.md │ │ ├── deploy-procedure.md │ │ ├── cicd-codemagic.md │ │ └── error-tracking.md │ │ ├── part4 │ │ └── state-management-intro.md │ │ ├── part2 │ │ ├── dart-intro.md │ │ ├── basic-syntax.md │ │ ├── type-system.md │ │ └── records.md │ │ ├── part3 │ │ └── widgets.md │ │ ├── appendix │ │ ├── tools.md │ │ └── error-handling.md │ │ ├── part5 │ │ └── advanced-routing.md │ │ ├── part10 │ │ └── custom-painting.md │ │ └── part7 │ │ ├── widget-test.md │ │ └── integration-test.md ├── assets │ └── houston.webp ├── content.config.ts └── styles │ └── custom.css ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── tsconfig.json ├── .gitignore ├── public └── favicon.svg ├── package.json ├── add-mermaid-classname.ts ├── .github └── workflows │ └── deploy.yml ├── SUMMARY.md ├── astro.config.mjs ├── sidebar.config.mjs └── llm.txt /README.md: -------------------------------------------------------------------------------- 1 | # Flutter 배우기 2 | 3 | 안녕하세요 :) -------------------------------------------------------------------------------- /src/content/docs/part9/monorepo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: melos를 이용한 모노레포 3 | --- 4 | -------------------------------------------------------------------------------- /src/assets/houston.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangJoo-Park/learn-flutter/HEAD/src/assets/houston.webp -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [ 4 | ".astro/types.d.ts", 5 | "**/*" 6 | ], 7 | "exclude": [ 8 | "dist", 9 | "node_modules" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsLoader } from '@astrojs/starlight/loaders'; 3 | import { docsSchema } from '@astrojs/starlight/schema'; 4 | 5 | export const collections = { 6 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | public/beoe 24 | -------------------------------------------------------------------------------- /src/content/docs/part1/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 변경사항 3 | --- 4 | 5 | 6 | ### 2025년 05월 16일 7 | 8 | - 📦 1. 시작하기 > 소개에 설문조사 추가 9 | 10 | ### 2025년 05월 15일 11 | 12 | - 📦 1. 시작하기에 **LLM 설정** 추가 13 | - 📦 1. 시작하기에 **변경사항** 추가 14 | - 💡 2. Dart 언어 기초 중 컬렉션과 반복문에 collection 패키지 안내 추가 15 | - `flutter pub run build_runner build` 를 `dart run build_runner build` 로 변경 16 | - JSON 직렬화 (freezed, json_serializable)에 freezed_annotation 팁 추가 17 | 18 | ### 2025년 05월 14일 19 | 20 | 초안 공개 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": "/Users/changjoopark/.local/share/mise/installs/flutter/3.29.3-stable", 3 | "debug.javascript.defaultRuntimeExecutable": { 4 | "pwa-node": "/Users/changjoopark/.local/share/mise/shims/node" 5 | }, 6 | "typescript.validate.enable": true, 7 | "javascript.validate.enable": true, 8 | "files.exclude": { 9 | "**/node_modules/**/*": true 10 | }, 11 | "search.exclude": { 12 | "**/node_modules/**/*": true 13 | }, 14 | "typescript.disableAutomaticTypeAcquisition": true, 15 | "typescript.tsserver.experimental.enableProjectDiagnostics": false, 16 | "typescript.tsserver.enableTracing": true 17 | } 18 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/custom.css: -------------------------------------------------------------------------------- 1 | @import url("https://statics.goorm.io/fonts/GoormSans/v1.0.0/GoormSans.min.css"); 2 | @import url("https://statics.goorm.io/fonts/GoormSansCode/v1.0.1/GoormSansCode.min.css"); 3 | 4 | 5 | :root { 6 | --sl-font-system: 'Goorm Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 7 | Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 8 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 9 | --sl-font-system-mono: 'Goorm Sans Code', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 10 | 'Courier New', monospace; 11 | --__sl-font: var(--sl-font, var(--sl-font-system)), var(--sl-font-system); 12 | --__sl-font-mono: var(--sl-font-mono, var(--sl-font-system-mono)), var(--sl-font-system-mono); 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter-in-korean", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "upgrade": "npx @astrojs/upgrade", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/sitemap": "^3.4.0", 15 | "@astrojs/starlight": "^0.34.3", 16 | "@beoe/cache": "^0.1.1", 17 | "@beoe/rehype-mermaid": "^0.4.2", 18 | "@expressive-code/plugin-line-numbers": "^0.41.2", 19 | "astro": "^5.7.12", 20 | "astro-expressive-code": "^0.41.2", 21 | "playwright": "^1.52.0", 22 | "rehype-mermaid": "^3.0.0", 23 | "sharp": "^0.32.5", 24 | "starlight-giscus": "^0.6.1", 25 | "starlight-llms-txt": "^0.5.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Flutter 앱 개발 가이드 3 | description: 모바일 및 태블릿 앱을 만들며 겪었던 문제를 공유합니다. 4 | template: splash 5 | hero: 6 | tagline: 모바일 및 태블릿 앱을 만들며 겪었던 문제를 공유합니다. 7 | image: 8 | file: ../../assets/houston.webp 9 | actions: 10 | - text: 시작하기 11 | link: /learn-flutter/part1/introduction/ 12 | icon: right-arrow 13 | --- 14 | 15 | import { Card, CardGrid } from '@astrojs/starlight/components'; 16 | 17 | ## Next steps 18 | 19 | 20 | 21 | Edit `src/content/docs/index.mdx` to see this page change. 22 | 23 | 24 | Add Markdown or MDX files to `src/content/docs` to create new pages. 25 | 26 | 27 | Edit your `sidebar` and other config in `astro.config.mjs`. 28 | 29 | 30 | Learn more in [the Starlight Docs](https://starlight.astro.build/). 31 | 32 | 33 | -------------------------------------------------------------------------------- /add-mermaid-classname.ts: -------------------------------------------------------------------------------- 1 | import { visit, CONTINUE } from "unist-util-visit" 2 | import type { Plugin } from 'unified'; 3 | import type { Root, Element } from 'hast'; 4 | 5 | const visitor = (node: any) => { 6 | const dataLanguageMermaid = "mermaid" 7 | const typeElement = "element" 8 | const tagNamePre = "pre" 9 | const classMermaid = dataLanguageMermaid 10 | 11 | const isPreElement = (node: any) => typeof node.type !== undefined && node.type === typeElement 12 | && node.tagName !== undefined && node.tagName === tagNamePre 13 | && node.properties !== undefined && node.properties.dataLanguage === dataLanguageMermaid 14 | 15 | if(!isPreElement(node)) { 16 | return CONTINUE 17 | } 18 | 19 | const element = node as Element 20 | const properties = element.properties 21 | const className = properties.className as Array 22 | properties.className = [...className, classMermaid] 23 | 24 | return CONTINUE 25 | } 26 | 27 | const addMermaidClass: Plugin = () => 28 | (ast: Root) => visit(ast, visitor) 29 | 30 | export default addMermaidClass 31 | -------------------------------------------------------------------------------- /src/content/docs/part1/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 소개 3 | --- 4 | 5 | 안녕하세요! 6 | 7 | 이 문서는 Flutter 프레임워크를 이용하여 앱을 개발하는 방법을 소개합니다. 8 | 9 | 현재 초안 단계이며 수정 및 추가 내용이 있을 예정입니다. 10 | 11 | 저희 팀은 4개 이상의 프로젝트를 Flutter 프레임워크를 이용하여 만들었습니다. 12 | 처음부터 Flutter로 작성한 앱과 기존의 네이티브로 개발한 앱을 Flutter로 마이그레이션 한 경험이 있습니다. 13 | 14 | iOS(+ iPadOS), Android(+ 태블릿)에서 동작하는 여러 앱 서비스를 Flutter로 만들어 운영중이며 약 100만 MAU를 가진 서비스를 안정적으로 만들어 배포하였습니다. 15 | 16 | 위 경험을 토대로 Flutter 프레임워크를 이용하여 앱 개발하면서 필요한 내용들을 본 문서에 담았습니다. 17 | 18 | 이 문서 혹은 개발과 관련된 문의는 댓글 혹은 changjoo.app@gmail.com 으로 문의바랍니다. 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | # Trigger the workflow every time you push to the `main` branch 5 | # Using a different branch name? Replace `main` with your branch’s name 6 | push: 7 | branches: [ main ] 8 | # Allows you to run this workflow manually from the Actions tab on GitHub. 9 | workflow_dispatch: 10 | 11 | # Allow this job to clone the repo and create a page deployment 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout your repository using git 22 | uses: actions/checkout@v4 23 | - name: Install, build, and upload your site 24 | uses: withastro/action@v3 25 | # with: 26 | # path: . # The root location of your Astro project inside the repository. (optional) 27 | # node-version: 20 # The specific version of Node that should be used to build your site. Defaults to 20. (optional) 28 | # package-manager: pnpm@latest # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional) 29 | 30 | deploy: 31 | needs: build 32 | runs-on: ubuntu-latest 33 | environment: 34 | name: github-pages 35 | url: ${{ steps.deployment.outputs.page_url }} 36 | steps: 37 | - name: Deploy to GitHub Pages 38 | id: deployment 39 | uses: actions/deploy-pages@v4 40 | -------------------------------------------------------------------------------- /src/content/docs/part1/llms.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: LLM 설정 3 | --- 4 | 5 | 이 문서는 코드 에디터를 위한 llms.txt 파일을 지원합니다. 6 | 7 | 다음 경로를 사용하는 에디터에 적용하시면 이 문서를 기준으로 프로젝트를 진행하실 수 있습니다. 8 | 9 | 10 | 루트 llms.txt 11 | 12 | ``` 13 | https://changjoo-park.github.io/learn-flutter/llms.txt 14 | ``` 15 | 16 | 17 | 간소화 버전 18 | 19 | ``` 20 | https://changjoo-park.github.io/learn-flutter/llms-small.txt 21 | ``` 22 | 23 | 전체 버전 24 | 25 | ``` 26 | https://changjoo-park.github.io/learn-flutter/llms-full.txt 27 | ``` 28 | 29 | 30 | 31 | ## Visual Studio Code 32 | 33 | 프롬프트에 다음과 같이 입력합니다. 34 | 35 | ```text 36 | #fetch https://changjoo-park.github.io/learn-flutter/llms-full.txt 37 | ``` 38 | 39 | 프로젝트 레벨에서 이용하려면 40 | 41 | 1. 터미널에서 프로젝트 경로로 이동 후 다음과 같이 입력합니다. 42 | 43 | ```sh 44 | curl -L https://changjoo-park.github.io/learn-flutter/llms-full.txt --create-dirs -o .vscode/learn_flutter.md 45 | ``` 46 | 47 | 2. `.vscode/settings.json` 에 다음을 입력합니다. 48 | 49 | ```json title=".vscode/settings.json" 50 | { 51 | "github.copilot.chat.codeGeneration.instructions": [ 52 | { 53 | "file": "./.vscode/learn_flutter.md" 54 | } 55 | ] 56 | } 57 | ``` 58 | 59 | 60 | 61 | ## Cursor 62 | 63 | 프롬프트에 다음과 같이 입력합니다. 64 | 65 | ```text 66 | @web https://changjoo-park.github.io/learn-flutter/llms-full.txt 67 | ``` 68 | 69 | 계속 사용하려면 70 | 71 | 1. `CMD` + `Shift` + `P` 를 눌러 명령 팔레트를 엽니다. 72 | 2. `Add new custom docs` 를 입력합니다. 73 | 3. 아래 내용을 입력합니다. 74 | 75 | ```text 76 | https://changjoo-park.github.io/learn-flutter/llms-full.txt 77 | ``` 78 | 79 | 4. 채팅 UI에서 @docs 를 입력한 후 추가된 문서를 선택합니다. 80 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Flutter 온보딩 핸드북 목차 2 | 3 | ## 📦 Part 1. 시작하기: 환경 설정과 첫 프로젝트 4 | 5 | - Flutter 소개 및 특징 6 | - 개발 환경 구성 7 | - Flutter SDK 설치 8 | - Visual Studio Code 설정 9 | - 에뮬레이터 / 실기기 연결 10 | - 첫 프로젝트 생성 및 실행 11 | - Flutter 프로젝트 구조 이해 12 | 13 | ## 💡 Part 2. Dart 언어 기초 14 | 15 | - Dart 소개 16 | - 기본 문법 및 변수 17 | - 타입 시스템 & 제네릭 18 | - 클래스, 생성자, 팩토리 19 | - 비동기 프로그래밍 (Future, async/await, Stream) 20 | - 컬렉션과 반복문 21 | - 예외 처리 22 | - Extension / Mixin 23 | - 레코드 & 패턴매칭 (Dart 3 이상) 24 | 25 | ## 🧱 Part 3. Flutter의 기본 구성 요소 26 | 27 | ### 위젯 개념과 주요 위젯 28 | 29 | - Stateless / Stateful 위젯 30 | - Widget Tree 이해 31 | - 주요 위젯 32 | - Text, Button, Image, Icon 33 | - Container, SizedBox, Padding 34 | - TextField, Form, GestureDetector, InkWell 35 | - Visibility, Offstage, Divider, Tooltip 36 | 37 | ### 레이아웃 위젯 38 | 39 | - Row, Column, Flex 40 | - Stack, Align, Positioned 41 | - Expanded, Flexible, Spacer 42 | - SingleChildScrollView, Wrap 43 | - AspectRatio, LayoutBuilder 44 | - OrientationBuilder, MediaQuery 45 | - ConstrainedBox, IntrinsicHeight 등 46 | 47 | ## 🎨 Part 4. 상태 관리 48 | 49 | - 상태 관리 입문 50 | - setState, ValueNotifier 51 | - InheritedWidget, Provider 52 | - Riverpod 소개 및 실습 53 | - 실습: TodoList 개선 (상태 관리 포함) 54 | 55 | ## 🚦 Part 5. 네비게이션과 화면 구성 56 | 57 | - Navigator 1.0 (push/pop) 58 | - Navigator 2.0 개념 59 | - go_router 사용법 60 | - 라우트 가드, ShellRoute, DeepLink 61 | - 실습: 복수 화면 전환 및 데이터 전달 62 | - Drawer, BottomNavigationBar, TabBar 63 | 64 | ## 🔌 Part 6. 외부와의 연동 (서버 & Firebase) 65 | 66 | - Dio를 통한 API 통신 67 | - Interceptor, cancelToken, 오류 처리 68 | - JSON 직렬화 (`json_serializable`, `freezed`) 69 | - Firebase 연동 70 | - 초기 설정 71 | - Firebase Cloud Messaging 72 | - Firebase Auth & Firestore (간단히) 73 | - Firebase Analytics / Crashlytics 74 | 75 | ## 🧪 Part 7. 테스트와 디버깅 76 | 77 | - 단위 테스트 (unit) 78 | - 위젯 테스트 (widget) 79 | - 통합 테스트 (integration) 80 | - mockito, golden test, coverage 81 | - Flutter DevTools 사용법 82 | - 로그 관리 (talker) 83 | 84 | ## 🚀 Part 8. 앱 배포 및 운영 85 | 86 | - 빌드 모드 (debug / profile / release) 87 | - Android / iOS 배포 절차 88 | - keystore, signing, TestFlight 89 | - Codemagic을 활용한 CI/CD 구성 90 | - 환경 분리 및 flavor 설정 91 | - 사용자 분석 도구 92 | - Firebase Analytics 93 | - Posthog 94 | - 에러 추적 95 | - Crashlytics, Sentry 96 | 97 | ## 🧭 Part 9. 프로젝트 구조 & 아키텍처 98 | 99 | - 기능별 vs 계층별 폴더 구조 100 | - 클린 아키텍처 도입하기 101 | - 의존성 주입 개념 102 | - 패키지 작성 및 관리 103 | - pub.dev 탐색 / dev_dependencies 구분 104 | - internal 패키지 분리 전략 105 | - 모노레포 구조 및 melos 도입 106 | - 사내 Flutter 코드 스타일 가이드 107 | 108 | ## 🌍 Part 10. 보완 학습: 확장성과 품질 109 | 110 | - CustomPainter와 RenderBox 이해 111 | - 위젯 캐싱 112 | - RepaintBoundary 113 | - 애니메이션 구성 (Hero, AnimatedXXX) 114 | - 접근성 (Semantics 등) 115 | - 다국어 처리 (intl, flutter_localizations) 116 | - 퍼포먼스 튜닝 체크리스트 117 | - 추천 패키지 모음 118 | 119 | ## 📚 부록 120 | 121 | - 개발 도구와 링크 모음 122 | - 공식 문서, 블로그, 영상 추천 123 | - Flutter 오류 대응법 가이드 124 | - 코드 템플릿 및 예제 모음 링크 125 | - 자주 묻는 질문 (FAQ) 126 | - 소셜 로그인 127 | - 네이버 로그인 128 | - 카카오 로그인 129 | - 애플 로그인 130 | - iOS 라이브 액티비티 131 | - WidgetBook 문서화 132 | - 주석 133 | - 코드 스타일 134 | - llms.txt 135 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { rehypeMermaid } from "@beoe/rehype-mermaid"; 2 | import { getCache } from "@beoe/cache"; 3 | import starlightGiscus from 'starlight-giscus' 4 | 5 | const googleAnalyticsId = 'G-FTYYK8J5MY' 6 | 7 | const cache = await getCache(); 8 | 9 | // @ts-check 10 | import { defineConfig } from "astro/config"; 11 | import sitemap from "@astrojs/sitemap"; 12 | import starlight from "@astrojs/starlight"; 13 | import astroExpressiveCode from "astro-expressive-code"; 14 | import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers"; 15 | import { sidebars } from "./sidebar.config.mjs"; 16 | import starlightLlmsTxt from 'starlight-llms-txt' 17 | 18 | 19 | // https://astro.build/config 20 | export default defineConfig({ 21 | compressHTML: true, 22 | prefetch: false, 23 | base: "learn-flutter", 24 | site: "https://changjoo-park.github.io/", 25 | integrations: [ 26 | sitemap(), 27 | astroExpressiveCode({ 28 | themes: ["dracula"], 29 | plugins: [ 30 | pluginLineNumbers(), 31 | ], 32 | }), 33 | starlight({ 34 | credits: true, 35 | title: "Flutter 배우기", 36 | customCss: [ 37 | './src/styles/custom.css', 38 | ], 39 | head: [ 40 | // Adding google analytics 41 | { 42 | tag: 'script', 43 | attrs: { 44 | src: `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`, 45 | }, 46 | }, 47 | { 48 | tag: 'script', 49 | content: ` 50 | window.dataLayer = window.dataLayer || []; 51 | function gtag(){dataLayer.push(arguments);} 52 | gtag('js', new Date()); 53 | 54 | gtag('config', '${googleAnalyticsId}'); 55 | `, 56 | }, 57 | ], 58 | social: [ 59 | { 60 | icon: "github", 61 | label: "GitHub", 62 | href: "https://github.com/changjoo-park/learn-flutter", 63 | }, 64 | ], 65 | editLink: { 66 | text: "Edit this page on GitHub", 67 | icon: "github", 68 | href: "https://github.com/changjoo-park/learn-flutter/edit/main/docs/", 69 | }, 70 | sidebar: sidebars, 71 | plugins: [ 72 | starlightLlmsTxt({ 73 | projectName: "Flutter 배우기", 74 | }), 75 | starlightGiscus({ 76 | repo: 'changjoo-park/learn-flutter', 77 | repoId: 'R_kgDOOpJgKQ', 78 | category: 'Q&A', 79 | categoryId: 'DIC_kwDOOpJgKc4CqIHA', 80 | inputPosition: 'top', 81 | mapping: 'pathname', 82 | reactionsEnabled: true, 83 | emitMetadata: true, 84 | theme: 'preferred_color_scheme', 85 | lang: 'ko', 86 | }) 87 | ], 88 | }), 89 | ], 90 | markdown: { 91 | rehypePlugins: [ 92 | [ 93 | rehypeMermaid, 94 | { 95 | strategy: "file", // alternatively use "data-url" 96 | fsPath: "public/beoe", // add this to gitignore 97 | webPath: "/beoe", 98 | darkScheme: "class", 99 | cache, 100 | }, 101 | ], 102 | ], 103 | }, 104 | }); 105 | -------------------------------------------------------------------------------- /src/content/docs/part8/build-modes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 빌드 모드 (debug / profile / release) 3 | --- 4 | 5 | Flutter는 세 가지 주요 빌드 모드를 제공합니다. 각 모드는 개발 과정의 다른 단계에서 사용되며, 각기 다른 최적화와 기능을 제공합니다. 6 | 7 | ## 빌드 모드 개요 8 | 9 | Flutter의 빌드 모드는 다음과 같습니다: 10 | 11 | ## Debug 모드 12 | 13 | Debug 모드는 개발 과정에서 주로 사용하는 모드입니다. 14 | 15 | ### 특징 16 | 17 | - **Hot Reload/Restart**: 코드 변경 사항을 빠르게 확인할 수 있습니다. 18 | - **디버깅 도구**: 콘솔 로그, 디버거 연결, 인스펙터 등 개발 도구 사용 가능합니다. 19 | - **확인용 배너**: 앱 우측 상단에 DEBUG 배너가 표시됩니다. 20 | - **비최적화 빌드**: 성능이 최적화되지 않고 디버깅 정보가 포함되어 있습니다. 21 | 22 | ### 실행 방법 23 | 24 | ```bash 25 | # 명시적으로 debug 모드로 실행 26 | flutter run --debug 27 | 28 | # 기본값이므로 일반적으로 다음과 같이 실행 29 | flutter run 30 | ``` 31 | 32 | ### 사용 시나리오 33 | 34 | - 앱 개발 및 기능 테스트 35 | - 코드 디버깅 36 | - UI 구현 및 확인 37 | 38 | ## Profile 모드 39 | 40 | Profile 모드는 성능 분석과 프로파일링을 위한 모드입니다. 41 | 42 | ### 특징 43 | 44 | - **성능 트래킹**: Timeline, DevTools 등을 통한 성능 측정 가능 45 | - **일부 디버깅 비활성화**: Hot Reload, 일부 디버깅 기능은 비활성화 46 | - **실제 성능과 유사**: Release 모드와 유사한 성능 특성을 가지지만, 프로파일링 도구 사용 가능 47 | - **Flutter Inspector**: UI 레이아웃 및 렌더링 분석 가능 48 | 49 | ### 실행 방법 50 | 51 | ```bash 52 | flutter run --profile 53 | ``` 54 | 55 | > **주의**: Profile 모드는 에뮬레이터/시뮬레이터에서 정확한 성능 측정이 어려우므로 실제 기기에서 실행하는 것이 좋습니다. 56 | 57 | ### 사용 시나리오 58 | 59 | - 앱 성능 분석 60 | - 병목 현상 파악 61 | - 메모리 사용량 및 프레임 드롭 확인 62 | - 실제 기기에서의 사용자 경험 검증 63 | 64 | ## Release 모드 65 | 66 | Release 모드는 최종 사용자에게 배포하기 위한 최적화된 빌드 모드입니다. 67 | 68 | ### 특징 69 | 70 | - **최적화된 성능**: 모든 성능 최적화 기능 활성화 71 | - **코드 최소화**: 사용하지 않는 코드 제거 및 최소화 72 | - **디버깅 기능 비활성화**: 모든 디버깅 도구와 코드 제거 73 | - **R8/ProGuard (Android)**: 코드 축소, 난독화 및 최적화 74 | 75 | ### 실행 방법 76 | 77 | ```bash 78 | flutter run --release 79 | ``` 80 | 81 | ### 빌드 방법 82 | 83 | ```bash 84 | # Android APK 빌드 85 | flutter build apk --release 86 | 87 | # Android App Bundle 빌드 88 | flutter build appbundle --release 89 | 90 | # iOS 빌드 91 | flutter build ios --release 92 | ``` 93 | 94 | ### 사용 시나리오 95 | 96 | - 앱 스토어 제출 97 | - 사용자 배포 98 | - 최종 성능 테스트 99 | - 배포 전 검증 100 | 101 | ## 모드 간 비교 102 | 103 | | 기능 | Debug | Profile | Release | 104 | | ---------------- | ----- | ------- | ------- | 105 | | 성능 최적화 | ❌ | ✅ | ✅ | 106 | | 코드 크기 최적화 | ❌ | ✅ | ✅ | 107 | | Hot Reload | ✅ | ❌ | ❌ | 108 | | 디버거 연결 | ✅ | 제한적 | ❌ | 109 | | 성능 오버헤드 | 높음 | 낮음 | 없음 | 110 | | 앱 크기 | 큼 | 중간 | 작음 | 111 | | 프로파일링 도구 | ✅ | ✅ | ❌ | 112 | | Assert 문 실행 | ✅ | ❌ | ❌ | 113 | 114 | ## 모드 전환 시 주의사항 115 | 116 | ### Debug에서 Release로 전환 시 확인 사항 117 | 118 | 1. **assert 문**: Debug 모드에서만, Release 모드에서는 무시됩니다. 119 | 2. **환경 변수**: kDebugMode, kProfileMode, kReleaseMode 플래그를 사용한 조건부 코드 확인 120 | 3. **로그 출력**: 불필요한 print() 문 제거 검토 121 | 122 | ```dart 123 | // 빌드 모드에 따른 조건부 코드 예시 124 | if (kDebugMode) { 125 | print('이 메시지는 Debug 모드에서만 출력됩니다'); 126 | } else if (kProfileMode) { 127 | // 프로파일 모드 특화 코드 128 | } else if (kReleaseMode) { 129 | // 릴리즈 모드 특화 코드 130 | } 131 | ``` 132 | 133 | 4. **플랫폼 채널**: 네이티브 코드와의 통신이 제대로 작동하는지 확인 134 | 5. **타이밍 차이**: 디버그 모드보다 릴리즈 모드에서 실행 속도가 빠를 수 있음을 고려 135 | 136 | ## 빌드 모드 활용 팁 137 | 138 | ### 다양한 모드 테스트 139 | 140 | 개발 과정에서 정기적으로 Profile 및 Release 모드로 앱을 테스트하여 실제 사용자 경험을 확인하는 것이 좋습니다. 141 | 142 | ### 조건부 코드 작성 143 | 144 | ```dart 145 | // 개발 중에만 필요한 코드 146 | if (kDebugMode) { 147 | // 개발 환경에서만 사용할 추가 기능 148 | enableDevFeatures(); 149 | } 150 | 151 | // 릴리즈에서만 활성화할 코드 152 | if (kReleaseMode) { 153 | // 분석 도구 초기화 등 154 | initializeAnalytics(); 155 | } 156 | ``` 157 | 158 | ### Flavor와 함께 사용 159 | 160 | 빌드 모드는 Flavor(제품 환경)와 함께 사용하여 개발, 스테이징, 프로덕션 환경을 구분할 수 있습니다. 161 | 162 | ``` 163 | Debug + Dev Flavor = 개발 환경 테스트 164 | Profile + Staging Flavor = 스테이징 성능 테스트 165 | Release + Production Flavor = 최종 배포 빌드 166 | ``` 167 | 168 | ## 결론 169 | 170 | Flutter의 세 가지 빌드 모드(Debug, Profile, Release)는 각각 다른 목적으로 사용되며, 개발 과정의 다양한 단계에서 활용됩니다. 개발 중에는 Debug 모드를 사용하여 빠른 반복 개발을, 성능 테스트에는 Profile 모드를, 최종 배포에는 Release 모드를 사용하는 것이 권장됩니다. 각 모드의 특성을 이해하고 적절히 활용하면 효율적인 개발과 최적화된 앱 배포가 가능합니다. 171 | -------------------------------------------------------------------------------- /src/content/docs/part4/state-management-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 상태 관리 입문 3 | --- 4 | 5 | Flutter 앱을 개발하면서 가장 중요한 개념 중 하나는 '상태 관리(State Management)'입니다. 이 장에서는 Flutter에서의 상태 관리 개념과 다양한 상태 관리 방법에 대해 알아보겠습니다. 6 | 7 | ## 상태(State)란 무엇인가? 8 | 9 | 상태는 앱의 동작 중에 변할 수 있는 데이터를 의미합니다. 사용자 입력, 네트워크 응답, 시간 경과 등에 따라 앱의 UI가 변경되어야 할 때, 이러한 변경사항을 관리하는 데이터를 '상태'라고 합니다. 10 | 11 | Flutter에서 상태는 크게 다음과 같이 분류할 수 있습니다: 12 | 13 | 14 | ### 1. 임시 상태(Ephemeral State) 15 | 16 | - 단일 위젯 내에서만 사용되는 간단한 상태 17 | - UI의 일시적인 변화를 관리 (예: 버튼 눌림 상태, 입력 필드 포커스 등) 18 | - `StatefulWidget`과 `setState()`로 쉽게 관리 가능 19 | 20 | ### 2. 앱 상태(App State) 21 | 22 | - 앱의 여러 부분에서 공유되는 데이터 23 | - 장기적으로 유지되어야 하는 정보 (예: 사용자 설정, 인증 토큰, 쇼핑 카트 내용) 24 | - 전역적으로 접근 가능해야 하며, 효율적인 관리가 필요 25 | - 상태 관리 라이브러리(Provider, Riverpod, Bloc 등)를 활용하여 관리 26 | 27 | ## 상태 관리가 중요한 이유 28 | 29 | 상태 관리는 다음과 같은 이유로 중요합니다: 30 | 31 | 1. **UI 일관성**: 상태 변화에 따라 UI가 일관되게 업데이트되어야 함 32 | 2. **코드 구조화**: 앱의 상태 로직과 UI 로직을 분리하여 유지보수성 향상 33 | 3. **성능 최적화**: 필요한 부분만 효율적으로 다시 빌드하여 성능 개선 34 | 4. **확장성**: 앱의 규모가 커져도 상태를 효과적으로 관리할 수 있는 구조 필요 35 | 36 | 37 | ## 상태 관리 방식의 진화 38 | 39 | Flutter에서의 상태 관리는 다음과 같이 진화해 왔습니다: 40 | 41 | ### 1. StatefulWidget과 setState() 42 | 43 | Flutter의 기본적인 상태 관리 메커니즘입니다. 간단한 상태를 위젯 내부에서 관리합니다. 44 | 45 | ```dart 46 | class CounterWidget extends StatefulWidget { 47 | @override 48 | _CounterWidgetState createState() => _CounterWidgetState(); 49 | } 50 | 51 | class _CounterWidgetState extends State { 52 | int _count = 0; 53 | 54 | void _incrementCount() { 55 | setState(() { 56 | _count++; 57 | }); 58 | } 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | return Column( 63 | children: [ 64 | Text('카운트: $_count'), 65 | ElevatedButton( 66 | onPressed: _incrementCount, 67 | child: Text('증가'), 68 | ), 69 | ], 70 | ); 71 | } 72 | } 73 | ``` 74 | 75 | 특징: 76 | 77 | - 장점: 간단하고 직관적 78 | - 단점: 깊은 위젯 트리에서 상태 전달이 어려움(Prop drilling) 79 | 80 | ### 2. InheritedWidget 81 | 82 | Flutter의 내장 메커니즘으로, 위젯 트리 아래로 데이터를 효율적으로 전달합니다. 83 | 84 | ```dart 85 | class CounterInheritedWidget extends InheritedWidget { 86 | final int count; 87 | final Function incrementCount; 88 | 89 | CounterInheritedWidget({ 90 | required this.count, 91 | required this.incrementCount, 92 | required Widget child, 93 | }) : super(child: child); 94 | 95 | @override 96 | bool updateShouldNotify(CounterInheritedWidget oldWidget) { 97 | return count != oldWidget.count; 98 | } 99 | 100 | static CounterInheritedWidget of(BuildContext context) { 101 | return context.dependOnInheritedWidgetOfExactType()!; 102 | } 103 | } 104 | ``` 105 | 106 | 특징: 107 | 108 | - 장점: 위젯 트리를 통한 데이터 전파 109 | - 단점: 직접 구현하기 복잡함 110 | 111 | ### 3. Provider 패턴 112 | 113 | InheritedWidget을 래핑한 패키지로, 더 사용하기 쉬운 API를 제공합니다. 114 | 115 | ```dart 116 | // 상태 클래스 117 | class CounterModel with ChangeNotifier { 118 | int _count = 0; 119 | int get count => _count; 120 | 121 | void increment() { 122 | _count++; 123 | notifyListeners(); 124 | } 125 | } 126 | 127 | // Provider 설정 128 | ChangeNotifierProvider( 129 | create: (context) => CounterModel(), 130 | child: MyApp(), 131 | ), 132 | 133 | // 데이터 사용 134 | Consumer( 135 | builder: (context, counter, child) { 136 | return Text('카운트: ${counter.count}'); 137 | }, 138 | ) 139 | ``` 140 | 141 | 특징: 142 | 143 | - 장점: 사용하기 쉽고 직관적 144 | - 단점: 복잡한 상태 관리에는 한계가 있음 145 | 146 | ### 4. 현대적 상태 관리 솔루션 147 | 148 | 현재는 다양한 상태 관리 라이브러리가 존재합니다: 149 | 150 | - **Riverpod**: Provider의 개선 버전으로, 컴파일 타임 안전성 및 테스트 용이성 강화 151 | - **Bloc/Cubit**: 비즈니스 로직을 명확하게 분리하는 패턴 152 | - **MobX**: 반응형 프로그래밍 기반의 상태 관리 153 | - **Redux**: 예측 가능한 상태 컨테이너 154 | 155 | ## 상태 관리 선택 가이드 156 | 157 | 어떤 상태 관리 솔루션을 선택해야 할까요? 다음 요소를 고려하세요: 158 | 159 | 1. **앱 복잡도**: 단순한 앱은 setState()나 Provider로도 충분할 수 있음 160 | 2. **팀 경험**: 팀이 이미 익숙한 솔루션이 있다면 고려 161 | 3. **학습 곡선**: 일부 솔루션은 배우기 어려울 수 있음 162 | 4. **성능 요구사항**: 대규모 앱의 경우 성능 최적화된 솔루션 필요 163 | 5. **테스트 용이성**: 상태 로직의 테스트 용이성도 중요한 고려사항 164 | 165 | ## 이 장의 다음 섹션들 166 | 167 | 이 장의 나머지 부분에서는 Flutter의 다양한 상태 관리 방법을 자세히 다룰 것입니다: 168 | 169 | 1. **setState와 ValueNotifier**: Flutter의 기본 상태 관리 메커니즘 170 | 2. **InheritedWidget과 Provider**: 위젯 트리를 통한 상태 공유 171 | 3. **Riverpod**: 현대적인 상태 관리 솔루션 172 | 4. **TodoList 실습**: 실제 애플리케이션에 상태 관리 적용하기 173 | 174 | ## 요약 175 | 176 | - 상태 관리는 Flutter 애플리케이션 개발에서 핵심 개념 177 | - 상태는 임시 상태와 앱 상태로 분류됨 178 | - 다양한 상태 관리 솔루션이 존재하며, 앱의 복잡도와 요구사항에 따라 선택 179 | - 효과적인 상태 관리는 유지보수성, 성능, 확장성을 향상시킴 180 | 181 | 다음 섹션에서는 Flutter의 기본 상태 관리 메커니즘인 `setState()`와 `ValueNotifier`에 대해 자세히 알아보겠습니다. 182 | -------------------------------------------------------------------------------- /sidebar.config.mjs: -------------------------------------------------------------------------------- 1 | export const sidebars = [ 2 | { 3 | label: "📦 1. 시작하기", 4 | items: [ 5 | { label: "소개", slug: "part1/introduction" }, 6 | { label: "변경사항", slug: "part1/changelog" }, 7 | { label: "개발 환경 구성", slug: "part1/setup" }, 8 | { label: "LLM 설정", slug: "part1/llms", badge: 'new' }, 9 | { label: "첫 프로젝트 생성 및 실행", slug: "part1/first-project" }, 10 | { label: "Flutter 프로젝트 구조 이해", slug: "part1/project-structure" }, 11 | ], 12 | }, 13 | { 14 | label: "💡 2. Dart 언어 기초", 15 | items: [ 16 | { label: "Dart 소개", slug: "part2/dart-intro" }, 17 | { label: "기본 문법 및 변수", slug: "part2/basic-syntax" }, 18 | { label: "타입 시스템 & 제네릭", slug: "part2/type-system" }, 19 | { label: "클래스, 생성자, 팩토리", slug: "part2/classes" }, 20 | { label: "컬렉션과 반복문", slug: "part2/collections" }, 21 | { label: "비동기 프로그래밍", slug: "part2/async" }, 22 | { label: "예외 처리", slug: "part2/exceptions" }, 23 | { label: "Extension / Mixin", slug: "part2/extensions" }, 24 | { label: "레코드 & 패턴매칭", slug: "part2/records" }, 25 | ], 26 | }, 27 | { 28 | label: "🧱 3. Flutter의 기본 구성 요소", 29 | items: [ 30 | { label: "위젯 개념과 주요 위젯", slug: "part3/widgets" }, 31 | { 32 | label: "Stateless / Stateful 위젯 상세", 33 | slug: "part3/stateless-stateful", 34 | }, 35 | { label: "Widget Tree 이해", slug: "part3/widget-tree" }, 36 | { label: "주요 위젯", slug: "part3/basic-widgets" }, 37 | { label: "레이아웃 위젯", slug: "part3/layout-widgets" }, 38 | ], 39 | }, 40 | { 41 | label: "🎨 4. 상태 관리", 42 | items: [ 43 | { label: "상태 관리 입문", slug: "part4/state-management-intro" }, 44 | { 45 | label: "setState, ValueNotifier", 46 | slug: "part4/setstate-valuenotifier", 47 | }, 48 | { label: "InheritedWidget, Provider", slug: "part4/inherited-provider" }, 49 | { label: "Riverpod 소개 및 실습", slug: "part4/riverpod" }, 50 | ], 51 | }, 52 | { 53 | label: "🚦 5. 네비게이션과 화면 구성", 54 | items: [ 55 | { label: "Navigator 1.0", slug: "part5/navigator1" }, 56 | { label: "Navigator 2.0", slug: "part5/navigator2" }, 57 | { label: "go_router 사용법", slug: "part5/go-router" }, 58 | { 59 | label: "라우트 가드, ShellRoute, DeepLink", 60 | slug: "part5/advanced-routing", 61 | }, 62 | { label: "실습: 복수 화면 전환", slug: "part5/multi-screen" }, 63 | { 64 | label: "Drawer, BottomNavigationBar, TabBar", 65 | slug: "part5/navigation-widgets", 66 | }, 67 | ], 68 | }, 69 | { 70 | label: "🔌 6. 외부와의 연동", 71 | items: [ 72 | { label: "Dio를 통한 API 통신", slug: "part6/dio" }, 73 | { 74 | label: "JSON 직렬화 (freezed, json_serializable)", 75 | slug: "part6/json-serialization", 76 | }, 77 | ], 78 | }, 79 | { 80 | label: "🧪 7. 테스트와 디버깅", 81 | items: [ 82 | { label: "단위 테스트", slug: "part7/unit-test" }, 83 | { label: "위젯 테스트", slug: "part7/widget-test" }, 84 | { label: "통합 테스트", slug: "part7/integration-test" }, 85 | // { label: "Flutter DevTools", slug: "part7/devtools" }, 86 | // { label: "로그 관리", slug: "part7/logging" }, 87 | ], 88 | }, 89 | { 90 | label: "🚀 8. 앱 배포 및 운영", 91 | items: [ 92 | { label: "빌드 모드", slug: "part8/build-modes" }, 93 | { label: "Android / iOS 배포", slug: "part8/deploy-procedure" }, 94 | { label: "Codemagic CI/CD", slug: "part8/cicd-codemagic", badge: "🚧" }, 95 | { 96 | label: "환경 분리 및 flavor", 97 | slug: "part8/environment-flavors", 98 | badge: "BETA", 99 | }, 100 | { 101 | label: "사용자 분석 도구", 102 | slug: "part8/analytics-tools", 103 | badge: "BETA", 104 | }, 105 | { label: "에러 추적", slug: "part8/error-tracking" }, 106 | ], 107 | }, 108 | { 109 | label: "🧭 9. 프로젝트 구조 & 아키텍처", 110 | items: [ 111 | { label: "기능별 vs 계층별 폴더 구조", slug: "part9/folder-structure" }, 112 | { label: "멀티 모듈 아키텍처", slug: "part9/multi-module" }, 113 | ], 114 | }, 115 | { 116 | label: "🌍 10. 보완 학습", 117 | items: [ 118 | { label: "CustomPainter와 RenderBox", slug: "part10/custom-painting" }, 119 | { label: "위젯 캐싱", slug: "part10/widget-caching" }, 120 | { label: "애니메이션", slug: "part10/animations" }, 121 | { label: "접근성", slug: "part10/accessibility" }, 122 | { label: "다국어 처리", slug: "part10/internationalization" }, 123 | { label: "성능 최적화", slug: "part10/performance", badge: "BETA" }, 124 | // { label: "추천 패키지", slug: "part10/recommended-packages" }, 125 | ], 126 | }, 127 | { 128 | label: "📚 부록", 129 | items: [ 130 | // { label: "개발 도구와 링크", slug: "appendix/tools" }, 131 | { label: "Flutter 오류 대응법", slug: "appendix/error-handling" }, 132 | { label: "코드 템플릿", slug: "appendix/code-templates" }, 133 | { label: "소셜 로그인", slug: "appendix/social-login", badge: "🚧" }, 134 | { label: "iOS 라이브 액티비티", slug: "appendix/live-activities" }, 135 | { label: "WidgetBook", slug: "appendix/widgetbook" }, 136 | { label: "FAQ", slug: "appendix/faq" }, 137 | ], 138 | }, 139 | ]; 140 | -------------------------------------------------------------------------------- /src/content/docs/part1/setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 개발 환경 구성 3 | banner: 4 | content: | 5 | 안녕하세요! 이 페이지를 보시는 분들에게 설문을 받고 있습니다. 6 | 여기를 눌러서 참여부탁드려요! 7 | --- 8 | 9 | Flutter 개발을 시작하기 위해 필요한 환경을 구성해 보겠습니다. 이 과정에는 Flutter SDK 설치, 코드 에디터 설정, 그리고 개발 도구 구성이 포함됩니다. 10 | 11 | ### 여러 버전을 사용하고 싶을 때 12 | 13 | 가이드에 있는 공식적인 방법도 좋지만 여러 버전을 사용하고 싶을 때는 [Flutter Version Manager](https://fvm.app) 또는 [mise](https://mise.jdx.dev)를 사용하세요. Flutter의 최신 버전 업데이트 이후에 바로 사용하는 경우 패키지가 호환이 안되는 경우가 있어 버전을 유지하는데 어려움을 겪을 수 있습니다. 14 | 15 | 저는 mise를 조금 더 추천 드립니다. Flutter Version Manager와 달리 Flutter 외에 앱 개발에 필요한 Ruby, Node.js 등의 버전도 관리할 수 있습니다. 16 | 17 | ## Flutter SDK 설치 18 | 19 | ### Windows에서 설치 20 | 21 | 1. [Flutter 공식 사이트](https://flutter.dev/docs/get-started/install/windows)에서 Flutter SDK를 다운로드합니다. 22 | 2. 다운로드한 zip 파일을 원하는 위치에 압축 해제합니다 (예: `C:\dev\flutter`). 23 | 3. 환경 변수 설정: 24 | - 시스템 환경 변수에서 `Path` 변수에 Flutter SDK의 `bin` 폴더 경로를 추가합니다. 25 | - 예: `C:\dev\flutter\bin` 26 | 4. 명령 프롬프트를 열고 다음 명령어를 실행하여 설치를 확인합니다: 27 | ```bash 28 | flutter doctor 29 | ``` 30 | 31 | ### macOS에서 설치 32 | 33 | 1. **Homebrew를 이용한 설치** (권장): 34 | 35 | ```bash 36 | brew install --cask flutter 37 | ``` 38 | 39 | 2. **수동 설치**: 40 | - [Flutter 공식 사이트](https://flutter.dev/docs/get-started/install/macos)에서 Flutter SDK를 다운로드합니다. 41 | - 다운로드한 zip 파일을 원하는 위치에 압축 해제합니다 (예: `~/development/flutter`). 42 | - 환경 변수 설정: `.zshrc` 또는 `.bash_profile` 파일에 다음 내용을 추가합니다: 43 | ```bash 44 | export PATH="$PATH:~/development/flutter/bin" 45 | ``` 46 | - 터미널을 열고 다음 명령어를 실행하여 설치를 확인합니다: 47 | ```bash 48 | flutter doctor 49 | ``` 50 | 51 | ### Linux에서 설치 52 | 53 | 1. [Flutter 공식 사이트](https://flutter.dev/docs/get-started/install/linux)에서 Flutter SDK를 다운로드합니다. 54 | 2. 다운로드한 tar.xz 파일을 원하는 위치에 압축 해제합니다: 55 | ```bash 56 | tar xf flutter_linux_-stable.tar.xz -C ~/development 57 | ``` 58 | 3. 환경 변수 설정: `.bashrc` 또는 `.zshrc` 파일에 다음 내용을 추가합니다: 59 | ```bash 60 | export PATH="$PATH:~/development/flutter/bin" 61 | ``` 62 | 4. 터미널을 열고 다음 명령어를 실행하여 설치를 확인합니다: 63 | ```bash 64 | flutter doctor 65 | ``` 66 | 67 | ## Visual Studio Code 설정 68 | 69 | Visual Studio Code는 Flutter 개발을 위한 권장 에디터입니다. 70 | 71 | ### VS Code 설치 72 | 73 | 1. [Visual Studio Code 공식 사이트](https://code.visualstudio.com/)에서 에디터를 다운로드하고 설치합니다. 74 | 2. VS Code를 실행합니다. 75 | 76 | ### Flutter와 Dart 플러그인 설치 77 | 78 | 1. VS Code의 Extensions 탭(`Ctrl+Shift+X` 또는 `Cmd+Shift+X`)을 엽니다. 79 | 2. "Flutter"를 검색하고 Flutter 확장 프로그램을 설치합니다 (Dart 확장 프로그램도 자동으로 설치됩니다). 80 | 3. VS Code를 재시작합니다. 81 | 82 | ### Dart DevTools 설정 83 | 84 | Dart DevTools는 Flutter 앱 디버깅에 유용한 도구입니다. 85 | 86 | 1. VS Code에서 Command Palette(`Ctrl+Shift+P` 또는 `Cmd+Shift+P`)를 엽니다. 87 | 2. "Flutter: Open DevTools"를 입력하고 선택합니다. 88 | 3. 브라우저에서 DevTools가 열립니다. 89 | 90 | ## 에뮬레이터 / 실기기 연결 91 | 92 | ### Android 에뮬레이터 설정 93 | 94 | 1. [Android Studio](https://developer.android.com/studio)를 다운로드하고 설치합니다. 95 | 2. Android Studio를 실행하고 "SDK Manager"를 엽니다. 96 | 3. "SDK Tools" 탭에서 "Android SDK Build-Tools", "Android SDK Command-line Tools", "Android Emulator", "Android SDK Platform-Tools"를 설치합니다. 97 | 4. "AVD Manager"를 열고 "Create Virtual Device"를 클릭합니다. 98 | 5. 원하는 디바이스를 선택하고 시스템 이미지를 다운로드한 후 에뮬레이터를 생성합니다. 99 | 6. 에뮬레이터를 실행합니다. 100 | 101 | ### iOS 시뮬레이터 설정 (macOS만 해당) 102 | 103 | 1. App Store에서 Xcode를 설치합니다. 104 | 2. Xcode를 실행하고 필요한 구성 요소를 설치합니다. 105 | 3. 터미널에서 다음 명령어를 실행하여, Flutter와 Xcode 간의 라이센스 동의를 진행합니다: 106 | ```bash 107 | sudo xcodebuild -license 108 | ``` 109 | 4. iOS 시뮬레이터를 실행합니다: 110 | ```bash 111 | open -a Simulator 112 | ``` 113 | 114 | ### 실제 안드로이드 기기 연결 115 | 116 | 1. 안드로이드 디바이스의 설정에서 "개발자 옵션"을 활성화합니다: 117 | - 설정 > 휴대폰 정보 > 소프트웨어 정보 > 빌드 번호를 7번 탭합니다. 118 | 2. 개발자 옵션에서 "USB 디버깅"을 활성화합니다. 119 | 3. USB 케이블로 디바이스를 컴퓨터에 연결합니다. 120 | 4. 디바이스에 표시되는 USB 디버깅 권한 요청을 수락합니다. 121 | 5. 터미널에서 다음 명령어로 연결된 디바이스를 확인합니다: 122 | ```bash 123 | flutter devices 124 | ``` 125 | 126 | ### 실제 iOS 기기 연결 (macOS만 해당) 127 | 128 | 1. Apple Developer 계정이 필요합니다. 129 | 2. Xcode를 열고 "Preferences > Accounts"에서 Apple ID를 추가합니다. 130 | 3. USB 케이블로 iOS 기기를 Mac에 연결합니다. 131 | 4. Xcode에서 프로젝트를 열고 디바이스를 선택한 후 "Trust"를 선택합니다. 132 | 5. 터미널에서 다음 명령어로 연결된 디바이스를 확인합니다: 133 | ```bash 134 | flutter devices 135 | ``` 136 | 137 | ## Flutter Doctor로 확인하기 138 | 139 | 설치 과정이 완료되면 다음 명령어를 실행하여 환경 설정이 올바르게 되었는지 확인합니다: 140 | 141 | ```bash 142 | flutter doctor -v 143 | ``` 144 | 145 | 이 명령어는 Flutter SDK, Android toolchain, iOS toolchain (macOS만 해당), VS Code 등의 설치 상태를 확인하고, 문제가 있다면 해결 방법을 제안합니다. 146 | 147 | ## 추가 설정 (선택사항) 148 | 149 | ### Git 설정 150 | 151 | Flutter 프로젝트는 Git을 이용한 버전 관리를 권장합니다: 152 | 153 | 1. [Git 공식 사이트](https://git-scm.com/)에서 Git을 다운로드하고 설치합니다. 154 | 2. 터미널 또는 명령 프롬프트에서 Git 설정을 구성합니다: 155 | ```bash 156 | git config --global user.name "Your Name" 157 | git config --global user.email "your.email@example.com" 158 | ``` 159 | 160 | ### 추가 권장 VS Code 확장 프로그램 161 | 162 | - **Awesome Flutter Snippets**: 유용한 Flutter 코드 스니펫 제공 163 | - **Flutter Widget Snippets**: 위젯 코드 생성 지원 164 | - **Pubspec Assist**: 의존성 관리 도우미 165 | - **Error Lens**: 인라인 오류 하이라이팅 166 | - **Git Lens**: Git 통합 향상 167 | 168 | ## 문제 해결 169 | 170 | ### "flutter: command not found" 오류 171 | 172 | 환경 변수가 올바르게 설정되지 않았을 수 있습니다. 시스템의 PATH 변수에 Flutter bin 디렉토리가 포함되어 있는지 확인하세요. 173 | 174 | ### Android Studio 설치 문제 175 | 176 | 안드로이드 설정에 문제가 있을 경우: 177 | 178 | ```bash 179 | flutter doctor --android-licenses 180 | ``` 181 | 182 | 명령어를 실행하여 Android 라이센스를 동의합니다. 183 | 184 | ### 에뮬레이터 성능 문제 185 | 186 | Windows와 Linux 사용자는 BIOS에서 가상화 기술(Intel VT-x 또는 AMD-V)이 활성화되어 있는지 확인하세요. 187 | 188 | ## 결론 189 | 190 | 이제 Flutter 개발을 위한 환경 설정이 완료되었습니다. 다음 섹션에서는 첫 번째 Flutter 프로젝트를 생성하고 실행하는 방법을 알아보겠습니다. 191 | -------------------------------------------------------------------------------- /src/content/docs/part2/dart-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dart 소개 3 | --- 4 | 5 | ## Dart란 무엇인가? 6 | 7 | Dart는 Google에서 개발한 클라이언트 최적화 프로그래밍 언어로, 모든 플랫폼에서 빠르고 안정적인 애플리케이션을 개발하기 위해 설계되었습니다. Dart는 Flutter 프레임워크의 기반이 되는 언어이며, 웹, 모바일, 데스크톱 애플리케이션을 개발하는 데 사용됩니다. 8 | 9 | ## Dart의 역사 10 | 11 | - **2011년**: Google I/O에서 처음 발표 12 | - **2013년**: Dart 1.0 출시 13 | - **2018년**: Dart 2.0 출시 (타입 안전성 강화) 14 | - **2021년**: Dart 2.13 출시 (null 안전성 도입) 15 | - **2023년**: Dart 3.0 출시 (레코드, 패턴 매칭 도입) 16 | 17 | Dart는 초기에 JavaScript를 대체하기 위한 웹 프로그래밍 언어로 시작했지만, 현재는 Flutter를 통한 크로스 플랫폼 애플리케이션 개발에 주로 사용됩니다. 18 | 19 | ## Dart의 주요 특징 20 | 21 | ### 1. 객체 지향 언어 22 | 23 | Dart는 클래스 기반의 객체 지향 언어입니다. 모든 것이 객체이며, 모든 객체는 클래스의 인스턴스입니다. 심지어 함수와 `null`도 객체입니다. 24 | 25 | ```dart 26 | class Person { 27 | String name; 28 | int age; 29 | 30 | Person(this.name, this.age); 31 | 32 | void introduce() { 33 | print('안녕하세요, 저는 $name이고 $age살입니다.'); 34 | } 35 | } 36 | 37 | void main() { 38 | final person = Person('홍길동', 30); 39 | person.introduce(); // 출력: 안녕하세요, 저는 홍길동이고 30살입니다. 40 | } 41 | ``` 42 | 43 | ### 2. 강력한 타입 시스템 44 | 45 | Dart는 정적 타입 언어이지만, 타입 추론을 지원하여 타입 명시를 생략할 수 있습니다. 46 | 47 | ```dart 48 | // 타입 명시 49 | String name = '홍길동'; 50 | int age = 30; 51 | 52 | // 타입 추론 53 | var name = '홍길동'; // String으로 추론 54 | var age = 30; // int로 추론 55 | final height = 175.5; // double로 추론 56 | ``` 57 | 58 | ### 3. 비동기 프로그래밍 지원 59 | 60 | Dart는 `Future`, `Stream`, `async`, `await` 등을 통해 비동기 프로그래밍을 지원합니다. 61 | 62 | ```dart 63 | Future fetchData() async { 64 | // 비동기 작업 시뮬레이션 65 | await Future.delayed(Duration(seconds: 2)); 66 | return '데이터'; 67 | } 68 | 69 | void main() async { 70 | print('데이터 요청 시작'); 71 | final data = await fetchData(); 72 | print('받은 데이터: $data'); 73 | } 74 | ``` 75 | 76 | ### 4. Null 안전성 77 | 78 | Dart 2.12부터 Null 안전성을 도입하여, 변수가 null 가능성을 타입 시스템에서 명시합니다. 79 | 80 | ```dart 81 | // null이 될 수 없는 변수 82 | String name = '홍길동'; 83 | // name = null; // 컴파일 오류 84 | 85 | // null이 될 수 있는 변수 86 | String? nullableName = '홍길동'; 87 | nullableName = null; // 허용됨 88 | ``` 89 | 90 | ### 5. 다중 플랫폼 지원 91 | 92 | Dart는 여러 플랫폼에서 실행될 수 있습니다: 93 | 94 | - **네이티브 플랫폼**: Dart는 AoT(Ahead-of-Time) 컴파일을 통해 네이티브 바이너리로 컴파일됩니다. Flutter 앱은 이 방식으로 배포됩니다. 95 | - **웹 플랫폼**: Dart는 JavaScript로 컴파일되어 브라우저에서 실행됩니다. 96 | - **개발 환경**: Dart는 JIT(Just-in-Time) 컴파일을 통해 개발 중 핫 리로드와 같은 기능을 제공합니다. 97 | 98 | ### 6. 풍부한 표준 라이브러리 99 | 100 | Dart는 다양한 기능을 제공하는 풍부한 표준 라이브러리를 포함하고 있습니다: 101 | 102 | - 컬렉션 (`List`, `Map`, `Set` 등) 103 | - 비동기 처리 (`Future`, `Stream`) 104 | - 파일 I/O 105 | - HTTP 클라이언트 106 | - 정규 표현식 107 | - 직렬화 지원 108 | 109 | ## Dart 실행 환경 110 | 111 | Dart 코드는 다양한 환경에서 실행될 수 있습니다: 112 | 113 | ### 1. Dart VM 114 | 115 | Dart Virtual Machine(VM)은 Dart 코드를 직접 실행하는 환경으로, 개발 중 코드를 빠르게 실행하고 디버깅할 수 있습니다. 116 | 117 | ```bash 118 | dart run main.dart 119 | ``` 120 | 121 | ### 2. Flutter 122 | 123 | Flutter는 Dart를 사용하여 크로스 플랫폼 모바일 애플리케이션을 개발하는 프레임워크입니다. 124 | 125 | ```bash 126 | flutter run 127 | ``` 128 | 129 | ### 3. Web (Dart2JS) 130 | 131 | Dart 코드는 JavaScript로 컴파일되어 웹 브라우저에서 실행될 수 있습니다. 132 | 133 | ```bash 134 | dart compile js main.dart -o main.js 135 | ``` 136 | 137 | ### 4. Native (Native AOT) 138 | 139 | Dart 코드는 네이티브 바이너리로 컴파일되어 독립 실행 파일로 배포될 수 있습니다. 140 | 141 | ```bash 142 | dart compile exe main.dart -o main 143 | ``` 144 | 145 | ## Dart 패키지 생태계 146 | 147 | Dart는 `pub.dev`라는 공식 패키지 저장소를 통해 풍부한 패키지 생태계를 제공합니다. 이 저장소에는 다양한 기능을 제공하는 수천 개의 오픈 소스 패키지가 있습니다. 148 | 149 | 패키지를 프로젝트에 추가하려면 `pubspec.yaml` 파일에 의존성을 추가합니다: 150 | 151 | ```yaml 152 | dependencies: 153 | http: ^1.0.0 154 | path: ^1.8.0 155 | ``` 156 | 157 | 그리고 다음 명령으로 패키지를 설치합니다: 158 | 159 | ```bash 160 | dart pub get 161 | ``` 162 | 163 | ## Dart와 다른 언어 비교 164 | 165 | ### Java와 비교 166 | 167 | ```dart 168 | // Dart 169 | class Person { 170 | String name; 171 | int age; 172 | 173 | Person(this.name, this.age); 174 | 175 | void sayHello() { 176 | print('Hello, I am $name'); 177 | } 178 | } 179 | 180 | // Java 181 | public class Person { 182 | private String name; 183 | private int age; 184 | 185 | public Person(String name, int age) { 186 | this.name = name; 187 | this.age = age; 188 | } 189 | 190 | public void sayHello() { 191 | System.out.println("Hello, I am " + name); 192 | } 193 | } 194 | ``` 195 | 196 | ### JavaScript와 비교 197 | 198 | ```dart 199 | // Dart 200 | void main() { 201 | final list = [1, 2, 3, 4, 5]; 202 | final doubled = list.map((item) => item * 2).toList(); 203 | print(doubled); // [2, 4, 6, 8, 10] 204 | } 205 | 206 | // JavaScript 207 | function main() { 208 | const list = [1, 2, 3, 4, 5]; 209 | const doubled = list.map(item => item * 2); 210 | console.log(doubled); // [2, 4, 6, 8, 10] 211 | } 212 | ``` 213 | 214 | ### Swift와 비교 215 | 216 | ```dart 217 | // Dart 218 | class Person { 219 | String name; 220 | int? age; 221 | 222 | Person(this.name, {this.age}); 223 | } 224 | 225 | // Swift 226 | class Person { 227 | let name: String 228 | var age: Int? 229 | 230 | init(name: String, age: Int? = nil) { 231 | self.name = name 232 | self.age = age 233 | } 234 | } 235 | ``` 236 | 237 | ## Dart의 장점 238 | 239 | 1. **통합 개발 환경**: 단일 언어로 모바일, 웹, 데스크톱 앱을 개발할 수 있습니다. 240 | 2. **생산성**: 핫 리로드, 풍부한 도구, 직관적인 문법으로 개발 생산성을 높입니다. 241 | 3. **성능**: AoT 컴파일을 통해 네이티브 성능에 가까운 실행 속도를 제공합니다. 242 | 4. **안정성**: 강력한 타입 시스템과 null 안전성으로 많은 런타임 오류를 방지합니다. 243 | 5. **확장성**: 표준 라이브러리와 풍부한 패키지 생태계를 통해 다양한 기능을 추가할 수 있습니다. 244 | 245 | ## Dart 개발 환경 설정 246 | 247 | ### VS Code에서 Dart 개발 환경 설정 248 | 249 | 1. Dart SDK 설치 (Flutter SDK를 설치했다면 이미 포함되어 있습니다) 250 | 2. VS Code 설치 251 | 3. Dart 확장 프로그램 설치 252 | 4. 새 Dart 프로젝트 생성: 253 | ```bash 254 | dart create my_dart_project 255 | ``` 256 | 5. VS Code에서 프로젝트 열기 257 | 6. `main.dart` 파일 실행하기: F5 또는 "Run" 버튼 클릭 258 | 259 | ## 결론 260 | 261 | Dart는 현대적인 애플리케이션 개발을 위한 강력하고 유연한 프로그래밍 언어입니다. 특히 Flutter와 함께 사용하면, 단일 코드베이스로 고품질의 크로스 플랫폼 애플리케이션을 개발할 수 있습니다. 262 | 263 | 다음 장에서는 Dart의 기본 문법과 변수에 대해 더 자세히 알아보겠습니다. 264 | -------------------------------------------------------------------------------- /llm.txt: -------------------------------------------------------------------------------- 1 | # Tabling User Flutter 프로젝트 LLM 가이드 2 | 3 | ## 1. 프로젝트 개요 4 | 5 | 이 문서는 `tabling_user` Flutter 애플리케이션 프로젝트를 이해하고 개발하는 데 도움을 주기 위한 LLM 가이드입니다. Tabling 사용자 앱은 고객이 레스토랑을 검색하고, 예약하고, 웨이팅 목록에 참여하는 등의 기능을 제공하는 앱입니다. 6 | 7 | ## 2. 프로젝트 구조 8 | 9 | ### 2.1. 모노레포(Monorepo) 구조 10 | 11 | Tabling Flutter 프로젝트는 모노레포 구조로 조직되어 있으며, 여러 앱과 패키지를 포함하고 있습니다. 12 | 13 | tabling_c_flutter/ 14 | ├── apps/ 15 | │ ├── tabling_user/ # 유저 타입 앱 (이 문서의 주제) 16 | │ ├── tabling_web_app/ # 웹 앱 17 | │ ├── server_board/ 18 | │ ├── tabling_web/ 19 | │ ├── reservation/ 20 | │ ├── ceoboard/ 21 | │ ├── storybook/ 22 | │ └── tabling_pager/ 23 | └── packages/ # 공유 패키지 24 | ├── tabling_ui/ # UI 컴포넌트 패키지 25 | ├── tabling_models/ # 데이터 모델 패키지 26 | ├── tabling_rest_client/ # API 클라이언트 패키지 27 | ├── tabling_shared_data/ # 공유 데이터 패키지 28 | ├── tabling_restaurant_card/ # 레스토랑 카드 UI 컴포넌트 29 | ├── tabling_format/ # 포맷팅 유틸리티 30 | ├── tabling_analytics/ # 분석 도구 31 | ├── tabling_image/ # 이미지 관련 유틸리티 32 | └── tabling_video_player/ # 비디오 플레이어 컴포넌트 33 | 34 | ### 2.2. tabling_user 앱 구조 35 | 36 | tabling_user 앱은 다음과 같은 디렉토리 구조를 가지고 있습니다: 37 | 38 | tabling_user/ 39 | ├── lib/ 40 | │ ├── analytics/ # 분석 관련 코드 41 | │ ├── animations/ # 애니메이션 관련 코드 42 | │ ├── constants/ # 상수 정의 43 | │ ├── extensions/ # 확장 메소드 44 | │ ├── locales/ # 다국어 지원 45 | │ ├── mixins/ # 믹스인 46 | │ ├── models/ # 앱 특화 모델 (패키지 모델 외) 47 | │ ├── pages/ # 페이지 정의 (라우팅 진입점) 48 | │ ├── providers/ # Riverpod 프로바이더 49 | │ ├── repositories/ # 데이터 리포지토리 50 | │ ├── router/ # 라우팅 설정 51 | │ ├── services/ # 비즈니스 로직 서비스 52 | │ ├── utils/ # 유틸리티 함수 53 | │ ├── view_models/ # 뷰 모델 54 | │ ├── app.dart # 앱 진입점 55 | │ ├── flavors.dart # 환경 설정 56 | │ ├── main.dart # 메인 진입점 57 | │ ├── main_develop.dart # 개발 환경 진입점 58 | │ ├── main_production.dart # 프로덕션 환경 진입점 59 | │ └── main_staging.dart # 스테이징 환경 진입점 60 | 61 | ## 3. 아키텍처 패턴 62 | 63 | ### 3.1. Page -> Screen -> Widget 패턴 64 | 65 | tabling_user 앱은 Page -> Screen -> Widget 패턴을 사용하여 UI를 구성합니다: 66 | 67 | 1. **Page**: 68 | - 라우팅 시스템의 진입점 69 | - 앱의 `/pages` 디렉토리에 정의됨 70 | - 해당 화면의 라우팅, 상태 관리 및 초기화 로직을 담당 71 | - 일반적으로 Provider나 View Model과 연결되어 데이터 상태를 관리 72 | - 화면 내용을 구성하는 Screen 컴포넌트를 포함 73 | 74 | 2. **Screen**: 75 | - `tabling_ui` 패키지의 `/screens` 디렉토리에 정의됨 76 | - 특정 페이지의 주요 레이아웃과 UI 로직을 담당 77 | - 재사용 가능한 단위로 설계됨 78 | - 상위 Page로부터 데이터와 이벤트 핸들러를 전달받음 79 | - 여러 작은 Widget들을 조합하여 화면을 구성 80 | 81 | 3. **Widget**: 82 | - `tabling_ui` 패키지의 `/widgets` 디렉토리에 정의됨 83 | - 작고 재사용 가능한 UI 컴포넌트 84 | - 버튼, 텍스트 필드, 카드 등 기본 UI 요소 85 | - 디자인 시스템에 따라 일관된 스타일과 동작을 제공 86 | 87 | ### 3.2. 상태 관리 88 | 89 | tabling_user 앱은 Riverpod를 사용하여 상태를 관리합니다: 90 | 91 | - `/providers` 디렉토리: 글로벌 상태 및 서비스 프로바이더 92 | - 각 Page 디렉토리 내 `provider.dart`: 해당 페이지 특화 프로바이더 93 | - 각 기능 디렉토리 내 `viewmodel.dart`: 상태와 비즈니스 로직 캡슐화 94 | 95 | ## 4. 주요 패키지 및 종속성 96 | 97 | ### 4.1. 내부 패키지 98 | 99 | 앱은 다음과 같은 내부 패키지를 활용합니다: 100 | 101 | - **tabling_ui**: 디자인 시스템 구현, UI 컴포넌트 제공 102 | - **tabling_models**: 앱에서 사용되는 데이터 모델 정의 103 | - **tabling_rest_client**: API 통신 클라이언트 104 | - **tabling_shared_data**: 앱 간 공유 데이터 105 | - **tabling_restaurant_card**: 레스토랑 카드 UI 컴포넌트 106 | - **tabling_format**: 날짜, 시간, 금액 등 포맷팅 유틸리티 107 | - **tabling_analytics**: 분석 및 이벤트 추적 108 | - **tabling_image**: 이미지 관련 유틸리티 109 | 110 | ### 4.2. 외부 종속성 111 | 112 | 주요 외부 라이브러리: 113 | 114 | - **flutter_riverpod**: 상태 관리 115 | - **go_router**: 라우팅 116 | - **firebase_core, firebase_analytics, firebase_messaging 등**: Firebase 서비스 117 | - **cached_network_image**: 네트워크 이미지 캐싱 118 | - **flutter_facebook_auth, kakao_flutter_sdk_user, sign_in_with_apple**: 소셜 로그인 119 | - **infinite_scroll_pagination**: 무한 스크롤 120 | - **easy_debounce**: 입력 디바운싱 121 | - **permission_handler**: 권한 관리 122 | - **sentry_flutter**: 에러 모니터링 123 | - **geolocator**: 위치 정보 124 | - **flutter_local_notifications**: 로컬 알림 125 | - **animations, flutter_animate, lottie**: 애니메이션 효과 126 | 127 | ## 5. 코드 규칙 및 패턴 128 | 129 | ### 5.1. 파일 명명 규칙 130 | 131 | - **페이지**: `page.dart` (예: `/pages/my/reviews/page.dart`) 132 | - **화면**: `*_screen.dart` (예: `search_screen.dart`) 133 | - **위젯**: 기능/유형에 따른 이름 (예: `tabling_search_field.dart`) 134 | - **프로바이더**: `provider.dart` 135 | - **뷰모델**: `viewmodel.dart` 또는 `view_model.dart` 136 | - **모델**: `*_model.dart` 또는 `entity.dart` 137 | 138 | ### 5.2. 코드 구조화 패턴 139 | 140 | 1. **기능별 디렉토리 구조**: 141 | - 각 주요 기능은 자체 디렉토리에 캡슐화됨 142 | - 예: `/pages/my/reviews/` - 리뷰 관련 페이지 및 로직 143 | 144 | 2. **Provider-ViewModel 패턴**: 145 | - Provider: 상태 관리 및 의존성 주입 146 | - ViewModel: UI 로직과 상태를 캡슐화 147 | - 예: `provider.dart` + `viewmodel.dart` 148 | 149 | 3. **이벤트 기반 통신**: 150 | - 화면 간 통신은 이벤트 기반 패턴 사용 151 | - `ScreenEvent` 클래스를 통한 일관된 이벤트 처리 152 | - 상위 컴포넌트에 이벤트 위임 (콜백 함수) 153 | 154 | ## 6. 개발 워크플로우 155 | 156 | ### 6.1. 새로운 기능 개발 157 | 158 | 1. **모델 정의**: 159 | - 필요한 데이터 모델이 `tabling_models`에 있는지 확인 160 | - 없다면 추가 요청 또는 로컬 모델 정의 161 | 162 | 2. **API 통합**: 163 | - `tabling_rest_client`를 통해 API 호출 164 | - 리포지토리 패턴으로 데이터 접근 추상화 165 | 166 | 3. **상태 관리 설정**: 167 | - Riverpod Provider 및 ViewModel 구현 168 | - 상태 및 비즈니스 로직 구현 169 | 170 | 4. **UI 구현**: 171 | - Page 컴포넌트 구현 (라우팅 진입점) 172 | - 필요한 Screen 컴포넌트 구현 또는 재사용 173 | - `tabling_ui` 위젯 활용 174 | 175 | ### 6.2. 환경 설정 176 | 177 | - **Flavors**: 개발, 스테이징, 프로덕션 환경 구분 178 | - **환경별 진입점**: `main_develop.dart`, `main_staging.dart`, `main_production.dart` 179 | - **firebase_app_check**: 인증된 앱 요청 보장 180 | 181 | ## 7. UI/UX 가이드라인 182 | 183 | ### 7.1. 디자인 시스템 184 | 185 | tabling_ui 패키지는 다음과 같은 디자인 시스템 요소를 제공합니다: 186 | 187 | - **토큰 시스템**: 188 | - `/tokens/core`: 색상, 간격, 타이포그래피 등 기본 디자인 토큰 189 | - 일관된 디자인 적용을 위해 하드코딩된 값 대신 토큰 사용 권장 190 | 191 | - **위젯 컴포넌트**: 192 | - `/widgets`: 버튼, 텍스트 필드, 카드 등 재사용 가능한 UI 컴포넌트 193 | - 커스텀 UI보다 기존 컴포넌트 재사용 권장 194 | 195 | - **테마**: 196 | - `/theme`: 앱 전체 테마 및 스타일링 197 | - 앱 테마를 일관되게 유지하기 위해 테마 설정 준수 198 | 199 | ### 7.2. 반응형 디자인 200 | 201 | - **LayoutBuilder 및 MediaQuery** 활용 202 | - 다양한 화면 크기에 적응하는 UI 설계 203 | - 기기 방향 변경 대응 204 | 205 | ## 8. 테스팅 및 품질 보증 206 | 207 | ### 8.1. 테스트 유형 208 | 209 | - **단위 테스트**: `/test` 디렉토리 210 | - **통합 테스트**: `/integration_test` 디렉토리 211 | - **위젯 테스트**: UI 컴포넌트 테스트 212 | 213 | ### 8.2. 코드 품질 도구 214 | 215 | - **analysis_options.yaml**: 린트 규칙 정의 216 | - **custom_lint**: 추가 린트 규칙 217 | - **lefthook.yml**: 커밋 전 검사 218 | 219 | ## 9. 배포 및 릴리스 220 | 221 | ### 9.1. CI/CD 222 | 223 | - **codemagic.yaml**: CI/CD 파이프라인 구성 224 | - **GitHub Actions**: 추가 자동화 작업 225 | 226 | ### 9.2. 버전 관리 227 | 228 | - **pubspec.yaml**: 앱 버전 및 빌드 번호 관리 229 | - **melos.yaml**: 모노레포 패키지 버전 관리 230 | 231 | ## 10. 문제 해결 및 디버깅 232 | 233 | ### 10.1. 로깅 234 | 235 | - **talker_flutter**: 고급 로깅 및 디버깅 도구 236 | - **sentry_flutter**: 에러 모니터링 및 보고 237 | 238 | ### 10.2. 성능 모니터링 239 | 240 | - **firebase_performance**: 앱 성능 모니터링 241 | - **firebase_crashlytics**: 크래시 보고 및 분석 242 | 243 | ## 11. 공통 개발 작업 244 | 245 | ### 11.1. 새 페이지 추가 246 | 247 | 1. `/pages/[feature]/` 디렉토리 생성 248 | 2. `page.dart` 파일 구현 (Page 컴포넌트) 249 | 3. Provider 및 ViewModel 구현 250 | 4. `/router/` 디렉토리에 라우트 등록 251 | 252 | ### 11.2. 기존 페이지 수정 253 | 254 | 1. 관련 Page 컴포넌트 식별 255 | 2. 필요한 상태 및 이벤트 핸들러 수정 256 | 3. 필요에 따라 Screen 또는 Widget 컴포넌트 수정 257 | 258 | ### 11.3. 새 API 통합 259 | 260 | 1. `tabling_rest_client`에서 API 엔드포인트 확인/추가 261 | 2. 필요한 모델 업데이트/추가 262 | 3. Repository 구현/수정 263 | 4. Provider 및 ViewModel 업데이트 264 | 265 | ## 12. 결론 266 | 267 | tabling_user 프로젝트는 Page -> Screen -> Widget 패턴과 Riverpod를 사용한 상태 관리를 통해 구조화된 Flutter 앱입니다. 다양한 내부 패키지를 활용하여 일관된 UI/UX 및 기능을 제공합니다. 이 가이드를 참고하여 프로젝트의 구조를 이해하고 효율적으로 개발에 기여할 수 있습니다. 268 | -------------------------------------------------------------------------------- /src/content/docs/part1/first-project.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 첫 프로젝트 생성 및 실행 3 | --- 4 | import { FileTree } from '@astrojs/starlight/components'; 5 | 6 | 이제 Flutter SDK와 개발 환경이 설정되었으므로, 첫 번째 Flutter 프로젝트를 생성하고 실행해 보겠습니다. 7 | 8 | ## 프로젝트 생성하기 9 | 10 | Flutter 프로젝트는 다양한 방법으로 생성할 수 있습니다. 터미널을 사용하거나 Visual Studio Code 또는 Android Studio와 같은 IDE를 사용할 수 있습니다. 11 | 12 | ### 터미널을 이용한 생성 13 | 14 | 1. 터미널을 열고 프로젝트를 생성할 디렉토리로 이동합니다. 15 | 2. 다음 명령어를 실행하여 새 Flutter 프로젝트를 생성합니다: 16 | 17 | ```bash 18 | flutter create my_first_app 19 | ``` 20 | 21 | 이 명령어는 `my_first_app`이라는 이름의 새 Flutter 프로젝트를 생성합니다. 22 | 23 | 3. 프로젝트 디렉토리로 이동합니다: 24 | 25 | ```bash 26 | cd my_first_app 27 | ``` 28 | 29 | ### VS Code를 이용한 생성 30 | 31 | 1. Visual Studio Code를 실행합니다. 32 | 2. Command Palette(`Ctrl+Shift+P` 또는 `Cmd+Shift+P`)를 열고 "Flutter: New Project"를 입력하고 선택합니다. 33 | 3. 프로젝트 이름을 입력합니다 (예: "my_first_app"). 34 | 4. 프로젝트를 저장할 디렉토리를 선택합니다. 35 | 5. VS Code가 자동으로 새 Flutter 프로젝트를 생성합니다. 36 | 37 | ### Android Studio를 이용한 생성 38 | 39 | 1. Android Studio를 실행합니다. 40 | 2. "Create New Flutter Project"를 선택합니다. 41 | 3. "Flutter Application"을 선택하고 "Next"를 클릭합니다. 42 | 4. 프로젝트 이름과 저장 위치, Flutter SDK 경로를 지정하고 "Next"를 클릭합니다. 43 | 5. 추가 정보를 입력하고 "Finish"를 클릭합니다. 44 | 45 | ## 프로젝트 구조 탐색 46 | 47 | Flutter 프로젝트가 생성되면, 다음과 같은 기본 파일 구조가 만들어집니다: 48 | 49 | 50 | 51 | 52 | - my_first_app/ 53 | - .dart_tool/ # Dart 도구 관련 파일 54 | - .idea/ # IDE 설정 (Android Studio) 55 | - android/ # 안드로이드 특화 코드 56 | - build/ # 빌드 출력 파일 57 | - ios/ # iOS 특화 코드 58 | - lib/ # Dart 코드 59 | - main.dart # 앱의 진입점 60 | - linux/ # Linux 특화 코드 61 | - macos/ # macOS 특화 코드 62 | - test/ # 테스트 코드 63 | - web/ # 웹 특화 코드 64 | - windows/ # Windows 특화 코드 65 | - .gitignore # Git 무시 파일 66 | - .metadata # Flutter 메타데이터 67 | - analysis_options.yaml # Dart 분석 설정 68 | - pubspec.lock # 의존성 버전 잠금 파일 69 | - pubspec.yaml # 프로젝트 설정 및 의존성 70 | - README.md # 프로젝트 설명 71 | 72 | 73 | 74 | 75 | 이 중에서 가장 중요한 파일은 다음과 같습니다: 76 | 77 | - **lib/main.dart**: 앱의 메인 코드가 위치한 파일입니다. 78 | - **pubspec.yaml**: 앱의 메타데이터와 의존성을 정의하는 파일입니다. 79 | - **android/**, **ios/**: 플랫폼별 설정이 포함된 디렉토리입니다. 80 | 81 | ## 기본 앱 코드 이해하기 82 | 83 | 기본적으로 생성된 `lib/main.dart` 파일을 살펴보겠습니다. 이 파일은 간단한 카운터 앱을 구현하고 있습니다: 84 | 85 | ```dart 86 | import 'package:flutter/material.dart'; 87 | 88 | void main() { 89 | runApp(const MyApp()); 90 | } 91 | 92 | class MyApp extends StatelessWidget { 93 | const MyApp({super.key}); 94 | 95 | @override 96 | Widget build(BuildContext context) { 97 | return MaterialApp( 98 | title: 'Flutter Demo', 99 | theme: ThemeData( 100 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 101 | useMaterial3: true, 102 | ), 103 | home: const MyHomePage(title: 'Flutter Demo Home Page'), 104 | ); 105 | } 106 | } 107 | 108 | class MyHomePage extends StatefulWidget { 109 | const MyHomePage({super.key, required this.title}); 110 | 111 | final String title; 112 | 113 | @override 114 | State createState() => _MyHomePageState(); 115 | } 116 | 117 | class _MyHomePageState extends State { 118 | int _counter = 0; 119 | 120 | void _incrementCounter() { 121 | setState(() { 122 | _counter++; 123 | }); 124 | } 125 | 126 | @override 127 | Widget build(BuildContext context) { 128 | return Scaffold( 129 | appBar: AppBar( 130 | backgroundColor: Theme.of(context).colorScheme.inversePrimary, 131 | title: Text(widget.title), 132 | ), 133 | body: Center( 134 | child: Column( 135 | mainAxisAlignment: MainAxisAlignment.center, 136 | children: [ 137 | const Text( 138 | 'You have pushed the button this many times:', 139 | ), 140 | Text( 141 | '$_counter', 142 | style: Theme.of(context).textTheme.headlineMedium, 143 | ), 144 | ], 145 | ), 146 | ), 147 | floatingActionButton: FloatingActionButton( 148 | onPressed: _incrementCounter, 149 | tooltip: 'Increment', 150 | child: const Icon(Icons.add), 151 | ), 152 | ); 153 | } 154 | } 155 | ``` 156 | 157 | 이 코드의 주요 구성 요소를 간략하게 설명하면: 158 | 159 | 1. **main() 함수**: 앱의 진입점으로, `runApp()`을 호출하여 앱을 시작합니다. 160 | 2. **MyApp 클래스**: 앱의 전체적인 테마와 구조를 정의하는 StatelessWidget입니다. 161 | 3. **MyHomePage 클래스**: 앱의 홈 화면을 정의하는 StatefulWidget입니다. 162 | 4. **\_MyHomePageState 클래스**: 홈 화면의 상태를 관리하는 State 클래스입니다. 163 | 164 | ## 앱 실행하기 165 | 166 | 이제 생성된 Flutter 앱을 실행해 보겠습니다. 167 | 168 | ### 터미널에서 실행 169 | 170 | 프로젝트 디렉토리에서 다음 명령어를 실행합니다: 171 | 172 | ```bash 173 | flutter run 174 | ``` 175 | 176 | 이 명령어는 연결된 기기나 에뮬레이터에서 앱을 실행합니다. 여러 기기가 연결되어 있다면, 실행할 기기를 선택하라는 메시지가 표시됩니다. 177 | 178 | 특정 기기에서 실행하려면 다음과 같이 명령할 수 있습니다: 179 | 180 | ```bash 181 | flutter run -d 182 | ``` 183 | 184 | 기기 ID는 `flutter devices` 명령어로 확인할 수 있습니다. 185 | 186 | ### VS Code에서 실행 187 | 188 | 1. VS Code에서 프로젝트를 엽니다. 189 | 2. 하단 상태 표시줄에서 기기 선택기를 클릭하여 실행할 기기를 선택합니다. 190 | 3. F5 키를 누르거나 "Run > Start Debugging"을 선택합니다. 191 | 192 | ### Android Studio에서 실행 193 | 194 | 1. Android Studio에서 프로젝트를 엽니다. 195 | 2. 상단 도구 모음에서 기기 선택기를 사용하여 실행할 기기를 선택합니다. 196 | 3. 실행 버튼(▶)을 클릭합니다. 197 | 198 | ## 앱 수정하기 199 | 200 | 이제 앱을 조금 수정해 보겠습니다. `lib/main.dart` 파일을 열고 다음과 같이 변경해 보세요: 201 | 202 | 1. 앱 제목 변경: 203 | 204 | ```dart 205 | return MaterialApp( 206 | title: '내 첫 번째 Flutter 앱', 207 | // ... 208 | ``` 209 | 210 | 2. 홈 화면 타이틀 변경: 211 | 212 | ```dart 213 | home: const MyHomePage(title: '내 첫 번째 Flutter 앱'), 214 | ``` 215 | 216 | 3. 카운터 텍스트 변경: 217 | 218 | ```dart 219 | const Text( 220 | '버튼을 눌러 카운터를 증가시키세요:', 221 | ), 222 | ``` 223 | 224 | ## 핫 리로드 사용하기 225 | 226 | Flutter의 가장 강력한 기능 중 하나는 핫 리로드입니다. 이는 앱을 다시 시작하지 않고도 코드 변경 사항을 즉시 확인할 수 있게 해줍니다. 227 | 228 | 1. 앱이 실행 중인 상태에서 코드를 수정합니다. 229 | 2. 변경 사항을 저장합니다. 230 | 3. VS Code 또는 Android Studio에서는 자동으로 핫 리로드가 실행됩니다. 231 | 4. 터미널에서 실행 중인 경우, `r` 키를 눌러 핫 리로드를 실행합니다. 232 | 233 | 핫 리로드는 상태를 유지하므로, 예를 들어 카운터 값은 리셋되지 않습니다. 234 | 235 | ## 핫 리스타트 사용하기 236 | 237 | 때로는 변경 사항이 핫 리로드로 적용되지 않을 수 있습니다. 이런 경우 핫 리스타트를 사용할 수 있습니다: 238 | 239 | 1. VS Code 또는 Android Studio에서 핫 리스타트 버튼을 클릭합니다. 240 | 2. 터미널에서 실행 중인 경우, `R` 키(대문자)를 눌러 핫 리스타트를 실행합니다. 241 | 242 | 핫 리스타트는 앱을 다시 시작하지만, 컴파일 과정은 건너뛰므로 일반적인 재시작보다 빠릅니다. 243 | 244 | ## 더 많은 기기에서 실행하기 245 | 246 | Flutter 앱은 다양한 플랫폼에서 실행할 수 있습니다. 247 | 248 | ### 웹에서 실행하기 249 | 250 | 웹 버전을 실행하려면 다음 명령어를 사용합니다: 251 | 252 | ```bash 253 | flutter run -d chrome 254 | ``` 255 | 256 | 또는 VS Code와 Android Studio에서 Chrome을 기기로 선택할 수 있습니다. 257 | 258 | ### 데스크톱에서 실행하기 259 | 260 | 데스크톱 버전을 실행하려면 다음 명령어를 사용합니다: 261 | 262 | ```bash 263 | # Windows 264 | flutter run -d windows 265 | 266 | # macOS 267 | flutter run -d macos 268 | 269 | # Linux 270 | flutter run -d linux 271 | ``` 272 | 273 | ## 릴리즈 모드로 실행하기 274 | 275 | 기본적으로 `flutter run` 명령어는 디버그 모드로 앱을 실행합니다. 릴리즈 모드로 실행하려면 다음 명령어를 사용합니다: 276 | 277 | ```bash 278 | flutter run --release 279 | ``` 280 | 281 | 릴리즈 모드는 디버그 정보가 제거되고 성능이 최적화된 버전입니다. 282 | 283 | ## 앱 빌드하기 284 | 285 | 개발이 완료된 앱을 배포 가능한 형태로 빌드하려면 다음 명령어를 사용합니다: 286 | 287 | ```bash 288 | # Android APK 289 | flutter build apk 290 | 291 | # Android App Bundle 292 | flutter build appbundle 293 | 294 | # iOS 295 | flutter build ios 296 | 297 | # 웹 298 | flutter build web 299 | 300 | # Windows 301 | flutter build windows 302 | 303 | # macOS 304 | flutter build macos 305 | 306 | # Linux 307 | flutter build linux 308 | ``` 309 | 310 | ## 결론 311 | 312 | 축하합니다! 첫 번째 Flutter 앱을 성공적으로 생성하고 실행했습니다. 이 기본 앱을 출발점으로 삼아, Flutter의 다양한 위젯과 기능을 탐색하며 더 복잡한 앱을 개발할 수 있습니다. 313 | 314 | 다음 섹션에서는 Flutter 프로젝트의 구조를 더 자세히 살펴보고, 앱 개발에 필요한 주요 개념들을 배워보겠습니다. 315 | -------------------------------------------------------------------------------- /src/content/docs/part3/widgets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 위젯 개념과 주요 위젯 3 | --- 4 | 5 | Flutter의 핵심은 위젯(Widget)입니다. Flutter 애플리케이션은 여러 위젯들로 구성되어 있으며, 위젯은 UI의 구성 요소를 나타냅니다. 이 장에서는 Flutter 위젯의 기본 개념과 위젯 시스템의 작동 방식을 알아보겠습니다. 6 | 7 | ## 위젯이란? 8 | 9 | 위젯(Widget)은 Flutter에서 UI를 구성하는 기본 단위입니다. 버튼, 텍스트, 이미지, 레이아웃, 스크롤 등 화면에 보이는 모든 요소는 위젯입니다. Flutter의 철학은 "모든 것이 위젯"이라는 개념에 기반하고 있습니다. 10 | 11 | 위젯은 다음과 같은 특징을 가지고 있습니다: 12 | 13 | 1. **불변성(Immutable)**: 위젯은 생성된 후 변경할 수 없습니다. UI를 변경하려면 새로운 위젯을 생성해야 합니다. 14 | 2. **계층 구조**: 위젯은 트리 구조로 조직되며, 부모 위젯은 자식 위젯을 포함할 수 있습니다. 15 | 3. **선언적 UI**: Flutter는 현재 애플리케이션 상태에 따라 UI가 어떻게 보여야 하는지 선언적으로 정의합니다. 16 | 4. **합성(Composition)**: 작은 위젯들을 조합하여 복잡한 UI를 구성합니다. 17 | 18 | ## Flutter 위젯의 주기 19 | 20 | Flutter 위젯은 생성, 구성, 렌더링의 주기를 거칩니다: 21 | 22 | 23 | 1. **위젯 생성**: 위젯 클래스의 인스턴스가 생성됩니다. 24 | 2. **빌드 메서드 호출**: 위젯의 `build()` 메서드가 호출되어 위젯 트리를 구성합니다. 25 | 3. **요소 트리 생성**: 위젯 트리를 기반으로 Element 트리가 생성되거나 업데이트됩니다. 26 | 4. **RenderObject 생성**: Element 트리에 따라 RenderObject 트리가 생성되거나 업데이트됩니다. 27 | 5. **화면에 렌더링**: RenderObject 트리를 기반으로 UI가 화면에 렌더링됩니다. 28 | 6. **위젯 상태 변경**: 상태 변경 시 위젯이 다시 빌드됩니다. 29 | 30 | ## 위젯 유형 31 | 32 | Flutter 위젯은 크게 두 가지 유형으로 나눌 수 있습니다: 33 | 34 | ### 1. Stateless 위젯 35 | 36 | Stateless 위젯은 내부 상태를 가지지 않는 정적인 위젯입니다. 생성 시 전달받은 속성(properties)만 사용하며, 한 번 빌드되면 변경되지 않습니다. 간단한 UI 요소나 변경이 필요 없는 화면에 적합합니다. 37 | 38 | ```dart 39 | class GreetingWidget extends StatelessWidget { 40 | final String name; 41 | 42 | const GreetingWidget({Key? key, required this.name}) : super(key: key); 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return Text('안녕하세요, $name님!'); 47 | } 48 | } 49 | ``` 50 | 51 | ### 2. Stateful 위젯 52 | 53 | Stateful 위젯은 내부 상태를 가지고 있으며, 상태가 변경되면 UI가 다시 빌드됩니다. 사용자 입력, 네트워크 응답, 시간 경과 등에 따라 변경되는 UI 요소에 적합합니다. 54 | 55 | ```dart 56 | class CounterWidget extends StatefulWidget { 57 | const CounterWidget({Key? key}) : super(key: key); 58 | 59 | @override 60 | _CounterWidgetState createState() => _CounterWidgetState(); 61 | } 62 | 63 | class _CounterWidgetState extends State { 64 | int _counter = 0; 65 | 66 | void _incrementCounter() { 67 | setState(() { 68 | _counter++; 69 | }); 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return Column( 75 | mainAxisAlignment: MainAxisAlignment.center, 76 | children: [ 77 | Text('카운터: $_counter'), 78 | ElevatedButton( 79 | onPressed: _incrementCounter, 80 | child: Text('증가'), 81 | ), 82 | ], 83 | ); 84 | } 85 | } 86 | ``` 87 | 88 | ## 위젯 트리 89 | 90 | Flutter 애플리케이션의 UI는 위젯 트리로 표현됩니다. 모든 Flutter 앱은 루트 위젯에서 시작하여 자식 위젯들로 구성된 트리 구조를 가집니다. 91 | 92 | 위젯 트리의 특징: 93 | 94 | 1. **부모-자식 관계**: 위젯은 하나의 부모와 여러 자식을 가질 수 있습니다. 95 | 2. **단방향 데이터 흐름**: 데이터는 부모에서 자식으로 흐릅니다. 96 | 3. **렌더링 최적화**: Flutter는 변경된 위젯만 다시 빌드하여 성능을 최적화합니다. 97 | 98 | ## 위젯의 세 가지 트리 99 | 100 | Flutter는 UI 렌더링을 위해 세 가지 트리를 관리합니다: 101 | 102 | 1. **Widget 트리**: UI의 설계도로, 사용자가 작성한 위젯 클래스의 인스턴스들로 구성됩니다. 103 | 2. **Element 트리**: Widget과 RenderObject를 연결하는 중간 계층으로, 위젯의 수명 주기를 관리합니다. 104 | 3. **RenderObject 트리**: 실제 화면에 그려지는 객체들을 표현하며, 레이아웃 계산과 페인팅을 담당합니다. 105 | 106 | ## Flutter 위젯의 구성 방식 107 | 108 | Flutter 위젯은 합성(Composition)을 통해 구성됩니다. 작은 위젯들을 조합하여 복잡한 UI를 만들 수 있습니다. 109 | 110 | ```dart 111 | Scaffold( 112 | appBar: AppBar( 113 | title: Text('Flutter 앱'), 114 | actions: [ 115 | IconButton( 116 | icon: Icon(Icons.settings), 117 | onPressed: () {}, 118 | ), 119 | ], 120 | ), 121 | body: Center( 122 | child: Column( 123 | mainAxisAlignment: MainAxisAlignment.center, 124 | children: [ 125 | Text('Hello, Flutter!'), 126 | SizedBox(height: 20), 127 | ElevatedButton( 128 | onPressed: () {}, 129 | child: Text('버튼'), 130 | ), 131 | ], 132 | ), 133 | ), 134 | floatingActionButton: FloatingActionButton( 135 | onPressed: () {}, 136 | child: Icon(Icons.add), 137 | ), 138 | ) 139 | ``` 140 | 141 | 이 예제에서 `Scaffold`, `AppBar`, `Text`, `Center`, `Column` 등 여러 위젯들이 중첩되어 하나의 화면을 구성합니다. 142 | 143 | ## Flutter 기본 위젯의 분류 144 | 145 | Flutter 위젯은 기능에 따라 여러 카테고리로 분류할 수 있습니다: 146 | 147 | ### 구조적 위젯 148 | 149 | 애플리케이션의 구조를 정의하는 위젯입니다: 150 | 151 | - **MaterialApp**: Material Design 앱의 진입점 152 | - **CupertinoApp**: iOS 스타일 앱의 진입점 153 | - **Scaffold**: 기본 앱 구조 제공 (앱바, 드로워, 바텀 시트 등) 154 | - **AppBar**: 앱 상단의 앱 바 155 | 156 | ### 시각적 위젯 157 | 158 | 화면에 콘텐츠를 표시하는 위젯입니다: 159 | 160 | - **Text**: 텍스트 표시 161 | - **Image**: 이미지 표시 162 | - **Icon**: 아이콘 표시 163 | - **Card**: 둥근 모서리와 그림자가 있는 카드 164 | 165 | ### 레이아웃 위젯 166 | 167 | 위젯들을 배치하고 정렬하는 위젯입니다: 168 | 169 | - **Container**: 패딩, 마진, 배경색, 크기 등을 설정할 수 있는 범용 컨테이너 170 | - **Row/Column**: 위젯을 가로/세로로 배열 171 | - **Stack**: 위젯들을 겹쳐서 배치 172 | - **ListView**: 스크롤 가능한 목록 173 | 174 | ### 입력 위젯 175 | 176 | 사용자 입력을 받는 위젯입니다: 177 | 178 | - **TextField**: 텍스트 입력 179 | - **Checkbox**: 체크박스 180 | - **Radio**: 라디오 버튼 181 | - **Slider**: 슬라이더 182 | 183 | ### 상호작용 위젯 184 | 185 | 사용자 상호작용을 처리하는 위젯입니다: 186 | 187 | - **GestureDetector**: 다양한 제스처 인식 188 | - **InkWell**: 터치 효과가 있는 영역 189 | - **Draggable**: 드래그 가능한 위젯 190 | 191 | ### 애니메이션 위젯 192 | 193 | 애니메이션 효과를 제공하는 위젯입니다: 194 | 195 | - **AnimatedContainer**: 속성 변경 시 애니메이션 효과 196 | - **Hero**: 화면 전환 시 애니메이션 효과 197 | - **FadeTransition**: 페이드 인/아웃 효과 198 | 199 | ## 위젯 속성 전달 200 | 201 | Flutter 위젯은 생성자를 통해 속성을 전달받아 UI를 구성합니다: 202 | 203 | ```dart 204 | Container( 205 | width: 200, 206 | height: 100, 207 | margin: EdgeInsets.all(10), 208 | padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), 209 | decoration: BoxDecoration( 210 | color: Colors.blue, 211 | borderRadius: BorderRadius.circular(8), 212 | boxShadow: [ 213 | BoxShadow( 214 | color: Colors.black26, 215 | offset: Offset(0, 2), 216 | blurRadius: 6, 217 | ), 218 | ], 219 | ), 220 | child: Center( 221 | child: Text( 222 | 'Flutter', 223 | style: TextStyle( 224 | color: Colors.white, 225 | fontSize: 24, 226 | fontWeight: FontWeight.bold, 227 | ), 228 | ), 229 | ), 230 | ) 231 | ``` 232 | 233 | 이 예제에서 `Container`, `Center`, `Text` 위젯은 각각 다양한 속성(width, height, decoration, style 등)을 전달받아 특정한 모양과 스타일을 가진 UI를 생성합니다. 234 | 235 | ## 위젯 키(Key) 236 | 237 | 위젯 키는 Flutter가 위젯 트리에서 위젯을 식별하는 데 사용됩니다. 특히 동적으로 변경되는 위젯 목록에서 중요한 역할을 합니다. 238 | 239 | ```dart 240 | ListView.builder( 241 | itemCount: items.length, 242 | itemBuilder: (context, index) { 243 | return ListTile( 244 | key: ValueKey(items[index].id), // 고유 ID를 키로 사용 245 | title: Text(items[index].title), 246 | ); 247 | }, 248 | ) 249 | ``` 250 | 251 | 위젯 키의 주요 종류: 252 | 253 | - **ValueKey**: 단일 값을 기반으로 한 키 254 | - **ObjectKey**: 객체 식별자를 기반으로 한 키 255 | - **UniqueKey**: 매번 고유한 키를 생성 256 | - **GlobalKey**: 전역적으로 접근 가능한 키, 위젯의 상태에 접근하거나 위젯의 크기/위치를 파악하는 데 사용 257 | 258 | ## 위젯 제약 조건(Constraints) 259 | 260 | Flutter의 레이아웃 시스템은 부모 위젯이 자식 위젯에게 제약 조건(constraints)을 전달하고, 자식 위젯은 이 제약 조건 내에서 자신의 크기를 결정하는 방식으로 작동합니다. 261 | 262 | 제약 조건은 최소/최대 너비와 높이로 구성되며, 자식 위젯은 이 범위 내에서 크기를 결정합니다: 263 | 264 | ```dart 265 | ConstrainedBox( 266 | constraints: BoxConstraints( 267 | minWidth: 100, 268 | maxWidth: 200, 269 | minHeight: 50, 270 | maxHeight: 100, 271 | ), 272 | child: Container( 273 | color: Colors.blue, 274 | width: 150, // minWidth와 maxWidth 사이의 값 275 | height: 75, // minHeight와 maxHeight 사이의 값 276 | ), 277 | ) 278 | ``` 279 | 280 | ## 위젯 렌더링 과정 281 | 282 | Flutter의 위젯 렌더링 과정은 다음과 같습니다: 283 | 284 | 1. **레이아웃 단계**: 부모 위젯이 자식 위젯에게 제약 조건을 전달하고, 자식 위젯은 자신의 크기를 결정합니다. 285 | 2. **페인팅 단계**: 위젯의 외관이 렌더링됩니다. 286 | 3. **합성 단계**: 렌더링된 레이어들이 화면에 합성됩니다. 287 | 288 | 289 | ## 결론 290 | 291 | Flutter의 위젯 시스템은 UI 구성을 위한 강력하고 유연한 방법을 제공합니다. "모든 것이 위젯"이라는 철학을 통해 일관된 방식으로 복잡한 UI를 구축할 수 있습니다. 위젯의 불변성, 선언적 특성, 합성 패턴은 Flutter가 효율적이고 예측 가능한 UI 프레임워크가 되는 데 중요한 역할을 합니다. 292 | 293 | 다음 장에서는 Stateless 위젯과 Stateful 위젯에 대해 더 자세히 알아보겠습니다. 294 | -------------------------------------------------------------------------------- /src/content/docs/appendix/tools.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 개발 도구와 링크 모음 3 | --- 4 | 5 | Flutter 개발을 효과적으로 수행하기 위해서는 적절한 도구를 활용하고, 좋은 학습 자료를 참고하는 것이 중요합니다. 이 페이지에서는 Flutter 개발자에게 유용한 도구와 리소스들을 소개합니다. 6 | 7 | ## 개발 도구 8 | 9 | ### IDE 및 에디터 10 | 11 | | 도구 | 설명 | 링크 | 12 | | ------------------ | --------------------------------------------------------------- | ------------------------------------------------ | 13 | | **VS Code** | 가벼운 에디터로 Flutter 확장 프로그램 지원 | [다운로드](https://code.visualstudio.com/) | 14 | | **Android Studio** | Google의 공식 Android 개발 IDE, 전문적인 Flutter 개발 환경 제공 | [다운로드](https://developer.android.com/studio) | 15 | 16 | ### VS Code 필수 확장 프로그램 17 | 18 | | 확장 프로그램 | 설명 | 19 | | ---------------------------- | ------------------------------------ | 20 | | **Flutter** | Flutter SDK와 통합된 지원 제공 | 21 | | **Dart** | Dart 언어 지원 | 22 | | **Awesome Flutter Snippets** | 자주 사용되는 Flutter 코드 조각 제공 | 23 | | **Flutter Widget Snippets** | 위젯 코드 스니펫 제공 | 24 | | **Pubspec Assist** | pubspec.yaml 파일 관리 도우미 | 25 | | **Git History** | Git 이력 관리 시각화 | 26 | | **Error Lens** | 인라인 오류 하이라이팅 | 27 | 28 | ### 디버깅 및 성능 분석 도구 29 | 30 | | 도구 | 설명 | 링크 | 31 | | -------------------- | ---------------------------------- | ----------------------------------------------------------- | 32 | | **Flutter DevTools** | 성능 및 디버깅 도구 모음 | [문서](https://docs.flutter.dev/development/tools/devtools) | 33 | | **Dart Observatory** | 메모리 및 CPU 프로파일링 | [문서](https://dart.dev/tools/dart-devtools) | 34 | | **Flipper** | Facebook의 모바일 앱 디버깅 플랫폼 | [웹사이트](https://fbflipper.com/) | 35 | | **Sentry** | 실시간 에러 추적 | [웹사이트](https://sentry.io/) | 36 | 37 | ### CI/CD 도구 38 | 39 | | 도구 | 설명 | 링크 | 40 | | ------------------ | ----------------------- | ------------------------------------------ | 41 | | **Codemagic** | Flutter 특화 CI/CD 도구 | [웹사이트](https://codemagic.io/) | 42 | | **Bitrise** | 모바일 앱 CI/CD 플랫폼 | [웹사이트](https://www.bitrise.io/) | 43 | | **GitHub Actions** | GitHub 내장 CI/CD | [문서](https://docs.github.com/en/actions) | 44 | | **fastlane** | 앱 자동화 배포 도구 | [웹사이트](https://fastlane.tools/) | 45 | 46 | ## 학습 자료 47 | 48 | ### 공식 문서 49 | 50 | | 리소스 | 설명 | 링크 | 51 | | -------------------------- | ------------------------- | --------------------------------------------- | 52 | | **Flutter 공식 문서** | 상세한 API 문서 및 가이드 | [웹사이트](https://docs.flutter.dev/) | 53 | | **Dart 공식 문서** | Dart 언어 문서 | [웹사이트](https://dart.dev/guides) | 54 | | **Material Design 가이드** | 구글의 디자인 가이드라인 | [웹사이트](https://material.io/design) | 55 | | **Flutter 쿡북** | 일반적인 문제 해결 방법 | [웹사이트](https://docs.flutter.dev/cookbook) | 56 | 57 | ### 추천 블로그 및 뉴스레터 58 | 59 | | 리소스 | 설명 | 링크 | 60 | | --------------------- | ----------------------------- | -------------------------------------------- | 61 | | **Flutter Medium** | Flutter 팀의 공식 블로그 | [링크](https://medium.com/flutter) | 62 | | **Flutter Community** | 커뮤니티 기여 아티클 | [링크](https://medium.com/flutter-community) | 63 | | **Flutter Weekly** | 주간 Flutter 뉴스 및 튜토리얼 | [링크](https://flutterweekly.net/) | 64 | | **Flutter Awesome** | 큐레이션된 패키지 및 가이드 | [링크](https://flutterawesome.com/) | 65 | | **Flutter Force** | 뉴스레터 및 팁 | [링크](https://twitter.com/flutterforce) | 66 | 67 | ### 영상 자료 68 | 69 | | 리소스 | 설명 | 링크 | 70 | | ----------------------- | ------------------------- | -------------------------------------------------- | 71 | | **Flutter 공식 유튜브** | Flutter 팀의 영상 | [링크](https://www.youtube.com/c/flutterdev) | 72 | | **The Flutter Way** | 고품질 UI 구현 튜토리얼 | [링크](https://www.youtube.com/c/TheFlutterWay) | 73 | | **Flutter Explained** | 개념 설명 및 실습 | [링크](https://www.youtube.com/c/FlutterExplained) | 74 | | **Reso Coder** | 아키텍처 및 고급 주제 | [링크](https://www.youtube.com/c/ResoCoder) | 75 | | **Code With Andrea** | 심층적인 Flutter 튜토리얼 | [링크](https://www.youtube.com/c/CodeWithAndrea) | 76 | 77 | ### 온라인 코스 78 | 79 | | 리소스 | 설명 | 링크 | 80 | | ---------------------------------------- | ------------------- | ---------------------------------------------------------------------------------- | 81 | | **Flutter 부트캠프** | Udemy 인기 코스 | [링크](https://www.udemy.com/course/flutter-bootcamp-with-dart/) | 82 | | **Flutter 앱 개발 - Zero to Mastery** | 전체 개발 과정 학습 | [링크](https://www.udemy.com/course/flutter-made-easy-zero-to-mastery/) | 83 | | **Dart and Flutter: 완전 개발자 가이드** | Academind 코스 | [링크](https://www.udemy.com/course/learn-flutter-dart-to-build-ios-android-apps/) | 84 | 85 | ## 커뮤니티 86 | 87 | | 커뮤니티 | 설명 | 링크 | 88 | | ---------------------------- | ----------------------- | ------------------------------------------------------------------ | 89 | | **Stack Overflow** | 질문 및 답변 | [Flutter 태그](https://stackoverflow.com/questions/tagged/flutter) | 90 | | **GitHub Discussions** | Flutter 리포지토리 토론 | [링크](https://github.com/flutter/flutter/discussions) | 91 | | **Discord Flutter 커뮤니티** | 실시간 채팅 | [초대 링크](https://discord.gg/flutter) | 92 | | **Reddit Flutter** | 포럼 토론 | [링크](https://www.reddit.com/r/FlutterDev/) | 93 | 94 | ## 유용한 패키지 모음 사이트 95 | 96 | | 사이트 | 설명 | 링크 | 97 | | -------------------- | ------------------------------- | ----------------------------------- | 98 | | **Pub.dev** | 공식 Dart/Flutter 패키지 저장소 | [링크](https://pub.dev/) | 99 | | **Flutter Gems** | 큐레이션된 Flutter 패키지 | [링크](https://fluttergems.dev/) | 100 | | **It's All Widgets** | Flutter로 제작된 앱 사례 | [링크](https://itsallwidgets.com/) | 101 | | **Flutter Awesome** | 패키지 및 앱 모음 | [링크](https://flutterawesome.com/) | 102 | 103 | ## 문제 해결 자료 104 | 105 | | 리소스 | 설명 | 링크 | 106 | | ------------------------- | ---------------------- | ------------------------------------------------------------------------------- | 107 | | **Flutter GitHub Issues** | 알려진 이슈 및 해결책 | [링크](https://github.com/flutter/flutter/issues) | 108 | | **Flutter Doctor 가이드** | 환경 문제 진단 및 해결 | [문서](https://docs.flutter.dev/get-started/install/windows#run-flutter-doctor) | 109 | | **Flutter 포럼** | 공식 포럼 | [링크](https://flutter.dev/community) | 110 | 111 | ## 코드 품질 및 분석 도구 112 | 113 | | 도구 | 설명 | 링크 | 114 | | ------------------ | ----------------------- | --------------------------------------------------------- | 115 | | **Dart Analyzer** | 코드 정적 분석 도구 | [문서](https://dart.dev/guides/language/analysis-options) | 116 | | **Effective Dart** | Dart 코딩 스타일 가이드 | [문서](https://dart.dev/guides/language/effective-dart) | 117 | | **Flutter Lints** | 권장 린트 규칙 | [패키지](https://pub.dev/packages/flutter_lints) | 118 | -------------------------------------------------------------------------------- /src/content/docs/part2/basic-syntax.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 기본 문법 및 변수 3 | --- 4 | 5 | ## Dart 프로그램의 구조 6 | 7 | Dart 프로그램은 최상위 함수, 변수, 클래스 등으로 구성됩니다. 모든 Dart 프로그램은 `main()` 함수에서 시작합니다. 8 | 9 | ```dart 10 | // 가장 기본적인 Dart 프로그램 11 | void main() { 12 | print('안녕하세요, Dart!'); 13 | } 14 | ``` 15 | 16 | `main()` 함수는 프로그램의 진입점이며, `void`는 반환 값이 없음을 의미합니다. 커맨드라인에서 인자를 받을 때는 다음과 같이 작성할 수 있습니다. 17 | 18 | ```dart 19 | void main(List arguments) { 20 | print('프로그램 인자: $arguments'); 21 | } 22 | ``` 23 | 24 | ## 주석 25 | 26 | Dart에서는 세 가지 유형의 주석을 사용할 수 있습니다. 27 | 28 | ```dart 29 | // 한 줄 주석 30 | 31 | /* 32 | 여러 줄 주석 33 | 여러 줄에 걸쳐 작성할 수 있습니다. 34 | */ 35 | 36 | /// 문서화 주석 37 | /// 다트독(dartdoc) 도구가 API 문서를 생성할 때 사용합니다. 38 | /// 클래스, 함수, 변수 등의 설명을 작성할 때 유용합니다. 39 | ``` 40 | 41 | ## 기본 데이터 타입 42 | 43 | Dart는 다음과 같은 기본 데이터 타입을 제공합니다: 44 | 45 | ```dart 46 | // 숫자 타입 47 | int integerValue = 42; // 정수 48 | double doubleValue = 3.14; // 실수 49 | num numValue = 10; // int나 double의 상위 타입 50 | 51 | // 문자열 타입 52 | String greeting = '안녕하세요'; 53 | 54 | // 불리언 타입 55 | bool isTrue = true; 56 | bool isFalse = false; 57 | 58 | // 리스트 (배열) 59 | List numbers = [1, 2, 3, 4, 5]; 60 | 61 | // 맵 (key-value 쌍) 62 | Map person = { 63 | 'name': '홍길동', 64 | 'age': 30, 65 | 'isStudent': false 66 | }; 67 | 68 | // 집합 (중복 없는 컬렉션) 69 | Set uniqueNames = {'홍길동', '김철수', '이영희'}; 70 | ``` 71 | 72 | ## 변수 선언 73 | 74 | Dart에서는 다양한 방법으로 변수를 선언할 수 있습니다. 75 | 76 | ### var 77 | 78 | 타입을 명시적으로 선언하지 않고, 초기값에서 타입을 추론합니다. 79 | 80 | ```dart 81 | var name = '홍길동'; // String으로 추론 82 | var age = 30; // int로 추론 83 | var height = 175.5; // double로 추론 84 | 85 | // 타입이 추론된 후에는 다른 타입의 값을 할당할 수 없습니다. 86 | name = '김철수'; // 가능 (String → String) 87 | // name = 42; // 오류 (String → int) 88 | ``` 89 | 90 | ### 명시적 타입 91 | 92 | 변수의 타입을 명시적으로 선언합니다. 93 | 94 | ```dart 95 | String name = '홍길동'; 96 | int age = 30; 97 | double height = 175.5; 98 | ``` 99 | 100 | ### final과 const 101 | 102 | 한 번 할당하면 변경할 수 없는 상수 변수를 선언합니다. 103 | 104 | ```dart 105 | // final: 런타임에 값이 결정되는 상수 106 | final String name = '홍길동'; 107 | final currentTime = DateTime.now(); // 타입 추론 가능 108 | 109 | // const: 컴파일 타임에 값이 결정되는 상수 110 | const int maxUsers = 100; 111 | const double pi = 3.14159; 112 | 113 | // name = '김철수'; // 오류: final 변수는 재할당 불가 114 | // maxUsers = 200; // 오류: const 변수는 재할당 불가 115 | ``` 116 | 117 | `final`과 `const`의 차이점: 118 | 119 | - `final`: 런타임에 값이 결정됩니다. 런타임에 계산되는 값도 가능합니다. 120 | - `const`: 컴파일 타임에 값이 결정됩니다. 컴파일 시점에 알 수 있는 상수값만 가능합니다. 121 | 122 | ```dart 123 | // 런타임 값을 사용하는 예 124 | final now = DateTime.now(); // 가능 125 | // const today = DateTime.now(); // 오류: 컴파일 시점에 값을 알 수 없음 126 | ``` 127 | 128 | ### late 129 | 130 | `late` 키워드는 변수를 나중에 초기화할 것임을 나타냅니다. Null 안전성이 도입된 Dart 2.12 이후에 유용합니다. 131 | 132 | ```dart 133 | late String name; 134 | 135 | void initName() { 136 | name = '홍길동'; // 나중에 값 할당 137 | } 138 | 139 | void main() { 140 | initName(); 141 | print(name); // '홍길동' 142 | 143 | // late 변수는 초기화 전에 접근하면 런타임 오류 발생 144 | late String address; 145 | // print(address); // 오류: 초기화되지 않은 late 변수에 접근 146 | } 147 | ``` 148 | 149 | ### 동적 타입 (dynamic) 150 | 151 | `dynamic` 타입은 변수의 타입을 런타임까지 확정하지 않습니다. 타입 안전성이 필요하지 않을 때 사용합니다. 152 | 153 | ```dart 154 | dynamic value = '문자열'; 155 | print(value); // '문자열' 156 | 157 | value = 42; 158 | print(value); // 42 159 | 160 | value = true; 161 | print(value); // true 162 | ``` 163 | 164 | ## 문자열 165 | 166 | Dart에서는 작은따옴표(`'`) 또는 큰따옴표(`"`)를 사용하여 문자열을 생성할 수 있습니다. 167 | 168 | ```dart 169 | String _single = '작은따옴표 문자열'; 170 | String _double = "큰따옴표 문자열"; 171 | ``` 172 | 173 | ### 문자열 보간(Interpolation) 174 | 175 | 문자열 내에서 변수나 표현식을 사용할 수 있습니다. 176 | 177 | ```dart 178 | String name = '홍길동'; 179 | int age = 30; 180 | 181 | // $변수명 형태로 변수 값을 포함할 수 있습니다. 182 | String message = '제 이름은 $name이고, 나이는 $age살입니다.'; 183 | 184 | // ${표현식} 형태로 표현식 결과를 포함할 수 있습니다. 185 | String ageNextYear = '내년에는 ${age + 1}살이 됩니다.'; 186 | ``` 187 | 188 | ### 여러 줄 문자열 189 | 190 | 여러 줄에 걸친 문자열은 삼중 따옴표(`'''` 또는 `"""`)를 사용합니다. 191 | 192 | ```dart 193 | String multiLine = ''' 194 | 이것은 195 | 여러 줄에 걸친 196 | 문자열입니다. 197 | '''; 198 | 199 | String anotherMultiLine = """ 200 | 이것도 201 | 여러 줄에 걸친 202 | 문자열입니다. 203 | """; 204 | ``` 205 | 206 | ### 원시 문자열 (Raw String) 207 | 208 | 문자열 앞에 `r`을 붙이면 이스케이프 시퀀스를 처리하지 않는 원시 문자열이 됩니다. 209 | 210 | ```dart 211 | String escaped = 'C:\\Program Files\\Dart'; // 이스케이프 시퀀스 사용 212 | String raw = r'C:\Program Files\Dart'; // 원시 문자열 (이스케이프 처리 안 함) 213 | ``` 214 | 215 | ## 연산자 216 | 217 | ### 산술 연산자 218 | 219 | ```dart 220 | int a = 10; 221 | int b = 3; 222 | 223 | print(a + b); // 13 (덧셈) 224 | print(a - b); // 7 (뺄셈) 225 | print(a * b); // 30 (곱셈) 226 | print(a / b); // 3.3333333333333335 (나눗셈, 결과는 double) 227 | print(a ~/ b); // 3 (정수 나눗셈, 결과는 int) 228 | print(a % b); // 1 (나머지) 229 | ``` 230 | 231 | ### 증감 연산자 232 | 233 | ```dart 234 | int a = 10; 235 | 236 | a++; // 후위 증가 (a = a + 1) 237 | ++a; // 전위 증가 238 | print(a); // 12 239 | 240 | a--; // 후위 감소 (a = a - 1) 241 | --a; // 전위 감소 242 | print(a); // 10 243 | ``` 244 | 245 | ### 할당 연산자 246 | 247 | ```dart 248 | int a = 10; 249 | 250 | a += 5; // a = a + 5 251 | print(a); // 15 252 | 253 | a -= 3; // a = a - 3 254 | print(a); // 12 255 | 256 | a *= 2; // a = a * 2 257 | print(a); // 24 258 | 259 | a ~/= 5; // a = a ~/ 5 260 | print(a); // 4 261 | ``` 262 | 263 | ### 비교 연산자 264 | 265 | ```dart 266 | int a = 10; 267 | int b = 5; 268 | 269 | print(a == b); // false (같음) 270 | print(a != b); // true (다름) 271 | print(a > b); // true (초과) 272 | print(a < b); // false (미만) 273 | print(a >= b); // true (이상) 274 | print(a <= b); // false (이하) 275 | ``` 276 | 277 | ### 논리 연산자 278 | 279 | ```dart 280 | bool condition1 = true; 281 | bool condition2 = false; 282 | 283 | print(condition1 && condition2); // false (AND) 284 | print(condition1 || condition2); // true (OR) 285 | print(!condition1); // false (NOT) 286 | ``` 287 | 288 | ### 타입 테스트 연산자 289 | 290 | ```dart 291 | var value = '문자열'; 292 | 293 | print(value is String); // true (value가 String 타입인지 확인) 294 | print(value is! int); // true (value가 int 타입이 아닌지 확인) 295 | 296 | // as 연산자는 타입 변환에 사용됩니다. 297 | dynamic someValue = 'Dart'; 298 | String text = someValue as String; 299 | ``` 300 | 301 | ### 조건 연산자 302 | 303 | ```dart 304 | // 조건 ? 값1 : 값2 305 | int a = 10; 306 | int b = 5; 307 | int max = a > b ? a : b; // a가 b보다 크면 a, 아니면 b 308 | print(max); // 10 309 | 310 | // ?? 연산자: 왼쪽 피연산자가 null이면 오른쪽 피연산자 반환 311 | String? name; 312 | String displayName = name ?? '이름 없음'; 313 | print(displayName); // '이름 없음' 314 | ``` 315 | 316 | ### 캐스케이드 연산자 (..) 317 | 318 | 객체에 대해 연속적인 작업을 수행할 수 있는 캐스케이드 연산자입니다. 319 | 320 | ```dart 321 | class Person { 322 | String name = ''; 323 | int age = 0; 324 | 325 | void introduce() { 326 | print('내 이름은 $name이고, 나이는 $age살입니다.'); 327 | } 328 | } 329 | 330 | void main() { 331 | var person = Person() 332 | ..name = '홍길동' 333 | ..age = 30 334 | ..introduce(); 335 | 336 | // 위 코드는 다음과 동일합니다: 337 | // var person = Person(); 338 | // person.name = '홍길동'; 339 | // person.age = 30; 340 | // person.introduce(); 341 | } 342 | ``` 343 | 344 | ## null 안전성 345 | 346 | Dart 2.12부터 도입된 null 안전성을 활용하면 null 참조 오류를 컴파일 타임에 방지할 수 있습니다. 347 | 348 | ### nullable과 non-nullable 타입 349 | 350 | ```dart 351 | // non-nullable 타입 (null을 할당할 수 없음) 352 | String name = '홍길동'; 353 | // name = null; // 컴파일 오류 354 | 355 | // nullable 타입 (null을 할당할 수 있음) 356 | String? nullableName = '홍길동'; 357 | nullableName = null; // 허용됨 358 | ``` 359 | 360 | ### null 검사와 null 조건 접근 361 | 362 | ```dart 363 | String? name = getNullableName(); 364 | 365 | // null 검사 후 사용 366 | if (name != null) { 367 | print('이름의 길이: ${name.length}'); 368 | } 369 | 370 | // 조건 프로퍼티 접근 (?.): 객체가 null이면 전체 표현식이 null이 됨 371 | print('이름의 길이: ${name?.length}'); 372 | 373 | // null 병합 연산자 (??): 왼쪽 피연산자가 null이면 오른쪽 피연산자 반환 374 | print('이름: ${name ?? '이름 없음'}'); 375 | 376 | // null 정의 연산자 (??=): 변수가 null이면 값을 할당 377 | name ??= '이름 없음'; 378 | ``` 379 | 380 | ### Non-null 단언 연산자 (!) 381 | 382 | 변수가 null이 아님을 컴파일러에게 알려주는 연산자입니다. 변수가 실제로 null이면 런타임 오류가 발생합니다. 383 | 384 | ```dart 385 | String? name = '홍길동'; 386 | 387 | // name이 null이 아니라고 확신할 때 사용 388 | String nonNullName = name!; 389 | 390 | // 그러나 실제로 null이면 런타임 오류 발생 391 | name = null; 392 | // String error = name!; // 런타임 오류: null 참조 393 | ``` 394 | 395 | ## 결론 396 | 397 | 이 장에서는 Dart의 기본 문법과 변수 선언, 데이터 타입, 연산자, null 안전성 등에 대해 알아보았습니다. 이러한 기본 개념은 Dart 프로그래밍의 토대가 되며, Flutter를 활용한 앱 개발에도 필수적입니다. 398 | 399 | 다음 장에서는 Dart의 타입 시스템과 제네릭에 대해 더 자세히 알아보겠습니다. 400 | -------------------------------------------------------------------------------- /src/content/docs/part8/deploy-procedure.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Android / iOS 배포 절차 3 | --- 4 | 5 | Flutter 앱을 개발한 후 사용자들에게 제공하기 위해 앱 스토어에 배포하는 절차를 알아봅니다. Android와 iOS 플랫폼은 각각 다른 배포 프로세스를 가지고 있습니다. 6 | 7 | ## 배포 준비 체크리스트 8 | 9 | 앱 스토어에 제출하기 전에 다음 항목을 먼저 확인하세요: 10 | 11 | - [ ] 모든 주요 기능 테스트 완료 12 | - [ ] 앱 아이콘 및 스플래시 스크린 구현 13 | - [ ] 다양한 화면 크기/해상도 테스트 14 | - [ ] 접근성 지원 확인 15 | - [ ] 개인정보 처리방침 준비 16 | - [ ] 앱 스크린샷 및 설명 준비 17 | 18 | ## Android 앱 배포 절차 19 | 20 | ### 1. 배포용 키스토어 생성 21 | 22 | Android 앱을 서명하기 위한 키스토어(keystore) 파일을 생성해야 합니다. 이 키는 앱 업데이트 시 동일한 키로 서명해야 하므로 안전하게 보관해야 합니다. 23 | 24 | ```bash 25 | keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload 26 | ``` 27 | 28 | ### 2. 키스토어 설정 29 | 30 | `android/app/build.gradle` 파일에 키스토어 정보를 추가합니다. 보안을 위해 다음과 같이 별도의 파일로 관리합니다. 31 | 32 | 1. `android/key.properties` 파일 생성: 33 | 34 | ```properties 35 | storePassword=<키스토어 비밀번호> 36 | keyPassword=<키 비밀번호> 37 | keyAlias=upload 38 | storeFile=<키스토어 파일 경로, 예: /Users/username/upload-keystore.jks> 39 | ``` 40 | 41 | 2. `android/app/build.gradle` 파일 수정: 42 | 43 | ```txt 44 | // 파일 상단에 다음 코드 추가 45 | def keystoreProperties = new Properties() 46 | def keystorePropertiesFile = rootProject.file('key.properties') 47 | if (keystorePropertiesFile.exists()) { 48 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 49 | } 50 | 51 | android { 52 | // 기존 코드 ... 53 | 54 | signingConfigs { 55 | release { 56 | keyAlias keystoreProperties['keyAlias'] 57 | keyPassword keystoreProperties['keyPassword'] 58 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 59 | storePassword keystoreProperties['storePassword'] 60 | } 61 | } 62 | buildTypes { 63 | release { 64 | signingConfig signingConfigs.release 65 | // 기타 릴리즈 설정 ... 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | ### 3. 앱 버전 설정 72 | 73 | `pubspec.yaml` 파일에서 앱 버전을 설정합니다. 74 | 75 | ```yaml 76 | version: 1.0.0+1 # <버전 이름>+<빌드 번호> 77 | ``` 78 | 79 | ### 4. 앱 매니페스트 설정 80 | 81 | `android/app/src/main/AndroidManifest.xml` 파일에서 필요한 권한과 설정을 확인합니다. 82 | 83 | ```xml 84 | 85 | 86 | 87 | 88 | 89 | 93 | 94 | 95 | 96 | ``` 97 | 98 | ### 5. 앱 번들/APK 생성 99 | 100 | Google Play는 Android App Bundle(AAB) 형식을 권장합니다. 101 | 102 | ```bash 103 | # App Bundle 생성 (권장) 104 | flutter build appbundle 105 | 106 | # 또는 APK 생성 107 | flutter build apk --release 108 | ``` 109 | 110 | 빌드된 파일은 다음 위치에서 찾을 수 있습니다: 111 | 112 | - App Bundle: `build/app/outputs/bundle/release/app-release.aab` 113 | - APK: `build/app/outputs/flutter-apk/app-release.apk` 114 | 115 | ### 6. Google Play Console에 앱 등록 116 | 117 | 1. [Google Play Console](https://play.google.com/console)에 로그인합니다. 118 | 2. "새 앱 만들기"를 선택합니다. 119 | 3. 앱 정보(이름, 언어, 앱/게임 여부, 유료/무료 여부)를 입력합니다. 120 | 4. 개인정보 처리방침 URL을 제공합니다. 121 | 5. 다음 정보를 등록합니다: 122 | - 앱 카테고리 및 태그 123 | - 연락처 정보 124 | - 스크린샷, 프로모션 이미지, 앱 아이콘 125 | - 앱 설명(짧은 설명 및 전체 설명) 126 | 127 | ### 7. 앱 번들 업로드 128 | 129 | 1. "앱 릴리즈" > "프로덕션" 트랙 선택 130 | 2. "새 릴리즈 만들기" 클릭 131 | 3. 생성한 App Bundle(.aab) 파일 업로드 132 | 4. 릴리즈 노트 작성 133 | 5. 검토 후 출시 134 | 135 | ### 8. 출시 및 검토 136 | 137 | Google Play 검토 프로세스는 보통 몇 시간에서 며칠까지 소요될 수 있습니다. 검토 완료 후 앱이 출시됩니다. 138 | 139 | ## iOS 앱 배포 절차 140 | 141 | ### 1. Apple Developer Program 가입 142 | 143 | iOS 앱을 App Store에 배포하려면 연간 $99의 비용으로 [Apple Developer Program](https://developer.apple.com/programs/)에 가입해야 합니다. 144 | 145 | ### 2. Xcode에서 인증서 및 프로비저닝 프로필 설정 146 | 147 | 1. Xcode 열기: `open ios/Runner.xcworkspace` 148 | 2. "Signing & Capabilities" 탭에서 팀 선택 및 자동 서명 활성화 149 | 3. 번들 ID 설정 (고유한 식별자, 예: com.yourcompany.appname) 150 | 151 | ### 3. 앱 버전 및 빌드 번호 설정 152 | 153 | `pubspec.yaml` 파일에서 버전을 설정합니다: 154 | 155 | ```yaml 156 | version: 1.0.0+1 # <버전 이름>+<빌드 번호> 157 | ``` 158 | 159 | iOS 특정 버전은 Xcode의 Runner 프로젝트 설정이나 `ios/Runner/Info.plist` 파일에서도 확인/수정할 수 있습니다. 160 | 161 | ### 4. iOS 앱 설정 162 | 163 | `ios/Runner/Info.plist` 파일에서 필요한 설정을 확인합니다: 164 | 165 | ```xml 166 | CFBundleDisplayName 167 | 앱 이름 168 | 169 | 170 | NSCameraUsageDescription 171 | 카메라 사용 이유 설명 172 | ``` 173 | 174 | ### 5. 앱 아이콘 설정 175 | 176 | `ios/Runner/Assets.xcassets/AppIcon.appiconset`에 다양한 크기의 앱 아이콘을 추가합니다. 177 | 178 | ### 6. 릴리즈 빌드 생성 179 | 180 | ```bash 181 | flutter build ios --release 182 | ``` 183 | 184 | ### 7. Xcode에서 Archive 생성 185 | 186 | 1. Xcode에서 "Product" > "Destination" > "Any iOS Device" 선택 187 | 2. "Product" > "Archive" 선택 188 | 3. Archive가 완료되면 Xcode Organizer가 자동으로 열립니다 189 | 190 | ### 8. TestFlight를 통한 테스트 (선택 사항) 191 | 192 | TestFlight를 통해 앱을 테스터에게 배포하여 최종 테스트를 진행할 수 있습니다. 193 | 194 | 1. Xcode Organizer에서 Archive 선택 195 | 2. "Distribute App" > "App Store Connect" > "Upload" 선택 196 | 3. 앱 배포 옵션 설정 (자동 서명 권장) 197 | 4. 업로드 완료 후 [App Store Connect](https://appstoreconnect.apple.com/)에서 TestFlight 구성 198 | 5. 내부 및 외부 테스터 추가 199 | 200 | ### 9. App Store Connect에서 앱 정보 설정 201 | 202 | 1. [App Store Connect](https://appstoreconnect.apple.com/)에 로그인 203 | 2. "내 앱" > "+" > "새로운 앱" 선택 204 | 3. 다음 정보 입력: 205 | - 앱 이름, 기본 언어, 번들 ID 206 | - SKU (내부 추적용 고유 ID) 207 | - 사용자 액세스 설정 208 | 209 | ### 10. 앱 정보 등록 210 | 211 | 1. App Store 정보 탭에서 다음 항목 작성: 212 | - 프로모션 텍스트 (최대 170자) 213 | - 설명 (최대 4,000자) 214 | - 키워드 (최대 100자) 215 | - 지원 URL 및 마케팅 URL 216 | - 스크린샷 (다양한 기기 크기별) 217 | - 앱 미리보기 영상 (선택 사항) 218 | - 앱 아이콘 (1024x1024 픽셀) 219 | - 연령 등급 220 | - 개인정보 처리방침 URL 221 | - 가격 및 가용성 222 | 223 | ### 11. 앱 심사 제출 224 | 225 | 1. "앱 버전" 섹션에서 제출할 빌드 선택 226 | 2. 필요한 수출 규정 준수 정보 제공 227 | 3. "심사를 위해 제출" 클릭 228 | 229 | ### 12. 앱 심사 및 출시 230 | 231 | Apple의 앱 심사는 보통 1-3일 소요됩니다. 거부될 경우 이유가 제공되며, 수정 후 재제출할 수 있습니다. 승인되면 "출시 준비됨" 상태가 되고, 수동 또는 자동으로 출시할 수 있습니다. 232 | 233 | ## CI/CD를 활용한 자동화 배포 234 | 235 | 배포 과정을 자동화하기 위해 CI/CD 도구를 활용할 수 있습니다. 대표적인 도구로는 Codemagic, Fastlane, GitHub Actions 등이 있습니다. 236 | 237 | ### Codemagic 활용 예시 238 | 239 | `codemagic.yaml` 파일 구성: 240 | 241 | ```yaml 242 | workflows: 243 | android-workflow: 244 | name: Android Release 245 | environment: 246 | vars: 247 | KEYSTORE_PATH: /tmp/keystore.jks 248 | FCI_KEYSTORE_FILE: Encrypted(...) # 키스토어 파일 암호화 249 | flutter: stable 250 | scripts: 251 | - name: Set up keystore 252 | script: echo $FCI_KEYSTORE_FILE | base64 --decode > $KEYSTORE_PATH 253 | - name: Build AAB 254 | script: flutter build appbundle --release 255 | artifacts: 256 | - build/app/outputs/bundle/release/app-release.aab 257 | publishing: 258 | google_play: 259 | credentials: Encrypted(...) # Google Play API 키 260 | track: internal # 또는 alpha, beta, production 261 | 262 | ios-workflow: 263 | name: iOS Release 264 | environment: 265 | flutter: stable 266 | xcode: latest 267 | cocoapods: default 268 | scripts: 269 | - name: Build iOS 270 | script: flutter build ios --release --no-codesign 271 | - name: Set up code signing 272 | script: | 273 | keychain initialize 274 | app-store-connect fetch-signing-files $(BUNDLE_ID) --type IOS_APP_STORE 275 | keychain add-certificates 276 | xcode-project use-profiles 277 | - name: Build IPA 278 | script: xcode-project build-ipa --workspace ios/Runner.xcworkspace --scheme Runner 279 | artifacts: 280 | - build/ios/ipa/*.ipa 281 | publishing: 282 | app_store_connect: 283 | api_key: Encrypted(...) # App Store Connect API 키 284 | submit_to_testflight: true 285 | ``` 286 | 287 | ## 배포 관련 팁 288 | 289 | ### 앱 크기 최적화 290 | 291 | - 사용하지 않는 리소스 제거 292 | - 이미지 최적화 및 압축 293 | - ProGuard/R8 활성화 (Android) 294 | 295 | ### 버전 관리 전략 296 | 297 | Semantic Versioning(SemVer) 규칙을 따르는 것이 좋습니다: 298 | 299 | - `MAJOR.MINOR.PATCH+BUILD_NUMBER` 300 | - MAJOR: 호환되지 않는 API 변경 301 | - MINOR: 호환되는 기능 추가 302 | - PATCH: 버그 수정 303 | - BUILD_NUMBER: 앱 스토어용 빌드 번호 (매 배포마다 증가) 304 | 305 | ### 단계적 출시 306 | 307 | 대규모 업데이트는 단계적으로 출시하는 것이 좋습니다: 308 | 309 | 1. 내부 테스트 → 2. 알파/베타 테스트 → 3. 제한된 사용자 그룹 → 4. 전체 출시 310 | 311 | ## Flutter 배포 관련 자주 묻는 질문 312 | 313 | ### Q: 앱이 너무 큰데 어떻게 크기를 줄일 수 있나요? 314 | 315 | A: `flutter build apk --split-per-abi`로 ABI별 분할, 미사용 리소스 제거, 이미지 최적화, 코드 축소(R8/ProGuard) 활성화 등을 시도해보세요. 316 | 317 | ### Q: 앱이 심사에서 거부됐어요. 어떻게 해야 하나요? 318 | 319 | A: 거부 사유를 주의 깊게 읽고, 해당 문제를 수정한 후 재제출하세요. 명확하지 않은 경우 Apple/Google의 개발자 지원에 문의하세요. 320 | 321 | ### Q: iOS와 Android 배포 중 어떤 것을 먼저 해야 하나요? 322 | 323 | A: 일반적으로 iOS 심사가 더 오래 걸리므로 iOS를 먼저 제출하고, 이후 Android를 제출하는 것이 효율적입니다. 324 | 325 | ## 결론 326 | 327 | 앱 배포는 Flutter 개발 과정의 중요한 마무리 단계입니다. 이 가이드를 통해 Android와 iOS 플랫폼에 앱을 성공적으로 배포하는 전체 과정을 이해할 수 있습니다. 각 플랫폼의 요구사항과 프로세스를 잘 파악하고, CI/CD 도구를 활용하면 효율적이고 안정적인 배포가 가능합니다. 328 | -------------------------------------------------------------------------------- /src/content/docs/part1/project-structure.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Flutter 프로젝트 구조 이해 3 | --- 4 | import { FileTree } from '@astrojs/starlight/components'; 5 | 6 | Flutter 프로젝트는 여러 디렉토리와 파일로 구성되어 있으며, 각각은 프로젝트의 특정 측면을 담당합니다. 이 구조를 이해하면 Flutter 앱을 더 효율적으로 개발하고 관리할 수 있습니다. 7 | 8 | ## Flutter 프로젝트의 기본 구조 9 | 10 | 기본적인 Flutter 프로젝트 구조는 다음과 같습니다: 11 | 12 | 13 | - my_flutter_app/ 14 | - .dart_tool/ # Dart 도구 관련 파일 15 | - .idea/ # IDE 설정 (Android Studio) 16 | - android/ # 안드로이드 특화 코드 17 | - build/ # 빌드 출력 파일 18 | - ios/ # iOS 특화 코드 19 | - lib/ # Dart 코드 20 | - main.dart # 앱의 진입점 21 | - linux/ # Linux 특화 코드 22 | - macos/ # macOS 특화 코드 23 | - test/ # 테스트 코드 24 | - web/ # 웹 특화 코드 25 | - windows/ # Windows 특화 코드 26 | - .gitignore # Git 무시 파일 27 | - .metadata # Flutter 메타데이터 28 | - analysis_options.yaml # Dart 분석 설정 29 | - pubspec.lock # 의존성 버전 잠금 파일 30 | - pubspec.yaml # 프로젝트 설정 및 의존성 31 | - README.md # 프로젝트 설명 32 | 33 | 34 | 이제 각 디렉토리와 파일의 역할을 자세히 살펴보겠습니다. 35 | 36 | ## 주요 디렉토리 37 | 38 | ### lib/ 디렉토리 39 | 40 | `lib/` 디렉토리는 Flutter 프로젝트의 핵심으로, 앱의 Dart 소스 코드가 저장되는 위치입니다. 41 | 42 | 43 | - lib/ 44 | - main.dart # 앱의 진입점 45 | - models/ # 데이터 모델 46 | - screens/ # 화면 UI 47 | - widgets/ # 재사용 가능한 위젯 48 | - services/ # 비즈니스 로직, API 호출 등 49 | - utils/ # 유틸리티 기능 50 | 51 | 52 | **중요: 기본적으로 생성되는 것은 `main.dart` 파일뿐이며, 나머지 폴더 구조는 개발자가 필요에 따라 생성하고 구성합니다.** 53 | 54 | #### main.dart 55 | 56 | `main.dart` 파일은 앱의 진입점으로, `main()` 함수와 루트 위젯을 포함합니다: 57 | 58 | ```dart 59 | import 'package:flutter/material.dart'; 60 | 61 | void main() { 62 | runApp(const MyApp()); 63 | } 64 | 65 | class MyApp extends StatelessWidget { 66 | const MyApp({super.key}); 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return MaterialApp( 71 | title: 'Flutter Demo', 72 | theme: ThemeData( 73 | primarySwatch: Colors.blue, 74 | ), 75 | home: const MyHomePage(title: 'Flutter Demo Home Page'), 76 | ); 77 | } 78 | } 79 | ``` 80 | 81 | ### test/ 디렉토리 82 | 83 | `test/` 디렉토리는 앱의 자동화된 테스트 코드를 포함합니다. 단위 테스트, 위젯 테스트 등을 이 디렉토리에 작성합니다. 84 | 85 | 86 | - test/ 87 | - widget_test.dart # 위젯 테스트 88 | - unit/ 89 | - models_test.dart # 단위 테스트 90 | 91 | 92 | 기본적으로 생성되는 `widget_test.dart` 파일은 앱의 메인 위젯을 테스트하는 간단한 예제를 포함합니다: 93 | 94 | ```dart 95 | import 'package:flutter/material.dart'; 96 | import 'package:flutter_test/flutter_test.dart'; 97 | import 'package:my_flutter_app/main.dart'; 98 | 99 | void main() { 100 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 101 | await tester.pumpWidget(const MyApp()); 102 | expect(find.text('0'), findsOneWidget); 103 | expect(find.text('1'), findsNothing); 104 | await tester.tap(find.byIcon(Icons.add)); 105 | await tester.pump(); 106 | expect(find.text('0'), findsNothing); 107 | expect(find.text('1'), findsOneWidget); 108 | }); 109 | } 110 | ``` 111 | 112 | ### android/ 디렉토리 113 | 114 | `android/` 디렉토리는 Android 플랫폼 관련 코드와 설정을 포함합니다. 115 | 116 | 117 | - android/ 118 | - app/ 119 | - src/ 120 | - main/ 121 | - AndroidManifest.xml # 앱 선언 및 권한 122 | - kotlin/ # Kotlin 소스 코드 123 | - res/ # 리소스 (아이콘, 문자열 등) 124 | - profile/ 125 | - build.gradle # 앱 수준 빌드 설정 126 | - ... 127 | - build.gradle # 프로젝트 수준 빌드 설정 128 | - gradle/ 129 | - gradle.properties 130 | - ... 131 | 132 | 133 | 특히 중요한 파일들: 134 | 135 | - **AndroidManifest.xml**: 앱의 이름, 아이콘, 필요한 권한 등을 정의합니다. 136 | - **build.gradle**: 앱의 버전, 의존성, 빌드 설정 등을 구성합니다. 137 | 138 | ### ios/ 디렉토리 139 | 140 | `ios/` 디렉토리는 iOS 플랫폼 관련 코드와 설정을 포함합니다. 141 | 142 | 143 | - ios/ 144 | - Runner/ 145 | - AppDelegate.swift # iOS 앱 진입점 146 | - Info.plist # 앱 구성 및 권한 147 | - Assets.xcassets/ # 이미지 에셋 148 | - ... 149 | - Runner.xcodeproj/ # Xcode 프로젝트 파일 150 | - ... 151 | 152 | 153 | 특히 중요한 파일들: 154 | 155 | - **Info.plist**: 앱 이름, 버전, 권한 등의 메타데이터를 포함합니다. 156 | - **AppDelegate.swift**: iOS 앱의 진입점 및 초기화 로직을 포함합니다. 157 | 158 | ### web/, macos/, linux/, windows/ 디렉토리 159 | 160 | 이 디렉토리들은 각각 웹, macOS, 리눅스, 윈도우 플랫폼을 위한 코드와 설정을 포함합니다. 구조는 플랫폼마다 다르지만, 모두 해당 플랫폼의 네이티브 코드와 구성을 담고 있습니다. 161 | 162 | ## 주요 설정 파일 163 | 164 | ### pubspec.yaml 165 | 166 | `pubspec.yaml`은 Flutter 프로젝트의 핵심 설정 파일로, 앱의 메타데이터, 의존성, 에셋 등을 정의합니다: 167 | 168 | ```yaml 169 | name: my_flutter_app 170 | description: A new Flutter project. 171 | 172 | # The following defines the version and build number for your application. 173 | version: 1.0.0+1 174 | 175 | environment: 176 | sdk: ">=3.0.0 <4.0.0" 177 | 178 | # Dependencies 179 | dependencies: 180 | flutter: 181 | sdk: flutter 182 | http: ^1.0.0 183 | shared_preferences: ^2.1.1 184 | 185 | dev_dependencies: 186 | flutter_test: 187 | sdk: flutter 188 | flutter_lints: ^2.0.0 189 | 190 | # Flutter-specific configurations 191 | flutter: 192 | uses-material-design: true 193 | 194 | # Assets 195 | assets: 196 | - assets/images/ 197 | - assets/fonts/ 198 | 199 | # Fonts 200 | fonts: 201 | - family: Roboto 202 | fonts: 203 | - asset: assets/fonts/Roboto-Regular.ttf 204 | - asset: assets/fonts/Roboto-Bold.ttf 205 | weight: 700 206 | ``` 207 | 208 | 주요 항목들: 209 | 210 | - **name**: 앱의 패키지 이름 211 | - **version**: 앱의 버전(`버전 코드+빌드 번호` 형식) 212 | - **dependencies**: 앱이 사용하는 패키지 의존성 213 | - **dev_dependencies**: 개발 시에만 필요한 패키지 의존성 214 | - **flutter**: Flutter 특화 설정 (에셋, 폰트, 테마 등) 215 | 216 | ### analysis_options.yaml 217 | 218 | `analysis_options.yaml`은 Dart 코드 분석기의 설정을 정의하여 코드 품질과 일관성을 유지하는 데 도움을 줍니다: 219 | 220 | ```yaml 221 | include: package:flutter_lints/flutter.yaml 222 | 223 | linter: 224 | rules: 225 | - avoid_print 226 | - avoid_empty_else 227 | - prefer_const_constructors 228 | - sort_child_properties_last 229 | 230 | analyzer: 231 | errors: 232 | missing_required_param: error 233 | missing_return: error 234 | ``` 235 | 236 | ### .gitignore 237 | 238 | `.gitignore` 파일은 Git 버전 관리 시스템에서 무시해야 할 파일들을 지정합니다. Flutter 프로젝트에서는 빌드 결과물, 임시 파일, IDE 파일 등이 여기에 포함됩니다. 239 | 240 | ## 추가 디렉토리와 파일 (선택적) 241 | 242 | 개발자들은 프로젝트의 규모와 복잡성에 따라 추가 디렉토리를 생성할 수 있습니다: 243 | 244 | ### assets/ 디렉토리 245 | 246 | 247 | - assets/ 248 | - images/ # 이미지 파일 249 | - fonts/ # 폰트 파일 250 | - data/ # JSON, CSV 등의 데이터 파일 251 | 252 | 253 | 이 디렉토리는 `pubspec.yaml`에 명시적으로 등록해야 합니다: 254 | 255 | ```yaml 256 | flutter: 257 | assets: 258 | - assets/images/ 259 | - assets/fonts/ 260 | - assets/data/ 261 | ``` 262 | 263 | ### l10n/ 또는 i18n/ 디렉토리 264 | 265 | 다국어 지원을 위한 디렉토리: 266 | 267 | 268 | - l10n/ 269 | - app_en.arb # 영어 번역 270 | - app_ko.arb # 한국어 번역 271 | - app_ja.arb # 일본어 번역 272 | 273 | 274 | ## 프로젝트 구조화 패턴 275 | 276 | Flutter 앱의 구조는 프로젝트의 성격과 팀의 선호도에 따라 달라질 수 있습니다. 일반적으로 많이 사용되는 패턴은 다음과 같습니다: 277 | 278 | ### 기능별 구조 (Feature-First) 279 | 280 | 앱의 기능별로 디렉토리를 구성하는 방식: 281 | 282 | 283 | - lib/ 284 | - main.dart 285 | - features/ 286 | - auth/ 287 | - screens/ 288 | - widgets/ 289 | - models/ 290 | - services/ 291 | - home/ 292 | - profile/ 293 | - settings/ 294 | - shared/ 295 | - widgets/ 296 | - utils/ 297 | - constants/ 298 | 299 | 300 | 이 구조는 기능이 많은 대규모 앱에 적합합니다. 301 | 302 | ### 계층별 구조 (Layer-First) 303 | 304 | 앱의 아키텍처 계층별로 디렉토리를 구성하는 방식: 305 | 306 | 307 | - lib/ 308 | - main.dart 309 | - screens/ # 모든 화면 UI 310 | - widgets/ # 모든 재사용 위젯 311 | - models/ # 모든 데이터 모델 312 | - services/ # 모든 서비스 로직 313 | - repositories/ # 데이터 액세스 로직 314 | - utils/ # 유틸리티 함수 315 | 316 | 317 | 이 구조는 작거나 중간 규모의 앱에 적합합니다. 318 | 319 | ### MVVM 또는 Clean Architecture 320 | 321 | 보다 체계적인 아키텍처 패턴을 적용한 구조: 322 | 323 | 324 | - lib/ 325 | - main.dart 326 | - ui/ 327 | - screens/ 328 | - widgets/ 329 | - viewmodels/ 330 | - models/ 331 | - services/ 332 | - repositories/ 333 | - core/ 334 | - utils/ 335 | - constants/ 336 | 337 | 338 | 이 구조는 코드의 유지 관리성과 테스트 가능성을 높이는 데 도움이 됩니다. 339 | 340 | ## 모범 사례 및 권장 사항 341 | 342 | ### 1. 명확한 명명 규칙 343 | 344 | - 파일 이름: `snake_case.dart` (예: `user_profile.dart`) 345 | - 클래스 이름: `PascalCase` (예: `UserProfile`) 346 | - 변수 및 함수 이름: `camelCase` (예: `userName`, `getUserInfo()`) 347 | 348 | ### 2. 프로젝트 구조 일관성 유지 349 | 350 | - 처음부터 명확한 구조 계획 수립 351 | - 프로젝트 전체에 동일한 규칙 적용 352 | - 팀 내 구조 합의 및 문서화 353 | 354 | ### 3. 관련 코드 그룹화 355 | 356 | - 관련된 코드는 함께 위치 357 | - 너무 깊은 중첩 디렉토리 피하기 (일반적으로 3-4 수준 이내) 358 | - 디렉토리 이름은 내용을 명확히 반영 359 | 360 | ### 4. 불필요한 분할 피하기 361 | 362 | - 파일이 너무 많아지면 관리가 어려울 수 있음 363 | - 단일 위젯이나 작은 기능을 여러 파일로 나누지 않기 364 | - 너무 큰 파일도 피하기 (일반적으로 300-500줄 이내) 365 | 366 | ## 기타 고려 사항 367 | 368 | ### 환경 구성 369 | 370 | 다양한 환경(개발, 스테이징, 프로덕션)에 대한 구성을 지원하기 위해 다음과 같은 접근 방식을 사용할 수 있습니다: 371 | 372 | 373 | - lib/ 374 | - main.dart # 공통 진입점 375 | - main_dev.dart # 개발 환경 진입점 376 | - main_staging.dart # 스테이징 환경 진입점 377 | - main_prod.dart # 프로덕션 환경 진입점 378 | - config/ 379 | - app_config.dart # 환경 설정 클래스 380 | - dev_config.dart 381 | - staging_config.dart 382 | - prod_config.dart 383 | 384 | 385 | ### 라우팅 구성 386 | 387 | 앱의 화면 전환을 관리하기 위한 라우팅 구성: 388 | 389 | 390 | - lib/ 391 | - router/ 392 | - app_router.dart # 라우터 설정 393 | - routes.dart # 라우트 상수 394 | 395 | 396 | ### 상태 관리 397 | 398 | 선택한 상태 관리 솔루션에 따라 구조가 달라질 수 있습니다: 399 | 400 | 401 | - lib/ 402 | - providers/ # Riverpod/Provider 403 | - blocs/ # Flutter_bloc 404 | - stores/ # MobX 405 | - state/ # 기타 상태 관리 406 | 407 | 408 | ## 결론 409 | 410 | Flutter 프로젝트 구조는 규모와 복잡성에 따라 다양하게 적용할 수 있습니다. 중요한 것은 팀이 이해하기 쉽고 유지 관리가 용이한 일관된 구조를 선택하는 것입니다. 프로젝트가 성장함에 따라 구조도 진화할 수 있으므로, 리팩토링과 개선에 열린 자세를 유지하는 것이 좋습니다. 411 | 412 | 이제 Flutter 개발 환경 설정과 프로젝트 구조에 대한 이해를 바탕으로, 다음 챕터에서 Dart 언어 기초를 배워보겠습니다. 413 | -------------------------------------------------------------------------------- /src/content/docs/part5/advanced-routing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 라우트 가드, ShellRoute, DeepLink 3 | --- 4 | 5 | 이 장에서는 go_router를 활용한 고급 라우팅 기법에 대해 알아보겠습니다. 라우트 가드를 통한 접근 제어, ShellRoute를 이용한 중첩 네비게이션, 그리고 딥 링크를 통한 앱 외부에서의 접근 방법을 살펴보겠습니다. 6 | 7 | ## 라우트 가드 (Route Guards) 8 | 9 | 라우트 가드는 특정 경로에 대한 접근을 제어하는 메커니즘으로, 사용자 인증이나 권한 검사 등에 활용됩니다. 10 | 11 | ### redirect를 활용한 기본 라우트 가드 12 | 13 | go_router의 `redirect` 기능을 사용하여 라우트 가드를 구현할 수 있습니다: 14 | 15 | ```dart 16 | final GoRouter router = GoRouter( 17 | routes: [...], 18 | 19 | // 전역 리다이렉트 (모든 라우트에 적용) 20 | redirect: (context, state) { 21 | // 현재 인증 상태 확인 22 | final isLoggedIn = AuthService.isLoggedIn; 23 | 24 | // 로그인이 필요한 경로 목록 25 | final protectedRoutes = ['/profile', '/settings', '/cart']; 26 | final isProtectedRoute = protectedRoutes.any( 27 | (route) => state.matchedLocation.startsWith(route), 28 | ); 29 | 30 | // 로그인 페이지 여부 확인 31 | final isLoginRoute = state.matchedLocation == '/login'; 32 | 33 | // 로그인 되지 않았고 보호된 경로로 접근 시도 34 | if (!isLoggedIn && isProtectedRoute) { 35 | return '/login?redirect=${state.matchedLocation}'; 36 | } 37 | 38 | // 이미 로그인된 상태에서 로그인 페이지 접근 시도 39 | if (isLoggedIn && isLoginRoute) { 40 | return '/'; 41 | } 42 | 43 | // 조건에 해당하지 않으면 리다이렉트 없음 (원래 경로 유지) 44 | return null; 45 | }, 46 | ); 47 | ``` 48 | 49 | ### 특정 라우트에 대한 가드 50 | 51 | 개별 라우트에 대해서도 리다이렉트를 설정할 수 있습니다: 52 | 53 | ```dart 54 | GoRoute( 55 | path: '/admin', 56 | redirect: (context, state) { 57 | final user = AuthService.currentUser; 58 | 59 | // 관리자 권한 확인 60 | if (user == null || !user.hasAdminRole) { 61 | return '/access-denied'; 62 | } 63 | 64 | // 권한이 있으면 원래 경로로 진행 65 | return null; 66 | }, 67 | builder: (context, state) => AdminDashboard(), 68 | ), 69 | ``` 70 | 71 | ### 상태 변화에 따른 리다이렉트 갱신 72 | 73 | 인증 상태가 변경될 때 라우트를 재평가하기 위해 `refreshListenable`을 사용합니다: 74 | 75 | ```dart 76 | // 인증 상태 관리를 위한 ChangeNotifier 77 | class AuthNotifier extends ChangeNotifier { 78 | bool _isLoggedIn = false; 79 | 80 | bool get isLoggedIn => _isLoggedIn; 81 | 82 | Future login() async { 83 | _isLoggedIn = true; 84 | notifyListeners(); // 상태 변경 알림 (라우터가 이를 감지) 85 | } 86 | 87 | Future logout() async { 88 | _isLoggedIn = false; 89 | notifyListeners(); 90 | } 91 | } 92 | 93 | // 인증 상태 변경을 감지하는 라우터 94 | final GoRouter router = GoRouter( 95 | refreshListenable: authNotifier, // 인증 상태 변경 감지 96 | redirect: (context, state) { 97 | // 리다이렉트 로직... 98 | }, 99 | routes: [...], 100 | ); 101 | ``` 102 | 103 | ## ShellRoute를 이용한 네비게이션 104 | 105 | ShellRoute는 중첩 네비게이션을 구현하는 데 사용되며, 특히 바텀 네비게이션 바나 탭 바와 같은 영구적인 UI 요소가 있는 앱에 유용합니다. 106 | 107 | ### StatefulShellRoute 기본 개념 108 | 109 | StatefulShellRoute는 여러 브랜치(branch)를 관리하는 라우트로, 각 브랜치는 자체 네비게이션 상태와 히스토리를 가집니다: 110 | 111 | 112 | ### 바텀 네비게이션 바 구현 113 | 114 | StatefulShellRoute를 사용하여 바텀 네비게이션 바를 구현해 보겠습니다: 115 | 116 | ```dart 117 | final GoRouter router = GoRouter( 118 | initialLocation: '/', 119 | routes: [ 120 | // StatefulShellRoute 정의 (indexedStack 방식 사용) 121 | StatefulShellRoute.indexedStack( 122 | // 쉘 UI 구성 123 | builder: (context, state, navigationShell) { 124 | return ScaffoldWithNavBar(navigationShell: navigationShell); 125 | }, 126 | // 브랜치 정의 127 | branches: [ 128 | // 홈 탭 129 | StatefulShellBranch( 130 | routes: [ 131 | GoRoute( 132 | path: '/', 133 | builder: (context, state) => HomeScreen(), 134 | routes: [ 135 | // 홈 탭 내부의 중첩 라우트 136 | GoRoute( 137 | path: 'details/:id', 138 | builder: (context, state) { 139 | final id = state.pathParameters['id']!; 140 | return DetailsScreen(id: id); 141 | }, 142 | ), 143 | ], 144 | ), 145 | ], 146 | ), 147 | 148 | // 검색 탭 149 | StatefulShellBranch( 150 | routes: [ 151 | GoRoute( 152 | path: '/search', 153 | builder: (context, state) => SearchScreen(), 154 | ), 155 | ], 156 | ), 157 | 158 | // 프로필 탭 159 | StatefulShellBranch( 160 | routes: [ 161 | GoRoute( 162 | path: '/profile', 163 | builder: (context, state) => ProfileScreen(), 164 | ), 165 | ], 166 | ), 167 | ], 168 | ), 169 | ], 170 | ); 171 | 172 | // 바텀 네비게이션 바가 있는 스캐폴드 173 | class ScaffoldWithNavBar extends StatelessWidget { 174 | final StatefulNavigationShell navigationShell; 175 | 176 | const ScaffoldWithNavBar({required this.navigationShell}); 177 | 178 | @override 179 | Widget build(BuildContext context) { 180 | return Scaffold( 181 | body: navigationShell, // 현재 브랜치의 화면 표시 182 | bottomNavigationBar: BottomNavigationBar( 183 | currentIndex: navigationShell.currentIndex, 184 | items: const [ 185 | BottomNavigationBarItem(icon: Icon(Icons.home), label: '홈'), 186 | BottomNavigationBarItem(icon: Icon(Icons.search), label: '검색'), 187 | BottomNavigationBarItem(icon: Icon(Icons.person), label: '프로필'), 188 | ], 189 | onTap: (index) { 190 | // 탭 인덱스에 해당하는 브랜치로 이동 191 | navigationShell.goBranch(index); 192 | }, 193 | ), 194 | ); 195 | } 196 | } 197 | ``` 198 | 199 | ### 브랜치 간 이동과 상태 유지 200 | 201 | StatefulShellRoute의 강점은 각 브랜치 내의 네비게이션 상태가 유지된다는 점입니다: 202 | 203 | ```dart 204 | // 검색 화면에서 사용자가 검색 결과로 이동한 후 205 | // 다른 탭으로 이동했다가 다시 검색 탭으로 돌아오면 206 | // 검색 결과 화면이 그대로 유지됩니다. 207 | 208 | // 홈 탭에서 상세 화면으로 이동 209 | context.go('/details/123'); 210 | 211 | // 프로필 탭으로 이동 (홈 탭의 상태는 유지됨) 212 | context.go('/profile'); 213 | 214 | // 다시 홈 탭으로 이동하면 상세 화면이 표시됨 215 | context.go('/'); 216 | ``` 217 | 218 | ## 딥 링크 (Deep Linking) 219 | 220 | 딥 링크는 앱의 특정 화면으로 직접 접근할 수 있는 외부 링크로, 웹 URL, 푸시 알림 등에서 활용됩니다. 221 | 222 | ### 안드로이드 딥 링크 설정 223 | 224 | 1. **AndroidManifest.xml 설정**: 225 | 226 | ```xml 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 246 | 247 | 248 | 249 | 250 | ``` 251 | 252 | ### iOS 딥 링크 설정 253 | 254 | 1. **Info.plist 설정**: 255 | 256 | ```xml 257 | CFBundleURLTypes 258 | 259 | 260 | CFBundleTypeRole 261 | Editor 262 | CFBundleURLName 263 | com.example.app 264 | CFBundleURLSchemes 265 | 266 | myapp 267 | 268 | 269 | 270 | 271 | 272 | com.apple.developer.associated-domains 273 | 274 | applinks:example.com 275 | 276 | ``` 277 | 278 | ### Flutter에서 딥 링크 처리 279 | 280 | go_router를 사용하면 딥 링크 처리가 매우 간단합니다: 281 | 282 | ```dart 283 | // 딥 링크를 자동으로 처리하는 설정 284 | void main() { 285 | // 앱 초기화 286 | WidgetsFlutterBinding.ensureInitialized(); 287 | 288 | // 라우터 설정 289 | final router = GoRouter( 290 | // 라우트 설정 291 | routes: [...], 292 | ); 293 | 294 | runApp(MyApp(router: router)); 295 | } 296 | 297 | class MyApp extends StatelessWidget { 298 | final GoRouter router; 299 | 300 | const MyApp({required this.router}); 301 | 302 | @override 303 | Widget build(BuildContext context) { 304 | return MaterialApp.router( 305 | routerConfig: router, 306 | title: '딥 링크 예제', 307 | // ... 308 | ); 309 | } 310 | } 311 | ``` 312 | 313 | ### 딥 링크 테스트 314 | 315 | 딥 링크를 테스트하기 위해 터미널에서 다음 명령어를 실행할 수 있습니다: 316 | 317 | **안드로이드**: 318 | 319 | ```bash 320 | adb shell am start -a android.intent.action.VIEW -d "https://example.com/product/123" com.example.app 321 | ``` 322 | 323 | **iOS 시뮬레이터**: 324 | 325 | ```bash 326 | xcrun simctl openurl booted "https://example.com/product/123" 327 | ``` 328 | 329 | ### 푸시 알림에서 딥 링크 처리 330 | 331 | 푸시 알림을 통한 딥 링크를 처리하는 방법: 332 | 333 | ```dart 334 | // firebase_messaging 패키지 사용 예제 335 | FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { 336 | final deepLink = message.data['deepLink'] as String?; 337 | if (deepLink != null) { 338 | // 앱이 실행 중일 때 푸시 알림을 탭하면 해당 경로로 이동 339 | router.go(deepLink); 340 | } 341 | }); 342 | 343 | // 앱이 종료된 상태에서 푸시 알림을 탭한 경우 344 | Future setupInteractedMessage() async { 345 | RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); 346 | 347 | if (initialMessage != null) { 348 | final deepLink = initialMessage.data['deepLink'] as String?; 349 | if (deepLink != null) { 350 | // 딥 링크로 이동 351 | router.go(deepLink); 352 | } 353 | } 354 | } 355 | ``` 356 | 357 | ## 고급 라우팅 테크닉 358 | 359 | ### 1. 동적 라우트 생성 360 | 361 | API에서 가져온 데이터를 기반으로 동적으로 라우트를 생성할 수 있습니다: 362 | 363 | ```dart 364 | // 동적 라우트 생성 예제 365 | Future> buildDynamicRoutes() async { 366 | // API에서 카테고리 목록 가져오기 367 | final categories = await apiService.getCategories(); 368 | 369 | // 각 카테고리에 대한 라우트 생성 370 | return categories.map((category) { 371 | return GoRoute( 372 | path: '/category/${category.slug}', 373 | builder: (context, state) => CategoryScreen(category: category), 374 | ); 375 | }).toList(); 376 | } 377 | ``` 378 | 379 | ### 2. 라우트 전환 애니메이션 커스터마이징 380 | 381 | 라우트 간 전환 애니메이션을 세밀하게 제어할 수 있습니다: 382 | 383 | ```dart 384 | GoRoute( 385 | path: '/details/:id', 386 | pageBuilder: (context, state) { 387 | final id = state.pathParameters['id']!; 388 | 389 | // 히어로 애니메이션을 위한 전환 페이지 390 | return CustomTransitionPage( 391 | key: state.pageKey, 392 | child: ProductDetailsScreen(id: id), 393 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 394 | // 페이드 트랜지션 395 | return FadeTransition( 396 | opacity: CurveTween(curve: Curves.easeInOut).animate(animation), 397 | child: child, 398 | ); 399 | }, 400 | ); 401 | }, 402 | ), 403 | ``` 404 | 405 | ## 요약 406 | 407 | - **라우트 가드**를 사용하여 인증 상태에 따라 접근을 제어할 수 있습니다. 408 | - **StatefulShellRoute**는 바텀 네비게이션 바와 같은 중첩 네비게이션을 효과적으로 구현할 수 있게 해줍니다. 409 | - **딥 링크**를 통해 앱 외부에서 특정 화면으로 직접 접근할 수 있습니다. 410 | - **안드로이드와 iOS** 모두에서 딥 링크를 설정하는 방법이 다릅니다. 411 | - **푸시 알림**에서 딥 링크를 처리하여 특정 화면으로 이동할 수 있습니다. 412 | - **동적 라우트 생성**, **커스텀 전환 애니메이션** 등 고급 라우팅 기법을 활용할 수 있습니다. 413 | 414 | 다음 섹션에서는 이러한 고급 라우팅 기법을 실제 앱에 적용하는 복수 화면 전환 실습을 진행하겠습니다. 415 | -------------------------------------------------------------------------------- /src/content/docs/part2/type-system.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 타입 시스템 & 제네릭 3 | --- 4 | 5 | ## Dart 타입 시스템 개요 6 | 7 | Dart는 정적 타입 언어로, 컴파일 시간에 타입 검사를 수행합니다. 그러나 타입 추론을 지원하여 타입 선언을 생략할 수 있는 유연성도 제공합니다. Dart 2부터는 타입 안전성이 강화되었고, Dart 2.12부터는 null 안전성이 도입되었습니다. 8 | 9 | ## 기본 타입 10 | 11 | ### 기본 제공 타입 12 | 13 | Dart에는 다음과 같은 기본 타입이 있습니다: 14 | 15 | ```dart 16 | // 숫자 타입 17 | int integer = 42; 18 | double decimal = 3.14; 19 | num number = 10; // int나 double의 상위 타입 20 | 21 | // 문자열 22 | String text = '안녕하세요'; 23 | 24 | // 불리언 25 | bool flag = true; 26 | 27 | // 리스트(배열) 28 | List numbers = [1, 2, 3]; 29 | 30 | // 맵(딕셔너리) 31 | Map person = {'name': '홍길동', 'age': 30}; 32 | 33 | // 집합 34 | Set uniqueNames = {'홍길동', '김철수', '이영희'}; 35 | 36 | // 심볼 37 | Symbol symbol = #symbolName; 38 | ``` 39 | 40 | ### 특수 타입 41 | 42 | Dart에는 특수한 용도의 타입도 있습니다: 43 | 44 | ```dart 45 | // void: 값을 반환하지 않는 함수의 반환 타입 46 | void printMessage() { 47 | print('메시지 출력'); 48 | } 49 | 50 | // dynamic: 모든 타입을 허용하는 동적 타입 51 | dynamic dynamicValue = '문자열'; 52 | dynamicValue = 42; // 타입 변경 가능 53 | 54 | // Object: 모든 객체의 기본 타입 55 | Object objectValue = 'Hello'; 56 | 57 | // Null: null 값의 타입 (Dart 2.12 이전) 58 | ``` 59 | 60 | ## 타입 추론 61 | 62 | Dart는 변수의 초기값을 기반으로 타입을 추론할 수 있습니다: 63 | 64 | ```dart 65 | // 타입 추론 66 | var name = '홍길동'; // String 타입으로 추론 67 | var age = 30; // int 타입으로 추론 68 | var height = 175.5; // double 타입으로 추론 69 | var active = true; // bool 타입으로 추론 70 | var items = [1, 2, 3]; // List 타입으로 추론 71 | 72 | // 함수에서도 반환 타입 추론 73 | var getName = () { 74 | return '홍길동'; // String 반환 타입으로 추론 75 | }; 76 | 77 | // 컬렉션에서도 타입 추론 78 | var people = [ // List> 타입으로 추론 79 | {'name': '홍길동', 'age': 30}, 80 | {'name': '김철수', 'age': 25}, 81 | ]; 82 | ``` 83 | 84 | ## 타입 체크와 캐스팅 85 | 86 | ### is와 is! 연산자 87 | 88 | 타입을 확인하기 위해 `is`와 `is!` 연산자를 사용합니다: 89 | 90 | ```dart 91 | Object value = '문자열'; 92 | 93 | if (value is String) { 94 | // value는 이 블록 내에서 String 타입으로 취급됨 (스마트 캐스팅) 95 | print('문자열 길이: ${value.length}'); 96 | } 97 | 98 | if (value is! int) { 99 | print('정수가 아닙니다'); 100 | } 101 | ``` 102 | 103 | ### as 연산자 104 | 105 | 타입 캐스팅을 위해 `as` 연산자를 사용합니다: 106 | 107 | ```dart 108 | Object value = '문자열'; 109 | 110 | // String으로 캐스팅 111 | String text = value as String; 112 | print(text.toUpperCase()); 113 | 114 | // 잘못된 캐스팅은 런타임 오류 발생 115 | // int number = value as int; // 오류: String을 int로 캐스팅 불가 116 | ``` 117 | 118 | ## 제네릭(Generics) 119 | 120 | 제네릭은 타입을 매개변수로 사용하여 코드를 재사용할 수 있게 해주는 기능입니다. 121 | 122 | ### 제네릭 클래스 123 | 124 | ```dart 125 | // 제네릭 클래스 정의 126 | class Box { 127 | T value; 128 | 129 | Box(this.value); 130 | 131 | T getValue() { 132 | return value; 133 | } 134 | 135 | void setValue(T newValue) { 136 | value = newValue; 137 | } 138 | } 139 | 140 | // 제네릭 클래스 사용 141 | void main() { 142 | // String 타입의 Box 143 | var stringBox = Box('안녕하세요'); 144 | print(stringBox.getValue()); // '안녕하세요' 145 | 146 | // int 타입의 Box 147 | var intBox = Box(42); 148 | print(intBox.getValue()); // 42 149 | 150 | // 타입 추론을 통한 인스턴스화 151 | var doubleBox = Box(3.14); // Box로 추론 152 | } 153 | ``` 154 | 155 | ### 제네릭 함수 156 | 157 | ```dart 158 | // 제네릭 함수 정의 159 | T first(List items) { 160 | return items.first; 161 | } 162 | 163 | // 제네릭 함수 사용 164 | void main() { 165 | var names = ['홍길동', '김철수', '이영희']; 166 | var firstString = first(names); 167 | print(firstString); // '홍길동' 168 | 169 | var numbers = [1, 2, 3, 4, 5]; 170 | var firstInt = first(numbers); // 타입 추론으로 T는 int로 결정 171 | print(firstInt); // 1 172 | } 173 | ``` 174 | 175 | ### 제네릭 타입 제한 176 | 177 | 특정 타입이나 상위 타입으로 제한할 수 있습니다: 178 | 179 | ```dart 180 | // 상위 타입 제한 181 | class NumberBox { 182 | T value; 183 | 184 | NumberBox(this.value); 185 | 186 | void square() { 187 | // T가 num의 하위 타입이므로 곱셈 연산 가능 188 | print(value * value); 189 | } 190 | } 191 | 192 | void main() { 193 | var intBox = NumberBox(10); 194 | intBox.square(); // 100 195 | 196 | var doubleBox = NumberBox(2.5); 197 | doubleBox.square(); // 6.25 198 | 199 | // var stringBox = NumberBox('오류'); // 컴파일 오류: String은 num의 하위 타입이 아님 200 | } 201 | ``` 202 | 203 | ### 다양한 제네릭 적용 204 | 205 | ```dart 206 | // 다중 타입 매개변수 207 | class Pair { 208 | K first; 209 | V second; 210 | 211 | Pair(this.first, this.second); 212 | } 213 | 214 | // 제네릭 확장 215 | class IntBox extends Box { 216 | IntBox(int value) : super(value); 217 | 218 | void increment() { 219 | setValue(getValue() + 1); 220 | } 221 | } 222 | 223 | // 제네릭 타입 별칭 224 | typedef StringList = List; 225 | typedef KeyValueMap = Map; 226 | ``` 227 | 228 | ## 컬렉션 타입과 제네릭 229 | 230 | ### List와 제네릭 231 | 232 | ```dart 233 | // 타입 지정 리스트 234 | List names = ['홍길동', '김철수', '이영희']; 235 | List scores = [90, 85, 95]; 236 | 237 | // 컬렉션 리터럴로 생성 238 | var fruits = ['사과', '바나나', '오렌지']; 239 | 240 | // 생성자로 생성 241 | var numbers = List.filled(5, 0); // [0, 0, 0, 0, 0] 242 | var evens = List.generate(5, (i) => i * 2); // [0, 2, 4, 6, 8] 243 | 244 | // 제네릭 메서드 사용 245 | var filteredNames = names.where((name) => name.length > 2).toList(); 246 | var mappedScores = scores.map((score) => score * 1.1).toList(); 247 | ``` 248 | 249 | ### Map과 제네릭 250 | 251 | ```dart 252 | // 타입 지정 맵 253 | Map ages = { 254 | '홍길동': 30, 255 | '김철수': 25, 256 | '이영희': 28, 257 | }; 258 | 259 | // 컬렉션 리터럴로 생성 260 | var scores = { 261 | '수학': 90.5, 262 | '영어': 85.0, 263 | '과학': 95.5, 264 | }; 265 | 266 | // 생성자로 생성 267 | var config = Map(); 268 | config['debug'] = true; 269 | config['timeout'] = 30; 270 | ``` 271 | 272 | ### Set과 제네릭 273 | 274 | ```dart 275 | // 타입 지정 집합 276 | Set uniqueNames = {'홍길동', '김철수', '이영희'}; 277 | 278 | // 컬렉션 리터럴로 생성 279 | var colors = {'빨강', '파랑', '녹색'}; 280 | 281 | // 생성자로 생성 282 | var numbers = Set.from([1, 2, 3, 3, 4]); // {1, 2, 3, 4} 283 | ``` 284 | 285 | ## 타입 시스템의 고급 기능 286 | 287 | ### typedef 288 | 289 | 함수 타입 또는 타입 별칭을 정의할 수 있습니다: 290 | 291 | ```dart 292 | // 함수 타입 정의 293 | typedef IntOperation = int Function(int a, int b); 294 | 295 | int add(int a, int b) => a + b; 296 | int subtract(int a, int b) => a - b; 297 | 298 | void calculate(IntOperation operation, int x, int y) { 299 | print('결과: ${operation(x, y)}'); 300 | } 301 | 302 | void main() { 303 | calculate(add, 10, 5); // 결과: 15 304 | calculate(subtract, 10, 5); // 결과: 5 305 | } 306 | ``` 307 | 308 | Dart 2.13부터는 함수 타입뿐만 아니라 모든 타입의 별칭을 정의할 수 있습니다: 309 | 310 | ```dart 311 | // 타입 별칭 정의 312 | typedef StringList = List; 313 | typedef UserInfo = Map; 314 | 315 | void printNames(StringList names) { 316 | for (var name in names) { 317 | print(name); 318 | } 319 | } 320 | 321 | void displayUserInfo(UserInfo user) { 322 | print('이름: ${user['name']}, 나이: ${user['age']}'); 323 | } 324 | 325 | void main() { 326 | StringList names = ['홍길동', '김철수', '이영희']; 327 | printNames(names); 328 | 329 | UserInfo user = {'name': '홍길동', '나이': 30}; 330 | displayUserInfo(user); 331 | } 332 | ``` 333 | 334 | ### 타입 프로모션 335 | 336 | Dart는 타입 검사 이후 변수의 타입을 자동으로 더 구체적인 타입으로 승격(프로모션)합니다: 337 | 338 | ```dart 339 | Object value = '안녕하세요'; 340 | 341 | // 타입 검사 후 자동으로 String으로 프로모션됨 342 | if (value is String) { 343 | // 이 블록 내에서는 value가 String 타입으로 취급됨 344 | print('대문자: ${value.toUpperCase()}'); 345 | print('길이: ${value.length}'); 346 | } 347 | 348 | // 블록 밖에서는 다시 원래 타입 (Object) 349 | // print(value.length); // 오류: Object에는 length 속성이 없음 350 | ``` 351 | 352 | ### 유니온 타입 (Dart 3) 353 | 354 | Dart 3부터는 유니온 타입을 지원합니다: 355 | 356 | ```dart 357 | Object value = '문자열'; 358 | 359 | // as 대신 패턴 매칭으로 타입 처리 360 | switch (value) { 361 | case String(): 362 | print('문자열: $value'); 363 | case int(): 364 | print('정수: $value'); 365 | default: 366 | print('기타 타입: $value'); 367 | } 368 | ``` 369 | 370 | ## 실전 예제: 제네릭 활용 371 | 372 | ### 데이터 캐싱 클래스 373 | 374 | ```dart 375 | class Cache { 376 | final Map _cache = {}; 377 | 378 | T? get(String key) { 379 | return _cache[key]; 380 | } 381 | 382 | void set(String key, T value) { 383 | _cache[key] = value; 384 | } 385 | 386 | bool has(String key) { 387 | return _cache.containsKey(key); 388 | } 389 | 390 | void remove(String key) { 391 | _cache.remove(key); 392 | } 393 | 394 | void clear() { 395 | _cache.clear(); 396 | } 397 | } 398 | 399 | // 사용 예 400 | void main() { 401 | var stringCache = Cache(); 402 | stringCache.set('greeting', '안녕하세요'); 403 | print(stringCache.get('greeting')); // '안녕하세요' 404 | 405 | var userCache = Cache>(); 406 | userCache.set('user1', {'name': '홍길동', 'age': 30}); 407 | var user = userCache.get('user1'); 408 | print('사용자: ${user?['name']}, 나이: ${user?['age']}'); 409 | } 410 | ``` 411 | 412 | ### Result 타입 413 | 414 | 성공 또는 실패 결과를 나타내는 제네릭 클래스: 415 | 416 | ```dart 417 | abstract class Result { 418 | Result(); 419 | 420 | factory Result.success(S value) = Success; 421 | factory Result.failure(E error) = Failure; 422 | 423 | bool get isSuccess; 424 | bool get isFailure; 425 | S? get value; 426 | E? get error; 427 | 428 | void when({ 429 | required void Function(S value) success, 430 | required void Function(E error) failure, 431 | }); 432 | } 433 | 434 | class Success extends Result { 435 | final S _value; 436 | 437 | Success(this._value); 438 | 439 | @override 440 | bool get isSuccess => true; 441 | 442 | @override 443 | bool get isFailure => false; 444 | 445 | @override 446 | S get value => _value; 447 | 448 | @override 449 | E? get error => null; 450 | 451 | @override 452 | void when({ 453 | required void Function(S value) success, 454 | required void Function(E error) failure, 455 | }) { 456 | success(_value); 457 | } 458 | } 459 | 460 | class Failure extends Result { 461 | final E _error; 462 | 463 | Failure(this._error); 464 | 465 | @override 466 | bool get isSuccess => false; 467 | 468 | @override 469 | bool get isFailure => true; 470 | 471 | @override 472 | S? get value => null; 473 | 474 | @override 475 | E get error => _error; 476 | 477 | @override 478 | void when({ 479 | required void Function(S value) success, 480 | required void Function(E error) failure, 481 | }) { 482 | failure(_error); 483 | } 484 | } 485 | 486 | // 사용 예 487 | Result fetchData() { 488 | try { 489 | // 데이터 가져오기 로직 490 | return Result.success('데이터'); 491 | } catch (e) { 492 | return Result.failure(Exception('데이터를 가져오는 중 오류 발생: $e')); 493 | } 494 | } 495 | 496 | void main() { 497 | var result = fetchData(); 498 | 499 | result.when( 500 | success: (data) { 501 | print('성공: $data'); 502 | }, 503 | failure: (error) { 504 | print('실패: $error'); 505 | }, 506 | ); 507 | } 508 | ``` 509 | 510 | ![fpdart code](https://raw.githubusercontent.com/SandroMaglione/fpdart/main/resources/screenshots/screenshot_fpdart.png) 511 | 512 | [fpdart](https://pub.dev/packages/fpdart)를 이용하면 Result외에 더 다양한 함수형 프로그래밍 기능을 사용하실 수 있습니다. 513 | 514 | ## 결론 515 | 516 | Dart의 타입 시스템과 제네릭은 타입 안전성과 코드 재사용성을 동시에 얻을 수 있게 해줍니다. 정적 타입 시스템은 컴파일 시간에 많은 오류를 잡아낼 수 있으며, 제네릭은 다양한 타입에 대해 동일한 로직을 적용할 수 있게 해줍니다. 517 | 518 | 다음 장에서는 Dart의 클래스, 생성자, 팩토리 등 객체 지향 프로그래밍의 핵심 개념에 대해 알아보겠습니다. 519 | -------------------------------------------------------------------------------- /src/content/docs/appendix/error-handling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Flutter 오류 대응법 가이드 3 | --- 4 | 5 | 6 | Flutter 앱을 개발하면서 다양한 오류에 직면할 수 있습니다. 이 문서에서는 자주 발생하는 Flutter 오류들과 그 해결 방법을 소개합니다. 7 | 8 | ## 개발 환경 오류 9 | 10 | ### Flutter Doctor 오류 11 | 12 | Flutter SDK를 설치한 후에는 항상 `flutter doctor` 명령어를 실행하여 개발 환경을 확인하는 것이 좋습니다. 13 | 14 | | 오류 메시지 | 원인 | 해결 방법 | 15 | | ------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------ | 16 | | `Flutter requires Android SDK` | Android SDK가 설치되지 않았거나 경로가 설정되지 않음 | Android Studio를 설치하거나, Android SDK 경로를 Flutter 환경 변수에 추가 | 17 | | `Android licenses not accepted` | Android 라이선스 동의가 필요함 | `flutter doctor --android-licenses` 실행 후 동의 | 18 | | `Xcode not installed` | iOS 개발을 위한 Xcode가 설치되지 않음 (macOS만 해당) | App Store에서 Xcode 설치 | 19 | | `CocoaPods not installed` | iOS 종속성 관리 도구가 설치되지 않음 | `sudo gem install cocoapods` 명령어로 설치 | 20 | 21 | ### pubspec.yaml 관련 오류 22 | 23 | ``` 24 | Because every version of flutter_package from sdk depends on intl ^0.17.0 and app depends on intl ^0.18.0, flutter_package from sdk is forbidden. 25 | ``` 26 | 27 | **원인**: 패키지 간 의존성 충돌 28 | **해결 방법**: 패키지 버전을 호환되는 범위로 조정하고 `flutter pub upgrade --major-versions` 실행 29 | 30 | ## 빌드 오류 31 | 32 | ### Gradle 관련 오류 33 | 34 | ``` 35 | Gradle task assembleDebug failed with exit code 1 36 | ``` 37 | 38 | **원인**: 다양한 Gradle 구성 문제 발생 가능 39 | **해결 방법**: 40 | 41 | 1. 안드로이드 폴더에서 `./gradlew clean` 실행 42 | 2. `flutter clean` 실행 후 다시 빌드 43 | 3. Gradle 버전 확인 및 업데이트 44 | 45 | ### iOS 빌드 오류 46 | 47 | ``` 48 | [!] CocoaPods could not find compatible versions for pod "Firebase/Core" 49 | ``` 50 | 51 | **원인**: CocoaPods 의존성 충돌 52 | **해결 방법**: 53 | 54 | 1. iOS 폴더에서 `pod repo update` 실행 55 | 2. `pod install --repo-update` 실행 56 | 3. `Podfile.lock` 삭제 후 `pod install` 다시 실행 57 | 58 | ### 코드 서명 오류 59 | 60 | ``` 61 | No provisioning profile matches the specified entitlements 62 | ``` 63 | 64 | **원인**: iOS 앱 서명 설정 문제 65 | **해결 방법**: 66 | 67 | 1. Xcode를 열고 팀 설정 확인 68 | 2. 프로비저닝 프로필 업데이트 69 | 3. 앱 ID와 번들 ID가 일치하는지 확인 70 | 71 | ## 런타임 오류 72 | 73 | ### 위젯 빌드 오류 74 | 75 | #### setState() 오류 76 | 77 | ``` 78 | setState() called after dispose(): _MyWidgetState#a7c89(lifecycle state: defunct, not mounted) 79 | ``` 80 | 81 | **원인**: 위젯이 제거된 후 setState 호출 82 | **해결 방법**: 83 | 84 | ```dart 85 | // 수정 전 86 | void fetchData() async { 87 | final data = await apiService.getData(); 88 | setState(() { 89 | this.data = data; 90 | }); 91 | } 92 | 93 | // 수정 후 94 | void fetchData() async { 95 | final data = await apiService.getData(); 96 | if (mounted) { 97 | setState(() { 98 | this.data = data; 99 | }); 100 | } 101 | } 102 | ``` 103 | 104 | #### RenderFlex 오버플로우 105 | 106 | ``` 107 | A RenderFlex overflowed by 20 pixels on the bottom 108 | ``` 109 | 110 | **원인**: 자식 위젯이 부모 위젯의 제약 조건을 초과 111 | **해결 방법**: 112 | 113 | 1. `Expanded` 또는 `Flexible` 위젯 사용 114 | 2. `SingleChildScrollView`로 스크롤 가능하게 만들기 115 | 3. `ConstrainedBox`를 사용하여 최대 크기 제한 116 | 117 | ```dart 118 | // 오류 발생 코드 119 | Column( 120 | children: [ 121 | LargeWidget(), 122 | AnotherLargeWidget(), 123 | ], 124 | ) 125 | 126 | // 해결 방법 (스크롤 적용) 127 | SingleChildScrollView( 128 | child: Column( 129 | children: [ 130 | LargeWidget(), 131 | AnotherLargeWidget(), 132 | ], 133 | ), 134 | ) 135 | 136 | // 또는 Expanded 사용 (부모가 Row/Column일 경우) 137 | Column( 138 | children: [ 139 | Expanded(child: LargeWidget()), 140 | AnotherWidget(), 141 | ], 142 | ) 143 | ``` 144 | 145 | ### 상태 관리 오류 146 | 147 | #### Provider 관련 오류 148 | 149 | ``` 150 | Error: Could not find the correct Provider above this Consumer Widget 151 | ``` 152 | 153 | **원인**: Provider가 위젯 트리의 상위에 없음 154 | **해결 방법**: 적절한 위치에 Provider 배치 155 | 156 | ```dart 157 | // 수정 전 (문제가 되는 구조) 158 | Widget build(BuildContext context) { 159 | return Consumer( 160 | builder: (context, model, child) { 161 | return Text(model.data); 162 | }, 163 | ); 164 | } 165 | 166 | // 수정 후 (올바른 구조) 167 | Widget build(BuildContext context) { 168 | return ChangeNotifierProvider( 169 | create: (_) => MyModel(), 170 | child: Consumer( 171 | builder: (context, model, child) { 172 | return Text(model.data); 173 | }, 174 | ), 175 | ); 176 | } 177 | ``` 178 | 179 | #### Riverpod 관련 오류 180 | 181 | ``` 182 | ProviderNotFoundException: No provider found for providerHash:xxx 183 | ``` 184 | 185 | **원인**: ProviderScope 범위 밖에서 provider에 접근 시도 186 | **해결 방법**: 앱 루트에 ProviderScope 추가 187 | 188 | ```dart 189 | void main() { 190 | runApp( 191 | ProviderScope( 192 | child: MyApp(), 193 | ), 194 | ); 195 | } 196 | ``` 197 | 198 | ### 네트워크 관련 오류 199 | 200 | #### CORS 오류 (웹) 201 | 202 | ``` 203 | Access to XMLHttpRequest has been blocked by CORS policy 204 | ``` 205 | 206 | **원인**: 웹 버전에서 CORS 정책 위반 207 | **해결 방법**: 208 | 209 | 1. 서버 측에서 적절한 CORS 헤더 설정 210 | 2. 개발 시에는 Chrome을 `--disable-web-security` 옵션으로 실행 211 | 3. 프록시 서버 사용 212 | 213 | #### 인증서 오류 214 | 215 | ``` 216 | HandshakeException: Handshake error in client 217 | ``` 218 | 219 | **원인**: SSL 인증서 문제 220 | **해결 방법**: 221 | 222 | ```dart 223 | // 주의: 프로덕션 앱에서는 사용하지 않는 것이 좋습니다 224 | HttpClient client = HttpClient() 225 | ..badCertificateCallback = 226 | (X509Certificate cert, String host, int port) => true; 227 | ``` 228 | 229 | ### 비동기 처리 오류 230 | 231 | #### FutureBuilder 에러 232 | 233 | ``` 234 | type 'Future' is not a subtype of type 'Future>' 235 | ``` 236 | 237 | **원인**: Future의 타입 불일치 238 | **해결 방법**: 명시적 타입 지정 239 | 240 | ```dart 241 | // 수정 전 242 | Future fetchData() async { 243 | // ... 244 | return data; 245 | } 246 | 247 | // 수정 후 248 | Future> fetchData() async { 249 | // ... 250 | return data; 251 | } 252 | ``` 253 | 254 | #### 스트림 오류 255 | 256 | ``` 257 | Bad state: Stream has already been listened to 258 | ``` 259 | 260 | **원인**: 단일 구독 스트림에 여러 번 구독 시도 261 | **해결 방법**: `Stream.asBroadcastStream()` 사용 또는 `StreamController`에서 `broadcast: true` 설정 262 | 263 | ## 플랫폼별 오류 264 | 265 | ### Android 특정 오류 266 | 267 | #### 다중 DEX 문제 268 | 269 | ``` 270 | Execution failed for task ':app:mergeDebugResources'. > java.lang.OutOfMemoryError 271 | ``` 272 | 273 | **원인**: 메서드 수가 DEX 제한을 초과 274 | **해결 방법**: `android/app/build.gradle`에 멀티덱스 활성화 275 | 276 | ```txt 277 | android { 278 | defaultConfig { 279 | multiDexEnabled true 280 | } 281 | } 282 | 283 | dependencies { 284 | implementation 'androidx.multidex:multidex:2.0.1' 285 | } 286 | ``` 287 | 288 | #### 권한 관련 오류 289 | 290 | ``` 291 | PlatformException: Permission denied 292 | ``` 293 | 294 | **원인**: 필요한 권한이 설정되지 않음 295 | **해결 방법**: `AndroidManifest.xml`에 필요한 권한 추가 및 런타임 권한 요청 구현 296 | 297 | ```xml 298 | 299 | 300 | 301 | 302 | 303 | ``` 304 | 305 | ### iOS 특정 오류 306 | 307 | #### Info.plist 관련 오류 308 | 309 | ``` 310 | This app has crashed because it attempted to access privacy-sensitive data without a usage description. 311 | ``` 312 | 313 | **원인**: 필요한 권한 설명이 Info.plist에 없음 314 | **해결 방법**: `ios/Runner/Info.plist`에 관련 설명 추가 315 | 316 | ```xml 317 | NSCameraUsageDescription 318 | 이 앱은 프로필 사진 등록을 위해 카메라에 접근합니다. 319 | NSPhotoLibraryUsageDescription 320 | 이 앱은 사진 업로드를 위해 갤러리에 접근합니다. 321 | ``` 322 | 323 | #### 미니멈 iOS 버전 오류 324 | 325 | ``` 326 | The iOS deployment target is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0. 327 | ``` 328 | 329 | **원인**: 지원되지 않는 iOS 최소 버전 설정 330 | **해결 방법**: `ios/Podfile` 및 Xcode 프로젝트 설정에서 최소 버전 업데이트 331 | 332 | ```ruby 333 | # Podfile 334 | platform :ios, '12.0' 335 | ``` 336 | 337 | ## Web 특정 오류 338 | 339 | ### 자바스크립트 오류 340 | 341 | ``` 342 | TypeError: Cannot read property 'X' of undefined 343 | ``` 344 | 345 | **원인**: 웹용 플러그인이 올바르게 초기화되지 않음 346 | **해결 방법**: 플랫폼 조건부 코드 사용 347 | 348 | ```dart 349 | import 'package:flutter/foundation.dart' show kIsWeb; 350 | 351 | if (kIsWeb) { 352 | // 웹 전용 코드 353 | } else { 354 | // 모바일 전용 코드 355 | } 356 | ``` 357 | 358 | ### 웹 리소스 로딩 오류 359 | 360 | ``` 361 | Failed to load resource: net::ERR_BLOCKED_BY_CLIENT 362 | ``` 363 | 364 | **원인**: 광고 차단기가 리소스 로딩 차단 365 | **해결 방법**: 리소스 URL 패턴 변경 또는 필수 리소스임을 사용자에게 알림 366 | 367 | ## 내부적인 패키지 오류 368 | 369 | ### 플러그인 호환성 문제 370 | 371 | ``` 372 | MissingPluginException(No implementation found for method X on channel Y) 373 | ``` 374 | 375 | **원인**: 플러그인 초기화 문제 또는 플랫폼 지원 부재 376 | **해결 방법**: 377 | 378 | 1. 앱 재시작 379 | 2. `flutter clean` 실행 후 다시 빌드 380 | 3. 플러그인 호환성 확인 및 조건부 코드 작성 381 | 382 | ### 패키지 버전 충돌 383 | 384 | ``` 385 | Undefined name 'X'. (The name X isn't a type and can't be used in a declaration) 386 | ``` 387 | 388 | **원인**: API 변경으로 인한 코드 호환성 문제 389 | **해결 방법**: 390 | 391 | 1. 패키지 버전 확인 및 호환되는 버전 사용 392 | 2. 최신 API에 맞게 코드 업데이트 393 | 3. 코드 마이그레이션 가이드 참조 394 | 395 | ## 메모리 관련 오류 396 | 397 | ### 메모리 누수 398 | 399 | #### 컨트롤러 누수 400 | 401 | ``` 402 | A Timer still exists even after the widget tree was disposed. 403 | ``` 404 | 405 | **원인**: dispose 메서드에서 타이머, 컨트롤러 등을 해제하지 않음 406 | **해결 방법**: 407 | 408 | ```dart 409 | class _MyWidgetState extends State { 410 | AnimationController _controller; 411 | Timer _timer; 412 | 413 | @override 414 | void initState() { 415 | super.initState(); 416 | _controller = AnimationController(vsync: this); 417 | _timer = Timer.periodic(Duration(seconds: 1), (_) => update()); 418 | } 419 | 420 | @override 421 | void dispose() { 422 | _controller.dispose(); // 컨트롤러 해제 423 | _timer.cancel(); // 타이머 취소 424 | super.dispose(); 425 | } 426 | } 427 | ``` 428 | 429 | #### 대용량 이미지 메모리 문제 430 | 431 | ``` 432 | Out of memory: Bytes allocation failed 433 | ``` 434 | 435 | **원인**: 과도한 메모리 사용 436 | **해결 방법**: 437 | 438 | 1. 이미지 캐싱 라이브러리 사용 (cached_network_image) 439 | 2. 적절한 크기의 이미지 로드 (ResizeImage 또는 서버 측 리사이징) 440 | 3. 메모리에 저장하는 데이터 제한 441 | 442 | ## 코드 품질 오류 443 | 444 | ### 린트 오류 445 | 446 | ``` 447 | The parameter 'onPressed' is required 448 | ``` 449 | 450 | **원인**: 필수 매개변수 누락 451 | **해결 방법**: 코드 분석 도구가 지적한 문제 해결 452 | 453 | ### 성능 이슈 454 | 455 | #### 과도한 빌드 호출 456 | 457 | **원인**: 불필요한 위젯 리빌드로 인한 성능 저하 458 | **해결 방법**: 459 | 460 | 1. `const` 생성자 사용 461 | 2. 상태 변경을 더 작은 위젯으로 분리 462 | 3. `RepaintBoundary`를 사용하여 리페인트 영역 제한 463 | 464 | ```dart 465 | // 개선 전 466 | ListView.builder( 467 | itemBuilder: (context, index) { 468 | return MyListItem(data: items[index]); 469 | } 470 | ) 471 | 472 | // 개선 후 473 | ListView.builder( 474 | itemBuilder: (context, index) { 475 | return RepaintBoundary( 476 | child: const MyListItem(data: items[index]), 477 | ); 478 | } 479 | ) 480 | ``` 481 | 482 | ## 디버깅 도구 483 | 484 | ### Flutter DevTools 485 | 486 | Flutter DevTools는 Flutter 앱 개발 시 강력한 디버깅 도구입니다. 다음과 같은 기능을 제공합니다: 487 | 488 | 1. **Inspector**: 위젯 트리 분석 및 레이아웃 디버깅 489 | 2. **Performance**: 프레임 드롭 분석 490 | 3. **Memory**: 메모리 사용량 및 누수 확인 491 | 4. **Network**: 네트워크 요청 모니터링 492 | 5. **Logging**: 로그 확인 및 필터링 493 | 494 | ### 효과적인 로깅 전략 495 | 496 | 효과적인 로깅은 문제를 빠르게 파악하는 데 도움이 됩니다: 497 | 498 | 1. **로깅 레벨 구분**: 499 | 500 | ```dart 501 | import 'package:logger/logger.dart'; 502 | 503 | final logger = Logger( 504 | printer: PrettyPrinter(), 505 | ); 506 | 507 | // 다양한 로깅 레벨 사용 508 | logger.v("Verbose log"); 509 | logger.d("Debug log"); 510 | logger.i("Info log"); 511 | logger.w("Warning log"); 512 | logger.e("Error log"); 513 | ``` 514 | 515 | 2. **예외 처리**: 516 | ```dart 517 | try { 518 | // 위험한 작업 519 | } catch (e, stackTrace) { 520 | logger.e("오류 발생", error: e, stackTrace: stackTrace); 521 | } 522 | ``` 523 | 524 | ## 결론 525 | 526 | Flutter 앱 개발 중 발생하는 대부분의 오류는 체계적인 접근 방식으로 해결할 수 있습니다. 문제를 정확히 이해하고, 검색 엔진이나 Stack Overflow 같은 자료를 활용하며, 필요하다면 Flutter 이슈 트래커나 커뮤니티에 도움을 요청하세요. 527 | 528 | 오류 메시지를 무시하지 말고, 로그를 주의 깊게 읽고 분석하는 습관을 들이면 문제 해결 능력이 크게 향상됩니다. 또한 정기적으로 Flutter와 종속성을 최신 상태로 유지하는 것도 많은 문제를 예방할 수 있는 좋은 방법입니다. 529 | -------------------------------------------------------------------------------- /src/content/docs/part8/cicd-codemagic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Codemagic을 활용한 CI/CD 구성 3 | --- 4 | 5 | Flutter 앱 개발에서 지속적 통합(CI) 및 지속적 배포(CD)는 개발 및 배포 과정을 자동화하여 효율성을 높이고 오류를 줄이는 중요한 요소입니다. Codemagic은 Flutter 앱에 특화된 CI/CD 플랫폼으로, 쉽게 구성하고 사용할 수 있습니다. 6 | 7 | ## CI/CD 개요 8 | 9 | CI/CD는 개발 워크플로우를 개선하기 위한 방법론입니다: 10 | 11 | - **지속적 통합(CI)**: 개발자가 코드 변경사항을 주기적으로 통합하고, 자동화된 빌드와 테스트를 통해 빠르게 문제를 발견하는 방식 12 | - **지속적 배포(CD)**: 빌드와 테스트를 통과한 코드를 자동으로 배포 환경에 릴리스하는 방식 13 | 14 | ## Codemagic 소개 15 | 16 | Codemagic은 Flutter 앱 개발을 위해 설계된 CI/CD 플랫폼으로, 다음과 같은 특징을 제공합니다: 17 | 18 | - Flutter 전용 빌드 환경 19 | - 다양한 플랫폼(iOS, Android, Web, macOS) 지원 20 | - 간편한 설정과 직관적인 UI 21 | - 자동 버전 관리 22 | - 앱스토어 및 구글 플레이 스토어 자동 배포 23 | - TestFlight, Firebase App Distribution 등 통합 24 | - 빌드 알림(이메일, Slack) 25 | 26 | ## Codemagic 설정 방법 27 | 28 | ### 1. 계정 생성 및 프로젝트 연결 29 | 30 | 1. [Codemagic 웹사이트](https://codemagic.io/signup)에서 계정 생성 31 | 2. GitHub, GitLab, Bitbucket 등 코드 리포지토리 연결 32 | 3. Flutter 프로젝트 선택 33 | 34 | ### 2. 빌드 설정 방법 35 | 36 | Codemagic에서는 두 가지 방법으로 빌드를 설정할 수 있습니다: 37 | 38 | 1. **UI를 통한 설정**: 웹 인터페이스에서 직관적으로 설정 39 | 2. **YAML 파일을 통한 설정**: `codemagic.yaml` 파일로 빌드 파이프라인 정의 40 | 41 | ### UI를 통한 설정 42 | 43 | 1. 프로젝트 선택 후 "Start your first build" 클릭 44 | 2. 빌드 설정 구성: 45 | - 빌드할 플랫폼 선택 (iOS / Android / Web) 46 | - Flutter 버전 선택 47 | - 빌드 트리거 설정 (브랜치, 태그 등) 48 | - 환경 변수 설정 49 | - 빌드 스크립트 설정 50 | 3. "Start new build" 클릭하여 빌드 시작 51 | 52 | ### YAML 파일을 통한 설정 53 | 54 | 프로젝트 루트에 `codemagic.yaml` 파일을 생성합니다: 55 | 56 | ```yaml 57 | workflows: 58 | flutter-app: 59 | name: Flutter App 60 | environment: 61 | flutter: stable 62 | xcode: latest 63 | cocoapods: default 64 | cache: 65 | cache_paths: 66 | - ~/.pub-cache 67 | - pubspec.lock 68 | triggering: 69 | events: 70 | - push 71 | branch_patterns: 72 | - pattern: "main" 73 | include: true 74 | scripts: 75 | - name: Flutter analyze 76 | script: flutter analyze 77 | - name: Flutter test 78 | script: flutter test 79 | - name: Build iOS 80 | script: | 81 | flutter build ios --release --no-codesign 82 | - name: Build Android 83 | script: | 84 | flutter build appbundle --release 85 | artifacts: 86 | - build/ios/ipa/*.ipa 87 | - build/app/outputs/bundle/release/app-release.aab 88 | ``` 89 | 90 | ## 기본 CI/CD 워크플로우 구성 91 | 92 | 일반적인 Flutter 앱의 CI/CD 워크플로우는 다음과 같습니다: 93 | 94 | ### 1. 코드 검증 단계 95 | 96 | ```yaml 97 | scripts: 98 | - name: Flutter analyze 99 | script: flutter analyze 100 | - name: Flutter format check 101 | script: flutter format --set-exit-if-changed . 102 | - name: Flutter test 103 | script: flutter test 104 | ``` 105 | 106 | ### 2. 빌드 단계 (Android) 107 | 108 | ```yaml 109 | scripts: 110 | - name: Build Android 111 | script: | 112 | flutter build apk --release 113 | flutter build appbundle --release 114 | artifacts: 115 | - build/app/outputs/flutter-apk/app-release.apk 116 | - build/app/outputs/bundle/release/app-release.aab 117 | ``` 118 | 119 | ### 3. 빌드 단계 (iOS) 120 | 121 | ```yaml 122 | scripts: 123 | - name: Set up code signing 124 | script: | 125 | echo $IOS_CERTIFICATE | base64 --decode > certificate.p12 126 | keychain add-certificates --certificate certificate.p12 --password $CERTIFICATE_PASSWORD 127 | app-store-connect fetch-signing-files $(BUNDLE_ID) --type IOS_APP_STORE --create 128 | keychain use-signing-files 129 | - name: Build iOS 130 | script: | 131 | flutter build ios --release 132 | cd ios 133 | xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release archive -archivePath Runner.xcarchive 134 | xcodebuild -exportArchive -archivePath Runner.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath ./build 135 | artifacts: 136 | - build/ios/ipa/*.ipa 137 | ``` 138 | 139 | ### 4. 배포 단계 140 | 141 | ```yaml 142 | publishing: 143 | app_store_connect: 144 | api_key: $APP_STORE_CONNECT_PRIVATE_KEY 145 | key_id: $APP_STORE_CONNECT_KEY_ID 146 | issuer_id: $APP_STORE_CONNECT_ISSUER_ID 147 | submit_to_testflight: true 148 | google_play: 149 | credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS 150 | track: internal # 또는 alpha, beta, production 151 | ``` 152 | 153 | ## 환경별 빌드 구성 (Flavors) 154 | 155 | 개발, 스테이징, 프로덕션 등 다양한 환경에 맞춰 빌드를 구성할 수 있습니다: 156 | 157 | ```yaml 158 | workflows: 159 | development: 160 | name: Development Build 161 | environment: 162 | flutter: stable 163 | triggering: 164 | events: 165 | - push 166 | branch_patterns: 167 | - pattern: "develop" 168 | include: true 169 | scripts: 170 | - name: Build Development 171 | script: flutter build apk --flavor development --target lib/main_development.dart 172 | 173 | staging: 174 | name: Staging Build 175 | environment: 176 | flutter: stable 177 | triggering: 178 | events: 179 | - push 180 | branch_patterns: 181 | - pattern: "staging" 182 | include: true 183 | scripts: 184 | - name: Build Staging 185 | script: flutter build apk --flavor staging --target lib/main_staging.dart 186 | 187 | production: 188 | name: Production Build 189 | environment: 190 | flutter: stable 191 | triggering: 192 | events: 193 | - push 194 | branch_patterns: 195 | - pattern: "main" 196 | include: true 197 | tag_patterns: 198 | - pattern: "v*.*.*" 199 | include: true 200 | scripts: 201 | - name: Build Production 202 | script: flutter build apk --flavor production --target lib/main_production.dart 203 | ``` 204 | 205 | ## 환경 변수 및 보안 206 | 207 | Codemagic에서는 다음과 같은 방법으로 보안 정보를 관리할 수 있습니다: 208 | 209 | ### 1. 웹 인터페이스를 통한 환경 변수 설정 210 | 211 | 1. 프로젝트 설정 > Environment variables 섹션 212 | 2. 변수 이름과 값 입력 213 | 3. 보안이 필요한 경우 "Secure" 옵션 선택 214 | 215 | ### 2. YAML 파일에서 환경 변수 참조 216 | 217 | ```yaml 218 | environment: 219 | vars: 220 | APP_ID: com.example.myapp 221 | flutter: stable 222 | scripts: 223 | - name: Use environment variable 224 | script: echo "Building app with ID: $APP_ID" 225 | ``` 226 | 227 | ### 3. 암호화된 파일 사용 228 | 229 | iOS 인증서나 Google Play 서비스 계정 키와 같은 파일은 암호화하여 사용할 수 있습니다: 230 | 231 | ```yaml 232 | environment: 233 | vars: 234 | ENCRYPTED_KEYSTORE_FILE: Encrypted(...) 235 | scripts: 236 | - name: Decode keystore 237 | script: echo $ENCRYPTED_KEYSTORE_FILE | base64 --decode > keystore.jks 238 | ``` 239 | 240 | ## 실전 활용 예제 241 | 242 | ### 예제 1: PR 검증 워크플로우 243 | 244 | Pull Request가 생성될 때 코드 품질을 검증하는 워크플로우: 245 | 246 | ```yaml 247 | workflows: 248 | pull-request-checks: 249 | name: Pull Request Checks 250 | instance_type: mac_mini_m1 251 | max_build_duration: 30 252 | environment: 253 | flutter: stable 254 | triggering: 255 | events: 256 | - pull_request 257 | scripts: 258 | - name: Get Flutter packages 259 | script: flutter pub get 260 | - name: Flutter analyze 261 | script: flutter analyze 262 | - name: Flutter format check 263 | script: flutter format --dry-run --set-exit-if-changed . 264 | - name: Flutter test 265 | script: flutter test --coverage 266 | - name: Upload coverage reports 267 | script: | 268 | # 코드 커버리지 보고서 업로드 스크립트 269 | bash <(curl -s https://codecov.io/bash) 270 | ``` 271 | 272 | ### 예제 2: 완전한 Android 빌드 및 배포 워크플로우 273 | 274 | Android 앱을 빌드하고 Google Play에 배포하는 워크플로우: 275 | 276 | ```yaml 277 | workflows: 278 | android-workflow: 279 | name: Android Release 280 | instance_type: linux 281 | max_build_duration: 60 282 | environment: 283 | android_signing: 284 | - keystore_reference 285 | vars: 286 | PACKAGE_NAME: "com.example.myapp" 287 | GOOGLE_PLAY_TRACK: internal 288 | flutter: stable 289 | triggering: 290 | events: 291 | - tag 292 | tag_patterns: 293 | - pattern: "v*.*.*" 294 | include: true 295 | scripts: 296 | - name: Set up build number 297 | script: | 298 | # 태그에서 버전 추출 299 | VERSION=$(echo $CM_TAG | cut -d'v' -f2) 300 | # pubspec.yaml 파일 업데이트 301 | sed -i "s/version: .*/version: $VERSION/" pubspec.yaml 302 | 303 | - name: Flutter test 304 | script: flutter test 305 | 306 | - name: Build AAB 307 | script: | 308 | flutter build appbundle \ 309 | --release \ 310 | --build-number=$(($(date +%s) / 60)) 311 | 312 | artifacts: 313 | - build/app/outputs/bundle/release/app-release.aab 314 | 315 | publishing: 316 | google_play: 317 | credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT 318 | track: $GOOGLE_PLAY_TRACK 319 | submit_as_draft: false 320 | ``` 321 | 322 | ### 예제 3: iOS 및 Android 동시 빌드 323 | 324 | iOS와 Android 앱을 동시에 빌드하고 배포하는 워크플로우: 325 | 326 | ```yaml 327 | workflows: 328 | ios-android-release: 329 | name: iOS & Android Release 330 | instance_type: mac_mini_m1 331 | max_build_duration: 120 332 | environment: 333 | ios_signing: 334 | distribution_type: app_store 335 | bundle_identifier: com.example.myapp 336 | flutter: stable 337 | triggering: 338 | events: 339 | - tag 340 | tag_patterns: 341 | - pattern: "v*.*.*" 342 | include: true 343 | scripts: 344 | - name: Set build number 345 | script: | 346 | BUILD_NUMBER=$(($(date +%s) / 60)) 347 | echo "Build number: $BUILD_NUMBER" 348 | 349 | - name: Flutter build iOS 350 | script: | 351 | flutter build ios --release \ 352 | --build-number=$BUILD_NUMBER \ 353 | --no-codesign 354 | 355 | - name: Flutter build Android 356 | script: | 357 | flutter build appbundle --release \ 358 | --build-number=$BUILD_NUMBER 359 | 360 | - name: iOS code signing and packaging 361 | script: | 362 | cd ios 363 | xcode-project use-profiles 364 | xcode-project build-ipa \ 365 | --workspace Runner.xcworkspace \ 366 | --scheme Runner 367 | 368 | artifacts: 369 | - build/ios/ipa/*.ipa 370 | - build/app/outputs/bundle/release/app-release.aab 371 | - flutter_drive.log 372 | 373 | publishing: 374 | app_store_connect: 375 | api_key: $APP_STORE_CONNECT_KEY 376 | submit_to_testflight: true 377 | google_play: 378 | credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT 379 | track: internal 380 | ``` 381 | 382 | ## 빌드 성능 최적화 팁 383 | 384 | Codemagic에서 빌드 시간을 단축하기 위한 팁: 385 | 386 | 1. **캐싱 활용**: 387 | 388 | ```yaml 389 | cache: 390 | cache_paths: 391 | - ~/.pub-cache 392 | - ~/.gradle 393 | - ~/.cocoapods 394 | ``` 395 | 396 | 2. **불필요한 스크립트 제거**: 397 | 테스트나 분석이 필요 없는 릴리스 빌드에서는 해당 스크립트 제거 398 | 399 | 3. **적절한 인스턴스 유형 선택**: 400 | 401 | ```yaml 402 | workflows: 403 | my-workflow: 404 | instance_type: mac_mini_m1 # 더 빠른 M1 인스턴스 사용 405 | ``` 406 | 407 | 4. **병렬 실행 활용**: 408 | ```yaml 409 | scripts: 410 | - name: Parallel jobs 411 | script: | 412 | flutter analyze & 413 | flutter test & 414 | wait # 모든 백그라운드 작업이 완료될 때까지 대기 415 | ``` 416 | 417 | ## 테스트 자동화 및 품질 관리 418 | 419 | ### 코드 커버리지 보고 420 | 421 | ```yaml 422 | scripts: 423 | - name: Run tests with coverage 424 | script: | 425 | flutter test --coverage 426 | lcov --remove coverage/lcov.info '**/*.g.dart' '**/*.freezed.dart' -o coverage/lcov.info 427 | genhtml coverage/lcov.info -o coverage/html 428 | artifacts: 429 | - coverage/html/** 430 | ``` 431 | 432 | ### 통합 테스트 433 | 434 | ```yaml 435 | scripts: 436 | - name: Integration tests 437 | script: | 438 | # 에뮬레이터 시작 439 | flutter emulators --launch flutter_emulator 440 | 441 | # 통합 테스트 실행 442 | flutter drive \ 443 | --driver=test_driver/integration_test.dart \ 444 | --target=integration_test/app_test.dart \ 445 | -d flutter_emulator 446 | ``` 447 | 448 | ## 결론 449 | 450 | Codemagic은 Flutter 앱 개발을 위한 강력한 CI/CD 도구로, 다양한 기능과 유연한 설정으로 개발 및 배포 과정을 효율적으로 자동화할 수 있습니다. 이 가이드에서 소개한 설정과 예제를 활용하여 프로젝트에 맞는 CI/CD 파이프라인을 구축하면 개발 생산성을 크게 향상시킬 수 있습니다. 451 | 452 | 기본적인 검증 및 빌드 자동화부터 시작해서, 점진적으로 배포 자동화, 테스트 자동화, 품질 관리 등을 추가하며 워크플로우를 개선하는 것이 좋은 접근 방식입니다. 453 | -------------------------------------------------------------------------------- /src/content/docs/part9/folder-structure.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 기능별 vs 계층별 폴더 구조 3 | --- 4 | import { FileTree } from '@astrojs/starlight/components'; 5 | 6 | Flutter 프로젝트를 시작할 때 가장 중요한 결정 중 하나는 코드를 어떻게 구성할 것인지 결정하는 것입니다. 적절한 프로젝트 구조는 코드의 가독성을 높이고, 확장성을 향상시키며, 팀 협업을 원활하게 합니다. 이 문서에서는 두 가지 주요 프로젝트 구조 접근 방식인 '기능별 구조'와 '계층별 구조'에 대해 살펴보고, 각각의 장단점 및 적합한 사용 사례를 분석합니다. 7 | 8 | ## 프로젝트 구조의 중요성 9 | 10 | 11 | 좋은 프로젝트 구조는 다음과 같은 이점을 제공합니다: 12 | 13 | 1. **새로운 개발자의 온보딩 시간 단축**: 직관적인 구조는 새로운 팀원이 프로젝트를 더 빠르게 이해하도록 돕습니다. 14 | 2. **코드 충돌 감소**: 잘 정의된 구조는 여러 개발자가 동시에 작업할 때 충돌을 줄입니다. 15 | 3. **기능 구현 시간 단축**: 관련 코드를 쉽게 찾고 수정할 수 있습니다. 16 | 4. **테스트 용이성**: 모듈화된 구조는 테스트를 더 쉽게 작성하고 실행할 수 있게 합니다. 17 | 5. **코드 재사용 촉진**: 잘 구성된 구조는 코드 재사용을 장려하고 중복을 줄입니다. 18 | 19 | ## 계층별 폴더 구조 20 | 21 | 계층별 폴더 구조(Layer-based Structure)는 코드를 기술적 관심사 또는 아키텍처 계층에 따라 구성하는 방식입니다. 22 | 23 | ### 계층별 구조의 기본 예시 24 | 25 | 26 | 27 | - lib/ 28 | - models/ # 데이터 모델 클래스 29 | - user.dart 30 | - product.dart 31 | - order.dart 32 | - views/ # UI 컴포넌트 및 화면 33 | - home_screen.dart 34 | - product_screen.dart 35 | - profile_screen.dart 36 | - controllers/ # 비즈니스 로직 및 상태 관리 37 | - auth_controller.dart 38 | - product_controller.dart 39 | - order_controller.dart 40 | - services/ # 외부 서비스 통신 (API, 데이터베이스 등) 41 | - api_service.dart 42 | - storage_service.dart 43 | - analytics_service.dart 44 | - utils/ # 유틸리티 함수 및 상수 45 | - constants.dart 46 | - extensions.dart 47 | - helpers.dart 48 | - main.dart 49 | 50 | 51 | 52 | ### 계층별 구조의 변형: MVVM 패턴 53 | 54 | 55 | 56 | - lib/ 57 | - data/ 58 | - models/ # 데이터 모델 59 | - repositories/ # 데이터 액세스 계층 60 | - data_sources/ # 로컬/원격 데이터 소스 61 | - domain/ 62 | - entities/ # 비즈니스 모델 63 | - repositories/ # 리포지토리 인터페이스 64 | - usecases/ # 비즈니스 규칙 및 로직 65 | - presentation/ 66 | - pages/ # 화면 67 | - widgets/ # 재사용 가능한 UI 컴포넌트 68 | - viewmodels/ # 뷰 모델 (상태 관리) 69 | - core/ 70 | - utils/ # 유틸리티 함수 71 | - constants/ # 상수 72 | - theme/ # 앱 테마 73 | - main.dart 74 | 75 | 76 | 77 | ### 계층별 구조의 장점 78 | 79 | 1. **기술적 관심사 분리**: 코드를 역할에 따라 명확하게 구분합니다. 80 | 2. **구조 이해 용이성**: 새로운 개발자가 아키텍처를 쉽게 이해할 수 있습니다. 81 | 3. **역할 기반 작업 분담**: 프론트엔드/백엔드 개발자가 각자 담당 영역에 집중할 수 있습니다. 82 | 4. **유사한 코드 패턴**: 같은 계층에 있는 코드는 유사한 패턴을 따르기 쉽습니다. 83 | 5. **기술 스택 변경 용이성**: 특정 계층의 기술을 교체할 때 영향 범위가 제한적입니다. 84 | 85 | ### 계층별 구조의 단점 86 | 87 | 1. **관련 코드 분산**: 하나의 기능을 구현하기 위해 여러 폴더를 탐색해야 합니다. 88 | 2. **파일 수 증가에 따른 복잡성**: 프로젝트가 커지면 각 폴더의 파일 수가 많아져 찾기 어려워집니다. 89 | 3. **기능 추가 시 여러 폴더 수정**: 새 기능 추가 시 여러 폴더에 걸쳐 파일을 생성/수정해야 합니다. 90 | 4. **관련 코드의 결합도 파악 어려움**: 서로 다른 폴더에 있는 관련 코드 간의 관계를 파악하기 어렵습니다. 91 | 5. **코드 재사용 저해**: 특정 기능에 특화된 코드를 식별하고 재사용하기 어려울 수 있습니다. 92 | 93 | ## 기능별 폴더 구조 94 | 95 | 기능별 폴더 구조(Feature-based Structure)는 코드를 비즈니스 기능이나 앱의 주요 기능에 따라 구성하는 방식입니다. 96 | 97 | ### 기능별 구조의 기본 예시 98 | 99 | 100 | 101 | - lib/ 102 | - features/ 103 | - auth/ # 인증 관련 기능 104 | - data/ 105 | - repositories/ 106 | - models/ 107 | - domain/ 108 | - usecases/ 109 | - presentation/ 110 | - pages/ 111 | - login_page.dart 112 | - signup_page.dart 113 | - widgets/ 114 | - providers/ 115 | - products/ # 제품 관련 기능 116 | - data/ 117 | - domain/ 118 | - presentation/ 119 | - pages/ 120 | - product_list_page.dart 121 | - product_detail_page.dart 122 | - widgets/ 123 | - providers/ 124 | - profile/ # 프로필 관련 기능 125 | - data/ 126 | - domain/ 127 | - presentation/ 128 | - core/ # 공통 기능 129 | - network/ 130 | - storage/ 131 | - theme/ 132 | - utils/ 133 | - main.dart 134 | 135 | 136 | 137 | ### 기능별 구조의 변형: DDD(Domain-Driven Design) 적용 138 | 139 | 140 | 141 | - lib/ 142 | - application/ # 애플리케이션 서비스 (UseCase) 143 | - auth/ 144 | - products/ 145 | - profile/ 146 | - domain/ # 도메인 모델 및 규칙 147 | - auth/ 148 | - products/ 149 | - profile/ 150 | - infrastructure/ # 인프라 계층 (리포지토리 구현, 데이터 소스) 151 | - auth/ 152 | - products/ 153 | - profile/ 154 | - presentation/ # UI 계층 155 | - auth/ 156 | - products/ 157 | - profile/ 158 | - shared/ # 공통 기능 159 | - constants/ 160 | - extensions/ 161 | - widgets/ 162 | - main.dart 163 | 164 | 165 | 166 | ### 기능별 구조의 장점 167 | 168 | 1. **관련 코드 근접성**: 하나의 기능과 관련된 모든 코드가 같은 폴더에 위치합니다. 169 | 2. **독립적인 기능 개발**: 각 기능을 독립적으로 개발하고 테스트할 수 있습니다. 170 | 3. **기능 단위의 캡슐화**: 각 기능은 자체 모델, 뷰, 로직을 포함하여 자율적입니다. 171 | 4. **기능별 작업 분담**: 팀원이 특정 기능에 집중하여 작업할 수 있습니다. 172 | 5. **확장성**: 새로운 기능을 추가할 때 기존 코드를 변경할 필요가 적습니다. 173 | 6. **코드 재사용**: 기능별로 특화된 코드를 쉽게 식별하고 재사용할 수 있습니다. 174 | 175 | ### 기능별 구조의 단점 176 | 177 | 1. **중복 코드 가능성**: 여러 기능 간에 유사한 코드가 중복될 수 있습니다. 178 | 2. **일관성 유지 어려움**: 각 기능별로 다른 패턴이 적용될 수 있습니다. 179 | 3. **기능 간 경계 설정 어려움**: 어떤 코드가 어느 기능에 속하는지 결정하기 어려울 수 있습니다. 180 | 4. **공통 코드 관리**: 여러 기능에서 사용하는 공통 코드의 위치를 결정하기 어려울 수 있습니다. 181 | 5. **아키텍처 이해의 어려움**: 전체 아키텍처를 한눈에 파악하기 어려울 수 있습니다. 182 | 183 | ## 실제 프로젝트 구조 예시: Riverpod + GoRouter 184 | 185 | ### 계층별 구조 예시 186 | 187 | 188 | 189 | - lib/ 190 | - models/ # 데이터 모델 191 | - user.dart 192 | - product.dart 193 | - order.dart 194 | - providers/ # Riverpod 프로바이더 195 | - auth_provider.dart 196 | - product_provider.dart 197 | - cart_provider.dart 198 | - repositories/ # 데이터 액세스 계층 199 | - auth_repository.dart 200 | - product_repository.dart 201 | - order_repository.dart 202 | - screens/ # 화면 위젯 203 | - auth/ 204 | - login_screen.dart 205 | - signup_screen.dart 206 | - products/ 207 | - product_list_screen.dart 208 | - product_detail_screen.dart 209 | - profile/ 210 | - profile_screen.dart 211 | - widgets/ # 재사용 위젯 212 | - product_card.dart 213 | - custom_button.dart 214 | - loading_indicator.dart 215 | - router/ # GoRouter 설정 216 | - router.dart 217 | - utils/ # 유틸리티 218 | - constants.dart 219 | - extensions.dart 220 | - main.dart 221 | 222 | 223 | 224 | ### 기능별 구조 예시 225 | 226 | 227 | 228 | - lib/ 229 | - features/ 230 | - auth/ 231 | - models/ 232 | - user.dart 233 | - repositories/ 234 | - auth_repository.dart 235 | - providers/ 236 | - auth_provider.dart 237 | - screens/ 238 | - login_screen.dart 239 | - signup_screen.dart 240 | - widgets/ 241 | - login_form.dart 242 | - social_login_buttons.dart 243 | - products/ 244 | - models/ 245 | - product.dart 246 | - repositories/ 247 | - product_repository.dart 248 | - providers/ 249 | - product_provider.dart 250 | - screens/ 251 | - product_list_screen.dart 252 | - product_detail_screen.dart 253 | - widgets/ 254 | - product_card.dart 255 | - cart/ 256 | - models/ 257 | - cart_item.dart 258 | - repositories/ 259 | - cart_repository.dart 260 | - providers/ 261 | - cart_provider.dart 262 | - screens/ 263 | - cart_screen.dart 264 | - checkout_screen.dart 265 | - widgets/ 266 | - cart_item_widget.dart 267 | - core/ 268 | - router/ 269 | - router.dart 270 | - theme/ 271 | - app_theme.dart 272 | - widgets/ 273 | - custom_button.dart 274 | - loading_indicator.dart 275 | - utils/ 276 | - constants.dart 277 | - extensions.dart 278 | - main.dart 279 | 280 | 281 | 282 | ## 하이브리드 접근 방식 283 | 284 | 많은 실제 프로젝트에서는 두 가지 접근 방식을 혼합하여 사용합니다. 다음은 하이브리드 구조의 예시입니다: 285 | 286 | 287 | 288 | - lib/ 289 | - features/ # 주요 기능별 구성 290 | - auth/ 291 | - products/ 292 | - cart/ 293 | - shared/ # 공유 컴포넌트 294 | - models/ # 공통 모델 295 | - widgets/ # 공통 위젯 296 | - services/ # 공통 서비스 297 | - utils/ # 유틸리티 298 | - core/ # 핵심 인프라 299 | - network/ # 네트워크 관련 300 | - storage/ # 로컬 스토리지 301 | - di/ # 의존성 주입 302 | - router/ # 라우팅 303 | - main.dart 304 | 305 | 306 | 307 | 이 접근 방식은 다음과 같은 이점을 제공합니다: 308 | 309 | 1. **주요 기능의 독립성**: 핵심 기능은 독립적으로 구성 310 | 2. **공통 코드 관리**: 여러 기능에서 공유하는 코드는 중앙에서 관리 311 | 3. **핵심 인프라 분리**: 앱의 기반이 되는 인프라 코드를 명확하게 분리 312 | 4. **유연성**: 프로젝트 요구사항에 맞게 구조를 조정할 수 있음 313 | 314 | ## Riverpod과 함께 사용하는 패턴 315 | 316 | Riverpod을 사용할 때는 프로바이더를 어떻게 구성할지도 중요한 고려 사항입니다: 317 | 318 | ### 계층별 구조에서의 Riverpod 패턴 319 | 320 | ```dart 321 | // providers/product_provider.dart 322 | final productRepositoryProvider = Provider((ref) { 323 | return ProductRepositoryImpl(ref.read(apiServiceProvider)); 324 | }); 325 | 326 | final productsProvider = FutureProvider>((ref) async { 327 | final repository = ref.watch(productRepositoryProvider); 328 | return repository.getProducts(); 329 | }); 330 | 331 | final productDetailsProvider = FutureProvider.family((ref, id) async { 332 | final repository = ref.watch(productRepositoryProvider); 333 | return repository.getProductById(id); 334 | }); 335 | ``` 336 | 337 | ### 기능별 구조에서의 Riverpod 패턴 338 | 339 | ```dart 340 | // features/products/providers/product_provider.dart 341 | final productRepositoryProvider = Provider((ref) { 342 | return ProductRepositoryImpl(ref.read(apiServiceProvider)); 343 | }); 344 | 345 | final productsProvider = FutureProvider>((ref) async { 346 | final repository = ref.watch(productRepositoryProvider); 347 | return repository.getProducts(); 348 | }); 349 | 350 | final productDetailsProvider = FutureProvider.family((ref, id) async { 351 | final repository = ref.watch(productRepositoryProvider); 352 | return repository.getProductById(id); 353 | }); 354 | ``` 355 | 356 | ### 추천 패턴: 계층적 프로바이더 구성 357 | 358 | ```dart 359 | // features/products/providers/product_provider.dart 360 | 361 | // 1. 데이터 소스 프로바이더 (최하위 계층) 362 | @riverpod 363 | ProductDataSource productDataSource(ProductDataSourceRef ref) { 364 | final apiClient = ref.watch(apiClientProvider); 365 | return ProductDataSourceImpl(apiClient); 366 | } 367 | 368 | // 2. 리포지토리 프로바이더 369 | @riverpod 370 | ProductRepository productRepository(ProductRepositoryRef ref) { 371 | final dataSource = ref.watch(productDataSourceProvider); 372 | return ProductRepositoryImpl(dataSource); 373 | } 374 | 375 | // 3. 유스케이스 프로바이더 (선택적) 376 | @riverpod 377 | GetProductsUseCase getProductsUseCase(GetProductsUseCaseRef ref) { 378 | final repository = ref.watch(productRepositoryProvider); 379 | return GetProductsUseCase(repository); 380 | } 381 | 382 | // 4. 상태 프로바이더 383 | @riverpod 384 | class ProductsNotifier extends _$ProductsNotifier { 385 | @override 386 | FutureOr> build() async { 387 | return _fetchProducts(); 388 | } 389 | 390 | Future> _fetchProducts() { 391 | final useCase = ref.watch(getProductsUseCaseProvider); 392 | return useCase.execute(); 393 | } 394 | 395 | Future refresh() async { 396 | state = const AsyncValue.loading(); 397 | state = await AsyncValue.guard(_fetchProducts); 398 | } 399 | } 400 | ``` 401 | 402 | ## 어떤 구조를 선택해야 할까? 403 | 404 | 프로젝트 구조 선택 시 고려해야 할 요소: 405 | 406 | ### 계층별 구조가 적합한 경우 407 | 408 | 1. **소규모 프로젝트**: 기능이 적고 단순한 앱 409 | 2. **명확한 기술적 분리**: 프론트엔드/백엔드 개발자 역할이 명확히 구분된 팀 410 | 3. **아키텍처 패턴 중시**: MVC, MVVM과 같은 아키텍처 패턴을 엄격히 따르고자 할 때 411 | 4. **초보 개발자 팀**: 명확한 폴더 구조가 필요한 경우 412 | 413 | ### 기능별 구조가 적합한 경우 414 | 415 | 1. **중대형 프로젝트**: 다양한 기능이 있는 복잡한 앱 416 | 2. **수직적 팀 구조**: 팀원이 특정 기능을 전담하는 경우 417 | 3. **마이크로서비스 지향**: 각 기능을 독립적으로 개발/테스트하고자 할 때 418 | 4. **기능 단위 배포**: 기능별로 점진적 배포를 계획하는 경우 419 | 5. **도메인 중심 설계**: DDD(Domain-Driven Design) 원칙을 따르는 경우 420 | 421 | ## 실제 업계 사례 422 | 423 | 대형 Flutter 앱들의 구조를 살펴보면 다음과 같은 패턴을 볼 수 있습니다: 424 | 425 | 1. **Google의 Flutter 샘플 앱**: 기능별 구조를 선호 (Flutter Gallery, Flutter Samples) 426 | 2. **Alibaba의 Flutter 앱**: 하이브리드 구조 채택 (일부 공통 모듈은 계층별, 주요 기능은 기능별) 427 | 3. **중소규모 앱**: 초기에는 계층별 구조로 시작하여 점차 기능별 또는 하이브리드 구조로 전환하는 경향 428 | 429 | ## 결론 430 | 431 | 프로젝트 구조는 정답이 없는 주제입니다. 중요한 것은 팀과 프로젝트 특성에 맞는 구조를 선택하고, 일관성 있게 유지하는 것입니다. 432 | 433 | - **소규모/단순 앱**: 계층별 구조가 직관적이고 빠르게 구성 가능 434 | - **중대형/복잡한 앱**: 기능별 구조 또는 하이브리드 구조가 유지보수와 확장성에 유리 435 | - **성장 예상 앱**: 처음부터 기능별 구조 또는 하이브리드 구조로 시작하는 것이 장기적으로 유리 436 | 437 | 어떤 구조를 선택하든, 코드 가독성, 유지보수성, 확장성, 팀 협업 용이성이라는 핵심 목표를 기억하며 구조를 설계해야 합니다. 또한 프로젝트가 발전함에 따라 구조를 재평가하고 필요에 따라 조정하는 유연성을 유지하는 것이 중요합니다. 438 | -------------------------------------------------------------------------------- /src/content/docs/part8/error-tracking.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 에러 추적. Crashlytics와 Sentry 3 | --- 4 | 5 | 모든 개발자는 완벽한 앱을 만들고 싶지만, 현실에서는 크래시와 예기치 않은 오류가 발생할 수 있습니다. 이러한 문제를 효과적으로 관리하기 위해 에러 추적 도구를 사용하는 것이 중요합니다. 이 문서에서는 Flutter 앱에서 사용할 수 있는 두 가지 주요 에러 추적 도구인 Firebase Crashlytics와 Sentry에 대해 알아보겠습니다. 6 | 7 | ## 에러 추적의 중요성 8 | 9 | 앱 출시 후 사용자 경험에 영향을 미치는 문제를 빠르게 발견하고 수정하는 것은 매우 중요합니다. 10 | 11 | 12 | 에러 추적 도구는 다음과 같은 이점을 제공합니다: 13 | 14 | 1. **실시간 모니터링**: 앱에서 발생하는 문제를 실시간으로 감지 15 | 2. **자세한 오류 정보**: 오류 발생 상황, 디바이스 정보, 앱 상태 등 상세 정보 제공 16 | 3. **우선순위 결정**: 가장 많이 발생하는 오류나 심각한, 최근에 발생한 오류에 집중 17 | 4. **사용자 영향 파악**: 얼마나 많은 사용자가 영향을 받는지 확인 18 | 5. **트렌드 분석**: 시간에 따른 오류 발생 패턴 분석 19 | 20 | ## Firebase Crashlytics 21 | 22 | Firebase Crashlytics는 Google에서 제공하는 경량 실시간 크래시 리포팅 도구입니다. 23 | 24 | ### Firebase Crashlytics 특징 25 | 26 | - **실시간 크래시 리포팅**: 오류 발생 직후 알림 27 | - **이슈 우선순위 지정**: 영향을 받는 사용자 수, 심각도에 따른 자동 정렬 28 | - **크래시 인사이트**: 문제의 근본 원인 파악 지원 29 | - **최소한의 앱 성능 영향**: 백그라운드에서 효율적으로 작동 30 | - **Firebase 생태계 통합**: Analytics, Performance Monitoring 등과 통합 31 | 32 | ### Firebase Crashlytics 설정하기 33 | 34 | #### 1. Firebase 설정 35 | 36 | Firebase 프로젝트가 아직 설정되지 않았다면, 먼저 [Firebase Console](https://console.firebase.google.com/)에서 프로젝트를 생성해야 합니다: 37 | 38 | 1. Firebase Console에서 프로젝트 생성 39 | 2. Flutter 앱 등록 (Android 및 iOS 패키지 이름 입력) 40 | 3. 설정 파일 다운로드 및 적용 (`google-services.json` 및 `GoogleService-Info.plist`) 41 | 42 | #### 2. 필요한 패키지 추가 43 | 44 | pubspec.yaml에 다음 패키지를 추가합니다: 45 | 46 | ```yaml 47 | dependencies: 48 | firebase_core: ^3.13.0 49 | firebase_crashlytics: ^4.3.5 50 | ``` 51 | 52 | 패키지를 설치합니다: 53 | 54 | ```bash 55 | flutter pub get 56 | ``` 57 | 58 | #### 3. Flutter 앱에서 Crashlytics 초기화 59 | 60 | 앱의 메인 파일에서 Crashlytics를 초기화합니다: 61 | 62 | ```dart 63 | import 'package:firebase_core/firebase_core.dart'; 64 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 65 | import 'package:flutter/foundation.dart'; 66 | import 'package:flutter/material.dart'; 67 | 68 | Future main() async { 69 | WidgetsFlutterBinding.ensureInitialized(); 70 | await Firebase.initializeApp(); 71 | 72 | // Crashlytics 설정 73 | await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(!kDebugMode); 74 | 75 | // Flutter 오류를 Crashlytics에 보고 76 | FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; 77 | 78 | // Zone 오류도 잡아서 Crashlytics에 보고 79 | runZonedGuarded>(() async { 80 | runApp(const MyApp()); 81 | }, (error, stack) { 82 | FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); 83 | }); 84 | } 85 | ``` 86 | 87 | ### Crashlytics 사용하기 88 | 89 | #### 1. 에러 기록하기 90 | 91 | ```dart 92 | try { 93 | // 위험한 작업 수행 94 | } catch (e, stack) { 95 | FirebaseCrashlytics.instance.recordError(e, stack); 96 | } 97 | ``` 98 | 99 | #### 2. 커스텀 로그 추가하기 100 | 101 | ```dart 102 | // 로그 추가 103 | FirebaseCrashlytics.instance.log('사용자가 결제 버튼 클릭'); 104 | 105 | // 로그와 함께 에러 기록 106 | FirebaseCrashlytics.instance.recordError( 107 | exception, 108 | stackTrace, 109 | reason: '결제 처리 중 오류', 110 | information: ['상품 ID: $productId', '사용자 ID: $userId'], 111 | ); 112 | ``` 113 | 114 | #### 3. 사용자 정보 설정 115 | 116 | ```dart 117 | // 비식별 사용자 ID 설정 118 | FirebaseCrashlytics.instance.setUserIdentifier(userId); 119 | 120 | // 커스텀 키-값 정보 추가 121 | FirebaseCrashlytics.instance.setCustomKey('subscription_type', 'premium'); 122 | FirebaseCrashlytics.instance.setCustomKey('last_purchase_date', '2023-10-15'); 123 | ``` 124 | 125 | #### 4. 테스트를 위한 강제 크래시 126 | 127 | ```dart 128 | ElevatedButton( 129 | onPressed: () { 130 | FirebaseCrashlytics.instance.crash(); // 앱 크래시 발생 131 | }, 132 | child: Text('테스트 크래시 발생'), 133 | ), 134 | ``` 135 | 136 | ### Riverpod과 함께 사용하기 137 | 138 | Riverpod을 사용하는 경우, 에러 관찰자를 만들 수 있습니다: 139 | 140 | ```dart 141 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 142 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 143 | 144 | // Crashlytics 프로바이더 145 | final crashlyticsProvider = Provider((ref) { 146 | return FirebaseCrashlytics.instance; 147 | }); 148 | 149 | // Riverpod 에러 관찰자 150 | class CrashlyticsObserver extends ProviderObserver { 151 | final FirebaseCrashlytics crashlytics; 152 | 153 | CrashlyticsObserver(this.crashlytics); 154 | 155 | @override 156 | void providerDidFail( 157 | ProviderBase provider, 158 | Object error, 159 | StackTrace stackTrace, 160 | ProviderContainer container, 161 | ) { 162 | crashlytics.recordError( 163 | error, 164 | stackTrace, 165 | reason: 'Provider 오류: ${provider.name ?? provider.runtimeType}', 166 | ); 167 | } 168 | } 169 | 170 | // main에서 설정 171 | void main() { 172 | // ... Firebase 초기화 코드 173 | 174 | final container = ProviderContainer( 175 | observers: [ 176 | CrashlyticsObserver(FirebaseCrashlytics.instance), 177 | ], 178 | ); 179 | 180 | runApp(UncontrolledProviderScope( 181 | container: container, 182 | child: const MyApp(), 183 | )); 184 | } 185 | ``` 186 | 187 | ## Sentry 188 | 189 | Sentry는 다양한 플랫폼에서 오류 추적, 성능 모니터링, 사용자 피드백 수집 등을 지원하는 종합 모니터링 플랫폼입니다. 190 | 191 | ### Sentry 특징 192 | 193 | - **다양한 플랫폼 지원**: Flutter, Web, 서버 등 통합 모니터링 194 | - **실시간 오류 추적**: 발생 직후 알림 195 | - **성능 모니터링**: 앱 성능 측정 및 병목 현상 파악 196 | - **릴리스 추적**: 특정 버전에서의 오류 추적 197 | - **풍부한 컨텍스트**: 사용자 정보, 브레드크럼, 태그 등 상세 정보 198 | - **자체 호스팅 옵션**: 온프레미스 설치 가능 199 | 200 | ### Sentry 설정하기 201 | 202 | #### 1. 계정 생성 및 프로젝트 설정 203 | 204 | 1. [Sentry 웹사이트](https://sentry.io/)에서 계정 생성 205 | 2. 새 프로젝트 생성 (Flutter 플랫폼 선택) 206 | 3. DSN(Data Source Name) 복사 207 | 208 | #### 2. 패키지 추가 209 | 210 | pubspec.yaml에 다음 패키지를 추가합니다: 211 | 212 | ```yaml 213 | dependencies: 214 | sentry_flutter: ^8.14.2 215 | ``` 216 | 217 | 패키지를 설치합니다: 218 | 219 | ```bash 220 | flutter pub get 221 | ``` 222 | 223 | #### 3. Sentry 초기화 224 | 225 | 앱의 메인 파일에서 Sentry를 초기화합니다: 226 | 227 | ```dart 228 | import 'package:flutter/material.dart'; 229 | import 'package:sentry_flutter/sentry_flutter.dart'; 230 | 231 | Future main() async { 232 | await SentryFlutter.init( 233 | (options) { 234 | options.dsn = 'https://your-dsn-here@o0.ingest.sentry.io/0'; 235 | options.tracesSampleRate = 1.0; // 성능 모니터링 활성화 (0.0 - 1.0) 236 | options.environment = 'production'; // 환경 설정 237 | options.attachScreenshot = true; // 스크린샷 첨부 (iOS/Android만 지원) 238 | options.attachViewHierarchy = true; // UI 계층 구조 첨부 239 | options.debug = false; // 프로덕션에서는 false로 설정 240 | }, 241 | appRunner: () => runApp(const MyApp()), 242 | ); 243 | } 244 | ``` 245 | 246 | ### Sentry 사용하기 247 | 248 | #### 1. 에러 캡처하기 249 | 250 | ```dart 251 | try { 252 | // 위험한 작업 수행 253 | } catch (exception, stackTrace) { 254 | await Sentry.captureException( 255 | exception, 256 | stackTrace: stackTrace, 257 | ); 258 | } 259 | ``` 260 | 261 | #### 2. 커스텀 이벤트 기록하기 262 | 263 | ```dart 264 | Sentry.captureMessage( 265 | '사용자가 결제를 완료했습니다', 266 | level: SentryLevel.info, 267 | ); 268 | ``` 269 | 270 | #### 3. 트랜잭션 및 성능 모니터링 271 | 272 | ```dart 273 | // 트랜잭션 시작 274 | final transaction = Sentry.startTransaction( 275 | 'processPayment', 276 | 'operation', 277 | ); 278 | 279 | try { 280 | // 작업 수행 281 | await processPayment(); 282 | 283 | // 작업 성공 284 | transaction.status = SpanStatus.ok(); 285 | } catch (exception) { 286 | // 작업 실패 287 | transaction.status = SpanStatus.internalError(); 288 | await Sentry.captureException(exception); 289 | } finally { 290 | // 트랜잭션 종료 291 | await transaction.finish(); 292 | } 293 | ``` 294 | 295 | #### 4. 사용자 정보 추가 296 | 297 | ```dart 298 | Sentry.configureScope((scope) { 299 | scope.setUser(SentryUser( 300 | id: 'user-123', 301 | email: 'user@example.com', 302 | ipAddress: '{{auto}}', 303 | data: {'subscription': 'premium'}, 304 | )); 305 | }); 306 | ``` 307 | 308 | #### 5. 브레드크럼 추가 309 | 310 | 브레드크럼은 오류 발생 전의 사용자 활동을 추적하는 데 유용합니다: 311 | 312 | ```dart 313 | // 사용자 활동 기록 314 | Sentry.addBreadcrumb( 315 | Breadcrumb( 316 | category: 'ui.click', 317 | message: '결제 버튼 클릭', 318 | level: SentryLevel.info, 319 | data: {'productId': '12345'}, 320 | ), 321 | ); 322 | ``` 323 | 324 | ### Riverpod과 함께 사용하기 325 | 326 | ```dart 327 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 328 | import 'package:sentry_flutter/sentry_flutter.dart'; 329 | 330 | // Riverpod 에러 관찰자 331 | class SentryProviderObserver extends ProviderObserver { 332 | @override 333 | void providerDidFail( 334 | ProviderBase provider, 335 | Object error, 336 | StackTrace stackTrace, 337 | ProviderContainer container, 338 | ) { 339 | Sentry.captureException( 340 | error, 341 | stackTrace: stackTrace, 342 | hint: Hint.withMap({ 343 | 'provider': provider.name ?? provider.runtimeType.toString(), 344 | }), 345 | ); 346 | } 347 | } 348 | 349 | // main에서 설정 350 | void main() async { 351 | await SentryFlutter.init( 352 | (options) { 353 | options.dsn = 'your-dsn-here'; 354 | // 기타 옵션 설정 355 | }, 356 | appRunner: () { 357 | final container = ProviderContainer( 358 | observers: [SentryProviderObserver()], 359 | ); 360 | 361 | runApp(UncontrolledProviderScope( 362 | container: container, 363 | child: const MyApp(), 364 | )); 365 | }, 366 | ); 367 | } 368 | ``` 369 | 370 | ## Crashlytics와 Sentry 비교 371 | 372 | 두 도구 모두 훌륭한 오류 추적 기능을 제공하지만, 몇 가지 차이점이 있습니다: 373 | 374 | | 기능 | Firebase Crashlytics | Sentry | 375 | | ------------- | --------------------------------------- | --------------------------- | 376 | | 가격 | 무료 | 제한적 무료 티어, 유료 플랜 | 377 | | 설정 난이도 | 쉬움 | 중간 | 378 | | 플랫폼 지원 | 모바일 중심 | 모바일, 웹, 서버 등 다양 | 379 | | 생태계 통합 | Firebase 서비스와 통합 | 다양한 플랫폼 지원 | 380 | | 성능 모니터링 | 제한적 (Firebase Performance 별도 사용) | 기본 포함 | 381 | | 사용자 피드백 | 지원하지 않음 | 지원 | 382 | | 자체 호스팅 | 지원하지 않음 | 지원 | 383 | | 실시간 알림 | 이메일, 슬랙 등 | 이메일, 슬랙, PagerDuty 등 | 384 | 385 | ### Crashlytics가 적합한 경우 386 | 387 | - Firebase 생태계를 이미 사용 중인 경우 388 | - 무료 솔루션이 필요한 경우 389 | - 모바일 앱에 특화된 기능이 필요한 경우 390 | - 간단한 설정을 선호하는 경우 391 | 392 | ### Sentry가 적합한 경우 393 | 394 | - 여러 플랫폼(모바일, 웹, 서버 등)을 모니터링하는 경우 395 | - 더 상세한 오류 컨텍스트가 필요한 경우 396 | - 성능 모니터링이 중요한 경우 397 | - 사용자 피드백 수집 기능이 필요한 경우 398 | - 데이터 주권이 중요하여 자체 호스팅이 필요한 경우 399 | 400 | ## 에러 추적 모범 사례 401 | 402 | ### 1. 의미 있는 오류 컨텍스트 제공 403 | 404 | ```dart 405 | try { 406 | await api.fetchData(); 407 | } catch (e, stack) { 408 | // 구체적인 맥락 정보 추가 409 | await errorTracker.recordError( 410 | e, 411 | stack, 412 | reason: 'API 데이터 가져오기 실패', 413 | information: [ 414 | '엔드포인트: /users', 415 | '요청 파라미터: $parameters', 416 | '네트워크 상태: ${connectivity.status}', 417 | ], 418 | ); 419 | } 420 | ``` 421 | 422 | ### 2. 사용자를 구분하되 개인정보 보호 423 | 424 | ```dart 425 | // 좋은 예: 식별 가능하지만 개인정보는 아닌 ID 사용 426 | errorTracker.setUserIdentifier('user_12345'); 427 | 428 | // 나쁜 예: 개인 이메일 사용 429 | errorTracker.setUserIdentifier('user@example.com'); // 개인정보! 430 | ``` 431 | 432 | ### 3. 중요 흐름에 브레드크럼 추가 433 | 434 | ```dart 435 | // 주요 사용자 액션 추적 436 | void onCheckoutButtonPressed() { 437 | // 브레드크럼 추가 438 | errorTracker.addBreadcrumb( 439 | 'checkout_started', 440 | {'cart_total': '$cartTotal', 'items_count': '${cart.items.length}'}, 441 | ); 442 | 443 | // 결제 로직 실행 444 | startCheckoutProcess(); 445 | } 446 | ``` 447 | 448 | ### 4. 릴리스 및 환경 구분 449 | 450 | ```dart 451 | // Crashlytics 452 | FirebaseCrashlytics.instance.setCustomKey('app_version', '1.2.3'); 453 | FirebaseCrashlytics.instance.setCustomKey('environment', 'production'); 454 | 455 | // Sentry 456 | await SentryFlutter.init( 457 | (options) { 458 | options.dsn = 'your-dsn'; 459 | options.release = '1.2.3'; 460 | options.environment = 'production'; 461 | }, 462 | appRunner: () => runApp(MyApp()), 463 | ); 464 | ``` 465 | 466 | ### 5. 비동기 초기화 패턴 사용 467 | 468 | ```dart 469 | Future initializeApp() async { 470 | try { 471 | await Firebase.initializeApp(); 472 | await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); 473 | 474 | // 다른 초기화 로직 475 | await loadSettings(); 476 | await authenticateUser(); 477 | } catch (e, stack) { 478 | FirebaseCrashlytics.instance.recordError(e, stack); 479 | // 폴백 또는 기본 설정으로 진행 480 | } 481 | } 482 | ``` 483 | 484 | ### 6. 디버그와 릴리스 모드 분리 485 | 486 | ```dart 487 | // 디버그 모드에서는 에러 추적 비활성화 488 | bool shouldEnableErrorTracking = kReleaseMode; 489 | 490 | // 또는 더 세분화된 제어 491 | bool shouldEnableErrorTracking = kReleaseMode || kProfileMode; 492 | 493 | FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled( 494 | shouldEnableErrorTracking, 495 | ); 496 | ``` 497 | 498 | ## 결론 499 | 500 | 효과적인 에러 추적은 안정적인 앱을 유지하고 사용자 경험을 향상시키는 데 필수적입니다. Firebase Crashlytics와 Sentry는 각자의 강점을 가진 뛰어난 도구로, 프로젝트의 요구사항에 맞게 선택할 수 있습니다. 501 | 502 | Crashlytics는 Firebase 생태계와의 통합, 무료 사용, 간편한 설정이 장점이며, Sentry는 크로스 플랫폼 지원, 성능 모니터링, 사용자 피드백, 자체 호스팅 등의 고급 기능을 제공합니다. 503 | 504 | 어떤 도구를 선택하든, 의미 있는 컨텍스트를 제공하고, 사용자 개인정보를 보호하며, 중요 흐름을 추적하는 등의 모범 사례를 따르는 것이 중요합니다. 이를 통해 오류를 효과적으로 감지하고 해결하여 사용자에게 더 안정적인 앱 경험을 제공할 수 있습니다. 505 | -------------------------------------------------------------------------------- /src/content/docs/part10/custom-painting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CustomPainter와 RenderBox 이해 3 | --- 4 | 5 | Flutter에서 복잡한 사용자 정의 UI를 구현하기 위해서는 기본 위젯만으로는 한계가 있습니다. 더 유연하고 세밀한 UI를 구현하기 위해 Flutter는 저수준 그래픽 API인 `CustomPainter`와 렌더링 시스템의 기본 요소인 `RenderBox`를 제공합니다. 이 장에서는 이 두 가지 중요한 개념을 자세히 알아보겠습니다. 6 | 7 | ## CustomPainter 개요 8 | 9 | `CustomPainter`는 Flutter에서 직접 캔버스에 그리기 위한 강력한 도구입니다. 벡터 그래픽, 차트, 복잡한 애니메이션, 커스텀 진행 표시기 등을 구현할 때 매우 유용합니다. 10 | 11 | 12 | ### CustomPainter 사용 방법 13 | 14 | `CustomPainter`를 사용하려면 다음 세 가지 주요 구성 요소가 필요합니다: 15 | 16 | 1. `CustomPainter`를 상속받는 클래스 17 | 2. `CustomPaint` 위젯 18 | 3. `Paint` 객체로 스타일 정의 19 | 20 | #### 1. CustomPainter 클래스 구현 21 | 22 | ```dart 23 | class MyPainter extends CustomPainter { 24 | @override 25 | void paint(Canvas canvas, Size size) { 26 | // 여기에 그리기 코드 작성 27 | } 28 | 29 | @override 30 | bool shouldRepaint(covariant MyPainter oldDelegate) { 31 | // 다시 그려야 하는지 결정 32 | return false; 33 | } 34 | } 35 | ``` 36 | 37 | #### 2. CustomPaint 위젯 사용 38 | 39 | ```dart 40 | CustomPaint( 41 | painter: MyPainter(), 42 | size: Size(300, 200), // 명시적 크기 지정 43 | child: Container(), // 선택적 자식 위젯 44 | ) 45 | ``` 46 | 47 | 또는 자식 위젯의 크기에 맞추기: 48 | 49 | ```dart 50 | SizedBox( 51 | width: 300, 52 | height: 200, 53 | child: CustomPaint( 54 | painter: MyPainter(), 55 | child: Container(), // 선택적 자식 위젯 56 | ), 57 | ) 58 | ``` 59 | 60 | ### 기본 도형 그리기 61 | 62 | `Canvas` 객체는 다양한 도형을 그리기 위한 메서드를 제공합니다: 63 | 64 | ```dart 65 | class BasicShapesPainter extends CustomPainter { 66 | @override 67 | void paint(Canvas canvas, Size size) { 68 | final paint = Paint() 69 | ..color = Colors.blue 70 | ..strokeWidth = 4.0 71 | ..style = PaintingStyle.stroke; 72 | 73 | // 선 그리기 74 | canvas.drawLine( 75 | Offset(0, 0), 76 | Offset(size.width, size.height), 77 | paint, 78 | ); 79 | 80 | // 사각형 그리기 81 | canvas.drawRect( 82 | Rect.fromLTWH(50, 50, 100, 100), 83 | paint..color = Colors.red, 84 | ); 85 | 86 | // 원 그리기 87 | canvas.drawCircle( 88 | Offset(size.width / 2, size.height / 2), 89 | 50, 90 | paint..color = Colors.green, 91 | ); 92 | 93 | // 둥근 사각형 그리기 94 | canvas.drawRRect( 95 | RRect.fromRectAndRadius( 96 | Rect.fromLTWH(200, 50, 100, 100), 97 | Radius.circular(20), 98 | ), 99 | paint..color = Colors.orange, 100 | ); 101 | } 102 | 103 | @override 104 | bool shouldRepaint(covariant BasicShapesPainter oldDelegate) => false; 105 | } 106 | ``` 107 | 108 | ### Path를 사용한 복잡한 도형 그리기 109 | 110 | 더 복잡한 도형은 `Path` 클래스를 사용하여 그릴 수 있습니다: 111 | 112 | ```dart 113 | class PathPainter extends CustomPainter { 114 | @override 115 | void paint(Canvas canvas, Size size) { 116 | final paint = Paint() 117 | ..color = Colors.purple 118 | ..style = PaintingStyle.stroke 119 | ..strokeWidth = 3; 120 | 121 | final path = Path(); 122 | 123 | // 시작점 설정 124 | path.moveTo(0, size.height / 2); 125 | 126 | // 곡선 그리기 127 | path.quadraticBezierTo( 128 | size.width / 4, 0, 129 | size.width / 2, size.height / 2, 130 | ); 131 | 132 | path.quadraticBezierTo( 133 | size.width * 3 / 4, size.height, 134 | size.width, size.height / 2, 135 | ); 136 | 137 | canvas.drawPath(path, paint); 138 | } 139 | 140 | @override 141 | bool shouldRepaint(covariant PathPainter oldDelegate) => false; 142 | } 143 | ``` 144 | 145 | ### 실제 예제: 커스텀 차트 구현 146 | 147 | 다음은 간단한 막대 차트를 구현하는 예제입니다: 148 | 149 | ```dart 150 | class BarChartPainter extends CustomPainter { 151 | final List dataPoints; 152 | final double maxValue; 153 | 154 | BarChartPainter({ 155 | required this.dataPoints, 156 | required this.maxValue, 157 | }); 158 | 159 | @override 160 | void paint(Canvas canvas, Size size) { 161 | final barWidth = size.width / dataPoints.length; 162 | final paint = Paint() 163 | ..color = Colors.blue 164 | ..style = PaintingStyle.fill; 165 | 166 | // 데이터 포인트에 따라 막대 그리기 167 | for (int i = 0; i < dataPoints.length; i++) { 168 | final barHeight = (dataPoints[i] / maxValue) * size.height; 169 | 170 | canvas.drawRect( 171 | Rect.fromLTWH( 172 | i * barWidth, 173 | size.height - barHeight, 174 | barWidth - 4, // 막대 사이 간격 175 | barHeight, 176 | ), 177 | paint, 178 | ); 179 | } 180 | 181 | // x축 그리기 182 | canvas.drawLine( 183 | Offset(0, size.height), 184 | Offset(size.width, size.height), 185 | Paint() 186 | ..color = Colors.black 187 | ..strokeWidth = 2, 188 | ); 189 | } 190 | 191 | @override 192 | bool shouldRepaint(covariant BarChartPainter oldDelegate) { 193 | return oldDelegate.dataPoints != dataPoints || 194 | oldDelegate.maxValue != maxValue; 195 | } 196 | } 197 | 198 | // 사용 예시 199 | CustomPaint( 200 | painter: BarChartPainter( 201 | dataPoints: [30, 80, 40, 90, 60], 202 | maxValue: 100, 203 | ), 204 | size: Size(300, 200), 205 | ) 206 | ``` 207 | 208 | ## shouldRepaint 메서드 최적화 209 | 210 | `shouldRepaint` 메서드는 이전에 그린 것과 비교하여 다시 그려야 하는지 결정합니다. 이 메서드를 올바르게 구현하면 성능을 크게 향상시킬 수 있습니다: 211 | 212 | ```dart 213 | class OptimizedPainter extends CustomPainter { 214 | final Color color; 215 | final double strokeWidth; 216 | 217 | OptimizedPainter({ 218 | required this.color, 219 | required this.strokeWidth, 220 | }); 221 | 222 | @override 223 | void paint(Canvas canvas, Size size) { 224 | // 그리기 로직... 225 | } 226 | 227 | @override 228 | bool shouldRepaint(covariant OptimizedPainter oldDelegate) { 229 | // 변경 사항이 있을 때만 다시 그리기 230 | return oldDelegate.color != color || 231 | oldDelegate.strokeWidth != strokeWidth; 232 | } 233 | } 234 | ``` 235 | 236 | ## CustomPainter 애니메이션 237 | 238 | `AnimationController`와 `CustomPainter`를 결합하여 애니메이션 효과를 만들 수 있습니다: 239 | 240 | ```dart 241 | class AnimatedCirclePainter extends CustomPainter { 242 | final double animationValue; 243 | 244 | AnimatedCirclePainter({required this.animationValue}); 245 | 246 | @override 247 | void paint(Canvas canvas, Size size) { 248 | final center = Offset(size.width / 2, size.height / 2); 249 | final radius = 50.0 + 20.0 * animationValue; 250 | 251 | final paint = Paint() 252 | ..color = Color.lerp( 253 | Colors.blue, 254 | Colors.red, 255 | animationValue, 256 | )! 257 | ..style = PaintingStyle.fill; 258 | 259 | canvas.drawCircle(center, radius, paint); 260 | } 261 | 262 | @override 263 | bool shouldRepaint(covariant AnimatedCirclePainter oldDelegate) { 264 | return oldDelegate.animationValue != animationValue; 265 | } 266 | } 267 | 268 | // 사용 예시 (StatefulWidget 내부) 269 | Widget build(BuildContext context) { 270 | return AnimatedBuilder( 271 | animation: _controller, // AnimationController 272 | builder: (context, child) { 273 | return CustomPaint( 274 | painter: AnimatedCirclePainter( 275 | animationValue: _controller.value, 276 | ), 277 | size: Size(300, 300), 278 | ); 279 | }, 280 | ); 281 | } 282 | ``` 283 | 284 | ## RenderBox 이해하기 285 | 286 | `RenderBox`는 Flutter 렌더링 시스템의 핵심 요소로, 위젯의 레이아웃과 그리기 로직을 담당합니다. 일반적으로 개발자는 직접 `RenderBox`를 다루기보다는 위젯 API를 통해 간접적으로 사용합니다. 287 | 288 | ### RenderBox 계층 구조 289 | 290 | Flutter의 렌더링 시스템은 세 가지 주요 계층으로 구성됩니다: 291 | 292 | 1. **위젯(Widget)**: 불변의 설정 객체 293 | 2. **요소(Element)**: 위젯의 인스턴스 294 | 3. **렌더 객체(RenderObject/RenderBox)**: 실제 레이아웃과 그리기 담당 295 | 296 | ### 커스텀 RenderBox 만들기 297 | 298 | 커스텀 `RenderBox`를 만드는 것은 고급 주제이지만, 특수한 레이아웃 동작이 필요할 때 유용합니다. 전체 과정은 다음과 같습니다: 299 | 300 | 1. `RenderBox`를 상속받는 클래스 구현 301 | 2. `SingleChildRenderObjectWidget`을 상속받는 위젯 구현 302 | 3. `SingleChildRenderObjectElement`를 확장하는 요소 구현 (선택적) 303 | 304 | 간단한 예제를 살펴보겠습니다: 305 | 306 | ```dart 307 | // 1. RenderBox 구현 308 | class RenderCenterSquare extends RenderBox { 309 | @override 310 | void performLayout() { 311 | // 원하는 크기 계산 312 | size = constraints.biggest; 313 | } 314 | 315 | @override 316 | void paint(PaintingContext context, Offset offset) { 317 | // 중앙에 정사각형 그리기 318 | final canvas = context.canvas; 319 | final squareSize = size.shortestSide / 2; 320 | final center = Offset(size.width / 2, size.height / 2); 321 | final rect = Rect.fromCenter( 322 | center: center, 323 | width: squareSize, 324 | height: squareSize, 325 | ); 326 | 327 | canvas.drawRect( 328 | rect, 329 | Paint()..color = Colors.purple, 330 | ); 331 | } 332 | } 333 | 334 | // 2. SingleChildRenderObjectWidget 구현 335 | class CenterSquare extends SingleChildRenderObjectWidget { 336 | const CenterSquare({Key? key, Widget? child}) : super(key: key, child: child); 337 | 338 | @override 339 | RenderObject createRenderObject(BuildContext context) { 340 | return RenderCenterSquare(); 341 | } 342 | } 343 | ``` 344 | 345 | ### RenderBox vs CustomPainter 346 | 347 | `RenderBox`와 `CustomPainter`의 주요 차이점은: 348 | 349 | 1. **목적**: 350 | 351 | - `CustomPainter`: 그리기 전용 352 | - `RenderBox`: 레이아웃과 그리기 모두 담당 353 | 354 | 2. **복잡성**: 355 | 356 | - `CustomPainter`: 비교적 간단한 구현 357 | - `RenderBox`: 더 복잡하지만 유연성 높음 358 | 359 | 3. **사용 사례**: 360 | - `CustomPainter`: 복잡한 그래픽, 차트, 사용자 정의 표시 361 | - `RenderBox`: 사용자 정의 레이아웃 동작이 필요한 경우 362 | 363 | ## 실전 사례: 서명 패드 구현 364 | 365 | `CustomPainter`를 사용한 실제 응용 사례로 서명 패드를 구현해 보겠습니다: 366 | 367 | ```dart 368 | class SignaturePainter extends CustomPainter { 369 | final List> strokes; 370 | 371 | SignaturePainter({required this.strokes}); 372 | 373 | @override 374 | void paint(Canvas canvas, Size size) { 375 | final paint = Paint() 376 | ..color = Colors.black 377 | ..strokeWidth = 3.0 378 | ..strokeCap = StrokeCap.round 379 | ..style = PaintingStyle.stroke; 380 | 381 | for (final stroke in strokes) { 382 | if (stroke.length < 2) continue; 383 | 384 | final path = Path(); 385 | path.moveTo(stroke[0].dx, stroke[0].dy); 386 | 387 | for (int i = 1; i < stroke.length; i++) { 388 | path.lineTo(stroke[i].dx, stroke[i].dy); 389 | } 390 | 391 | canvas.drawPath(path, paint); 392 | } 393 | } 394 | 395 | @override 396 | bool shouldRepaint(covariant SignaturePainter oldDelegate) { 397 | return oldDelegate.strokes != strokes; 398 | } 399 | } 400 | 401 | // 사용 예시 (StatefulWidget 내부) 402 | class SignaturePad extends StatefulWidget { 403 | @override 404 | _SignaturePadState createState() => _SignaturePadState(); 405 | } 406 | 407 | class _SignaturePadState extends State { 408 | List> strokes = []; 409 | List currentStroke = []; 410 | 411 | @override 412 | Widget build(BuildContext context) { 413 | return GestureDetector( 414 | onPanStart: (details) { 415 | setState(() { 416 | currentStroke = [details.localPosition]; 417 | strokes.add(currentStroke); 418 | }); 419 | }, 420 | onPanUpdate: (details) { 421 | setState(() { 422 | currentStroke.add(details.localPosition); 423 | // 참조를 통해 strokes가 자동으로 업데이트됨 424 | }); 425 | }, 426 | child: CustomPaint( 427 | painter: SignaturePainter(strokes: strokes), 428 | size: Size.infinite, 429 | ), 430 | ); 431 | } 432 | } 433 | ``` 434 | 435 | ## 성능 최적화 팁 436 | 437 | ### 1. Path 최적화 438 | 439 | 많은 점을 그릴 때는 개별 선보다 `Path`를 사용하는 것이 훨씬 효율적입니다: 440 | 441 | ```dart 442 | // 비효율적인 방법 443 | for (int i = 1; i < points.length; i++) { 444 | canvas.drawLine(points[i - 1], points[i], paint); 445 | } 446 | 447 | // 더 효율적인 방법 448 | final path = Path(); 449 | path.moveTo(points[0].dx, points[0].dy); 450 | for (int i = 1; i < points.length; i++) { 451 | path.lineTo(points[i].dx, points[i].dy); 452 | } 453 | canvas.drawPath(path, paint); 454 | ``` 455 | 456 | ### 2. 클리핑 사용 457 | 458 | 필요한 영역만 그리기 위해 클리핑을 활용할 수 있습니다: 459 | 460 | ```dart 461 | canvas.clipRect(Rect.fromLTWH(0, 0, size.width, size.height)); 462 | ``` 463 | 464 | ### 3. 캐싱 활용 465 | 466 | 자주 변경되지 않는 복잡한 그림은 캐싱을 고려하세요: 467 | 468 | ```dart 469 | class CachedPainter extends CustomPainter { 470 | ui.Image? _cachedImage; 471 | 472 | Future _createCachedImage(Size size) async { 473 | if (_cachedImage != null) return; 474 | 475 | final pictureRecorder = ui.PictureRecorder(); 476 | final canvas = Canvas(pictureRecorder); 477 | 478 | // 복잡한 그리기 작업 수행 479 | 480 | final picture = pictureRecorder.endRecording(); 481 | _cachedImage = await picture.toImage( 482 | size.width.toInt(), 483 | size.height.toInt(), 484 | ); 485 | } 486 | 487 | @override 488 | void paint(Canvas canvas, Size size) async { 489 | await _createCachedImage(size); 490 | if (_cachedImage != null) { 491 | canvas.drawImage(_cachedImage!, Offset.zero, Paint()); 492 | } 493 | } 494 | 495 | @override 496 | bool shouldRepaint(covariant CustomPainter oldDelegate) => false; 497 | } 498 | ``` 499 | 500 | ## 결론 501 | 502 | `CustomPainter`와 `RenderBox`는 Flutter에서 복잡한 UI와 그래픽을 구현하기 위한 강력한 도구입니다. `CustomPainter`는 비교적 쉽게 시작할 수 있으며 대부분의 사용자 정의 그리기 요구사항을 충족합니다. 반면 `RenderBox`는 더 복잡하지만 완전히 사용자 정의된 레이아웃 동작이 필요한 경우에 유용합니다. 503 | 504 | 이러한 도구를 이해하고 활용하면 기본 위젯으로는 구현하기 어려운 복잡한 UI 요소를 만들 수 있습니다. 차트, 그래프, 커스텀 애니메이션, 게임 UI 등 다양한 응용 분야에서 활용할 수 있습니다. 505 | 506 | 다음 장에서는 위젯 캐싱과 `RepaintBoundary`를 활용하여 Flutter 앱의 렌더링 성능을 최적화하는 방법에 대해 알아보겠습니다. 507 | -------------------------------------------------------------------------------- /src/content/docs/part7/widget-test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 위젯 테스트 3 | --- 4 | 5 | 단위 테스트가 개별 함수나 클래스의 동작을 검증하는 것이라면, 위젯 테스트는 Flutter 위젯의 렌더링과 상호작용을 검증합니다. 이 장에서는 Flutter에서 위젯 테스트를 작성하고 실행하는 방법을 알아보겠습니다. 6 | 7 | ## 위젯 테스트의 필요성 8 | 9 | 위젯 테스트를 작성해야 하는 이유는 다음과 같습니다: 10 | 11 | 1. **UI 일관성 보장**: 변경 사항이 위젯의 외관과 동작에 미치는 영향을 검증합니다. 12 | 2. **사용자 상호작용 검증**: 탭, 슬라이드, 스크롤 등 사용자 상호작용이 예상대로 작동하는지 확인합니다. 13 | 3. **상태 관리 테스트**: 위젯의 상태 변경이 UI에 올바르게 반영되는지 검증합니다. 14 | 4. **통합 검증**: 여러 위젯이 함께 작동할 때의 동작을 검증합니다. 15 | 5. **회귀 방지**: UI 변경이 기존 기능을 손상시키지 않는지 확인합니다. 16 | 17 | ## 위젯 테스트 설정 18 | 19 | 위젯 테스트는 `flutter_test` 패키지를 사용하며, 이 패키지는 Flutter SDK에 기본으로 포함되어 있습니다. `pubspec.yaml` 파일에 다음과 같이 의존성이 포함되어 있어야 합니다: 20 | 21 | ```yaml 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | ``` 26 | 27 | ## 기본 위젯 테스트 작성하기 28 | 29 | 간단한 위젯 테스트 예제를 살펴보겠습니다. 다음은 테스트할 간단한 카운터 앱 위젯입니다: 30 | 31 | ```dart 32 | // lib/widgets/counter.dart 33 | import 'package:flutter/material.dart'; 34 | 35 | class Counter extends StatefulWidget { 36 | const Counter({Key? key}) : super(key: key); 37 | 38 | @override 39 | _CounterState createState() => _CounterState(); 40 | } 41 | 42 | class _CounterState extends State { 43 | int _count = 0; 44 | 45 | void _increment() { 46 | setState(() { 47 | _count++; 48 | }); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Column( 54 | mainAxisAlignment: MainAxisAlignment.center, 55 | children: [ 56 | Text( 57 | 'Counter Value:', 58 | style: Theme.of(context).textTheme.headlineSmall, 59 | ), 60 | Text( 61 | '$_count', 62 | style: Theme.of(context).textTheme.headlineMedium, 63 | ), 64 | ElevatedButton( 65 | onPressed: _increment, 66 | child: const Text('Increment'), 67 | ), 68 | ], 69 | ); 70 | } 71 | } 72 | ``` 73 | 74 | 이제 이 `Counter` 위젯을 테스트하는 코드를 작성해 보겠습니다: 75 | 76 | ```dart 77 | // test/widgets/counter_test.dart 78 | import 'package:flutter/material.dart'; 79 | import 'package:flutter_test/flutter_test.dart'; 80 | import 'package:my_app/widgets/counter.dart'; 81 | 82 | void main() { 83 | testWidgets('Counter increments when button is pressed', (WidgetTester tester) async { 84 | // 위젯 렌더링 85 | await tester.pumpWidget(const MaterialApp( 86 | home: Scaffold( 87 | body: Counter(), 88 | ), 89 | )); 90 | 91 | // 초기 상태 확인: 카운터 값이 0인지 확인 92 | expect(find.text('0'), findsOneWidget); 93 | expect(find.text('1'), findsNothing); 94 | 95 | // 버튼 찾기 및 탭 동작 수행 96 | await tester.tap(find.byType(ElevatedButton)); 97 | 98 | // 위젯 리빌드 99 | await tester.pump(); 100 | 101 | // 변경된 상태 확인: 카운터 값이 1로 증가했는지 확인 102 | expect(find.text('0'), findsNothing); 103 | expect(find.text('1'), findsOneWidget); 104 | }); 105 | } 106 | ``` 107 | 108 | ### 중요한 단계 설명 109 | 110 | 1. **위젯 펌핑(Pumping)**: 111 | 112 | - `await tester.pumpWidget()`: 테스트 환경에 위젯을 렌더링합니다. 113 | - `await tester.pump()`: 위젯을 리빌드합니다(상태 변경 후 UI 업데이트). 114 | 115 | 2. **위젯 찾기(Finding)**: 116 | 117 | - `find.text()`: 특정 텍스트를 포함한 위젯을 찾습니다. 118 | - `find.byType()`: 특정 타입의 위젯을 찾습니다. 119 | - `find.byKey()`: 특정 키를 가진 위젯을 찾습니다. 120 | 121 | 3. **상호작용(Interaction)**: 122 | 123 | - `await tester.tap()`: 위젯을 탭합니다. 124 | - `await tester.drag()`: 드래그 동작을 수행합니다. 125 | - `await tester.enterText()`: 텍스트 필드에 텍스트를 입력합니다. 126 | 127 | 4. **검증(Verification)**: 128 | - `expect(finder, matcher)`: 찾은 위젯이 예상한 상태인지 확인합니다. 129 | - 일반적인 matcher: `findsOneWidget`, `findsNothing`, `findsNWidgets(n)` 등 130 | 131 | ## 다양한 위젯 테스트 기법 132 | 133 | ### 1. 텍스트 입력 테스트 134 | 135 | ```dart 136 | testWidgets('TextField updates when text is entered', (WidgetTester tester) async { 137 | final textFieldKey = Key('my-textfield'); 138 | 139 | await tester.pumpWidget(MaterialApp( 140 | home: Scaffold( 141 | body: TextField(key: textFieldKey), 142 | ), 143 | )); 144 | 145 | // 텍스트 입력 146 | await tester.enterText(find.byKey(textFieldKey), '안녕하세요'); 147 | await tester.pump(); 148 | 149 | // 입력된 텍스트 확인 150 | expect(find.text('안녕하세요'), findsOneWidget); 151 | }); 152 | ``` 153 | 154 | ### 2. 스크롤 테스트 155 | 156 | ```dart 157 | testWidgets('ListView can be scrolled', (WidgetTester tester) async { 158 | await tester.pumpWidget(MaterialApp( 159 | home: Scaffold( 160 | body: ListView.builder( 161 | itemCount: 100, 162 | itemBuilder: (context, index) => ListTile( 163 | title: Text('항목 $index'), 164 | ), 165 | ), 166 | ), 167 | )); 168 | 169 | // 항목 50은 처음에는 화면에 보이지 않음 170 | expect(find.text('항목 50'), findsNothing); 171 | 172 | // 아래로 스크롤 173 | await tester.dragUntilVisible( 174 | find.text('항목 50'), 175 | find.byType(ListView), 176 | Offset(0, -500), // 위쪽으로 드래그 177 | ); 178 | 179 | // 스크롤 후 항목 50이 화면에 보임 180 | expect(find.text('항목 50'), findsOneWidget); 181 | }); 182 | ``` 183 | 184 | ### 3. 탭과 제스처 테스트 185 | 186 | ```dart 187 | testWidgets('GestureDetector responds to tap', (WidgetTester tester) async { 188 | bool tapped = false; 189 | 190 | await tester.pumpWidget(MaterialApp( 191 | home: Scaffold( 192 | body: GestureDetector( 193 | onTap: () { 194 | tapped = true; 195 | }, 196 | child: Container( 197 | width: 100, 198 | height: 100, 199 | color: Colors.blue, 200 | ), 201 | ), 202 | ), 203 | )); 204 | 205 | // 초기 상태 206 | expect(tapped, false); 207 | 208 | // 탭 수행 209 | await tester.tap(find.byType(Container)); 210 | 211 | // 탭 후 상태 212 | expect(tapped, true); 213 | }); 214 | ``` 215 | 216 | ### 4. 애니메이션 테스트 217 | 218 | ```dart 219 | testWidgets('Animation completes correctly', (WidgetTester tester) async { 220 | await tester.pumpWidget(MaterialApp( 221 | home: MyAnimatedWidget(), 222 | )); 223 | 224 | // 애니메이션 시작 전 상태 확인 225 | final initialTransform = tester.widget(find.byType(Transform)).transform; 226 | 227 | // 애니메이션 트리거 228 | await tester.tap(find.byType(ElevatedButton)); 229 | 230 | // 애니메이션의 중간 프레임 확인 (300ms) 231 | await tester.pump(Duration(milliseconds: 300)); 232 | 233 | // 애니메이션 완료까지 기다림 234 | await tester.pumpAndSettle(); 235 | 236 | // 애니메이션 완료 후 상태 확인 237 | final finalTransform = tester.widget(find.byType(Transform)).transform; 238 | 239 | // 초기값과 최종값이 다른지 확인 240 | expect(initialTransform, isNot(equals(finalTransform))); 241 | }); 242 | ``` 243 | 244 | ### 5. 네비게이션 테스트 245 | 246 | ```dart 247 | testWidgets('Navigation works correctly', (WidgetTester tester) async { 248 | await tester.pumpWidget(MaterialApp( 249 | routes: { 250 | '/': (context) => HomeScreen(), 251 | '/details': (context) => DetailScreen(), 252 | }, 253 | )); 254 | 255 | // 홈 화면에서 시작 256 | expect(find.text('홈 화면'), findsOneWidget); 257 | expect(find.text('상세 화면'), findsNothing); 258 | 259 | // 상세 버튼 탭 260 | await tester.tap(find.byType(ElevatedButton)); 261 | await tester.pumpAndSettle(); // 화면 전환 애니메이션 완료까지 기다림 262 | 263 | // 상세 화면으로 이동했는지 확인 264 | expect(find.text('홈 화면'), findsNothing); 265 | expect(find.text('상세 화면'), findsOneWidget); 266 | }); 267 | ``` 268 | 269 | ## StatefulWidget 테스트 270 | 271 | StatefulWidget의 경우 내부 상태와 라이프사이클 메서드를 테스트해야 할 수 있습니다: 272 | 273 | ```dart 274 | testWidgets('StatefulWidget lifecycle and state updates', (WidgetTester tester) async { 275 | final myWidgetKey = GlobalKey(); 276 | 277 | await tester.pumpWidget(MaterialApp( 278 | home: MyWidget(key: myWidgetKey), 279 | )); 280 | 281 | // 직접 상태에 접근 282 | final state = myWidgetKey.currentState!; 283 | 284 | // 초기 상태 확인 285 | expect(state.counter, 0); 286 | 287 | // 상태 업데이트 메서드 호출 288 | state.incrementCounter(); 289 | await tester.pump(); 290 | 291 | // 업데이트된 상태 확인 292 | expect(state.counter, 1); 293 | expect(find.text('1'), findsOneWidget); 294 | }); 295 | ``` 296 | 297 | ## 임의의 시간이 지난 후 상태 테스트 298 | 299 | 일정 시간이 지난 후 위젯 상태를 확인하고 싶을 때는 `pump`와 `pumpAndSettle` 메서드를 활용합니다: 300 | 301 | ```dart 302 | testWidgets('Timer updates UI after delay', (WidgetTester tester) async { 303 | await tester.pumpWidget(MaterialApp( 304 | home: TimerWidget(), 305 | )); 306 | 307 | // 초기 상태 308 | expect(find.text('대기 중...'), findsOneWidget); 309 | 310 | // 3초 경과를 시뮬레이션 311 | await tester.pump(Duration(seconds: 3)); 312 | 313 | // 업데이트된 상태 314 | expect(find.text('완료!'), findsOneWidget); 315 | }); 316 | ``` 317 | 318 | ## 비동기 작업 테스트 319 | 320 | API 호출과 같은 비동기 작업을 포함하는 위젯을 테스트하려면 모킹(mocking)을 사용해야 합니다: 321 | 322 | ```dart 323 | testWidgets('Widget loads data from API', (WidgetTester tester) async { 324 | // Mock API 서비스 설정 325 | final mockService = MockApiService(); 326 | when(mockService.fetchData()).thenAnswer((_) async => {'name': '홍길동'}); 327 | 328 | await tester.pumpWidget(MaterialApp( 329 | home: DataWidget(apiService: mockService), 330 | )); 331 | 332 | // 초기 로딩 상태 333 | expect(find.byType(CircularProgressIndicator), findsOneWidget); 334 | 335 | // 데이터 로딩 완료를 기다림 336 | await tester.pumpAndSettle(); 337 | 338 | // 로딩된 데이터 확인 339 | expect(find.text('홍길동'), findsOneWidget); 340 | }); 341 | ``` 342 | 343 | ## 테스트 그룹화 및 셋업 344 | 345 | 관련 테스트를 그룹화하고 공통 설정을 추출할 수 있습니다: 346 | 347 | ```dart 348 | void main() { 349 | group('Counter Widget Tests', () { 350 | late Widget testWidget; 351 | 352 | setUp(() { 353 | testWidget = MaterialApp( 354 | home: Scaffold( 355 | body: Counter(), 356 | ), 357 | ); 358 | }); 359 | 360 | testWidgets('renders initial state correctly', (WidgetTester tester) async { 361 | await tester.pumpWidget(testWidget); 362 | expect(find.text('0'), findsOneWidget); 363 | }); 364 | 365 | testWidgets('increments counter when button is pressed', (WidgetTester tester) async { 366 | await tester.pumpWidget(testWidget); 367 | await tester.tap(find.byType(ElevatedButton)); 368 | await tester.pump(); 369 | expect(find.text('1'), findsOneWidget); 370 | }); 371 | }); 372 | } 373 | ``` 374 | 375 | ## 테스트에서 키 활용하기 376 | 377 | 테스트에서 위젯을 더 쉽게 찾기 위해 위젯에 키(Key)를 할당하는 것이 좋습니다: 378 | 379 | ```dart 380 | // lib/screens/login_screen.dart 381 | class LoginScreen extends StatelessWidget { 382 | // 키 상수 정의 383 | static const kEmailFieldKey = Key('email-field'); 384 | static const kPasswordFieldKey = Key('password-field'); 385 | static const kLoginButtonKey = Key('login-button'); 386 | 387 | @override 388 | Widget build(BuildContext context) { 389 | return Scaffold( 390 | body: Column( 391 | children: [ 392 | TextField(key: kEmailFieldKey, decoration: InputDecoration(labelText: 'Email')), 393 | TextField(key: kPasswordFieldKey, decoration: InputDecoration(labelText: 'Password')), 394 | ElevatedButton( 395 | key: kLoginButtonKey, 396 | onPressed: () {}, 397 | child: Text('Login'), 398 | ), 399 | ], 400 | ), 401 | ); 402 | } 403 | } 404 | 405 | // test/screens/login_screen_test.dart 406 | testWidgets('Login form submits with email and password', (WidgetTester tester) async { 407 | await tester.pumpWidget(MaterialApp(home: LoginScreen())); 408 | 409 | // 키를 사용하여 위젯 찾기 410 | await tester.enterText(find.byKey(LoginScreen.kEmailFieldKey), 'user@example.com'); 411 | await tester.enterText(find.byKey(LoginScreen.kPasswordFieldKey), 'password123'); 412 | await tester.tap(find.byKey(LoginScreen.kLoginButtonKey)); 413 | await tester.pump(); 414 | 415 | // 검증 로직... 416 | }); 417 | ``` 418 | 419 | ## 골든 테스트 (시각적 회귀 테스트) 420 | 421 | 골든 테스트는 위젯의 외관이 예상과 일치하는지 확인하는 테스트입니다: 422 | 423 | ```dart 424 | testWidgets('MyWidget looks correct', (WidgetTester tester) async { 425 | await tester.pumpWidget(MaterialApp( 426 | theme: ThemeData.light(), 427 | home: MyWidget(), 428 | )); 429 | 430 | await expectLater( 431 | find.byType(MyWidget), 432 | matchesGoldenFile('my_widget_light.png'), 433 | ); 434 | 435 | // 다크 테마에서도 확인 436 | await tester.pumpWidget(MaterialApp( 437 | theme: ThemeData.dark(), 438 | home: MyWidget(), 439 | )); 440 | 441 | await expectLater( 442 | find.byType(MyWidget), 443 | matchesGoldenFile('my_widget_dark.png'), 444 | ); 445 | }); 446 | ``` 447 | 448 | ## 팁과 베스트 프랙티스 449 | 450 | 1. **작은 위젯부터 테스트하기**: 복잡한 화면 대신 재사용 가능한 작은 위젯부터 테스트하세요. 451 | 452 | 2. **모킹 활용하기**: 외부 종속성(API, 데이터베이스 등)은 모킹하여 테스트하세요. 453 | 454 | 3. **테스트 가능성을 고려한 설계**: 위젯을 설계할 때 테스트 가능성을 고려하세요. 종속성 주입을 활용하면 테스트가 쉬워집니다. 455 | 456 | 4. **모든 상호작용 후 pump() 호출하기**: 상태 변경 후에는 항상 `tester.pump()`를 호출하여 UI를 업데이트하세요. 457 | 458 | 5. **키 활용하기**: 복잡한 UI에서는 위젯을 쉽게 찾을 수 있도록 키를 할당하세요. 459 | 460 | 6. **다양한 시나리오 테스트하기**: 정상 경로뿐만 아니라 오류 경로, 경계 조건도 테스트하세요. 461 | 462 | ## Riverpod 통합 위젯 테스트 463 | 464 | Riverpod를 사용하는 경우 테스트 설정 방법: 465 | 466 | ```dart 467 | testWidgets('CounterWidget with Riverpod', (WidgetTester tester) async { 468 | await tester.pumpWidget( 469 | ProviderScope( 470 | overrides: [ 471 | // 프로바이더 오버라이드 472 | counterProvider.overrideWithValue(10), 473 | ], 474 | child: MaterialApp( 475 | home: CounterWidget(), 476 | ), 477 | ), 478 | ); 479 | 480 | // 오버라이드된 값으로 위젯이 렌더링되었는지 확인 481 | expect(find.text('10'), findsOneWidget); 482 | 483 | // 상호작용 및 추가 검증 484 | await tester.tap(find.byType(ElevatedButton)); 485 | await tester.pump(); 486 | 487 | // 상태 업데이트 확인 488 | expect(find.text('11'), findsOneWidget); 489 | }); 490 | ``` 491 | 492 | ## 결론 493 | 494 | 위젯 테스트는 Flutter 앱의 UI가 예상대로 동작하는지 확인하는 중요한 단계입니다. 단위 테스트와 통합 테스트를 함께 사용하여 앱의 모든 측면을 테스트하는 것이 좋습니다. 위젯 테스트를 통해 UI 변경이 기존 기능을 손상시키지 않고, 사용자 상호작용이 올바르게 처리됨을 보장할 수 있습니다. 495 | 496 | 다음 장에서는 통합 테스트(Integration Test)에 대해 알아보겠습니다. 통합 테스트는 위젯 테스트보다 더 넓은 범위에서 앱의 여러 부분이 함께 작동하는 방식을 테스트합니다. 497 | -------------------------------------------------------------------------------- /src/content/docs/part7/integration-test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 통합 테스트 3 | --- 4 | 5 | 통합 테스트(Integration Test)는 앱의 다양한 부분들이 함께 작동하는 방식을 검증하는 테스트입니다. 단위 테스트가 작은 코드 조각을, 위젯 테스트가 UI 컴포넌트를 검증한다면, 통합 테스트는 실제 디바이스나 에뮬레이터에서 앱 전체의 동작을 확인합니다. 6 | 7 | ## 통합 테스트의 필요성 8 | 9 | 통합 테스트는 다음과 같은 이유로 중요합니다: 10 | 11 | 1. **실제 환경 검증**: 실제 디바이스나 에뮬레이터에서 앱의 동작을 테스트합니다. 12 | 2. **전체 기능 흐름 검증**: 사용자 시나리오에 따른 앱의 기능 흐름을 종합적으로 테스트합니다. 13 | 3. **성능 이슈 발견**: 실제 환경에서 발생할 수 있는 성능 문제를 조기에 발견합니다. 14 | 4. **기기별 호환성 검증**: 다양한 화면 크기와 OS 버전에서의 동작을 검증합니다. 15 | 5. **백엔드 연동 검증**: 실제 또는 테스트용 백엔드와의 통합 작동을 검증합니다. 16 | 17 | ## 테스트 종류별 특징 18 | 19 | ## 통합 테스트 설정 20 | 21 | Flutter에서 통합 테스트를 수행하려면 `integration_test` 패키지를 사용합니다: 22 | 23 | ### 1. 패키지 추가 24 | 25 | `pubspec.yaml` 파일에 다음 의존성을 추가합니다: 26 | 27 | ```yaml 28 | dev_dependencies: 29 | integration_test: 30 | sdk: flutter 31 | flutter_test: 32 | sdk: flutter 33 | ``` 34 | 35 | ### 2. 프로젝트 구조 설정 36 | 37 | 통합 테스트는 프로젝트 루트의 `integration_test` 디렉토리에 작성합니다: 38 | 39 | ``` 40 | my_app/ 41 | ├── lib/ 42 | ├── test/ # 단위 및 위젯 테스트 43 | ├── integration_test/ # 통합 테스트 44 | │ └── app_test.dart 45 | └── pubspec.yaml 46 | ``` 47 | 48 | ## 기본 통합 테스트 작성하기 49 | 50 | 간단한 카운터 앱의 통합 테스트 예제를 살펴보겠습니다: 51 | 52 | ```dart 53 | // integration_test/app_test.dart 54 | import 'package:flutter/material.dart'; 55 | import 'package:flutter_test/flutter_test.dart'; 56 | import 'package:integration_test/integration_test.dart'; 57 | import 'package:my_app/main.dart' as app; 58 | 59 | void main() { 60 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 61 | 62 | group('통합 테스트', () { 63 | testWidgets('카운터 증가 테스트', (WidgetTester tester) async { 64 | // 앱 실행 65 | app.main(); 66 | await tester.pumpAndSettle(); 67 | 68 | // 초기 상태 확인 - 카운터가 0인지 69 | expect(find.text('0'), findsOneWidget); 70 | 71 | // FloatingActionButton 찾기 72 | final Finder fab = find.byType(FloatingActionButton); 73 | 74 | // 버튼 탭하기 75 | await tester.tap(fab); 76 | await tester.pumpAndSettle(); 77 | 78 | // 탭 후 카운터가 1로 증가했는지 확인 79 | expect(find.text('1'), findsOneWidget); 80 | 81 | // 한 번 더 탭하기 82 | await tester.tap(fab); 83 | await tester.pumpAndSettle(); 84 | 85 | // 카운터가 2로 증가했는지 확인 86 | expect(find.text('2'), findsOneWidget); 87 | }); 88 | }); 89 | } 90 | ``` 91 | 92 | ### 주요 단계 설명 93 | 94 | 1. **초기화**: `IntegrationTestWidgetsFlutterBinding.ensureInitialized()`로 통합 테스트 환경을 초기화합니다. 95 | 2. **앱 실행**: `app.main()`으로 앱을 시작합니다. 96 | 3. **UI 안정화**: `tester.pumpAndSettle()`로 모든 애니메이션이 완료될 때까지 기다립니다. 97 | 4. **위젯 찾기**: `find`를 사용하여 상호작용할 위젯을 찾습니다. 98 | 5. **상호작용**: `tester.tap()`으로 위젯과 상호작용합니다. 99 | 6. **검증**: `expect`로 예상 결과를 확인합니다. 100 | 101 | ## 통합 테스트 실행하기 102 | 103 | 통합 테스트를 실행하는 방법은 여러 가지가 있습니다: 104 | 105 | ### 1. 명령줄에서 실행 106 | 107 | ```bash 108 | flutter test integration_test/app_test.dart 109 | ``` 110 | 111 | ### 2. 여러 디바이스에서 실행 112 | 113 | ```bash 114 | flutter test integration_test --device-id=all 115 | ``` 116 | 117 | ### 3. Firebase Test Lab에서 실행 118 | 119 | 통합 테스트를 Firebase Test Lab에서 실행하면 다양한 기기에서 테스트할 수 있습니다. 120 | 121 | #### Android의 경우: 122 | 123 | 먼저 테스트 APK 파일들을 빌드합니다: 124 | 125 | ```bash 126 | flutter build apk --profile 127 | flutter build apk --profile --target=integration_test/app_test.dart 128 | ``` 129 | 130 | 그런 다음 Firebase Test Lab으로 업로드하여 실행합니다: 131 | 132 | ```bash 133 | gcloud firebase test android run \ 134 | --type instrumentation \ 135 | --app build/app/outputs/apk/profile/app-profile.apk \ 136 | --test build/app/outputs/apk/androidTest/profile/app-profile-androidTest.apk \ 137 | --device model=Pixel2,version=28 138 | ``` 139 | 140 | #### iOS의 경우: 141 | 142 | XCUITest 파일을 빌드하고 Firebase Test Lab으로 업로드합니다: 143 | 144 | ```bash 145 | flutter build ios --profile --no-codesign 146 | pushd ios 147 | xcodebuild build-for-testing \ 148 | -workspace Runner.xcworkspace \ 149 | -scheme Runner \ 150 | -configuration Debug \ 151 | -derivedDataPath ../build/ios_integ 152 | popd 153 | 154 | gcloud firebase test ios run \ 155 | --xcode-version=10.0 \ 156 | --test build/ios_integ/Build/Products/Runner_iphoneos14.5-arm64.xctestrun 157 | ``` 158 | 159 | ## 고급 통합 테스트 기법 160 | 161 | ### 1. 스크린샷 캡처하기 162 | 163 | 테스트 과정에서 스크린샷을 캡처하여 UI 상태를 기록할 수 있습니다: 164 | 165 | ```dart 166 | testWidgets('스크린샷 캡처 테스트', (WidgetTester tester) async { 167 | app.main(); 168 | await tester.pumpAndSettle(); 169 | 170 | // 초기 화면 스크린샷 171 | await takeScreenshot(tester, 'initial_screen'); 172 | 173 | // 버튼 탭 174 | await tester.tap(find.byType(FloatingActionButton)); 175 | await tester.pumpAndSettle(); 176 | 177 | // 탭 후 화면 스크린샷 178 | await takeScreenshot(tester, 'after_tap'); 179 | }); 180 | 181 | Future takeScreenshot(WidgetTester tester, String name) async { 182 | final Directory dir = Directory('screenshots'); 183 | if (!dir.existsSync()) { 184 | dir.createSync(); 185 | } 186 | 187 | final ByteData bytes = await tester.takeScreenshot(); 188 | final File file = File('${dir.path}/$name.png'); 189 | file.writeAsBytesSync(bytes.buffer.asUint8List()); 190 | } 191 | ``` 192 | 193 | ### 2. 성능 프로파일링 194 | 195 | 테스트 중 앱의 성능을 측정할 수 있습니다: 196 | 197 | ```dart 198 | testWidgets('성능 테스트', (WidgetTester tester) async { 199 | app.main(); 200 | await tester.pumpAndSettle(); 201 | 202 | final Stopwatch stopwatch = Stopwatch()..start(); 203 | 204 | // 성능 테스트할 동작 수행 205 | for (int i = 0; i < 10; i++) { 206 | await tester.tap(find.byType(FloatingActionButton)); 207 | await tester.pumpAndSettle(); 208 | } 209 | 210 | stopwatch.stop(); 211 | print('10회 탭 수행 시간: ${stopwatch.elapsedMilliseconds}ms'); 212 | 213 | // 성능 기준 검증 214 | expect(stopwatch.elapsedMilliseconds, lessThan(2000)); // 2초 이내여야 함 215 | }); 216 | ``` 217 | 218 | ### 3. 네트워크 요청 모킹 219 | 220 | 통합 테스트에서 실제 네트워크 요청을 모킹하려면, 앱을 실행하기 전에 `HttpOverrides`를 설정합니다: 221 | 222 | ```dart 223 | import 'dart:io'; 224 | 225 | class MockHttpClient implements HttpClient { 226 | // HttpClient 메서드 구현... 227 | } 228 | 229 | class MockHttpOverrides extends HttpOverrides { 230 | @override 231 | HttpClient createHttpClient(SecurityContext? context) { 232 | return MockHttpClient(); 233 | } 234 | } 235 | 236 | void main() { 237 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 238 | 239 | setUp(() { 240 | HttpOverrides.global = MockHttpOverrides(); 241 | }); 242 | 243 | testWidgets('네트워크 요청 모킹 테스트', (WidgetTester tester) async { 244 | app.main(); 245 | await tester.pumpAndSettle(); 246 | 247 | // 네트워크 요청이 포함된 동작 테스트 248 | await tester.tap(find.byType(ElevatedButton)); 249 | await tester.pumpAndSettle(); 250 | 251 | // 모킹된 응답에 따른 UI 상태 검증 252 | expect(find.text('모킹된 데이터'), findsOneWidget); 253 | }); 254 | } 255 | ``` 256 | 257 | ### 4. 실제 사용자 흐름 테스트 258 | 259 | 실제 사용자 흐름을 시뮬레이션하는 종합적인 테스트를 작성할 수 있습니다: 260 | 261 | ```dart 262 | testWidgets('사용자 로그인 및 데이터 조회 흐름', (WidgetTester tester) async { 263 | app.main(); 264 | await tester.pumpAndSettle(); 265 | 266 | // 로그인 화면에서 이메일 필드 찾기 267 | expect(find.byKey(const Key('email_field')), findsOneWidget); 268 | 269 | // 이메일 입력 270 | await tester.enterText(find.byKey(const Key('email_field')), 'test@example.com'); 271 | await tester.pumpAndSettle(); 272 | 273 | // 비밀번호 입력 274 | await tester.enterText(find.byKey(const Key('password_field')), 'password123'); 275 | await tester.pumpAndSettle(); 276 | 277 | // 로그인 버튼 탭 278 | await tester.tap(find.byKey(const Key('login_button'))); 279 | await tester.pumpAndSettle(); 280 | 281 | // 로그인 후 홈 화면으로 이동했는지 확인 282 | expect(find.text('홈 화면'), findsOneWidget); 283 | 284 | // 데이터 조회 버튼 탭 285 | await tester.tap(find.byKey(const Key('fetch_data_button'))); 286 | await tester.pumpAndSettle(); 287 | 288 | // 로딩 인디케이터 표시 확인 289 | expect(find.byType(CircularProgressIndicator), findsOneWidget); 290 | 291 | // 데이터 로딩 완료 대기 (최대 10초) 292 | await tester.pumpAndSettle(const Duration(seconds: 10)); 293 | 294 | // 데이터가 정상적으로 표시되었는지 확인 295 | expect(find.byType(ListView), findsOneWidget); 296 | expect(find.byType(ListTile), findsWidgets); 297 | }); 298 | ``` 299 | 300 | ## 테스트 실행 구조 301 | 302 | 통합 테스트가 실행되는 방식을 이해하면 디버깅에 도움이 됩니다: 303 | 304 | ## 통합 테스트 모범 사례 305 | 306 | ### 1. 주요 사용자 경로 테스트하기 307 | 308 | 모든 기능을 통합 테스트하는 것은 비효율적입니다. 대신, 다음과 같은 주요 사용자 경로(Critical User Paths)에 집중하세요: 309 | 310 | - 사용자 등록 및 로그인 311 | - 주요 데이터 조회 및 생성 312 | - 결제 프로세스 313 | - 앱의 핵심 기능 314 | 315 | ### 2. 테스트 분리 및 구성 316 | 317 | 복잡한 통합 테스트는 논리적인 단계로 분리하세요: 318 | 319 | ```dart 320 | void main() { 321 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 322 | 323 | group('사용자 계정 테스트', () { 324 | testWidgets('회원가입', signUpTest); 325 | testWidgets('로그인', loginTest); 326 | testWidgets('프로필 수정', editProfileTest); 327 | }); 328 | 329 | group('콘텐츠 관리 테스트', () { 330 | testWidgets('콘텐츠 조회', viewContentTest); 331 | testWidgets('콘텐츠 생성', createContentTest); 332 | testWidgets('콘텐츠 편집', editContentTest); 333 | }); 334 | } 335 | 336 | // 각 테스트 함수 구현 337 | Future signUpTest(WidgetTester tester) async { 338 | // 회원가입 테스트 로직 339 | } 340 | 341 | Future loginTest(WidgetTester tester) async { 342 | // 로그인 테스트 로직 343 | } 344 | 345 | // 기타 테스트 함수... 346 | ``` 347 | 348 | ### 3. 공통 기능 추출 349 | 350 | 여러 테스트에서 반복되는 로직은 헬퍼 함수로 추출하세요: 351 | 352 | ```dart 353 | // 로그인 헬퍼 함수 354 | Future loginToApp(WidgetTester tester, {String email = 'test@example.com', String password = 'password123'}) async { 355 | await tester.enterText(find.byKey(const Key('email_field')), email); 356 | await tester.pumpAndSettle(); 357 | 358 | await tester.enterText(find.byKey(const Key('password_field')), password); 359 | await tester.pumpAndSettle(); 360 | 361 | await tester.tap(find.byKey(const Key('login_button'))); 362 | await tester.pumpAndSettle(); 363 | 364 | // 로그인 성공 확인 365 | expect(find.text('홈 화면'), findsOneWidget); 366 | } 367 | 368 | // 테스트에서 사용 369 | testWidgets('데이터 조회 테스트', (WidgetTester tester) async { 370 | app.main(); 371 | await tester.pumpAndSettle(); 372 | 373 | // 로그인 헬퍼 함수 사용 374 | await loginToApp(tester); 375 | 376 | // 추가 테스트 로직... 377 | }); 378 | ``` 379 | 380 | ### 4. 테스트 환경 설정 381 | 382 | 테스트별로 앱 상태를 초기화하여 테스트간 독립성을 유지하세요: 383 | 384 | ```dart 385 | setUp(() async { 386 | // 선택적: 앱 상태 초기화 (예: SharedPreferences 초기화) 387 | SharedPreferences.setMockInitialValues({}); 388 | 389 | // 선택적: 네트워크 요청 모킹 390 | HttpOverrides.global = MockHttpOverrides(); 391 | }); 392 | 393 | tearDown(() async { 394 | // 테스트 후 정리 작업 395 | HttpOverrides.global = null; 396 | }); 397 | ``` 398 | 399 | ### 5. 테스트 안정성 개선 400 | 401 | 통합 테스트는 불안정할 수 있으므로, 테스트 안정성을 높이는 방법을 적용하세요: 402 | 403 | ```dart 404 | // 요소가 나타날 때까지 기다리기 405 | Future waitForElement(WidgetTester tester, Finder finder, {Duration timeout = const Duration(seconds: 10)}) async { 406 | final end = DateTime.now().add(timeout); 407 | while (DateTime.now().isBefore(end)) { 408 | if (finder.evaluate().isNotEmpty) { 409 | return; 410 | } 411 | await tester.pump(const Duration(milliseconds: 100)); 412 | } 413 | 414 | // 시간 초과시 오류 415 | throw TimeoutException('요소를 찾을 수 없습니다: $finder', timeout); 416 | } 417 | 418 | // 사용 예 419 | testWidgets('비동기 데이터 로딩 테스트', (WidgetTester tester) async { 420 | app.main(); 421 | await tester.pumpAndSettle(); 422 | 423 | await tester.tap(find.byType(ElevatedButton)); 424 | await tester.pump(); // 첫 프레임만 업데이트 425 | 426 | // 로딩 인디케이터 확인 427 | expect(find.byType(CircularProgressIndicator), findsOneWidget); 428 | 429 | // 데이터가 로드될 때까지 기다림 430 | await waitForElement(tester, find.byType(ListView)); 431 | 432 | // 데이터 검증 433 | expect(find.byType(ListTile), findsWidgets); 434 | }); 435 | ``` 436 | 437 | ## CI/CD 통합 438 | 439 | 통합 테스트를 CI/CD 파이프라인에 통합하면 코드 품질을 지속적으로 검증할 수 있습니다: 440 | 441 | ### GitHub Actions 예제 442 | 443 | ```yaml 444 | name: Flutter Integration Tests 445 | 446 | on: 447 | push: 448 | branches: [main] 449 | pull_request: 450 | branches: [main] 451 | 452 | jobs: 453 | test: 454 | runs-on: macos-latest 455 | steps: 456 | - uses: actions/checkout@v3 457 | - uses: subosito/flutter-action@v2 458 | with: 459 | flutter-version: "3.10.0" 460 | channel: "stable" 461 | 462 | - name: Install dependencies 463 | run: flutter pub get 464 | 465 | - name: Run integration tests 466 | run: flutter test integration_test/app_test.dart 467 | 468 | # 선택적: 실제 기기에서 테스트 (Android) 469 | - name: Build and run Android integration tests 470 | uses: reactivecircus/android-emulator-runner@v2 471 | with: 472 | api-level: 29 473 | arch: x86_64 474 | profile: Nexus 6 475 | script: flutter test integration_test/app_test.dart -d `flutter devices | grep emulator | cut -d" " -f1` 476 | ``` 477 | 478 | ## Codemagic 예제 479 | 480 | ```yaml 481 | workflows: 482 | integration-test: 483 | name: Integration Tests 484 | instance_type: mac_mini_m1 485 | environment: 486 | flutter: stable 487 | scripts: 488 | - name: Get dependencies 489 | script: flutter pub get 490 | - name: Run integration tests on iOS Simulator 491 | script: | 492 | xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-14-4 493 | xcrun simctl boot Flutter-iPhone 494 | flutter test integration_test/app_test.dart -d Flutter-iPhone 495 | ``` 496 | 497 | ## 결론 498 | 499 | 통합 테스트는 Flutter 앱의 최종 품질을 보장하는 데 중요한 단계입니다. 단위 테스트와 위젯 테스트가 앱의 개별 부분을 검증한다면, 통합 테스트는 전체 앱이 실제 사용자 시나리오에서 올바르게 작동하는지 확인합니다. 500 | 501 | 통합 테스트는 시간과 리소스가 많이 소요되므로, 모든 기능을 테스트하기보다는 주요 사용자 경로와 비즈니스 크리티컬한 기능에 집중하는 것이 좋습니다. 테스트를 구조화하고 공통 기능을 추출하여 유지보수성을 높이세요. 502 | 503 | 다음 장에서는 테스팅 도구에 대해 더 자세히 알아보겠습니다. Mockito, golden test, coverage 등의 도구를 활용하여 Flutter 앱 테스트를 더욱 효과적으로 수행하는 방법을 살펴볼 것입니다. 504 | -------------------------------------------------------------------------------- /src/content/docs/part2/records.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 레코드 & 패턴매칭 3 | --- 4 | 5 | ## 레코드(Records) 6 | 7 | 레코드는 Dart 3.0에서 도입된 새로운 컬렉션 타입으로, 여러 필드를 그룹화하여 단일 객체로 전달할 수 있게 해줍니다. 클래스와 달리 명시적인 정의가 필요 없고, 불변(immutable)이며, 구조적으로 타입이 지정됩니다. 8 | 9 | ### 레코드 기본 10 | 11 | 레코드는 괄호(`()`)를 사용하여 생성하며, 쉼표로 구분된 여러 값을 담을 수 있습니다: 12 | 13 | ```dart 14 | // 간단한 레코드 15 | var person = ('홍길동', 30); 16 | print(person); // (홍길동, 30) 17 | 18 | // 위치 기반 필드 접근 19 | print(person.$1); // 홍길동 20 | print(person.$2); // 30 21 | ``` 22 | 23 | ### 명명된 필드가 있는 레코드 24 | 25 | 레코드에 이름이 있는 필드를 포함할 수 있습니다: 26 | 27 | ```dart 28 | // 명명된 필드가 있는 레코드 29 | var person = (name: '홍길동', age: 30); 30 | 31 | // 명명된 필드 접근 32 | print(person.name); // 홍길동 33 | print(person.age); // 30 34 | 35 | // 혼합 사용 36 | var data = ('홍길동', age: 30, active: true); 37 | print(data.$1); // 홍길동 38 | print(data.age); // 30 39 | print(data.active); // true 40 | ``` 41 | 42 | ### 레코드 타입 지정 43 | 44 | 레코드에 명시적인 타입을 지정할 수 있습니다: 45 | 46 | ```dart 47 | // 타입이 명시된 레코드 48 | (String, int) person = ('홍길동', 30); 49 | 50 | // 명명된 필드가 있는 레코드 타입 51 | ({String name, int age}) person = (name: '홍길동', age: 30); 52 | 53 | // 혼합 타입 54 | (String, {int age, bool active}) data = ('홍길동', age: 30, active: true); 55 | ``` 56 | 57 | ### 레코드의 유용성 58 | 59 | #### 1. 여러 값 반환 60 | 61 | 함수에서 여러 값을 반환할 때 유용합니다: 62 | 63 | ```dart 64 | // 여러 값 반환 65 | (String, int) getUserInfo() { 66 | return ('홍길동', 30); 67 | } 68 | 69 | void main() { 70 | var (name, age) = getUserInfo(); 71 | print('이름: $name, 나이: $age'); // 이름: 홍길동, 나이: 30 72 | } 73 | ``` 74 | 75 | #### 2. 구조 분해 할당 76 | 77 | 레코드는 구조 분해 할당을 지원합니다: 78 | 79 | ```dart 80 | var person = (name: '홍길동', age: 30); 81 | 82 | // 구조 분해 83 | var (name: userName, age: userAge) = person; 84 | print('이름: $userName, 나이: $userAge'); // 이름: 홍길동, 나이: 30 85 | 86 | // 간단한 구조 분해 87 | var (:name, :age) = person; 88 | print('이름: $name, 나이: $age'); // 이름: 홍길동, 나이: 30 89 | ``` 90 | 91 | #### 3. 그룹화된 데이터 전달 92 | 93 | 여러 값을 그룹화하여 전달할 때 유용합니다: 94 | 95 | ```dart 96 | void printPersonInfo((String, int) person) { 97 | print('이름: ${person.$1}, 나이: ${person.$2}'); 98 | } 99 | 100 | void printUserDetails({String name, int age, String? address}) { 101 | print('이름: $name, 나이: $age, 주소: ${address ?? '정보 없음'}'); 102 | } 103 | 104 | void main() { 105 | printPersonInfo(('홍길동', 30)); // 이름: 홍길동, 나이: 30 106 | 107 | var userDetails = (name: '김철수', age: 25, address: '서울시'); 108 | printUserDetails(userDetails); // 이름: 김철수, 나이: 25, 주소: 서울시 109 | } 110 | ``` 111 | 112 | ### 레코드 비교 113 | 114 | 레코드는 값 기반 비교를 지원합니다: 115 | 116 | ```dart 117 | void main() { 118 | var person1 = (name: '홍길동', age: 30); 119 | var person2 = (name: '홍길동', age: 30); 120 | var person3 = (name: '김철수', age: 25); 121 | 122 | print(person1 == person2); // true (동일한 값) 123 | print(person1 == person3); // false (다른 값) 124 | 125 | // 위치 기반 레코드도 같음 126 | var p1 = ('홍길동', 30); 127 | var p2 = ('홍길동', 30); 128 | print(p1 == p2); // true 129 | } 130 | ``` 131 | 132 | ### 레코드 사용 예제 133 | 134 | #### 1. 통계 계산 135 | 136 | ```dart 137 | (double min, double max, double average) calculateStats(List values) { 138 | if (values.isEmpty) { 139 | return (0, 0, 0); 140 | } 141 | double sum = 0; 142 | double min = values[0]; 143 | double max = values[0]; 144 | 145 | for (var value in values) { 146 | sum += value; 147 | if (value < min) min = value; 148 | if (value > max) max = value; 149 | } 150 | 151 | return (min, max, sum / values.length); 152 | } 153 | 154 | void main() { 155 | var numbers = [10.5, 25.3, 17.2, 8.7, 30.1]; 156 | var (min, max, avg) = calculateStats(numbers); 157 | 158 | print('최소값: $min'); // 최소값: 8.7 159 | print('최대값: $max'); // 최대값: 30.1 160 | print('평균값: $avg'); // 평균값: 18.36 161 | } 162 | ``` 163 | 164 | #### 2. API 응답 처리 165 | 166 | ```dart 167 | (bool success, {String? data, String? error}) fetchUserData(String userId) { 168 | // 서버 요청 시뮬레이션 169 | if (userId == 'user123') { 170 | return (true, data: '{"name": "홍길동", "email": "hong@example.com"}', error: null); 171 | } else { 172 | return (false, data: null, error: '사용자를 찾을 수 없습니다.'); 173 | } 174 | } 175 | 176 | void main() { 177 | // 성공 케이스 178 | var (success: isSuccess, data: userData, error: _) = fetchUserData('user123'); 179 | 180 | if (isSuccess && userData != null) { 181 | print('사용자 데이터: $userData'); 182 | } 183 | 184 | // 실패 케이스 185 | var result = fetchUserData('unknown'); 186 | 187 | if (!result.success) { 188 | print('오류: ${result.error}'); // 오류: 사용자를 찾을 수 없습니다. 189 | } 190 | } 191 | ``` 192 | 193 | ## 패턴 매칭(Pattern Matching) 194 | 195 | 패턴 매칭은 Dart 3.0에서 도입된 기능으로, 데이터 구조에서 특정 패턴을 검색하고 추출하는 강력한 방법을 제공합니다. 196 | 197 | ### 패턴 매칭 기본 198 | 199 | 패턴 매칭을 사용하면 복잡한 데이터 구조에서 데이터를 쉽게 추출하고 검사할 수 있습니다: 200 | 201 | ```dart 202 | // 레코드 패턴 매칭 203 | var person = ('홍길동', 30); 204 | 205 | var (name, age) = person; 206 | print('이름: $name, 나이: $age'); // 이름: 홍길동, 나이: 30 207 | 208 | // 리스트 패턴 매칭 209 | var numbers = [1, 2, 3]; 210 | 211 | var [first, second, third] = numbers; 212 | print('$first, $second, $third'); // 1, 2, 3 213 | 214 | // 맵 패턴 매칭 215 | var user = {'name': '홍길동', 'age': 30}; 216 | 217 | var {'name': userName, 'age': userAge} = user; 218 | print('이름: $userName, 나이: $userAge'); // 이름: 홍길동, 나이: 30 219 | ``` 220 | 221 | ### switch 문에서의 패턴 매칭 222 | 223 | 패턴 매칭은 `switch` 문에서 특히 강력합니다: 224 | 225 | ```dart 226 | void describe(Object obj) { 227 | switch (obj) { 228 | case int i when i > 0: 229 | print('양수: $i'); 230 | case int i when i < 0: 231 | print('음수: $i'); 232 | case int i when i == 0: 233 | print('0'); 234 | case String s when s.isEmpty: 235 | print('빈 문자열'); 236 | case String s: 237 | print('문자열: $s'); 238 | case List list: 239 | print('정수 리스트: $list'); 240 | case (String, int) pair: 241 | print('문자열-정수 쌍: ${pair.$1}, ${pair.$2}'); 242 | case (String name, int age): 243 | print('이름: $name, 나이: $age'); 244 | default: 245 | print('기타 객체: $obj'); 246 | } 247 | } 248 | 249 | void main() { 250 | describe(42); // 양수: 42 251 | describe(-10); // 음수: -10 252 | describe(0); // 0 253 | describe(''); // 빈 문자열 254 | describe('안녕하세요'); // 문자열: 안녕하세요 255 | describe([1, 2, 3]); // 정수 리스트: [1, 2, 3] 256 | describe(('홍길동', 30)); // 이름: 홍길동, 나이: 30 257 | describe(true); // 기타 객체: true 258 | } 259 | ``` 260 | 261 | ### if-case 문 262 | 263 | 패턴 매칭은 `if-case` 문에서도 사용할 수 있습니다: 264 | 265 | ```dart 266 | void processValue(Object value) { 267 | if (value case String s when s.length > 5) { 268 | print('긴 문자열: $s'); 269 | } else if (value case int n when n > 10) { 270 | print('큰 정수: $n'); 271 | } else if (value case (String name, int age)) { 272 | print('이름: $name, 나이: $age'); 273 | } else { 274 | print('처리할 수 없는 값: $value'); 275 | } 276 | } 277 | 278 | void main() { 279 | processValue('안녕하세요, 반갑습니다'); // 긴 문자열: 안녕하세요, 반갑습니다 280 | processValue(42); // 큰 정수: 42 281 | processValue(('홍길동', 30)); // 이름: 홍길동, 나이: 30 282 | processValue(true); // 처리할 수 없는 값: true 283 | } 284 | ``` 285 | 286 | ### 논리 OR 패턴 287 | 288 | 여러 패턴을 `|` 연산자로 결합할 수 있습니다: 289 | 290 | ```dart 291 | Object value = 42; 292 | 293 | switch (value) { 294 | case String s | int i: 295 | print('문자열 또는 정수: $value'); 296 | case List l | Map m: 297 | print('리스트 또는 맵'); 298 | default: 299 | print('기타 타입'); 300 | } 301 | ``` 302 | 303 | ### 중첩 패턴 매칭 304 | 305 | 패턴은 중첩될 수 있어 복잡한 데이터 구조에서도 사용할 수 있습니다: 306 | 307 | ```dart 308 | // 복잡한 데이터 구조 309 | var person = { 310 | 'name': '홍길동', 311 | 'age': 30, 312 | 'address': { 313 | 'city': '서울', 314 | 'zipcode': '12345' 315 | }, 316 | 'hobbies': ['독서', '여행', '음악'] 317 | }; 318 | 319 | // 중첩 패턴 매칭 320 | if (person case {'name': String name, 'address': {'city': String city}}) { 321 | print('$name은(는) $city에 살고 있습니다.'); // 홍길동은(는) 서울에 살고 있습니다. 322 | } 323 | 324 | // 리스트와 레코드의 중첩 패턴 325 | var data = [('홍길동', 30), ('김철수', 25)]; 326 | 327 | if (data case [(String s, int i), var rest]) { 328 | print('첫 번째 사람: $s, $i살'); // 첫 번째 사람: 홍길동, 30살 329 | print('나머지: $rest'); // 나머지: (김철수, 25) 330 | } 331 | ``` 332 | 333 | ### var 패턴과 와일드카드 패턴 334 | 335 | 변수에 값을 캡처하거나 값을 무시할 수 있습니다: 336 | 337 | ```dart 338 | // var 패턴 (값 캡처) 339 | var list = [1, 2, 3, 4, 5]; 340 | 341 | if (list case [var first, var second, ...]) { 342 | print('처음 두 값: $first, $second'); // 처음 두 값: 1, 2 343 | } 344 | 345 | // 와일드카드 패턴 (값 무시) 346 | var record = ('홍길동', 30, '서울'); 347 | 348 | if (record case (String name, _, var city)) { 349 | print('$name은(는) $city에 살고 있습니다.'); // 홍길동은(는) 서울에 살고 있습니다. 350 | } 351 | ``` 352 | 353 | ### 일정한 리스트 패턴 354 | 355 | 리스트의 특정 위치에 있는 값을 추출할 수 있습니다: 356 | 357 | ```dart 358 | var numbers = [1, 2, 3, 4, 5]; 359 | 360 | switch (numbers) { 361 | case [var first, var second, ...var rest]: 362 | print('첫 번째: $first'); // 첫 번째: 1 363 | print('두 번째: $second'); // 두 번째: 2 364 | print('나머지: $rest'); // 나머지: [3, 4, 5] 365 | default: 366 | print('빈 리스트 또는 다른 형태'); 367 | } 368 | 369 | // 특정 패턴 검색 370 | var list = [1, 2, 3, 4, 5]; 371 | 372 | if (list case [_, _, 3, ..., 5]) { 373 | print('리스트가 패턴과 일치합니다.'); // 리스트가 패턴과 일치합니다. 374 | } 375 | ``` 376 | 377 | ### 객체 패턴 378 | 379 | 클래스 인스턴스의 속성을 추출할 수도 있습니다: 380 | 381 | ```dart 382 | class Person { 383 | final String name; 384 | final int age; 385 | 386 | Person(this.name, this.age); 387 | } 388 | 389 | void describePerson(Person person) { 390 | switch (person) { 391 | case Person(name: 'Unknown', age: var a): 392 | print('알 수 없는 사람, 나이: $a'); 393 | case Person(name: var n, age: > 18): 394 | print('성인: $n'); 395 | case Person(name: var n): 396 | print('미성년자: $n'); 397 | } 398 | } 399 | 400 | void main() { 401 | describePerson(Person('홍길동', 30)); // 성인: 홍길동 402 | describePerson(Person('김영희', 15)); // 미성년자: 김영희 403 | } 404 | ``` 405 | 406 | ### 타입 패턴과 조건 패턴 407 | 408 | ```dart 409 | void process(dynamic value) { 410 | switch (value) { 411 | // 타입 패턴 412 | case int(): 413 | print('정수: $value'); 414 | 415 | // 타입 패턴과 조건 패턴 416 | case String() when value.length > 5: 417 | print('긴 문자열: $value'); 418 | case String(): 419 | print('짧은 문자열: $value'); 420 | 421 | // 리스트 + 조건 패턴 422 | case List l when l.every((e) => e > 0): 423 | print('양수 리스트: $value'); 424 | 425 | // 레코드 + 조건 패턴 426 | case (String n, int a) when a >= 18: 427 | print('성인: $n, $a살'); 428 | case (String n, int a): 429 | print('미성년자: $n, $a살'); 430 | 431 | default: 432 | print('기타 값: $value'); 433 | } 434 | } 435 | 436 | void main() { 437 | process(42); // 정수: 42 438 | process('Hello'); // 짧은 문자열: Hello 439 | process('안녕하세요, 반갑습니다'); // 긴 문자열: 안녕하세요, 반갑습니다 440 | process([1, 2, 3]); // 양수 리스트: [1, 2, 3] 441 | process([-1, 2, 3]); // 기타 값: [-1, 2, 3] 442 | process(('홍길동', 30)); // 성인: 홍길동, 30살 443 | process(('김영희', 15)); // 미성년자: 김영희, 15살 444 | } 445 | ``` 446 | 447 | ## 실전 예제 448 | 449 | ### 1. REST API 응답 처리 450 | 451 | ```dart 452 | // API 응답 시뮬레이션 453 | Map fetchUserResponse() { 454 | return { 455 | 'status': 'success', 456 | 'data': { 457 | 'user': { 458 | 'id': 123, 459 | 'name': '홍길동', 460 | 'email': 'hong@example.com', 461 | 'addresses': [ 462 | {'type': 'home', 'city': '서울', 'zipcode': '12345'}, 463 | {'type': 'work', 'city': '부산', 'zipcode': '67890'}, 464 | ] 465 | } 466 | } 467 | }; 468 | } 469 | 470 | void main() { 471 | var response = fetchUserResponse(); 472 | 473 | // 패턴 매칭으로 응답 처리 474 | switch (response) { 475 | case {'status': 'success', 'data': {'user': var user}}: 476 | if (user case {'name': String name, 'email': String email, 'addresses': var addresses}) { 477 | print('사용자 이름: $name, 이메일: $email'); 478 | 479 | if (addresses case List> addressList) { 480 | for (var address in addressList) { 481 | if (address case {'type': 'home', 'city': String city}) { 482 | print('집 주소: $city'); 483 | } 484 | } 485 | } 486 | } 487 | case {'status': 'error', 'message': String message}: 488 | print('오류: $message'); 489 | default: 490 | print('알 수 없는 응답'); 491 | } 492 | } 493 | ``` 494 | 495 | ### 2. 데이터 변환 및 검증 496 | 497 | ```dart 498 | // 데이터 변환 및 검증 499 | (bool isValid, {String? data, String? error}) validateUserData(Object input) { 500 | return switch (input) { 501 | Map map when map.containsKey('name') && map.containsKey('age') => 502 | map['age'] is int && map['age'] > 0 503 | ? (true, data: '유효한 사용자 데이터', error: null) 504 | : (false, data: null, error: '나이는 양의 정수여야 합니다.'), 505 | 506 | (String name, var age) when age is int && age > 0 => 507 | (true, data: '유효한 사용자 레코드', error: null), 508 | 509 | String s when RegExp(r'^[a-zA-Z0-9]+,\s*\d+$').hasMatch(s) => 510 | (true, data: '유효한 사용자 문자열', error: null), 511 | 512 | _ => 513 | (false, data: null, error: '지원하지 않는 형식') 514 | }; 515 | } 516 | 517 | void main() { 518 | // 다양한 입력 형식 테스트 519 | var result1 = validateUserData({'name': '홍길동', 'age': 30}); 520 | var result2 = validateUserData(('김철수', 25)); 521 | var result3 = validateUserData('이영희, 20'); 522 | var result4 = validateUserData({'name': '박지성', 'age': -5}); 523 | var result5 = validateUserData(42); 524 | 525 | for (var result in [result1, result2, result3, result4, result5]) { 526 | if (result.isValid) { 527 | print('검증 성공: ${result.data}'); 528 | } else { 529 | print('검증 실패: ${result.error}'); 530 | } 531 | } 532 | } 533 | ``` 534 | 535 | ## 결론 536 | 537 | 레코드와 패턴 매칭은 Dart 3에서 도입된 강력한 기능으로, 코드를 더 간결하고 표현력 있게 만들 수 있습니다. 레코드는 여러 값을 그룹화하고 반환하는 간편한 방법을 제공하며, 패턴 매칭은 복잡한 데이터 구조에서 값을 쉽게 추출하고 분석할 수 있게 해줍니다. 538 | 539 | 이러한 기능은 다음과 같은 상황에서 특히 유용합니다: 540 | 541 | - 함수에서 여러 값 반환 542 | - 데이터 구조에서 특정 패턴 검색 543 | - 조건부 로직 간소화 544 | - API 응답 처리 545 | - 데이터 변환 및 검증 546 | 547 | Dart의 레코드와 패턴 매칭을 활용하면 더 선언적이고 안전한 코드를 작성할 수 있으며, 이는 Flutter 애플리케이션에서도 큰 도움이 됩니다. 548 | 549 | 다음 장에서는 Dart의 비동기 프로그래밍에 대해 알아보겠습니다. 550 | --------------------------------------------------------------------------------