
Row, Column, Flex 안에서 남는 공간을 차지하도록 설계된 위젯
빈 공간을 만들 때 사용
직접 width나 height를 지정하지 않고, 비율(flex)로 공간을 나눌 수 있음
Row(
children: [
Text('왼쪽'),
Spacer(), // 자동으로 남는 공간 차지
Text('오른쪽'),
],
)
위 예제에서 왼쪽과 오른쪽 텍스트 사이에 Spacer가 남는 공간을 채움
결과: 텍스트가 양 끝으로 배치됨
flex를 지정하면 여러 Spacer가 있을 때 공간 비율을 조정 가능
Row(
children: [
Text('왼쪽'),
Spacer(flex: 2), // 2/3 공간 차지
Text('가운데'),
Spacer(flex: 1), // 1/3 공간 차지
Text('오른쪽'),
],
)
첫 번째 Spacer가 두 번째 Spacer보다 2배 넓은 공간을 차지함
빈 공간만 차지하며, child를 가질 수 없음
주로 정렬, 간격 조정용으로 사용
Padding이나 SizedBox와 달리 비율 기반으로 공간을 나눌 수 있어 유연함
Flutter에서 위젯 트리 전체 또는 일부 위젯에 데이터를 전달할 때 사용하는 위젯
부모 → 자식 위젯으로 데이터를 효율적으로 공유할 수 있음
일반적으로 상태 관리의 기반으로 사용됨
Provider, Riverpod, Bloc 등 많은 상태 관리 패키지들이 내부적으로 InheritedWidget 사용
class MyInheritedWidget extends InheritedWidget {
final int counter;
MyInheritedWidget({
required this.counter,
required Widget child,
}) : super(child: child);
// 데이터 변경 시 재빌드 조건
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) {
return oldWidget.counter != counter;
}
// 위젯 트리 어디서든 접근할 수 있게 하는 편의 메서드
static MyInheritedWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}
}
MyInheritedWidget(
counter: 5,
child: MyChildWidget(),
)
class MyChildWidget extends StatelessWidget {
Widget build(BuildContext context) {
final inherited = MyInheritedWidget.of(context);
return Text('Counter: ${inherited?.counter}');
}
}
dependOnInheritedWidgetOfExactType를 사용하면 값이 바뀌면 자동으로 rebuild됨.
| 특징 | 설명 |
|---|---|
| 데이터 전달 | 위젯 트리 깊은 곳까지 데이터 전달 가능 |
| 재빌드 관리 | updateShouldNotify로 효율적 재빌드 가능 |
| 범위 제한 가능 | 특정 위젯 트리 범위에만 데이터 적용 가능 |
| 불변성 권장 | 값이 변경되면 새 위젯을 생성하여 전달 |
class WriteState {
bool isWriting;
WriteState(this.isWriting);
}
isWriting
글 작성/수정 중인지 여부
true → 로딩 상태
false → 입력 가능 상태
class WriteViewModel extends Notifier<WriteState> {
final Post? arg;
WriteViewModel(this.arg);
WriteState build() {
return WriteState(false);
}
Riverpod Notifier 기반 ViewModel
argFuture<bool> insert({
required String writer,
required String title,
required String content,
}) async {
state = WriteState(true);
if (arg == null) {
final result = await postRepository.insert(
title: title,
content: content,
writer: writer,
imageUrl: "http://picsum.potos/300/300",
);
state = WriteState(false);
return result;
}
else {
final result = await postRepository.update(
id: arg!.id,
title: title,
content: content,
writer: writer,
imageUrl: "https://picsum.photos/200/300",
);
state = WriteState(false);
return result;
}
state = WriteState(false);
final writeViewModelProvider = NotifierProvider.autoDispose
.family<WriteViewModel, WriteState, Post?>((arg) {
return WriteViewModel(arg);
});
WriteViewModel
Post == null → 글 작성
Post != null → 글 수정
WriteState.isWriting
작성/수정 중 UI 제어용 상태
최신 Riverpod에서 AutoDisposeFamilyNotifier 대체 구조
CRUD가 발생한 뒤 화면을 바로 갱신하는 방법은 두 가지
1. CRUD 이후 다른 ViewModel을 직접 갱신
2. Firestore 실시간 스트림 사용 (권장)
Stream<List<Post>> postListStream() {
final fireStore = FirebaseFirestore.instance;
final collectionRef = fireStore.collection('posts');
final stream = collectionRef.snapshots();
final newStream = stream.map((event) {
return event.docs.map((e) {
return Post.fromJson({'id': e.id, ...e.data()});
}).toList();
});
return newStream;
}
역할
posts 컬렉션 변경 감지
추가 / 수정 / 삭제 발생 시마다 자동으로 List<Post> 방출
final collectionRef = fireStore
.collection('posts')
.orderBy('createAt', descending: true);
orderBy 정렬 적용 가능
Stream<Post?> postStream(String id) {
final fireStore = FirebaseFirestore.instance;
final collectionRef = fireStore.collection('posts');
final docRef = collectionRef.doc(id);
final stream = docRef.snapshots();
final newStream = stream.map((e) {
if (e.data() == null) {
return null;
}
return Post.fromJson({'id': e.id, ...e.data()!});
});
return newStream;
}
역할
특정 게시글 하나만 실시간 감시
삭제되면 null 반환
class HomeViewModel extends Notifier<List<Post>> {
List<Post> build() {
getAllPost();
return [];
}
void getAllPost() async {
final postRepo = PostRepository();
// final posts = await postRepo.getAll();
// state = posts ?? [];
final stream = postRepo.postListStream();
final streamSubScription = stream.listen((posts) {
state = posts;
});
// 중요!!
ref.onDispose(() {
streamSubScription.cancel();
});
}
}
listen()으로 스트림 구독
Firestore 변경 → state 자동 업데이트
onDispose에서 반드시 구독 해제 (메모리 안전)
void listenStream() {
final stream = postRepository.postStream(arg.id);
final streamSub = stream.listen((data) {
if (data != null) {
state = data;
}
});
ref.onDispose(() {
streamSub.cancel();
});
}
역할
게시글 상세 화면 실시간 동기화
수정 시 자동 반영
삭제 시 state == null
flutter pub add firebase_storage
flutter pub add image_picker
firebase_storage : 이미지 파일 업로드
image_picker : 갤러리에서 이미지 선택
ios/Runner/info.plist
<key>NSPhotoLibraryUsageDescription</key>
<string>사진 업로드를 위한 라이브러리 권한을 허용해 주세요</string>
iOS 앱 설정 파일
앱 이름, 권한 요청 문구 등 정의
권한 없으면 앱 실행 중 바로 크래시
onTap: () async {
// 1. 이미지 피커 객체 생성
ImagePicker imagePicker = ImagePicker();
// 2. 이미지 피커 객체의 pickImage 메서드 호출
XFile? xFile = await imagePicker.pickImage(
source: ImageSource.gallery,
);
print('${xFile?.path}');
},
build()
WriteState build() {
return WriteState(false, arg?.imageUrl);
}
수정 화면이면 기존 이미지 URL 유지
신규 작성이면 null
void uploadImage(XFile xFile) async {
try {
// firebase store 사용법
// 1. 객체 가지고 오기
final storage = FirebaseStorage.instance;
// 2. 스토리지 참조 만들기
Reference ref = storage.ref();
// 3. 파일 참조 만들기
Reference fileRef = ref.child(
'${DateTime.now().microsecondsSinceEpoch}_${xFile.path}',
);
// 4. 쓰기
await fileRef.putFile(File(xFile.path));
// 5. 파일에 접근할 수 있는 url 만들기
String imageUrl = await fileRef.getDownloadURL();
state = WriteState(state.isWriting, imageUrl);
} catch (e) {
print(e);
}
}
파일명 중복 방지 → timestamp 사용
putFile()로 업로드
getDownloadURL()로 접근 가능한 URL 획득
업로드 후 상태 갱신 → UI 즉시 반영
이러면 블로그 만들기 끝..!!!!
앱 이름: flutter_market_app
아키텍처: MVVM
상태 관리: Riverpod
화면 구성: 여러 페이지로 구성, 각 페이지 별 레이아웃 구현 예정
flutter_market_app/
├─ lib/
│ ├─ main.dart
│ ├─ ui/
│ │ ├─ pages/
│ │ │ ├─ welcome/
│ │ │ │ └─ welcome_page.dart
│ │ │ ├─ login/
│ │ │ │ └─ login_page.dart
│ │ │ ├─ join/
│ │ │ │ └─ join_page.dart
│ │ │ ├─ home/
│ │ │ │ └─ home_page.dart
│ │ │ ├─ address/
│ │ │ │ └─ address_search_page.dart
│ │ │ ├─ chat/
│ │ │ │ └─ chat_detail_page.dart
│ │ │ ├─ product/
│ │ │ │ ├─ product_detail_page.dart
│ │ │ │ └─ product_write_page.dart
│ └─ ...
Riverpod 설치
flutter pub add flutter_riverpod
Riverpod 사용을 위해 ProviderScope로 앱 감싸기
시작 화면: WelcomePage
import 'package:flutter/material.dart';
import 'package:flutter_market_app/ui/pages/welcome/welcome_page.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: WelcomePage(),
);
}
}
화면 전체를 Column + SizedBox.expand로 구성
이미지, 텍스트, 버튼 배치
버튼 클릭 시 다음 페이지로 이동
시작하기 → AddressSearchPage
로그인 → LoginPage
main.dart
앱 전반 버튼 디자인 통일
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.redAccent.shade100),
highlightColor: Colors.redAccent.shade100,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll(Colors.white),
backgroundColor: WidgetStatePropertyAll(Colors.redAccent.shade100),
minimumSize: WidgetStatePropertyAll(Size.fromHeight(50)),
textStyle: WidgetStatePropertyAll(
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
),
),
키보드 등장 시 화면 축소 문제 해결: ListView 사용
TextFormField + 유효성 검사
로그인 버튼 클릭 시 onLoginClick() 호출
validator_utils.dart
class ValidatorUtil {
static String? validatorId(String? value) {
if (value?.trim().isEmpty ?? true) {
return "아이디를 입력해주세요";
}
if (value!.length < 2) {
return "아이디는 2글자 이상이여야 합니다";
}
}
static String? validatorNickname(String? value) {
if (value?.trim().isEmpty ?? true) {
return "닉네임를 입력해주세요";
}
if (value!.length < 2) {
return "닉네임은 2글자 이상이여야 합니다";
}
}
static String? validatorPassword(String? value) {
if (value?.trim().isEmpty ?? true) {
return "비밀번호를 입력해주세요";
}
if (value!.length < 2) {
return "비밀번호는 2글자 이상이여야 합니다";
}
}
}
아이디, 비밀번호, 닉네임 각각 별도 위젯으로 정의
import 'package:flutter/material.dart';
import 'package:flutter_market_app/core/validator_utils.dart';
class IdTextFormField extends StatelessWidget {
const IdTextFormField({super.key, required this.controller});
final TextEditingController controller;
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(hintText: "아이디를 입력해주세요"),
validator: ValidatorUtil.validatorId,
controller: controller,
);
}
}
v3.26 이후
MaterialStateOutlineInputBorder.resolveWith대신WidgetStateInputBorder.resolveWith사용
main.dart
inputDecorationTheme: InputDecorationTheme(
hintStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
// TextField의 내부 여백
contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 20),
// TextField의 상태가 변경(포커스 받았을때, 아닐때, 유효성 검사 에러 등) 시 호출
border: WidgetStateInputBorder.resolveWith((states) {
// 1. states Set 안에 WidgetState.focus가 포함 되어 있을 때
if (states.contains(WidgetState.focused)) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey, width: 2),
);
// 2. states Set 안에 WidgetState.error 포함 되어 있을 때
} else if (states.contains(WidgetState.error)) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.red[100]!, width: 2),
);
}
// 3. 기본 값
return OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!, width: 1),
);
}),
),
사용자가 동네(주소)를 검색/선택하는 화면
주소 선택 후 → JoinPage로 이동
AppBar에 TextField 배치
키보드 외 영역 터치 시 키보드 닫기 → GestureDetector
주소 목록은 ListView.builder
버튼 1개만 높이 줄이기 → SizedBox
주소 선택 후 회원가입 정보 입력
아이디 / 비밀번호 / 닉네임 입력
입력 필드가 많으므로 ListView 사용
기존에 만든 공통 TextFormField 위젯 재사용
Form + validator 사용
앱 하단의 탭 네비게이션
탭 선택 시 인덱스 변경 → 화면 전환
탭 인덱스만 관리 → int 사용
📁 lib/ui/pages/home/home_view_model.dart
class HomeViewModel extends Notifier<int> {
int build() {
return 0;
}
void onIndexChanged(int newIndex) {
state = newIndex;
}
}
final homeViewModel = NotifierProvider.autoDispose<HomeViewModel, int>(() {
return HomeViewModel();
});
✔ 현재 선택된 탭 인덱스를 전역 상태로 관리
✔ BottomNavigationBar, FAB, 화면 전환에서 공통 사용
위젯 분리 이유
HomePage 코드 단순화
상태 관리 로직과 UI 분리
📁 home_bottom_navigation_bar.dart
class HomeBottomNavigationBar extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
final index = ref.watch(homeViewModel);
final vm = ref.read(homeViewModel.notifier);
return BottomNavigationBar(
currentIndex: index,
onTap: vm.onIndexChanged,
iconSize: 28,
selectedLabelStyle: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
unselectedLabelStyle: TextStyle(fontSize: 12),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: "홈",
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.chat_bubble_2),
activeIcon: Icon(CupertinoIcons.chat_bubble_2_fill),
label: "채팅",
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.person),
activeIcon: Icon(CupertinoIcons.person_fill),
label: "나의 마켓",
),
],
);
},
);
}
}
여러 위젯을 겹쳐두고, 그중 하나만 보여주는 위젯
IndexedStack(
index: 0,
children: [
WidgetA(),
WidgetB(),
WidgetC(),
],
)
index에 해당하는 위젯만 화면에 보임
나머지 위젯들도 전부 유지된 상태로 존재함
홈 탭(index = 0)일 때만 노출
다른 탭에서는 숨김
FloatingActionButton.extended 이용해 label 들어간 버튼
📁 home_floating_action_button.dart
class HomeFloatingActionButton extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
if (ref.watch(homeViewModel) != 0) {
return const SizedBox();
}
return FloatingActionButton.extended(
label: Text("상품 등록", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
icon: Icon(Icons.add),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => ProductWritePage()),
);
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
),
backgroundColor: Theme.of(context).highlightColor,
foregroundColor: Colors.white,
);
},
);
}
}
home_tab/
├─ home_tab.dart
└─ widgets/
├─ home_tab_app_bar.dart
├─ home_tab_list_view.dart
└─ product_list_item.dart
Column
├─ HomeTabAppBar
└─ HomeTabListView (상품 리스트)
📁 lib/core/snackbar_utils.dart
import 'package:flutter/material.dart';
class SnackbarUtils {
static void showSnackBr(BuildContext context, String text) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(text),
behavior: SnackBarBehavior.floating,
showCloseIcon: true,
),
);
}
}
스낵바 따로 분리
onTap: () {
SnackbarUtils.showSnackBr(context, "아직 준비중입니다!");
},
상품 이미지
제목 / 시간 / 가격
관심(하트) 아이콘
터치 시 → ProductDetailPage 이동
Row
├─ 상품 이미지
└─ Expanded
├─ 제목
├─ 시간
├─ 가격
└─ 좋아요 수
ListView.separated 사용
Divider로 아이템 간 구분
padding 적용
ListView.separated(
itemCount: 10,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
separatorBuilder: (context, index) => const Divider(height: 20),
itemBuilder: (context, index) {
return ProductListItem();
},
),
📁 lib/ui/widgets/user_profile_image.dart
ChatTab / MyTab 모두에서 프로필 이미지 필요
null 이미지 대응
class UserProfileImage extends StatelessWidget {
const UserProfileImage({super.key, this.dimension, this.imgSrc});
final double? dimension;
final String? imgSrc;
Widget build(BuildContext context) {
if (imgSrc == null) {
return Container(
width: dimension,
height: dimension,
decoration: BoxDecoration(color: Colors.grey, shape: BoxShape.circle),
);
}
return SizedBox.square(
dimension: dimension,
child: AspectRatio(
aspectRatio: 1,
child: ClipRRect(
borderRadius: BorderRadius.circular(100),
child: Image.network(imgSrc!, fit: BoxFit.cover),
),
),
);
}
}
채팅 목록 리스트
터치 시 → ChatDetailPage 이동
Row
├─ 프로필 이미지
└─ Expanded
├─ 닉네임 + 시간
└─ 마지막 메시지
ListView.separated 사용
Divider로 구분
SafeArea
└─ ListView
├─ MyProfileBox
├─ Section Label
├─ Menu Item
├─ Divider
└─ ...
프로필 이미지
닉네임
프로필 수정 버튼
Row
├─ UserProfileImage
├─ 닉네임
└─ 프로필수정 버튼
Scaffold
├─ AppBar
│ └─ ProductDetailActions
├─ body (ListView)
│ ├─ ProductDetailPicture
│ └─ ProductDetailBody
└─ bottomSheet
└─ ProductDetailBottomShee
PageView.builder
PageView.builder(
itemCount: 3,
itemBuilder: ...
)
특징
좌우 스와이프 가능
ListView.builder와 사용법 거의 동일
상품 이미지 여러 장 표현에 적합
Scaffold.bottomSheet
항상 하단 고정
키보드 올라와도 사라지지 않음
bottomPadding: MediaQuery.of(context).padding.bottom
❗ Scaffold 기준 context에서만 정확
아이폰 홈 인디케이터 영역 대응
height: 50 + bottomPadding
VerticalDivider는 Row 안에서 쓰는 세로 구분선
VerticalDivider(
width: 20, // 구분선이 차지하는 가로 공간
indent: 10, // 위쪽 여백
endIndent: 10, // 아래쪽 여백
color: Colors.grey, // 선 색상
)
버튼이나 텍스트 사이를 깔끔하게 나눌 때 사용
Scaffold
├─ AppBar (내 물건 팔기)
└─ Body
└─ Form
└─ ListView
├─ ProductWritePictureArea
├─ ProductCategoryBox
├─ 상품명 TextFormField
├─ 가격 TextFormField
├─ 내용 TextFormField
└─ 작성 완료 버튼
가로 스크롤 이미지 리스트
이미지 추가 버튼 UI
ListView(
scrollDirection: Axis.horizontal,
)
AspectRatio(1) → 정사각형 유지
ClipRRect → 이미지 둥근 모서리
GestureDetector → 이미지 추가 버튼
클릭 시 팝업 메뉴 표시
선택된 값은 onSelected로 전달
PopupMenuButton<String>(
onSelected: (value) {}, // 선택되었을 때 일어날 이벤트
itemBuilder: (context) => [...], // 아이템 각 항목
child: ... // 팝업 누르기 전 보여질 상태
)
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
position: PopupMenuPosition.under, //child 아래로 보여지게
color: Colors.white,
PopupMenuItem<String> categoryItem(String text, bool isSelected) {
return PopupMenuItem<String>(
value: text,
child: Text(
text,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : null,
color: isSelected ? Colors.black : Colors.grey,
),
),
);
}
categoryItem('디지털 가전', false),
현재 선택된 카테고리 강조 가능
child는 버튼처럼 보이는 UI
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
숫자 키보드 + 숫자만 입력 허용
Scaffold
├ AppBar
├ body (Column)
│ ├ ChatDetailProductArea // 상단 상품 정보
│ └ ChatDetailListView // 채팅 리스트
└ bottomSheet
└ ChatDetailBottomSheet // 메시지 입력 영역
상대방 메시지 UI
Row
├ 프로필 이미지 (또는 빈 공간)
└ 메시지 + 시간
showProfile이 false면 SizedBox(width: 50)
→ 메시지 정렬 유지
말풍선:
Container
BorderRadius.circular(16)
시간은 작은 폰트 + 흐린 색
📌 연속 메시지에서 프로필 숨기는 패턴 구현
내가 보낸 메시지 UI
오른쪽 정렬
crossAxisAlignment: CrossAxisAlignment.end
프로필 이미지 없음
구조는 ReceiveItem과 거의 동일
📌 좌/우 정렬만 다르고 재사용 구조는 동일
메시지 입력 + 전송 버튼
키보드 영역 대응
StatefulWidget
→ TextEditingController 관리
MediaQuery.of(context).padding.bottom
→ 아이폰 홈 인디케이터 영역 대응
전송 시:
텍스트 출력
컨트롤러 초기화
하위에 있는 모든 Text 위젯에 기본 텍스트 스타일을 한 번에 적용하는 위젯
DefaultTextStyle(
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black,
),
child: Column(
children: [
Text('제목'),
Text('날짜'),
],
),
);
color는 반드시 지정해야 함 (안 하면 에러 날 수 있음)
개별 Text에서 style 주면 그게 우선
AI는 이제 단순한 도구가 아니라 협업의 대상으로 확장됨
AI가 만든 코드를 검증해야 하는 이유를 이해한다
AI 사용 가능 영역과 위험 영역을 구분하는 기준을 갖는다
Flutter 개발에서 AI를 생산성 도구로 활용하는 질문법을 익힌다
유지보수 불가: 내가 이해하지 못한 코드는 수정이 불가능함
해결 능력 저하: 에러 발생 시 질문만 반복하며 스스로 문제를 정의하는 힘이 약해짐
책임 소재: AI는 코드에 대해 책임을 지지 않음. 책임은 오로지 개발자의 몫
코드 복사-붙여넣기 위주의 개발은 지식 축적을 방해함
AI 의존성: AI 없이는 개발을 시작조차 할 수 없는 '고착 상태' 발생
존재하지 않는 위젯, 라이브러리, API를 당당하게 제안함
구 버전 API 제안: 이미 사라진(Deprecated) 방식을 최신인 것처럼 설명함
그럴듯하지만 틀린 코드를 구별하지 못함
코드를 읽고 흐름을 설명할 수 있을 때
특정 구조를 선택한 이유를 논리적으로 설명할 수 있을 때
AI가 작성한 코드의 잘못된 부분을 지적하고 수정을 요구할 수 있을 때
AI는 정답을 알려주는 선생님이 아니라
지시에 따라 도면을 그리는 숙련된 작업자
AI의 기능, 한계, 오류 가능성을 이해하고 실무에 비판적으로 활용할 수 있는 능력
확률 기반: 코드를 이해하는 것이 아니라, 패턴과 확률에 따라 '다음 토큰'을 예측함
맥락 부족: 전체 아키텍처나 복잡한 비즈니스 요구사항을 완전히 파악하지 못하고, 주어진 Context 일부만 보고 판단함
문법 에러는 없지만 로직이 틀린 코드 생성
프로젝트의 일관성을 해치는 부적절한 구조
플러터 특유의 생명 주기(Lifecycle) / BuildContext 관련 오류
// ❌
Color.withOpacity(0.5)
// ✅
Color.withValues(alpha: 0.5)
거대한 기능을 한 번에 요청하지 않기
최소 단위로 쪼개어 요청
설계자가 통제 가능한 코드 양 유지
"로그인 기능을 만들 거야. [1. 유효성 검사], [2. 뷰모델 함수], [3. DB 연결부] 순으로 짤 거야. 우선 1번부터 시작하자."
이미 동작하는 코드를 기반으로 개선 요청
성능 / 가독성 / 확장성 관점에서 비교
AI를 코드 리뷰어로 활용
이 루프문의 시간 복잡도가 너무 높아
공간 복잡도를 희생하더라도 속도를 높이는 방향 (HashMap 활용)으로 개선해줘
에러 범위 제한 → 원인 후보 → 대안 비교 → 선택
❌ “안 돼요 고쳐주세요”
❌ 사고 과정을 AI에게 맡기기
- 범위 제한: 에러가 발생할 가능성이 있는 파일 나열 및 이유 설명 요청
- 정밀 진단: 코드 구조와 데이터 흐름상의 모순점 파악 요청
- 대안 평가: 해결책 후보 3가지를 추천받고 장단점 비교
- 정밀 타격: 선택한 대안을 바탕으로 특정 부분만 수정 반영
추상 개념을 일상적인 비유로 요청
구조를 머릿속에 그릴 수 있도록 도움
StatelessWidget과 StatefulWidget의 차이를 일상생활에 비유해서 설명해줘
이해 안 되는 개념을 단계적으로 파고들기
존재 이유, 대체 가능성 질문
TextField의 controller는 뭐야?
안 쓰면 어떤 문제가 생겨?
다른 해결 방법은 없어?
결과 코드 → 필요한 개념 추적
이론과 구현을 분리하지 않음
구글 로그인 기능 구현에 필요한
기술 스택을 나열하고 표준 코드 작성해줘

AI는 학습을 돕는 도구이자 생산성 도구 사고를 대신해주지는 않는다
AI가 만든 코드를 책임지는 사람은 항상 100% 개발자 본인
오늘은 블로그 어플 crud 구현 끝나고 마켓앱 ui 구현만 하루종일 했다.. 내일 완강하고 금요일에 개인 과제 시작하는 게 목표인데 할 수 있으까...??