Ver2.5 Flutter로 앱 개발, 너도 할 수 있어! 크로스 플랫폼 대세에 올라타기 🚀

Flutter로 앱 개발, 너도 할 수 있어! 크로스 플랫폼 대세에 올라타기 🚀
하나의 코드로 안드로이드와 iOS 앱을 동시에 만드는 마법, 플러터의 세계에 오신 것을 환영합니다!
이 글 하나로 당신도 앱 개발의 첫발을 내디딜 수 있을 거예요. 자, 준비됐나요?
들어가며: "앱 하나 만들고 싶은데..." 막막했던 당신에게
"나만의 아이디어를 담은 앱을 만들어보고 싶다."
"요즘 코딩이 대세라는데, 나도 뭔가 결과물을 만들어보고 싶어."
이런 생각, 한 번쯤 해보지 않았나요? 하지만 막상 시작하려고 하면 눈앞이 캄캄해집니다. 😵
iOS 앱을 만들려면 Swift나 Objective-C를 배워야 하고, 안드로이드 앱을 만들려면 Kotlin이나 Java를 배워야 한다니...
언제 다 배우고, 언제 앱을 만들죠? 두 개의 앱을 따로따로 개발하고 관리하는 건 생각만 해도 머리가 아픕니다.
바로 이 지점에서, 우리 같은 '효율'을 중시하는 현대인들을 위한 구세주가 등장합니다. 이름하여 크로스 플랫폼(Cross-Platform)! 말 그대로 '플랫폼을 가로지른다'는 뜻으로, 단 하나의 코드 베이스로 iOS와 안드로이드 앱을 동시에 만들어내는 개발 방식을 의미해요.
그리고 오늘, 우리는 이 크로스 플랫폼 세계의 '슈퍼스타' ⭐, Flutter(플러터)에 대해 이야기해볼 겁니다. 구글이 만들고 밀어주는, 현재 가장 핫한 프레임워크 중 하나죠. 이 글은 플러터가 무엇인지, 왜 좋은지, 그리고 어떻게 시작해야 하는지에 대한 모든 것을 친구처럼 친절하게 알려주는 당신만의 가이드가 될 거예요. 코딩을 전혀 모르는 '코알못'도, 다른 개발 분야에서 넘어오려는 분도 모두 환영합니다!
이 글을 끝까지 읽으면 당신이 얻게 될 것들:
1. 왜 플러터가 '대세'인지 확실히 이해하게 됩니다.
2. 내 컴퓨터에 플러터 개발 환경을 완벽하게 세팅할 수 있습니다.
3. 내 손으로 직접 첫 번째 플러터 앱을 만들고 실행해 볼 수 있습니다.
4. 플러터의 핵심 개념인 '위젯'과 '상태 관리'에 대한 감을 잡게 됩니다.
5. 앞으로 무엇을 더 공부해야 할지, 명확한 로드맵을 그릴 수 있습니다.
자, 이제 스크롤 내릴 준비만 하세요. 당신의 앱 개발 여정, 지금 바로 시작합니다!
1. 플러터, 대체 뭐가 그렇게 좋은데? (feat. 라이벌 열전)
"크로스 플랫폼 프레임워크가 플러터만 있는 것도 아니잖아요?"
맞아요. 아주 예리한 질문입니다. 페이스북(현 메타)이 만든 React Native라는 강력한 라이벌도 있고, 다른 여러 기술들도 존재하죠. 그럼에도 불구하고 왜 많은 개발자들이 플러터에 열광하는 걸까요? 그 이유를 속 시원하게 파헤쳐 봅시다.
1-1. 플러터의 심장: 스키아(Skia) 엔진과 다트(Dart) 언어
플러터의 가장 큰 특징이자 강력함의 원천은 바로 UI를 그리는 방식에 있습니다.
다른 크로스 플랫폼 프레임워크(예: React Native)는 우리가 작성한 코드를 각 운영체제(OS)가 기본으로 제공하는 UI 부품(네이티브 UI 컴포넌트)으로 '변환'해서 화면에 보여줍니다. 마치 통역사를 거치는 것과 같죠.
하지만 플러터는 다릅니다. 통역사를 두지 않아요. 대신, 구글이 직접 만든 스키아(Skia)라는 2D 그래픽 엔진을 사용해서 UI를 백지 위에 직접 그립니다. 🎨
이게 왜 중요하냐면,
첫째, 미친 속도와 성능을 보장합니다.
네이티브 부품으로 변환하는 중간 다리가 없으니 당연히 빠르겠죠? 플러터는 AOT(Ahead-of-Time) 컴파일을 통해 네이티브 ARM 코드로 직접 컴파일되기 때문에, 거의 네이티브 앱에 가까운 부드러운 성능을 보여줍니다. 60fps(초당 60프레임) 애니메이션은 기본, 120fps도 거뜬하죠.
둘째, 어떤 기기에서든 똑같은 UI를 보장합니다. (픽셀 퍼펙트!)
안드로이드 최신폰, 구형폰, 아이폰, 아이패드... 어디서 앱을 실행하든 개발자가 디자인한 UI가 픽셀 하나 틀리지 않고 똑같이 보입니다. OS 버전에 따라 버튼 모양이 미묘하게 달라지는 골치 아픈 문제를 원천 차단하는 거죠. 디자이너와 협업할 때 이건 정말 엄청난 장점이에요.
이런 플러터를 움직이는 언어는 바로 다트(Dart)입니다. 다트 역시 구글이 만들었죠. 자바, C#, 자바스크립트 같은 언어들을 써본 사람이라면 굉장히 익숙하게 느낄 문법을 가지고 있어요. 특히 다트는 두 가지 모드로 컴파일되는데, 이게 또 물건입니다.
- JIT (Just-in-Time) 컴파일: 개발 중일 때 사용됩니다. 코드를 수정하고 저장하는 즉시, 변경사항이 앱에 바로 반영되는 '핫 리로드(Hot Reload)' 기능을 가능하게 해줍니다. 디자인 수정하고, 결과 바로 확인하고... 개발 속도가 말도 안 되게 빨라져요. ⚡
- AOT (Ahead-of-Time) 컴파일: 앱을 출시(배포)할 때 사용됩니다. 코드를 기계어로 미리 번역해두기 때문에 앱 실행 속도가 매우 빠르고 성능이 최적화됩니다.
개발할 땐 빠르게, 실행할 땐 더 빠르게! 다트의 이 두 얼굴 덕분에 플러터 개발 경험은 정말 쾌적합니다.
1-2. 플러터 vs 리액트 네이티브 vs 네이티브: 당신의 선택은?
자, 그럼 이제 라이벌들과 직접 비교하며 어떤 상황에 플러터를 선택하는 게 좋을지 알아봅시다.
⚔️ 세기의 대결: 플러터 vs 리액트 네이티브 vs 네이티브
1. 개발 속도 & 생산성
- 플러터 & 리액트 네이티브 (승): 핫 리로드 기능 덕분에 압도적으로 빠릅니다. 하나의 코드로 두 개의 플랫폼을 커버하니 생산성은 말할 것도 없죠. 스타트업이나 빠른 프로토타입(MVP) 제작에 최적화되어 있습니다.
- 네이티브 (패): 코드를 수정할 때마다 매번 다시 빌드해야 해서 시간이 오래 걸립니다. iOS, 안드로이드 팀을 따로 운영해야 하니 비용과 시간도 두 배로 들죠.
2. 성능
- 네이티브 (승): 당연히 해당 플랫폼에 최적화된 네이티브가 가장 빠르고 안정적입니다. 특히 고사양 게임, AR/VR, 실시간 영상 처리 등 기기 성능을 극한까지 끌어써야 하는 앱이라면 네이티브가 정답입니다.
- 플러터 (무): 네이티브에 준하는 성능을 보여줍니다. 대부분의 일반적인 앱에서는 사용자가 성능 차이를 느끼기 거의 불가능합니다. 직접 UI를 그리는 방식 덕분에 애니메이션이 매우 부드럽습니다.
- 리액트 네이티브 (패): 자바스크립트 브릿지를 거치는 구조적 한계 때문에 복잡한 애니메이션이나 연산에서 약간의 성능 저하가 발생할 수 있습니다. 물론 지속적인 개선으로 많이 좋아졌습니다.
3. UI/UX 일관성
- 플러터 (승): 스키아 엔진으로 직접 그리므로, 모든 기기에서 100% 동일한 UI를 보장합니다. 브랜드의 디자인 아이덴티티를 강력하게 유지하고 싶을 때 최고의 선택입니다.
- 네이티브 & 리액트 네이티브 (무): 각 플랫폼의 기본 UI 가이드라인을 따르기 때문에 사용자에게 익숙한 경험을 제공할 수 있습니다. 하지만 OS 업데이트에 따라 UI가 미세하게 변경될 수 있고, 두 플랫폼의 디자인을 맞추는 데 추가적인 노력이 필요합니다.
4. 생태계 및 커뮤니티
- 리액트 네이티브 (승): 기반 언어인 자바스크립트/리액트의 생태계가 워낙 거대해서 관련 라이브러리나 자료를 찾기 매우 쉽습니다. 웹 개발자들이 쉽게 넘어올 수 있다는 장점도 있죠.
- 플러터 (무): 구글의 전폭적인 지원 아래 무섭게 성장하고 있습니다. 공식 문서가 매우 잘 되어 있고, pub.dev라는 공식 패키지 저장소에 수많은 라이브러리가 존재합니다. 커뮤니티도 매우 활발해요.
- 네이티브 (무): 가장 역사가 오래된 만큼, 안정적이고 방대한 자료를 가지고 있습니다.
결론: 언제 플러터를 선택해야 할까? 🤔
- 빠른 시간 안에 MVP(최소 기능 제품)를 만들어 시장 반응을 보고 싶을 때
- iOS와 안드로이드에서 일관된 브랜드 디자인 경험을 제공하고 싶을 때
- 한정된 개발 인력과 예산으로 효율을 극대화하고 싶을 때
- 화려하고 부드러운 애니메이션이 많이 들어가는 앱을 만들고 싶을 때
위와 같은 상황이라면 플러터는 정말 최고의 선택이 될 수 있습니다.
2. 개발 환경 세팅, 이것만 따라 하면 끝! (Windows & macOS)
백문이 불여일견! 이제 직접 플러터를 만져볼 시간입니다.
"개발 환경 세팅하다가 포기했어요"라는 말이 있을 정도로, 초심자에게 이 과정은 꽤 높은 허들일 수 있어요. 하지만 걱정 마세요. 최대한 쉽고 자세하게 설명해 드릴 테니, 차근차근 따라오기만 하면 됩니다.
2-1. 플러터 SDK 설치: 개발 도구 모음 다운로드
SDK(Software Development Kit)는 플러터 개발에 필요한 모든 도구들이 담겨있는 꾸러미라고 생각하면 쉬워요.
1. 공식 사이트에서 SDK 다운로드
먼저 플러터 공식 사이트(flutter.dev)에 접속해서 본인의 운영체제(Windows, macOS, Linux)에 맞는 최신 버전의 SDK를 다운로드합니다. 압축 파일 형태로 받아질 거예요.
2. 압축 풀고 원하는 위치에 놓기
다운로드한 압축 파일을 풉니다. 그리고 이 `flutter` 폴더를 앞으로 계속 사용할 위치로 옮겨주세요. 주의할 점! `C:\Program Files`처럼 관리자 권한이 필요한 폴더는 피하는 것이 좋습니다. 보통 사용자 폴더 아래에 `development`나 `src` 같은 폴더를 만들어 그 안에 두는 것을 추천합니다. (예: `C:\Users\YourUser\development\flutter`)
3. 환경 변수(PATH) 설정: "플러터야, 어디 있니?" 알려주기
이 단계가 가장 중요하고 헷갈리는 부분입니다. 컴퓨터에게 "앞으로 내가 'flutter'라는 명령어를 치면, 아까 옮겨둔 폴더 안에 있는 실행 파일을 찾아줘!"라고 알려주는 과정이에요.
-
Windows:
1. 검색창에 '환경 변수'를 검색하고 '시스템 환경 변수 편집'을 실행합니다.
2. '환경 변수' 버튼을 클릭합니다.
3. '사용자 변수' 또는 '시스템 변수' 목록에서 `Path`를 찾아 더블클릭합니다.
4. '새로 만들기'를 누르고, 아까 `flutter` 폴더를 옮겨둔 경로에 `\bin`을 붙여서 입력합니다. (예: `C:\Users\YourUser\development\flutter\bin`)
5. 확인을 눌러 모든 창을 닫습니다. -
macOS:
1. 터미널(Terminal) 앱을 엽니다.
2. 사용하는 셸(shell)에 따라 설정 파일이 다릅니다. 보통 `zsh`를 사용하므로, `vi ~/.zshrc` 또는 `open ~/.zshrc` 명령어로 설정 파일을 엽니다. (만약 `bash`를 쓴다면 `~/.bash_profile` 입니다.)
3. 파일 맨 아래에 다음 줄을 추가합니다. `[PATH_TO_FLUTTER_GIT_DIRECTORY]` 부분은 실제 `flutter` 폴더가 있는 경로로 바꿔주세요.
(예: `export PATH="$PATH:/Users/YourUser/development/flutter/bin"`)export PATH="$PATH:[PATH_TO_FLUTTER_GIT_DIRECTORY]/flutter/bin"
4. 파일을 저장하고 터미널을 완전히 껐다가 다시 켜거나, `source ~/.zshrc` 명령어를 실행하여 변경사항을 적용합니다.
2-2. `flutter doctor` 실행: 내 컴퓨터 건강검진
환경 변수 설정까지 마쳤다면, 이제 터미널(Windows에서는 PowerShell 또는 CMD)을 열고 아래 명령어를 입력해보세요.
flutter doctor
이 명령어는 플러터 개발에 필요한 것들이 제대로 설치되어 있는지 종합적으로 검진해주는 의사 선생님 같은 역할을 합니다. 👨⚕️
처음 실행하면 아마 `[!]`나 `[X]` 표시와 함께 여러 항목들이 빨갛게 뜰 거예요. 당황하지 마세요, 지극히 정상입니다! `flutter doctor`가 친절하게 무엇이 부족하고 어떻게 해결해야 하는지 알려줄 겁니다.
2-3. 플랫폼별 개발 환경 구축
이제 `flutter doctor`가 지적한 문제들을 하나씩 해결해 나갈 차례입니다. 우리는 안드로이드와 iOS 앱을 모두 만들고 싶으니, 두 플랫폼에 대한 개발 환경을 모두 구축해야 합니다.
[공통] 코드 에디터(IDE) 설치
플러터 코드를 작성하고 앱을 실행할 프로그램을 설치해야 합니다. 가장 인기 있는 두 가지 선택지가 있습니다.
- Visual Studio Code (VS Code): 가볍고 빠르며, 강력한 확장 기능으로 무장한 최고의 인기 에디터입니다. 'Flutter'와 'Dart' 확장 프로그램을 꼭 설치해주세요.
- Android Studio: 구글이 만든 안드로이드 개발 공식 툴입니다. 조금 무겁지만, 안드로이드 관련 기능(에뮬레이터 관리, 빌드 등)이 매우 강력하게 통합되어 있습니다. 마찬가지로 'Flutter'와 'Dart' 플러그인을 설치해야 합니다.
초보자를 위한 팁: 뭘 써야 할까요?
둘 다 훌륭하지만, 처음 시작하신다면 VS Code를 추천합니다. 훨씬 가볍고 직관적이어서 부담이 적습니다. 하지만 안드로이드 에뮬레이터를 설정하려면 어차피 안드로이드 스튜디오가 필요하니, 둘 다 설치해두는 것이 가장 좋습니다.
안드로이드 개발 환경 (Windows & macOS)
- Android Studio 설치: 구글 공식 사이트에서 다운로드하여 설치합니다. 설치 과정에서 'Android SDK', 'Android SDK Platform-Tools', 'Android Virtual Device' 등을 모두 체크하고 설치해주세요.
- 안드로이드 에뮬레이터 생성:
- Android Studio를 실행하고, Welcome 화면에서 'More Actions' > 'Virtual Device Manager'를 선택합니다.
- 'Create device'를 클릭하고, 원하는 기기(예: Pixel 6)를 선택한 후 Next를 누릅니다.
- 원하는 안드로이드 버전(System Image)을 다운로드하고 선택한 후, Finish를 눌러 가상 기기 생성을 완료합니다. - 안드로이드 라이선스 동의: 터미널에 아래 명령어를 입력하고, 나타나는 모든 질문에 `y`를 입력하여 라이선스에 동의합니다.
flutter doctor --android-licenses
이 과정이 끝나고 다시 `flutter doctor`를 실행하면, 'Android toolchain' 항목에 녹색 체크 `[✓]`가 뜰 겁니다!
iOS 개발 환경 (macOS 전용)
아쉽게도 iOS 앱을 개발하고 빌드하려면 반드시 macOS 운영체제가 설치된 맥(Mac) 컴퓨터가 필요합니다. 애플의 정책 때문이죠.
- Xcode 설치: App Store에서 Xcode를 검색하여 설치합니다. 용량이 매우 크니 시간적 여유를 가지고 진행하세요.
- Xcode Command Line Tools 설정: Xcode 설치 후, 터미널에 아래 명령어를 입력하여 커맨드 라인 도구를 설정합니다.
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer sudo xcodebuild -runFirstLaunch - CocoaPods 설치 및 설정: CocoaPods는 iOS 개발에 필요한 라이브러리들을 관리해주는 도구입니다. 터미널에 아래 명령어를 순서대로 입력하여 설치하고 설정합니다.
sudo gem install cocoapods pod setup
이제 다시 `flutter doctor`를 실행해보세요. Xcode와 CocoaPods 관련 항목에도 녹색 체크가 떴을 겁니다. 모든 항목에 녹색 체크가 떴다면, 당신은 이제 플러터 앱을 개발할 모든 준비를 마친 것입니다! 🎉
3. Hello, Flutter! 내 손으로 만드는 첫 번째 앱
이론 공부와 환경 설정은 여기까지! 이제 진짜 코드를 만져볼 시간입니다.
코딩 경험이 전혀 없어도 괜찮아요. 지금부터 우리는 플러터가 기본으로 제공하는 예제 앱을 함께 만들고, 그 구조를 하나하나 뜯어보며 플러터와 친해지는 시간을 가질 겁니다.
3-1. 프로젝트 생성과 실행
1. 프로젝트 생성하기
먼저, 앞으로 여러분의 플러터 프로젝트들을 모아둘 폴더를 하나 만드세요. 그리고 VS Code나 터미널에서 그 폴더로 이동합니다.
VS Code를 사용한다면, `Ctrl+Shift+P` (macOS: `Cmd+Shift+P`)를 눌러 커맨드 팔레트를 열고 'Flutter: New Project'를 검색하여 선택하세요. 'Application'을 선택하고, 프로젝트를 저장할 폴더를 지정한 뒤, 프로젝트 이름을 입력합니다. (예: `first_flutter_app`)
터미널을 사용한다면, 아래 명령어를 입력하세요.
flutter create first_flutter_app
⚠️ 프로젝트 이름 규칙!
프로젝트 이름은 반드시 소문자와 언더스코어(_)만 사용해야 합니다. (예: `my_awesome_app` (O), `MyAwesomeApp` (X), `my-awesome-app` (X))
명령을 실행하면 플러터가 마법처럼 필요한 파일과 폴더들을 자동으로 쫙 만들어줍니다.
2. 앱 실행하기
이제 생성된 프로젝트 폴더를 VS Code로 열어보세요.
- VS Code 우측 하단을 보면 현재 선택된 디바이스가 표시됩니다. (예: 'Chrome (web)') 이 부분을 클릭해서 아까 만들어둔 안드로이드 에뮬레이터나 iOS 시뮬레이터를 선택하세요. (실제 폰을 USB로 연결해도 목록에 나타납니다!)
- `F5` 키를 누르거나, 메뉴에서 'Run' > 'Start Debugging'을 클릭하면 앱 빌드가 시작됩니다.
- 처음 빌드할 때는 시간이 꽤 걸릴 수 있으니 커피 한 잔의 여유를 가지세요. ☕
잠시 후, 에뮬레이터 화면에 파란색 배경의 앱이 나타날 겁니다. 우측 하단에 `+` 버튼이 있고, 이 버튼을 누를 때마다 화면 중앙의 숫자가 1씩 올라가는 간단한 카운터 앱이죠. 축하합니다! 당신의 첫 번째 플러터 앱이 성공적으로 실행되었습니다! 🥳
3-2. 플러터 프로젝트 구조 훑어보기
왼쪽 파일 탐색기를 보면 수많은 파일과 폴더가 있어서 당황스러울 수 있어요. 하지만 90%는 우리가 거의 건드릴 일이 없는 것들이니 안심하세요. 지금은 딱 2개만 기억하면 됩니다.
우리는 앞으로 99%의 시간을 저 `lib` 폴더 안에서 보내게 될 겁니다. 그리고 외부 라이브러리를 가져다 쓰거나 폰트/이미지를 추가할 때 `pubspec.yaml` 파일을 수정하게 될 거고요. 간단하죠?
3-3. 카운터 앱 코드 해부하기: 플러터의 작동 원리
이제 `lib/main.dart` 파일을 열어봅시다. 주석이 많이 달려있어서 복잡해 보이지만, 주석을 싹 지우고 핵심 구조만 보면 생각보다 단순합니다. 함께 코드를 한 줄 한 줄(또는 한 덩어리씩) 뜯어보죠.
// 1. 필요한 패키지를 가져옵니다. material.dart는 구글의 머티리얼 디자인 위젯들을 포함해요.
import 'package:flutter/material.dart';
// 2. 앱의 시작점! main 함수가 runApp을 호출하면서 앱이 시작됩니다.
void main() {
runApp(const MyApp());
}
// 3. MyApp 클래스. 앱의 최상위 위젯이며, 전체적인 테마나 설정을 담당합니다.
// StatelessWidget은 '상태가 없는' 정적인 위젯입니다.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
// 4. MyHomePage 클래스. 실제 앱의 화면을 구성하는 위젯입니다.
// StatefulWidget은 '상태가 있는' 동적인 위젯입니다. (숫자가 바뀌어야 하니까!)
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
// 5. MyHomePage의 '상태'를 관리하는 _MyHomePageState 클래스.
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0; // 화면에 표시될 숫자 데이터 (이것이 바로 '상태'!)
// 버튼을 누를 때 호출될 함수
void _incrementCounter() {
// setState는 플러터에게 "상태가 바뀌었으니 화면을 다시 그려줘!"라고 알리는 중요한 함수입니다.
setState(() {
_counter++;
});
}
// 6. 실제 화면 UI를 그리는 부분!
@override
Widget build(BuildContext context) {
return Scaffold( // Scaffold는 앱 화면의 기본 구조(상단바, 본문 등)를 제공하는 뼈대 위젯입니다.
appBar: AppBar(
title: Text(widget.title),
),
body: Center( // body는 화면의 본문 영역입니다. Center는 자식 위젯을 가운데로 정렬해줍니다.
child: Column( // Column은 자식 위젯들을 세로로 배치합니다.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter', // _counter 변수를 화면에 표시
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton( // 우측 하단의 동그란 + 버튼
onPressed: _incrementCounter, // 버튼이 눌리면 _incrementCounter 함수를 실행
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
코드가 길어서 어지러우신가요? 핵심만 짚어드릴게요!
- 모든 것은 위젯(Widget)이다: `MyApp`, `MaterialApp`, `Scaffold`, `AppBar`, `Center`, `Column`, `Text`... 코드에 보이는 거의 모든 것이 '위젯'입니다. 플러터는 이 위젯들을 레고 블록처럼 조립해서 UI를 만들어요.
- StatelessWidget vs StatefulWidget: 이게 플러터의 첫 번째 고비이자 가장 중요한 개념입니다.
- StatelessWidget (상태 없는 위젯): 한 번 그려지면 절대 변하지 않는 정적인 화면입니다. 글씨만 보여주는 `Text` 위젯이나 아이콘 같은 것들이죠. `MyApp` 클래스가 여기에 해당합니다.
- StatefulWidget (상태 있는 위젯): 사용자의 행동(버튼 클릭 등)에 따라 화면 내용이 바뀌어야 하는 동적인 부분입니다. 카운터 앱에서는 숫자가 계속 바뀌어야 하므로 `MyHomePage`가 StatefulWidget으로 만들어졌어요.
- `build` 메소드: 모든 위젯은 `build`라는 메소드를 가지고 있습니다. 플러터는 이 메소드를 호출해서 "여기에 뭘 그려야 하지?"라고 물어보고, `build`가 리턴하는 위젯 트리를 화면에 그립니다.
- `setState()`: StatefulWidget의 핵심! `_incrementCounter` 함수 안에 있는 `setState()`가 호출되면, 플러터는 "아, 데이터가 바뀌었구나! `_MyHomePageState`의 `build` 메소드를 다시 실행해서 화면을 새로고침해야겠다!"라고 인식합니다. `setState()`가 없다면 `_counter` 변수 값은 바뀌어도 화면은 절대 변하지 않아요.
비유로 이해하기: StatelessWidget vs StatefulWidget
- StatelessWidget은 박물관에 걸린 '그림 액자' 같아요. 한 번 걸어두면 누가 보든, 언제 보든 항상 똑같은 모습이죠.
- StatefulWidget은 경기장의 '스코어보드' 같아요. 경기가 진행됨에 따라 점수가 계속 바뀌고, 그 바뀐 점수를 실시간으로 화면에 보여줘야 하죠. `setState()`는 점수가 바뀔 때마다 스코어보드 관리자에게 "점수판 업데이트해!"라고 외치는 것과 같습니다.
지금 당장 이 모든 걸 100% 이해하지 못해도 괜찮습니다. "아, 플러터는 위젯을 조립해서 화면을 만들고, 화면이 바뀌려면 StatefulWidget과 setState가 필요하구나" 정도만 기억하고 넘어가도 대성공입니다!
4. 레고처럼 조립하는 UI: 플러터 위젯의 모든 것
앞서 "플러터에서는 모든 것이 위젯이다"라고 말씀드렸죠. 이 말은 전혀 과장이 아닙니다. 화면에 보이는 텍스트, 버튼, 이미지뿐만 아니라, 눈에 보이지 않는 레이아웃, 정렬, 패딩, 애니메이션 효과까지도 모두 위젯으로 처리합니다.
이번 챕터에서는 가장 기본적이고 필수적인 위젯들을 소개할 거예요. 이것들만 잘 조합해도 웬만한 앱 화면은 뚝딱 만들어낼 수 있습니다. 마치 레고 기본 브릭 세트와 같다고 할 수 있죠!
4-1. 기본 중의 기본! 화면을 채우는 위젯들
-
`Container`: 가장 기본적이고 만능인 '상자' 위젯입니다. 네모난 영역을 만들고 싶을 때 사용해요.
- `color`: 상자의 배경색을 지정합니다.
- `width`, `height`: 가로, 세로 크기를 지정합니다.
- `padding`: 상자 안쪽 여백을 줍니다.
- `margin`: 상자 바깥쪽 여백을 줍니다.
- `decoration`: 그림자, 그라데이션, 둥근 모서리 등 화려한 장식을 할 수 있습니다.
Container( width: 100, height: 100, padding: const EdgeInsets.all(16.0), margin: const EdgeInsets.all(10.0), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.5), spreadRadius: 5, blurRadius: 7, offset: Offset(0, 3), // changes position of shadow ), ], ), child: Text('Hello'), ) -
`Text`: 화면에 글자를 표시하는 위젯입니다.
- `style` 속성을 통해 `TextStyle` 위젯으로 글자 크기, 색상, 굵기, 폰트 등을 자유롭게 꾸밀 수 있습니다.
Text( 'Welcome to Flutter!', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.deepPurple, ), ) -
`Image`: 이미지를 보여주는 위젯입니다.
- `Image.asset('assets/my_image.png')`: 프로젝트 내부에 저장된 이미지를 불러올 때 사용합니다.
- `Image.network('https://.../image.jpg')`: 인터넷에 있는 이미지를 URL로 불러올 때 사용합니다.
- `Icon`: 아이콘을 표시하는 위젯입니다. 플러터는 기본적으로 머티리얼 디자인 아이콘 세트를 내장하고 있어서 `Icons.heart`, `Icons.settings`처럼 쉽게 사용할 수 있습니다.
-
버튼 위젯들:
- `ElevatedButton`: 배경색이 있고 살짝 떠 있는 듯한 입체감이 있는 기본 버튼입니다.
- `TextButton`: 배경색 없이 텍스트만 있는 버튼입니다.
- `OutlinedButton`: 테두리만 있는 버튼입니다.
- 모든 버튼은 `onPressed` 속성에 버튼이 눌렸을 때 실행할 함수를 지정해야 합니다.
4-2. 레이아웃의 뼈대! 위젯을 배치하는 위젯들
위젯들을 만들었으면 이제 화면에 보기 좋게 배치해야겠죠? 레이아웃 위젯들은 다른 위젯들을 자식(child 또는 children)으로 품어서 정렬하고 배치하는 역할을 합니다.
-
`Row` & `Column`: 레이아웃의 알파이자 오메가! 가장 중요한 위젯입니다.
- `Row`: 자식 위젯들을 가로(수평) 방향으로 나란히 배치합니다.
- `Column`: 자식 위젯들을 세로(수직) 방향으로 차곡차곡 쌓습니다.
- `mainAxisAlignment`: 주축(Row는 가로, Column은 세로) 방향으로 자식들을 어떻게 정렬할지 결정합니다. (예: `MainAxisAlignment.center`는 가운데 정렬, `MainAxisAlignment.spaceBetween`은 양 끝에 붙이고 사이 간격을 균등하게 배분)
- `crossAxisAlignment`: 교차축(Row는 세로, Column은 가로) 방향으로 자식들을 어떻게 정렬할지 결정합니다.
- `Stack`: 위젯들을 겹겹이 쌓아 올리고 싶을 때 사용합니다. 마치 포토샵의 레이어처럼요. 이미지 위에 텍스트를 올리거나, 특정 위젯 위에 아이콘을 띄울 때 유용합니다.
-
`Expanded` & `Flexible`: `Row`나 `Column` 안에서 남는 공간을 어떻게 차지할지 결정하는 똑똑한 위젯입니다.
- `Expanded`: 남는 공간을 모조리 다 차지합니다. 여러 개의 `Expanded`가 있다면 `flex` 값의 비율대로 공간을 나눠 갖습니다.
- `Flexible`: 필요하다면 남는 공간을 차지하지만, 자식 위젯의 크기만큼만 차지할 수도 있습니다. `Expanded`보다 좀 더 유연하죠.
- `ListView` & `GridView`: 화면을 넘어가는 긴 목록이나 격자무늬 목록을 만들 때 사용합니다. 사용자가 스크롤할 수 있는 영역을 만들어주죠.
- `Padding`: 자식 위젯에게 안쪽 여백을 줍니다. `Container`의 `padding` 속성과 비슷하지만, 오직 여백만 주고 싶을 때 더 가볍고 명확하게 사용할 수 있습니다.
위젯 트리(Widget Tree)를 상상하라!
플러터 코드를 볼 때, 위젯들이 어떻게 중첩되어 있는지를 머릿속으로 그리는 연습을 하면 좋습니다. 이것을 '위젯 트리'라고 부릅니다.
예를 들어, `Scaffold`라는 큰 나무의 몸통이 있고, 그 안에 `appBar` 가지와 `body` 가지가 뻗어 나옵니다. `body` 가지에는 `Column`이라는 또 다른 가지가 있고, 그 `Column` 가지에 `Text`와 `ElevatedButton`이라는 잎사귀들이 달려있는 식이죠. 이 구조를 이해하면 복잡한 UI도 쉽게 분석하고 만들 수 있습니다.
지금 소개한 위젯들만 잘 조합해도 정말 다양한 화면을 만들 수 있습니다. 처음에는 어색하겠지만, 이것저것 바꿔보고 실행해보면서 각 위젯의 특징을 몸으로 익히는 것이 가장 빠른 학습 방법입니다.
5. 플러터를 움직이는 심장, 다트(Dart) 언어 뽀개기
플러터라는 멋진 자동차를 운전하려면, 그 엔진인 다트(Dart) 언어에 대해 알아야겠죠? 다행히 다트는 배우기 쉬운 언어에 속합니다. 특히 자바, 자바스크립트, C++ 같은 언어를 조금이라도 경험해봤다면 "어? 이거 완전 비슷한데?"라는 생각이 들 거예요.
여기서는 플러터 개발에 꼭 필요한 다트의 핵심 문법만 쏙쏙 뽑아서 알려드릴게요.
5-1. 변수: 데이터를 담는 그릇
변수는 숫자, 글자 같은 데이터를 저장하는 공간입니다.
- `var`: 타입 추론. 변수에 처음 할당되는 값의 종류를 보고 다트가 알아서 타입을 결정합니다. 한 번 타입이 정해지면 바꿀 수 없어요.
var name = 'Flutter'; // String 타입으로 결정됨 var version = 1.0; // double 타입으로 결정됨 // name = 123; // 에러! String 타입 변수에 int를 넣을 수 없음 - 명시적 타입: 변수의 타입을 직접 지정할 수도 있습니다.
String name = 'Dart'; int age = 10; double temperature = 36.5; bool isAwesome = true; - `final` vs `const`: 한 번 값을 할당하면 절대 바꿀 수 없는 변수(상수)를 만들 때 사용합니다.
- `final`: 앱이 실행되는 동안(런타임) 값이 결정됩니다. API 응답처럼 실행 시점에야 알 수 있는 값을 담을 때 사용해요.
- `const`: 코드를 작성하는 시점(컴파일 타임)에 값이 이미 결정되어 있어야 합니다. 절대 변하지 않을 고정된 값에 사용하며, `final`보다 성능상 이점이 있습니다.
final name = 'John'; // OK const PI = 3.14; // OK final currentTime = DateTime.now(); // OK (실행 시점의 시간이 할당됨) // const compileTime = DateTime.now(); // 에러! 컴파일 시점에는 현재 시간을 알 수 없음
5-2. 데이터 구조: 여러 데이터를 묶어서 관리하기
- `List`: 여러 데이터를 순서대로 담는 배열입니다. 자바스크립트의 배열과 비슷해요.
List<String> fruits = ['Apple', 'Banana', 'Orange']; print(fruits[0]); // 'Apple' 출력 fruits.add('Grape'); // 리스트에 'Grape' 추가 - `Map`: '키(Key)'와 '값(Value)'을 한 쌍으로 묶어서 저장합니다. 자바스크립트의 객체(Object)와 비슷해요.
Map<String, dynamic> user = { 'name': 'Chris', 'age': 30, 'isDeveloper': true, }; print(user['name']); // 'Chris' 출력
5-3. 함수: 코드 묶음에 이름 붙이기
특정 작업을 수행하는 코드 덩어리를 만들어두고, 필요할 때마다 이름으로 불러서 재사용할 수 있습니다.
// 기본 함수 형태
int add(int a, int b) {
return a + b;
}
// 코드가 한 줄이면 화살표(=>) 구문으로 축약 가능
int subtract(int a, int b) => a - b;
// 함수의 파라미터(매개변수)에 이름을 붙여서 가독성을 높일 수 있음 (Named Parameters)
void printUserInfo({required String name, int? age}) {
print('Name: $name, Age: $age');
}
// 호출할 때
printUserInfo(name: 'Alice', age: 25);
printUserInfo(name: 'Bob'); // age는 선택사항이므로 생략 가능
5-4. 널 안정성(Null Safety): '없음'과의 안전한 싸움
다트의 강력한 기능 중 하나입니다. 변수가 '값이 없음'(null) 상태가 될 수 있는지 여부를 코드에 명시해서, 'null' 때문에 발생하는 앱 다운(크래시)을 원천적으로 방지해줍니다.
- `?` (Nullable): 타입 뒤에 물음표를 붙이면, 이 변수는 '값이 있을 수도 있고, 없을 수도(null) 있다'는 뜻입니다.
String name = 'Flutter'; // null이 될 수 없음 String? nullableName; // null이 될 수 있음 - `!` (Assertion Operator): "이 변수는 지금 절대로 null이 아니야!"라고 개발자가 컴파일러에게 확신을 주는 기호입니다. 만약 null인데 `!`를 사용하면 앱이 다운되니, 100% 확신할 때만 사용해야 합니다.
// nullableName이 null이 아니라고 확신할 때 int length = nullableName!.length; - `??` (If-null Operator): 변수가 null이면, 대신 사용할 기본값을 지정해줍니다.
String playerName = nullableName ?? 'Guest'; // nullableName이 null이면 'Guest'를 사용
널 안정성, 왜 중요할까?
프로그래밍에서 가장 흔한 에러 중 하나가 바로 'Null Pointer Exception'입니다. "값이 없는 상자를 열어보려고 했어!"라는 의미의 에러죠. 다트의 널 안정성은 이런 실수를 코드를 짜는 단계에서부터 잡아주기 때문에, 훨씬 안정적이고 튼튼한 앱을 만들 수 있게 도와줍니다.
5-5. 비동기 프로그래밍: 기다림의 미학 (`async`, `await`, `Future`)
앱을 만들다 보면 시간이 오래 걸리는 작업들이 있습니다. 인터넷에서 데이터를 받아오거나, 기기에 파일을 저장하는 작업들이죠. 이런 작업을 기다리는 동안 앱 전체가 멈춰버리면(UI 프리징) 사용자는 앱이 고장났다고 생각할 겁니다.
비동기 프로그래밍은 이런 오래 걸리는 작업을 '백그라운드'에서 처리하고, 그동안 앱은 다른 일을 계속할 수 있게 해주는 기술입니다.
- `Future`: "지금 당장은 아니지만, 미래의 어떤 시점에 결과(데이터 또는 에러)를 돌려줄게"라는 약속의 증표입니다.
- `async`: 함수 뒤에 `async`를 붙이면, 이 함수는 비동기 함수가 되며 `Future`를 반환합니다.
- `await`: `async` 함수 안에서만 사용할 수 있으며, `Future`가 완료될 때까지 코드 실행을 '기다리게' 합니다. 하지만 앱 전체를 멈추는 게 아니라, 해당 함수 내에서만 기다리는 똑똑한 방식이죠.
// 2초 후에 데이터를 가져오는 가상의 함수
Future<String> fetchData() {
return Future.delayed(Duration(seconds: 2), () => 'Data from Server');
}
// 비동기 함수
void printData() async {
print('Fetching data...');
String data = await fetchData(); // fetchData()가 끝날 때까지 2초간 기다림
print(data); // 2초 후에 'Data from Server' 출력
print('Done!');
}
이 `async/await` 패턴은 서버와 통신하는 거의 모든 앱에서 필수적으로 사용되므로, 반드시 개념을 이해하고 넘어가야 합니다.
6. 앱의 상태를 지배하는 자, 스테이트 매니지먼트 정복하기
축하합니다! 플러터의 가장 큰 산 중 하나인 '상태 관리(State Management)'의 영역에 오셨습니다. 🥳
"아까 StatefulWidget이랑 `setState()` 배웠는데, 그걸로 관리하면 되는 거 아닌가요?"
네, 맞습니다. 카운터 앱처럼 아주 간단한 앱에서는 `setState()`만으로 충분합니다. 하지만 앱이 조금만 복잡해지면 `setState()`는 금방 한계에 부딪힙니다.
6-1. 왜 상태 관리가 필요한가? (`setState()`의 한계)
상상해보세요. 여러 화면으로 구성된 쇼핑 앱이 있습니다.
- `상품 목록 화면`에서 상품을 장바구니에 담습니다.
- `상단바`에 있는 장바구니 아이콘의 뱃지 숫자가 1 올라가야 합니다.
- `장바구니 화면`으로 이동하면, 방금 담은 상품이 목록에 보여야 합니다.
이 '장바구니 데이터'라는 하나의 상태를 여러 위젯이 공유하고, 한 곳에서 데이터가 바뀌면 관련된 모든 위젯이 동시에 업데이트되어야 합니다. 이걸 `setState()`만으로 구현하려면? 끔찍한 일이 벌어집니다. 😱
A 위젯의 데이터를 F 위젯에 전달하기 위해 중간에 있는 B, C, D, E 위젯을 거쳐야 하는 'Prop Drilling' 지옥이 펼쳐지거나, 코드가 스파게티처럼 얽혀서 어디서 데이터가 바뀌고 화면이 갱신되는지 추적조차 힘들어집니다.
그래서 우리는 '상태 관리 솔루션'을 사용합니다. 상태 관리 솔루션은 앱의 중요한 데이터(상태)를 위젯 트리 바깥의 별도 공간에서 중앙 집중적으로 관리하고, 이 데이터가 필요한 위젯들에게 "데이터 바뀌었어!"라고 알려주어 화면을 효율적으로 갱신하게 돕는 아키텍처 패턴입니다.
6-2. 초보자를 위한 최고의 선택: Provider
플러터에는 BLoC, Riverpod, GetX, MobX 등 수많은 상태 관리 솔루션이 있습니다. 각자 장단점과 철학이 다르지만, 처음 시작하는 분들에게는 구글에서도 추천하는 Provider가 가장 배우기 쉽고 직관적입니다.
Provider의 핵심 컨셉은 간단합니다.
1. `ChangeNotifier`: 상태(데이터)를 가지고 있는 '모델' 클래스입니다. 데이터가 변경되면 `notifyListeners()`를 호출해서 "나 바뀌었어!"라고 외칩니다.
2. `ChangeNotifierProvider`: 위에서 만든 `ChangeNotifier`를 위젯 트리에 '제공(provide)'해주는 역할을 합니다. 이 위젯의 하위에 있는 모든 위젯들은 이제 이 데이터에 접근할 수 있게 됩니다.
3. `Consumer` / `Provider.of`: 제공된 데이터를 '소비(consume)'하는 위젯입니다. 데이터를 가져와서 화면에 표시하거나, 데이터가 변경될 때마다 특정 부분만 다시 그리도록 할 수 있습니다.
카운터 앱을 Provider로 바꿔보기:
1. `provider` 패키지 추가
`pubspec.yaml` 파일의 `dependencies` 아래에 `provider: ^6.0.0` (버전은 최신으로)을 추가하고 저장합니다. VS Code는 보통 자동으로 패키지를 설치해줍니다.
2. `ChangeNotifier` 만들기 (`counter_model.dart`)
import 'package:flutter/material.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count; // 외부에서 _count 값을 읽을 수 있도록 getter 제공
void increment() {
_count++;
notifyListeners(); // 데이터가 바뀌었음을 구독자들에게 알림!
}
}
3. `main.dart`에서 Provider로 데이터 제공하기
void main() {
runApp(
// ChangeNotifierProvider로 CounterModel을 앱 전체에 제공
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
4. 화면에서 데이터 사용하고 변경하기
// ... MyHomePage의 build 메소드 안 ...
// 방법 1: Consumer 위젯 사용 (UI의 특정 부분만 재빌드하여 효율적)
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Consumer<CounterModel>(
builder: (context, counter, child) {
return Text(
'${counter.count}', // counter.count로 데이터에 접근
style: Theme.of(context).textTheme.headlineMedium,
);
},
),
],
),
),
// 방법 2: Provider.of 사용 (함수 호출 등 UI 빌드 외의 목적으로 데이터에 접근할 때 유용)
floatingActionButton: FloatingActionButton(
onPressed: () {
// listen: false는 UI를 다시 그릴 필요는 없고, 함수만 호출하고 싶을 때 사용
Provider.of<CounterModel>(context, listen: false).increment();
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
어떤가요? 이제 `MyHomePage`는 더 이상 `StatefulWidget`일 필요가 없습니다. 상태 데이터와 UI 로직이 완전히 분리되었죠. 앱이 아무리 커지고 복잡해져도, 상태는 `CounterModel`이, 화면은 위젯이 각자 책임지게 되므로 훨씬 깔끔하고 관리하기 쉬운 코드가 됩니다.
어떤 상태 관리 솔루션을 선택해야 할까요?
- Provider: 입문자에게 가장 추천. 배우기 쉽고 간단한 앱부터 중간 규모 앱까지 커버 가능합니다.
- Riverpod: Provider의 단점을 개선한 차세대 버전. 컴파일 시점에 에러를 잡을 수 있어 더 안전합니다. Provider에 익숙해진 후 다음 스텝으로 좋습니다.
- BLoC/Cubit: 비즈니스 로직이 매우 복잡하고, 이벤트와 상태를 엄격하게 분리하고 싶을 때 사용합니다. 대규모 프로젝트에 적합하며, 약간의 학습 곡선이 있습니다.
- GetX: 상태 관리, 라우팅, 의존성 주입 등 많은 기능을 하나로 합친 올인원 솔루션입니다. 코드가 간결해지지만, GetX 생태계에 대한 의존성이 높아진다는 단점도 있습니다.
정답은 없습니다. 프로젝트의 규모와 팀의 성향에 맞는 것을 선택하는 것이 중요합니다. 하지만 처음에는 무조건 Provider로 시작해서 상태 관리의 기본 개념을 확실히 잡는 것을 강력하게 추천합니다.
7. 이제 당신도 플러터 중급자! 🚀 필수 스킬 장착하기
기본 위젯, 다트 문법, 상태 관리까지 익혔다면 당신은 이미 플러터의 기본기를 탄탄히 다진 셈입니다. 이제 실제 앱다운 앱을 만들기 위해 필요한 몇 가지 필수 스킬들을 장착해 봅시다.
7-1. 세상과 소통하기: API 연동과 데이터 파싱
대부분의 앱은 자체적으로 모든 데이터를 가지고 있지 않습니다. 날씨 정보, 영화 목록, 사용자 게시물 등 서버에 있는 데이터를 네트워크 통신을 통해 가져와 화면에 보여주죠. 이 과정을 'API 연동'이라고 합니다.
1. `http` 패키지 추가
플러터에서 HTTP 통신을 하려면 `http` 패키지를 사용하는 것이 가장 일반적입니다. `pubspec.yaml`에 추가해주세요.
2. 데이터 요청하고 받기 (GET 요청)
보통 서버의 데이터는 JSON(JavaScript Object Notation)이라는 텍스트 형식으로 주고받습니다. `http.get()` 함수로 API를 호출하고, 응답받은 JSON 문자열을 `jsonDecode()` 함수로 다트의 `Map`이나 `List` 형태로 변환(파싱)합니다.
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> fetchPosts() async {
// JSONPlaceholder라는 무료 테스트 API 사용
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
try {
final response = await http.get(url);
if (response.statusCode == 200) { // 200은 통신 성공을 의미
// JSON 문자열을 Map<String, dynamic>으로 변환
final Map<String, dynamic> data = jsonDecode(response.body);
print('Title: ${data['title']}');
} else {
print('Failed to load post. Status code: ${response.statusCode}');
}
} catch (e) {
print('An error occurred: $e');
}
}
3. 모델 클래스로 안전하게 데이터 다루기
`Map`의 키를 문자열로 직접 접근(`data['title']`)하는 방식은 오타가 나기 쉽고, 어떤 데이터가 들어있는지 파악하기 어렵습니다. 그래서 보통 JSON 구조에 맞춰 '모델 클래스'를 만들고, JSON 데이터를 이 클래스의 객체로 변환해서 사용합니다. 이렇게 하면 타입 안정성도 확보되고, 코드 자동완성의 이점도 누릴 수 있습니다.
// post_model.dart
class Post {
final int userId;
final int id;
final String title;
final String body;
Post({required this.userId, required this.id, required this.title, required this.body});
// JSON(Map)을 Post 객체로 변환해주는 팩토리 생성자
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
// 사용할 때
if (response.statusCode == 200) {
final post = Post.fromJson(jsonDecode(response.body));
print('Title from model: ${post.title}');
}
7-2. 남의 도움 받기: 패키지 활용 (`pub.dev`)
플러터 개발의 가장 큰 즐거움 중 하나는 강력한 생태계입니다. 전 세계 개발자들이 만들어 공유한 수많은 '패키지(라이브러리)' 덕분에, 우리는 복잡한 기능들을 직접 구현할 필요 없이 몇 줄의 코드만으로 가져다 쓸 수 있습니다.
이 모든 패키지들은 `pub.dev`라는 공식 사이트에 모여있습니다.
패키지 사용법 (예: `shared_preferences`로 간단한 데이터 저장하기)
- `pub.dev`에서 패키지 검색: `pub.dev`에서 `shared_preferences`를 검색합니다. 이 패키지는 사용자의 간단한 설정값(예: 다크 모드 여부, 로그인 정보)을 기기에 저장할 때 사용합니다.
- `pubspec.yaml`에 추가: 'Installing' 탭에 있는 내용을 복사해서 `pubspec.yaml`의 `dependencies`에 추가합니다.
- 코드에서 사용하기:
import 'package:shared_preferences/shared_preferences.dart'; // 데이터 저장하기 Future<void> saveUsername(String username) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('username', username); } // 데이터 불러오기 Future<String?> loadUsername() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString('username'); }
URL을 앱 내에서 열고 싶을 땐 `url_launcher`, 기기의 정보를 얻고 싶을 땐 `device_info_plus`, 애니메이션을 쉽게 만들고 싶을 땐 `lottie` 등, 상상하는 거의 모든 기능에 대한 패키지가 존재합니다. 개발하다 막히는 기능이 있다면, 먼저 `pub.dev`를 검색해보는 습관을 들이는 것이 좋습니다.
7-3. 우리 앱만의 옷 입히기: 테마와 폰트
모든 화면의 버튼 색상, 글자 크기를 일일이 지정하는 것은 비효율적입니다. `MaterialApp`의 `theme` 속성을 이용하면 앱 전체에 적용될 공통 디자인 스타일(테마)을 한 곳에서 관리할 수 있습니다.
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// 앱의 기본 색상 견본
primarySwatch: Colors.teal,
// 앱의 전반적인 밝기 (light/dark)
brightness: Brightness.light,
// 폰트 설정
fontFamily: 'Pretendard', // 아래에서 추가할 커스텀 폰트 이름
// 텍스트 스타일 중앙 관리
textTheme: TextTheme(
headlineLarge: TextStyle(fontSize: 32.0, fontWeight: FontWeight.bold),
bodyMedium: TextStyle(fontSize: 16.0, color: Colors.black87),
),
// 버튼 스타일 중앙 관리
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal, // 버튼 배경색
foregroundColor: Colors.white, // 버튼 글자색
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
home: MyHomePage(),
)
커스텀 폰트 적용하기:
- 프로젝트 루트에 `assets/fonts` 같은 폴더를 만들고, 사용할 폰트 파일(.ttf, .otf)을 넣습니다.
- `pubspec.yaml` 파일을 열고 `flutter:` 섹션 아래에 다음과 같이 폰트를 등록합니다. (들여쓰기에 매우 주의해야 합니다!)
flutter: uses-material-design: true fonts: - family: Pretendard fonts: - asset: assets/fonts/Pretendard-Regular.ttf - asset: assets-fonts/Pretendard-Bold.ttf weight: 700 - 이제 `TextStyle(fontFamily: 'Pretendard')` 처럼 코드 어디서든 등록한 폰트를 사용할 수 있습니다.
8. 세상에 너의 앱을 보여줘! 빌드와 배포 A to Z
드디어 당신의 멋진 아이디어가 담긴 앱이 완성되었습니다! 이제 내 폰에서만 돌려보는 것을 넘어, 다른 사람들도 사용할 수 있도록 앱 스토어에 출시할 시간입니다. 이 과정을 '배포(Deploy)'라고 부릅니다.
배포 과정은 다소 복잡하고 까다로울 수 있습니다. 하지만 이 마지막 관문만 통과하면 당신도 어엿한 '앱 개발자' 타이틀을 얻게 되는 것이니, 조금만 더 힘을 내봅시다!
잠깐! 배포 전에 체크할 것들
- 앱 아이콘 변경: `flutter_launcher_icons` 패키지를 사용하면 쉽게 안드로이드와 iOS의 앱 아이콘을 한 번에 설정할 수 있습니다.
- 앱 이름 변경: 안드로이드는 `android/app/src/main/AndroidManifest.xml`에서, iOS는 `ios/Runner/Info.plist`에서 앱의 표시 이름을 변경할 수 있습니다.
- 패키지 ID (Bundle ID) 변경: `com.example.appName` 형태의 고유 식별자입니다. 스토어에 앱을 등록할 때 사용되므로, `com.yourcompany.yourappname`과 같이 고유한 이름으로 반드시 변경해야 합니다. `change_app_package_name` 같은 패키지를 사용하면 편리합니다.
8-1. 안드로이드 앱 출시 (Google Play Store)
안드로이드 앱은 APK 또는 AAB 파일 형태로 빌드하여 구글 플레이 스토어에 등록합니다. 현재 구글은 AAB(Android App Bundle) 형식을 필수로 요구하고 있습니다. AAB는 사용자 기기에 최적화된 버전의 앱만 다운로드하게 하여 앱 용량을 줄여주는 똑똑한 방식입니다.
1. 릴리즈(Release) 모드로 빌드하기
터미널에서 아래 명령어를 실행하여 출시용 AAB 파일을 생성합니다.
flutter build appbundle
빌드가 성공적으로 완료되면 `build/app/outputs/bundle/release/app-release.aab` 파일이 생성됩니다.
2. 앱 서명(Signing) 설정
하지만 그냥 빌드한 파일은 아무나 올릴 수 없습니다. "이 앱은 내가 만든 것이 맞다"는 것을 증명하는 '디지털 서명'이 필요합니다. 이 과정은 처음 할 때 조금 복잡합니다.
- 업로드 키 생성: `keytool`이라는 자바 프로그램을 이용해 암호화된 키 파일을 만듭니다.
- 키 정보 저장: 생성된 키의 비밀번호와 같은 민감한 정보를 `android/key.properties` 파일에 저장합니다. (이 파일은 절대 외부에 노출되면 안 됩니다!)
- `build.gradle` 설정: `android/app/build.gradle` 파일을 수정하여 빌드 시 `key.properties` 파일을 참조하여 자동으로 서명하도록 설정합니다.
자세한 과정은 플러터 공식 문서의 'Android 앱 배포' 가이드를 참고하는 것이 가장 정확합니다.
3. Google Play Console에 등록
- 구글 플레이 콘솔에 개발자 계정(최초 등록 시 25달러 비용 발생)을 만듭니다.
- 새 앱을 만들고, 스토어 등록정보(앱 이름, 설명, 스크린샷 등)를 모두 입력합니다.
- '프로덕션' 트랙에 위에서 생성한 `app-release.aab` 파일을 업로드합니다.
- 구글의 심사를 거쳐(보통 며칠 소요) 통과되면, 드디어 전 세계 사람들이 당신의 앱을 다운로드할 수 있게 됩니다!
8-2. iOS 앱 출시 (Apple App Store)
iOS 앱 배포는 안드로이드보다 조금 더 절차가 엄격하고, 반드시 Mac 컴퓨터와 Xcode가 필요합니다.
1. Apple 개발자 프로그램 등록
Apple Developer Program에 가입해야 합니다. 연간 99달러의 비용이 발생합니다. 이 멤버십이 있어야 앱을 스토어에 올릴 수 있습니다.
2. Xcode에서 프로젝트 설정
- 터미널에서 `open ios/Runner.xcworkspace` 명령어로 Xcode에서 플러터 프로젝트의 iOS 부분을 엽니다.
- 'Runner' 타겟의 'Signing & Capabilities' 탭에서 개발자 계정을 연동하고, Bundle Identifier가 올바른지 확인합니다.
3. 릴리즈 모드로 빌드 및 아카이브
- 터미널에서 아래 명령어로 릴리즈 빌드를 수행합니다.
flutter build ipa
- 또는 Xcode 메뉴에서 'Product' > 'Archive'를 선택하여 앱을 아카이브(배포용으로 패키징)합니다.
4. App Store Connect에 업로드
- 아카이브가 완료되면 Organizer 창이 뜹니다. 여기서 'Distribute App' 버튼을 눌러 App Store Connect(애플의 앱 관리 사이트)에 빌드된 앱을 업로드합니다.
- App Store Connect 사이트에 접속하여 새 앱을 만들고, 스토어 정보(스크린샷, 설명, 키워드 등)를 모두 입력합니다.
- 업로드한 빌드를 선택하고 '심사를 위해 제출' 버튼을 누릅니다.
애플의 심사는 구글보다 더 까다롭고 오래 걸리는 편입니다. 앱의 완성도, 가이드라인 준수 여부 등을 꼼꼼히 검토하므로, 리젝(거절)당하지 않도록 철저히 준비해야 합니다.
이 배포 과정이 너무 복잡하고 어렵게 느껴진다면, 전문가의 도움을 받는 것도 좋은 방법입니다. 재능넷과 같은 플랫폼에서는 앱 배포 경험이 풍부한 전문가들을 찾아 도움을 요청할 수 있으니, 혼자 끙끙 앓지 마세요!
마치며: 당신의 여정은 이제 시작입니다!
여기까지 따라오셨다면, 당신은 플러터라는 강력한 도구를 손에 쥐고 앱 개발의 세계에 성공적으로 첫발을 내디딘 것입니다. 정말 수고 많으셨습니다! 👏
환경 설정부터 위젯, 다트, 상태 관리, 그리고 배포까지... 짧은 시간 동안 정말 많은 것을 배웠습니다. 물론 아직은 모든 것이 낯설고 어색할 수 있습니다. 하지만 기억하세요. 모든 전문가는 초보자 시절을 거쳤습니다. 오늘 만든 카운터 앱이 비록 간단해 보일지라도, 이것은 당신의 상상력을 현실로 만들어낼 위대한 여정의 첫걸음입니다.
앞으로 무엇을 더 해야 할까요?
- 계속해서 만들어보세요: 나만의 투두리스트 앱, 간단한 일기장 앱, 동네 맛집 지도 앱 등 작은 프로젝트를 정해서 처음부터 끝까지 직접 만들어보세요. 실제 부딪히며 배우는 것만큼 좋은 공부는 없습니다.
- 공식 문서를 친구처럼: 플러터 공식 문서(flutter.dev)는 그 어떤 책이나 강의보다도 훌륭한 교과서입니다. 궁금한 위젯이 생기면 항상 공식 문서를 찾아보는 습관을 들이세요.
- 커뮤니티와 함께하세요: 혼자 개발하다 막히면 좌절하기 쉽습니다. 국내외 플러터 커뮤니티(오픈 카톡방, 페이스북 그룹 등)에 참여해서 질문도 하고, 다른 사람의 코드를 보며 영감을 얻으세요.
- 더 깊이 탐험하세요: 플러터의 세계는 무궁무진합니다. 화려한 애니메이션, Firebase를 이용한 백엔드 연동, 웹과 데스크톱으로의 확장 등 당신이 도전할 수 있는 새로운 영역들이 기다리고 있습니다. 때로는 재능넷 같은 곳에서 경험 많은 멘토를 찾아 조언을 구하는 것도 성장의 지름길이 될 수 있습니다.
이 글이 당신의 앱 개발 여정에 든든한 첫 번째 이정표가 되었기를 바랍니다.
이제 키보드를 잡으세요. 세상은 당신의 아이디어를 기다리고 있습니다.
Happy Fluttering! 💙
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.