
AnimatedList는 리스트 아이템이 추가·삭제될 때 애니메이션을 자동으로 적용해주는 위젯이다.
ListView → 아이템 변경 시 바로 갱신
AnimatedList → 아이템 삽입 / 제거 시 애니메이션 가능
📍 중요 포인트
상태(State)를 직접 관리해야 함
내부 데이터 리스트와 AnimatedList 상태를 항상 동기화해야 함
| 상황 | 추천 여부 |
|---|---|
| 아이템 추가/삭제 시 자연스러운 등장/퇴장 | ⭐⭐⭐⭐⭐ |
| 단순 정적 리스트 | ❌ (ListView 사용) |
| 실시간 장바구니, 위시리스트 | ⭐⭐⭐⭐ |
| 서버에서 전체 리스트 교체 | ⚠️ (다시 구성 필요) |
AnimatedList(
key: _listKey,
initialItemCount: items.length,
itemBuilder: (context, index, animation) {
return _buildItem(index, animation);
},
)
| 요소 | 설명 |
|---|---|
key | GlobalKey<AnimatedListState> 필수 |
initialItemCount | 초기 아이템 개수 |
itemBuilder | animation을 활용해 위젯 구성 |
index : 현재 아이템 위치
animation : 삽입/삭제 시 사용되는 애니메이션 값
final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey();
final List<Item> _itemList = [];
GlobalKey<AnimatedListState>
→ 리스트에 insert / remove 명령을 내리기 위해 필요
_itemList
→ 실제 데이터 저장용 리스트
void _addItem() {
_itemList.add('item ${_itemList.length + 1}');
_animatedListKey.currentState?.insertItem(
_itemList.length - 1,
duration: Duration(milliseconds: 300),
);
}
📌 순서가 핵심
1. 데이터 추가
2. insertItem(index) 호출
void _removeItem(int index) {
final removedItem = _itemList[index];
_itemList.removeAt(index);
_animatedListKey.currentState?.removeItem(
index,
(context, animation) {
return MyItem(animation, removedItem);
},
duration: Duration(milliseconds: 300),
);
}
📌 핵심 포인트
삭제 아이템을 먼저 저장
removeItem builder에서는
❌ _itemList[index] 사용 금지
JSON (JavaScript Object Notation)
JavaScript 객체 표기법 기반의 데이터 형식
서버 ↔ 클라이언트 간 데이터 교환에 가장 많이 사용
언어와 플랫폼에 독립적이라 서로 다른 환경에서도 사용 가능
객체 → 전송/저장 가능한 형태로 변환
서버로 보내거나 로컬 저장소에 저장할 때 필요
Dart 객체 → Map → JSON String
서버에서 받은 데이터를 다시 객체로 변환
JSON String → Map → Dart 객체
➡️ 서로 다른 언어(Dart ↔ Java 등)에서도 데이터를 주고받기 위해
공통 포맷으로 JSON을 사용
{} (중괄호)
key : value 쌍으로 구성
{
"name": "김현서",
"age": 24
}
⚠️ 규칙
key는 반드시 문자열
value는 아래 타입만 가능
| 타입 | 설명 |
|---|---|
| String | "text" |
| Number | int, double |
| Boolean | true, false |
| Array | [] |
| Object | {} |
| null | 값 없음 |
📘 예시: 강아지 정보 JSON
{
"name": "뽀삐",
"age": 3,
"isMale": true,
"favorite_foods": ["삼겹살", "연어", "고구마"],
"dislike_foods": [],
"contact": {
"mobile": "010-0000-0000",
"email": null
}
}
❌ 아니다!
JSON의 최상위(root) 는
객체 {} 일 수도 있고
리스트 [] 일 수도 있음
[
{ "id": 1, "name": "현서" },
{ "id": 2, "name": "서현" }
]
➡️ 서버 응답에서 리스트 형태 JSON 아주 흔함
📤 직렬화
Dart 객체 → Map → jsonEncode → String
📥 역직렬화
String → jsonDecode → Map → Dart 객체
Map / List → JSON String
서버로 데이터 보낼 때 사용
<import 'dart:convert';
void main() {
Map<String, dynamic> myInfo = {
"name": "감현서",
"age": 24,
};
String jsonString = jsonEncode(myInfo);
print(jsonString); // {"name":"김현서","age":24}
List list = [myInfo, myInfo];
String listJsonString = jsonEncode(list);
print(listJsonString);
// [{"name":"김현서","age":24},{"name":"김현서","age":24}]
}
📌 포인트
jsonEncode는 Map / List만 변환 가능
결과는 항상 String
JSON String → Map 또는 List
서버에서 받은 데이터 처리할 때 사용
리턴 타입은 dynamic
import 'dart:convert';
void main() {
String jsonString = """
{
"name": "김현서",
"age": 24
}
""";
var result = jsonDecode(jsonString);
print(result); // {name: 김현서, age: 24}
print(result.runtimeType); // Map<String, dynamic>
}
map['key'] 방식은
❌ 오타 잡기 어려움
❌ 타입 안정성 없음
클래스로 변환하면
IDE 자동완성 ✅
컴파일 타임 에러 확인 가능 ✅
유지보수 쉬움 ✅
class User {
User({
required this.name,
required this.age,
});
String name;
int age;
User.fromJson(Map<String, dynamic> map)
: this(
name: map['name'],
age: map['age'],
);
Map<String, dynamic> toJson() {
return {
"name": name,
"age": age,
};
}
}
사용 예시
import 'dart:convert';
void main() {
String jsonString = """
{
"name": "김현서",
"age": 24
}
""";
var jsonMap = jsonDecode(jsonString);
User user = User.fromJson(jsonMap);
print(user.toJson()); // {name: 김현서, age: 24}
}
User user2 = User(name: jsonMap['nane'], age: jsonMap['age']);
User user3 = User(name: jsonMap['name'], age: jsonMap['aga']);
➡️ 런타임에서만 오류 발견 가능
User user = User.fromJson(jsonMap);
➡️ JSON 구조가 바뀌면 한 곳만 수정하면 됨
JSON
{
"name": "뽀삐",
"age": 3,
"isMale": true
}
Pet 클래스
class Pet {
String name;
int age;
bool isMale;
Pet({
required this.name,
required this.age,
required this.isMale,
});
Pet.fromJson(Map<String, dynamic> json)
: this(
name: json["name"],
age: json["age"],
isMale: json["isMale"],
);
Map<String, dynamic> toJson() {
return {
"name": name,
"age": age,
"isMale": isMale,
};
}
}
JSON
{
"name": "뽀삐",
"age": 3,
"isMale": true,
"favorite_foods": ["삼겹살", "연어", "고구마"],
"contact": {
"mobile": "010-0000-0000",
"email": null
}
}
Contact 클래스
class Contact {
String mobile;
String? email;
Contact({
required this.mobile,
required this.email,
});
Contact.fromJson(Map<String, dynamic> json)
: this(
mobile: json["mobile"],
email: json["email"],
);
Map<String, dynamic> toJson() {
return {
"mobile": mobile,
"email": email,
};
}
}
Pet 클래스 (중첩 구조)
class Pet {
final String name;
final int age;
final bool isMale;
final List<String> favoriteFoods;
final Contact contact;
Pet({
required this.name,
required this.age,
required this.isMale,
required this.favoriteFoods,
required this.contact,
});
Pet.fromJson(Map<String, dynamic> json)
: this(
name: json["name"],
age: json["age"],
isMale: json["isMale"],
favoriteFoods: List<String>.from(json["favorite_foods"]),
contact: Contact.fromJson(json["contact"]),
);
Map<String, dynamic> toJson() {
return {
"name": name,
"age": age,
"isMale": isMale,
"favorite_foods": favoriteFoods,
"contact": contact.toJson(),
};
}
}
Model + View + ViewModel
역할을 명확히 나눠서 개발하는 아키텍처 패턴
UI 로직과 비즈니스 로직을 분리하는 것이 핵심
📦 Model
데이터를 담당하는 계층
서버, DB, API 등에서 받아오는 원본 데이터
데이터 클래스 자체도 Model에 포함됨
🖥 View
화면(UI) 담당 계층
Flutter에서는 Widget
ViewModel을 구독해서 상태 변화에 따라 화면만 그려줌
🧠 ViewModel
Model과 View 사이의 중간 관리자
Model에서 받은 데이터를 가공해서 View가 쓰기 좋은 상태로 변환
상태를 관리하고 변경을 알림

문제점
하나의 Widget이 너무 많은 역할 수행
UI + 로직 + 데이터 처리 혼합
코드가 길어지고 복잡해짐
테스트 어려움
👉 “작을 땐 괜찮은데, 커지면 지옥”

핵심 포인트
ViewModel은 View가 누군지 모름
View만 ViewModel을 알고 있음 (단방향 의존성)
✅ 코드 구조
역할 분리 → 코드 가독성 상승
유지보수 쉬움
중복 코드 감소
✅ 안정성
로직이 한 곳(ViewModel)에 모임
버그 발생 지점 명확
✅ 테스트
ViewModel 단위 테스트 가능
UI 없이 로직 테스트 가능
✅ 결합도 감소
View ↔ ViewModel 느슨한 결합
구조 변경에 강함
MVVM 식당
| 역할 | 식당에서의 역할 | 실제 코드에서의 역할 |
|---|---|---|
| Model | 주방 & 창고 (식재료, 요리 로직) | 데이터 그 자체 (DB, API 데이터) |
| View | 손님 & 테이블 (메뉴판 보고 음식 먹기) | 화면 (버튼, 텍스트, 위젯) |
| ViewModel | 홀 서빙 직원 (주문 전달, 음식 세팅) | 화면에 보여줄 데이터 가공 & 상태 관리 |

Flutter에서 사용하는 상태관리 라이브러리
ViewModel 역할을 쉽게 구현할 수 있음
상태 변경 시 View(Widget)에 자동으로 알려줌
MVVM 패턴에서 View ↔ ViewModel 연결을 단순화
| 개념 | 역할 |
|---|---|
| State | 관리할 데이터 |
| Notifier | 상태를 저장·변경하는 ViewModel |
| Provider | ViewModel을 생성·관리·공급 |
| Consumer / ref | Widget에서 상태 관찰 |
관리할 데이터만 담는 순수 클래스
보통 immutable하게 사용
class HomeState {
final int counter;
HomeState(this.counter);
}
Notifier<T> 를 상속
T = 관리할 상태 타입
build() → 초기 상태 설정
class HomeViewModel extends Notifier<HomeState> {
HomeState build() {
return HomeState(1); // 초기 상태
}
void updateState() {
// 반드시 새로운 객체로 교체
state = HomeState(state.counter + 1);
// ❌ 이렇게 하면 상태 변경 감지 안 됨
// state.counter++;
}
}
Riverpod은 객체가 바뀌었는지로 상태 변경을 판단
기존 객체 내부 값만 수정하면 재빌드 안 됨
ViewModel을 생성·보관·공급하는 역할
View에서는 Provider에게 요청만 하면 됨
final homeViewModelProvider =
NotifierProvider<HomeViewModel, HomeState>(
(){
return HomeViewModel()
);
| 제너릭 | 의미 |
|---|---|
HomeViewModel | 제공할 ViewModel |
HomeState | 관리하는 상태 |
Consumer 사용
WidgetRef(ref)를 통해 Provider 접근 가능
Consumer(
builder: (context, ref, child) {
final state = ref.watch(homeViewModelProvider);
return Column(
children: [
Text('카운터: ${state.counter}'),
],
);
},
);
| 메서드 | 설명 |
|---|---|
ref.watch(provider) | 상태 변경 감지 → 재빌드 |
ref.read(provider) | 1회성 읽기 (재빌드 X) |
ref.read(provider.notifier) | ViewModel 접근 |
ref.read(homeViewModelProvider.notifier).updateState();
Riverpod을 앱 전체에서 사용하려면 최상위에 감싸기
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
🧩 전체 흐름 한 줄 요약
State 생성 → Notifier(ViewModel) 작성 → Provider로 관리 → Widget에서 watch/read
구성 요소
State (붕어빵 개수)
가장 단순한 '데이터'
"현재 남은 붕어빵은 5개야."라는 정보 그 자체
붕어빵 가게에서 가장 중요한 핵심 정보
Notifier (붕어빵 사장님)
데이터를 '관리하고 바꾸는 사람'
사장님은 붕어빵이 팔리면 개수를 줄이고, 새로 구우면 개수를 늘리
단, Riverpod 사장님은 고집이 있어서 "기존 판에 숫자만 지우고 다시 쓰는 게 아니라, 항상 새 판으로 교체"해야만 손님들이 바뀐 걸 알아챈다고 생각해요. (Immutable/불변성)
Provider (붕어빵 매대/창구)
손님이 사장님과 대화할 수 있는 '연결 통로'
손님(Widget)이 사장님에게 직접 말을 걸 수는 없음
매대(Provider)를 통해서만 "사장님, 붕어빵 몇 개 남았나요?"라고 묻거나 "붕어빵 하나 주세요!"라고 요청할 수 있음
Consumer / ref (가게 손님)
화면에 나타나는 'UI(위젯)'
매대를 계속 쳐다보고 있다가 개수가 바뀌면 즉시 화면을 새로 그림 (ref.watch)
혹은 딱 한 번만 물어보고 주문만 하고 가기도 함 (ref.read)
손님(위젯)이 매대(Provider)에 가서 사장님과 소통할 때 쓰는 무전기가 바로 ref
ref.watch (실시간 전광판 감시)
ref.read (필요할 때만 호출)

// 1. 데이터: 붕어빵 개수 (State)
class BungeoState {
final int count;
BungeoState(this.count);
}
// 2. 사장님: 붕어빵 관리 (Notifier)
class BungeoViewModel extends Notifier<BungeoState> {
BungeoState build() => BungeoState(5); // 초기 개수 5개
void sell() {
// 반드시 '새 객체'로 교체해야 사장님이 알림을 보냅니다
state = BungeoState(state.count - 1);
}
}
// 3. 매대: 누구나 접근 가능 (Provider)
final bungeoProvider = NotifierProvider<BungeoViewModel, BungeoState>(() {
return BungeoViewModel();
});
class BungeoShopView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 📢 watch 버튼: 개수가 바뀌면 이 화면 전체가 자동으로 새로 그려짐
final bungeo = ref.watch(bungeoProvider);
return Scaffold(
body: Center(
child: Column(
children: [
Text('남은 붕어빵: ${bungeo.count}개'),
ElevatedButton(
onPressed: () {
// 📢 read 버튼: 버튼 누를 때만 사장님을 불러서 '판매' 시킴
ref.read(bungeoProvider.notifier).sell();
},
child: Text('붕어빵 팔기'),
),
],
),
),
);
}
}
버튼 클릭
서버에서 유저 정보 수신 (가정)
Riverpod을 이용해 상태 저장
UI 자동 갱신
{
"name": "김현서",
"age": 24
}
lib/
┣ home_page.dart // View
┣ home_view_model.dart // ViewModel + State + Provider
┣ user.dart // Model
┗ user_repository.dart // Data Source
데이터를 담는 순수 모델
user.dart
class User {
User({
required this.name,
required this.age,
});
final String name;
final int age;
User.fromJson(Map<String, dynamic> map)
: name = map['name'],
age = map['age'];
Map<String, dynamic> toJson() {
return {
"name": name,
"age": age,
};
}
}
서버 통신을 가정
JSON → Map → User 변환
user_repository.dart
class UserRepository {
Future<User> getUser() async {
await Future.delayed(Duration(seconds: 1));
String dummy = """
{
"name": "김현서",
"age": 24
}
""";
Map<String, dynamic> map = jsonDecode(dummy);
return User.fromJson(map);
}
}
화면에 필요한 데이터만 모아둔 상태 클래스
class HomeState {
User? user;
HomeState(this.user);
}
home_view_model.dart
class HomeViewModel extends Notifier<HomeState> {
HomeState build() {
return HomeState(null);
}
void getUser() async {
UserRepository userRepository = UserRepository();
User user = await userRepository.getUser();
state = HomeState(user);
}
}
final homeViewModelProvider = NotifierProvider<HomeViewModel, HomeState>(() {
return HomeViewModel();
});
📌 역할 요약
ViewModel 생성
상태 보관
여러 Widget에서 공유
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Consumer(
builder: (context, ref, child) {
final homeState = ref.watch(homeViewModelProvider);
return Column(
children: [
Text("name: ${homeState.user?.name}"),
Text("age: ${homeState.user?.age}"),
ElevatedButton(
onPressed: () {
final viewModel = ref.read(homeViewModelProvider.notifier);
viewModel.getUser();
},
child: Text("데이터 가져오기"),
),
],
);
},
),
);
}
}
하위 위젯으로 파라미터 전달 불필요
필요한 위젯에서 Consumer로 바로 접근
전역 상태 관리 가능
ViewModel 생성·유지·공급을 Riverpod이 담당
역할 분리 명확
Widget → UI만 담당
ViewModel → 상태 관리
Repository → 데이터 처리
유지보수 쉬움
UI 수정 ≠ 비즈니스 로직 수정
RiverPod 패키지 추가 flutter pub add flutter_riverpod
main()에서 ProviderScope로 최상위 위젯 감싸기
Widget 구현 (화면)
데이터를 담을 Model 클래스 생성
데이터를 가져오는 Repository 생성
Widget에서 사용할 State 클래스 생성
상태를 관리할 ViewModel 생성 (Notifier 상속)
ViewModel을 공급할 NotifierProvider 생성
Widget에서 Consumer 사용
Flutter 앱 안에서 웹 페이지를 보여주는 위젯
앱 내부에 웹 브라우저를 임베드한다고 생각하면 됨
Android / iOS의 네이티브 WebView 컴포넌트 사용
| 패키지 | 특징 |
|---|---|
flutter_webview | Flutter 공식 패키지, 기본 기능만 제공 |
flutter_inappwebview | 비공식이지만 기능 매우 풍부 (쿠키 관리, JS 통신, 권한 처리 등) |
flutter pub add flutter_inappwebview
InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri("https://www.naver.com/"),
),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
mediaPlaybackRequiresUserGesture: true,
userAgent: '브라우저 User-Agent 문자열',
),
)
initialUrlRequest
WebView가 처음 로드할 URL
javaScriptEnabled
JavaScript 사용 여부 (대부분 true 필수)
mediaPlaybackRequiresUserGesture
자동 재생 허용 여부
userAgent
WebView 차단 방지용 → 브라우저처럼 위장
이런 게 있다 정도만 알기
onWebViewCreated : WebView 생성 시
onLoadStart : 페이지 로딩 시작
onLoadStop : 페이지 로딩 완료
onPermissionRequest : 카메라, GPS 등 권한 요청 시
class DetailPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('해리포터와 마법사의 돌'),
),
body: InAppWebView(
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
mediaPlaybackRequiresUserGesture: true,
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
),
initialUrlRequest: URLRequest(
url: WebUri("https://www.naver.com/"),
),
),
);
}
}
Open API (Open Application Programming Interface)
외부 개발자가 특정 서비스나 소프트웨어의 기능을 사용할 수 있도록 공개한 인터페이스
외부 서비스의 기능이나 데이터를 사용할 수 있음
HTTP 통신 기반으로 동작
요청(Request)을 보내면 응답(Response)을 받는 구조
응답 데이터는 주로 JSON 형식
클라이언트에서 요청(Request) 전송
서버에서 요청 처리
JSON 형태의 데이터 응답(Response)
클라이언트에서 데이터 활용
flutter pub add http
http 패키지란?
Flutter / Dart에서 HTTP 요청(GET, POST 등) 을 보내기 위한 패키지
REST API 연동 시 필수
API 통신 담당
DTO(JSON) → Model 객체로 변환
ViewModel / UI에서 직접 통신하지 않도록 분리
import 'dart:convert';
import 'package:flutter_book_search_page/data/model/book.dart';
import 'package:http/http.dart';
class BookRepository {
// API 응답을 받아 List<Book>으로 가공해서 반환
Future<List<Book>?> search(String query) async {
// 네트워크 통신은 반드시 예외 처리
try {
Client client = Client();
Response result = await client.get(
Uri.parse(
'https://openapi.naver.com/v1/search/book.json?query=$query',
),
headers: {
'X-Naver-Client-Id': 'YOUR_CLIENT_ID',
'X-Naver-Client-Secret': 'YOUR_CLIENT_SECRET',
},
);
// HTTP 200 → 요청 성공
if (result.statusCode == 200) {
final json = jsonDecode(result.body);
return List.from(json['items'])
.map((e) => Book.fromJson(e))
.toList();
}
return null;
} catch (e) {
print(e);
return null;
}
}
}
try-catch 필수
→ 인터넷 미연결, 서버 오류 등 예외 발생 가능
statusCode == 200
→ 정상 응답 여부 확인
jsonDecode
→ JSON 문자열 → Map
fromJson()
→ DTO → Model 변환
JSON 문자열을 jsonDecode로 파싱
Book.fromJson()이 정상적으로 동작하는지 검증
문자열 타입 필드(discount) 값 확인
String dummyData = """
{
"title": "Harry! (Gedichte)",
"link": "...",
"image": "...",
"author": "",
"discount": "25360",
"publisher": "Books on Demand",
"pubdate": "20210519",
"isbn": "9783753499949",
"description": "text \\n text"
}
""";
Map<String, dynamic> map = jsonDecode(dummyData);
Book book = Book.fromJson(map);
expect(book.discount, '25360');
❌ 에러
FormatException: Control character in string
JSON 문자열 내부에 실제 줄바꿈(개행) 존재
Dart multiline string의 \n → 실제 개행 문자
JSON 규칙 위반
JSON 문자열 안에서는 반드시 escape 처리
\\n
StatefulWidget과 사용법은 동일
State 클래스 안에서 ref 사용 가능
ViewModel 상태를 구독(watch) 하고 메서드 호출(read) 가능
class HomePage extends ConsumerStatefulWidget {
ConsumerState<HomePage> createState() => _HomePageState();
}
class _HomePageState extends ConsumerState<HomePage> {
HomeState homeState = ref.watch(homeViewModelProvider);
watch
→ 상태 변화 감지 → UI 자동 리빌드
read
→ 상태 변경 / 함수 호출용
ref.read(homeViewModelProvider.notifier).search(text);
TextField 입력
↓
search(text)
↓
HomeViewModel.search()
↓
books 상태 변경
↓
UI 자동 갱신 (GridView)
Book 데이터 사용
Book book = homeState.books[index];
Image.network(
book.image,
fit: BoxFit.cover,
)
HomePage → HomeBottomSheet
showModalBottomSheet(
context: context,
builder: (context) {
return HomeBottomSheet(book);
},
);
Book을 생성자로 전달받음
class HomeBottomSheet extends StatelessWidget {
HomeBottomSheet(this.book);
Book book;
데이터 바인딩
Text(book.title)
Text(book.author)
Text(book.description)
Image.network(book.image)
이제 api 사용할 준비와 상태 관리 패키지를 사용할 준비가 되어서 설렌담 ㅎㅎ