[Flutter] riverpod 통신 with mock

만들면서 배우는 플러터 앱 프로그래밍
최재원's avatar
Jul 30, 2025
[Flutter] riverpod 통신 with mock

프로젝트 코드

graph LR view --> viewModel viewModel --> repository repository --> viewModel viewModel --> view

1. 프로젝트 생성

notion image
  • 안드로이드 스튜디오로 새 플러터 프로젝트 생성

2. 의존성 추가

flutter_riverpod: ^2.6.1 dio: ^5.8.0+1 // 통신 라이브러리

3. 창고 코드 작성(기본형)

1. 창고 데이터 타입

/// #### 3. 창고 데이터 타입 /// /// - 페이지이름으로 타입 이름을 작성 /// /// - 클래스 내부에 다른 클래스 같이 생긴 타입이 필요 하면 레코드를 사용 하자 /// - final을 사용해서 변경금지를 강제하자 class Post { final int id; final int userId; final String title; /// named type record List<({int id, String comment, String owner})> replies; Post(this.id, this.userId, this.title, this.replies); }
  • 화면에 사용될 데이터 타입
  • 스프링 서버의 DTO 라고 생각 하면 된다
  • 단일 데이터 타입이 아니라 컬랙션을 사용할 경우 레코드 타입을 사용하자
    • 선택적 매개변수를 사용해 이름이 있는 레코드 타입을 만들자
  • 이름은 페이지 이름

2. 창고

/// 2. 창고 /// - 페이지이름VM 으로 타입 이름을 작성 /// - 통신을 받을 동안 null 을 사용해야하니 ? 타입을 사용하자 class PostVM extends Notifier<Post?> { @override Post? build() { init(); // 비동기로 만들어서 일단 null 을 return 하자 return null; } /// 실행 타이밍 /// 1. build 할때 실행 void init() {} }
  • 창고가 생성되면 일단 state 를 null 로 한다
  • null 을 return 하기 때문에 타입에 ? 를 추가하자
  • 나중에 init() 메서드를 호출해서 데이터 통신 후 state 를 변경하자
  • 이름은 페이지 이름 + VM

3. 창고 관리자

/// 1. 창고 관리자 /// - 1번 타입 -> 창고 타입 /// - 2버 타입 -> 데이터 타입 final postProvider = NotifierProvider<PostVM, Post?>(() { return PostVM(); });
  • view 에서 watch, read 를 실행하면 창고 관리자가 생성된다
  • 창고 관리자는 생성 되면서 창고가 생성된다
  • 제네릭
      1. 창고 타입
      1. 데이터 타입
  • 이름은 페이지 이름 + Provider
 

4. 창고 코드 작성(모델 일체형)

전체코드
import 'package:data_sample/repository/post_repository.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; /// #### 1. 창고 관리자 /// - 1번 타입 -> 창고 타입 /// /// - 2번 타입 -> 데이터 타입 /// view에서 watch or read 하면 NotifierProvider 이놈이 실행된다 final postProvider = NotifierProvider<PostVM, Post?>(() { return PostVM(); // 비동기로 만들어서 일단 null 을 return 하자 }); /// 2. 창고 /// - 책임 : 데이터 변경 /// - 책임 : 비즈니스 로직(스프링의 서비스) /// - '페이지이름VM' 으로 타입 이름을 작성 /// /// - 통신을 받을 동안 null 을 사용해야하니 ? 타입을 사용하자 class PostVM extends Notifier<Post?> { PostRepository postRepository = PostRepository.instance; // 컴포지션 /// build() 함수는 무조건 동기적으로 작성해야한다 @override Post? build() { init(); return null; // build에서 return 하는 값이 state 값이 된다 } /// 2가지 전략 Future<void> update() async { // 통신 Post post = await postRepository.update(); state = state!.copyWith(title: post.title); // state = post; } /// 실행 타이밍 /// 1. build 할때 실행 Future<void> init() async { // 1. 데이터 받기 Post post = await postRepository.findById(1); // // 2. 파싱 -> repo에서 파싱하면 안씀 // Post post = Post.fromMap(data); state = post; } } /// #### 3. 창고 데이터 타입 /// /// - '페이지이름'으로 타입 이름을 작성 /// /// - 클래스 내부에 다른 클래스 같이 생긴 타입이 필요 하면 레코드를 사용 하자 /// - final을 사용해서 변경금지를 강제하자 class Post { final int? id; final int? userId; final String? title; /// named type record final List<({int id, String comment, String owner})> replies; Post(this.id, this.userId, this.title, this.replies); /// 1. toJson -> 요청 DTO /// 2. fromJson -> 응답 DTO. fromMap 으로 만들거다. (Dio 통신라이브러리가 json -> map 해준다). GPT가 만들어주는 어노테이션 쓰지말자 /// 무엇을(restApi서버 -> dio -> map 데이터) -> 어떻게 (post 데이터) /// 생성자로 만든다 Post Post.fromMap(Map<String, dynamic> map) : id = map["id"], userId = map["userId"], title = map["title"], replies = (map["replies"] != null) ? (map["replies"] as List) .map( (e) => ( id: e["id"] as int, comment: e["comment"] as String, owner: e["owner"] as String, ), ) .toList() : []; /// #### 3. copyWith -> 깊은 복사를 해야함 /// - ? 타입을 사용해서 변경하고 싶은 것만 넣자 Post copyWith({ int? id, int? userId, String? title, List<({int id, String comment, String owner})>? replies, }) { return Post( id ?? this.id, userId ?? this.userId, title ?? this.title, replies ?? this.replies, ); } }

1. 창고 데이터 타입

1. 필드 값

class Post { final int? id; final int? userId; final String? title; /// named type record final List<({int id, String comment, String owner})> replies; Post(this.id, this.userId, this.title, this.replies); ...
  • 필드 이름은 ?타입을 사용해서 통신으로 받지 못하더라도 예외가 발생하지 않게 하자
  • 필드 값들은 final 을 사용해 임의 변경을 강제로 차단하자

2. fromMap()

class Post{ ... Post.fromMap(Map<String, dynamic> map) : id = map["id"], userId = map["userId"], title = map["title"], replies = (map["replies"] != null) ? (map["replies"] as List) .map( (e) => ( id: e["id"] as int, comment: e["comment"] as String, owner: e["owner"] as String, ), ) .toList() : []; ...
  • 위 코드 말고 GPT 가 알려주는 코드는 사용하지 말자
  • 무엇을(restApi서버 -> dio -> map 데이터) -> 어떻게 (post 데이터)
  • 원래는 fromJson 유형이다. 통신으로 받아온 json 데이터를 다트의 클래스로 변환 하는 방법을 작성한다
  • dio 라이브러리는 통신을 하고 나서 json → map 으로 변환 해준다
    • 변환 되는 타입은 Map<String, dynamic> 이다
  • 따라서 우리는 map 을 객체로 변환하는 코드를 작성한다
  • 이름이 있는 생성자를 사용해 map 데이터를 클래스로 맵핑한다
    • static 메서드로 만들지 않는 이유는 Post 가 new 되어 있지 않은 상태에서 호출해야 하기 때문
  • dynamic 타입 이기 때문에 형변환이 필요함
    • Dart는 최상위 Map의 단순 필드 접근에서는 비교적 관대하게 추론함
    • 따라서 최상위 변수는 명시적 형변환이 필요 없음
    • 내부 Map은 명시적 형변환이 필요한 이유
      • map["replies"]List<dynamic>으로 간주됨
      • edynamic이기 때문에, e["id"] 같은 접근을 할 경우 Dart는 타입 정보를 추론할 수 없음
      • 따라서 e["id"]는 무조건 dynamic으로 취급됨
      • 각 필드를 명시적으로 타입 변환 (as int, as String) 해야 함

3. copyWith()

class Post{ ... Post copyWith({ int? id, int? userId, String? title, List<({int id, String comment, String owner})>? replies, }) { return Post( id ?? this.id, userId ?? this.userId, title ?? this.title, replies ?? this.replies, ); } }
  • 깊은 복사
    • 이유 → 모든 데이터가 아니라 일부만 변경했을 때 사용
    • 불변을 위해 객체를 새로 생성한다
  • ? 타입을 사용해 넣고 싶은 값만 넣을 수 있다
 

2. 창고

1. 기본 클래스

class PostVM extends Notifier<Post?> { PostRepository postRepository = PostRepository.instance; // 컴포지션 /// build() 함수는 무조건 동기적으로 작성해야한다 @override Post? build() { init(); return null; // build에서 return 하는 값이 state 값이 된다 } ... }
  • 책임
    • 데이터 변경
    • 서비스 로직
  • 통신에 필요한 repo 를 클래스 내부 변수로 할당해서 컴포지션 한다
    • instance는 싱글톤으로 만들어 졌다(static)
  • build() 메서드를 자동으로 실행한다. 창고가 new 될 때
    • 무조건 실행이기 때문에 동기적으로 작성해야 한다
    • 오버라이드로 구현한다
  • 통신으로 데이터를 변경한다면 일단 null 을 return

2. init()

class PostVM extends Notifier<Post?> { ... Future<void> init() async { // 1. 데이터 받기 Post post = await postRepository.findById(1); // // 2. 파싱 -> repo에서 파싱하면 안씀 // Post post = Post.fromMap(data); state = post; } }
  • vm 에 데이터를 초기화 시키는 로직이 들어간다
  • 실행 타이밍은 build 시 실행한다
  • 서버에 데이터를 요청 후 모델을 초기화 한다
  • repo 에서 map 데이터를 그대로 받아와 파싱을 할 수도 있다. repo에서 파싱해서 들고 올 수 도 있다

3. update()

class PostVM extends Notifier<Post?> { ... /// 2가지 전략 Future<void> update() async { // 통신 Post post = await postRepository.update(); // 일부 데이터만 받으면 state = state!.copyWith(title: post.title); // 전체 데이터를 받으면 // state = post; } ...
  • 필요한 추가 로직등을 여기에 메서드로 작성할 수 있다
  • 상태 업데이트가 필요하여 update() 메서드를 만들었다
  • repo 에 데이터를 전송한 뒤 응답 받은 데이터를 state 에 할당
    • 변경된 데이터의 응답을 일부만 받을 경우 copyWith() 을 사용한다
    • 변경된 데이터의 응답 전부를 받을 경우 그냥 바로 state 에 할당한다
  • 할당 할 때는 깊은 복사를 사용해야 riverpod 이 상태가 변경된 걸 감지한다

3. 창고 관리자

/// #### 1. 창고 관리자 /// - 1번 타입 -> 창고 타입 /// /// - 2번 타입 -> 데이터 타입 /// view에서 watch or read 하면 NotifierProvider 이놈이 실행된다 final postProvider = NotifierProvider<PostVM, Post?>(() { return PostVM(); // 비동기로 만들어서 일단 null 을 return 하자 });
  • view 에서 ConsumerWidget 을 사용해서 WidgetRef ref 에 접근 할 수 있게 되면
    • ref → 모든 viewMV(창고) 에 접근 할 수 있는 객체
  • ref.watch | ref.read 를 호출하면 그 때 NotifierProvider 가 실행이 되면서 창고와 창고 관리자가 초기화 된다
  • 첫 번째 타입 → 창고 타입
  • 두 번째 타입 → 데이터 타입
 

번외(초보자용)

초보자용 뷰모델(파싱을 여기서)
import 'package:data_sample/repository/post_repository.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; /// #### 1. 창고 관리자 /// - 1번 타입 -> 창고 타입 /// /// - 2번 타입 -> 데이터 타입 /// view에서 watch or read 하면 NotifierProvider 이놈이 실행된다 final postProvider = NotifierProvider<PostVM, Post?>(() { return PostVM(); // 비동기로 만들어서 일단 null 을 return 하자 }); /// 2. 창고 /// - 책임 : 데이터 변경 /// - 책임 : 비즈니스 로직(스프링의 서비스) /// - '페이지이름VM' 으로 타입 이름을 작성 /// /// - 통신을 받을 동안 null 을 사용해야하니 ? 타입을 사용하자 class PostVM extends Notifier<Post?> { PostRepository postRepository = PostRepository.instance; // 컴포지션 /// build() 함수는 무조건 동기적으로 작성해야한다 @override Post? build() { init(); return null; // build에서 return 하는 값이 state 값이 된다 } /// 실행 타이밍 /// 1. build 할때 실행 Future<void> init() async { // 1. 데이터 받기 Map<String, dynamic> data = await postRepository.findById(1); // 2. 파싱 Post post = Post.fromMap(data); state = post; } } /// #### 3. 창고 데이터 타입 /// /// - '페이지이름'으로 타입 이름을 작성 /// /// - 클래스 내부에 다른 클래스 같이 생긴 타입이 필요 하면 레코드를 사용 하자 /// - final을 사용해서 변경금지를 강제하자 class Post { int id; int userId; String title; /// named type record List<({int id, String comment, String owner})> replies; Post(this.id, this.userId, this.title, this.replies); /// 1. toJson -> 요청 DTO /// 2. fromJson -> 응답 DTO. fromMap 으로 만들거다. (Dio 통신라이브러리가 json -> map 해준다). GPT가 만들어주는 어노테이션 쓰지말자 /// 무엇을(restApi서버 -> dio -> map 데이터) -> 어떻게 (post 데이터) /// 생성자로 만든다 Post Post.fromMap(Map<String, dynamic> map) : id = map["id"], userId = map["userId"], title = map["title"], replies = (map["replies"] as List) .map((e) => (id: e["id"] as int, comment: e["comment"] as String, owner: e["owner"] as String)) .toList(); /// #### 3. copyWith -> 깊은 복사를 해야함 /// - ? 타입을 사용해서 변경하고 싶은 것만 넣자 Post copyWith({ int? id, int? userId, String? title, List<({int id, String comment, String owner})>? replies, }) { return Post( id ?? this.id, userId ?? this.userId, title ?? this.title, replies ?? this.replies, ); } }
초보자용 레파지토리(여기서 파싱x)
/// - 책임 : 서버 접근, db 접근 /// - 책임 : 데이터 파싱 /// 휴대기기 내부 db, 서버 db에 접근해야하기 때문에 각각 repo 를 만들어야 한다 /// 통신을 위한 repo class PostRepository { // const PostRepository(); // const 는 있는지 찾아야 한다. 싱글톤으로 만들면 바로 찾는다 static PostRepository instance = PostRepository._single(); // 싱글톤 생성 PostRepository._single(); // 이름있는 생성자만 만들면 기본생성자를 호출할 수 없다 Future<Map<String, dynamic>> findById(int id) async { // 1. 통신 // 자바스크립트의 setInterval Map<String, dynamic> response = await Future.delayed(Duration(seconds: 5), () { return _mockPost; }); // 2. 파싱 x -> 리턴 // 나중에 파싱에 오류 try catch 처리를 하자 return response; } } // 가짜 데이터 final Map<String, dynamic> _mockPost = { "id": 1, "userId": 3, "title": "제목1", "replies": [ {"id": 1, "comment": "댓글1", "owner": "ssar"}, {"id": 2, "comment": "댓글2", "owner": "cos"}, {"id": 3, "comment": "댓글3", "owner": "love"}, ], };
 
 
 

1차 유형 DTO 처럼 불변으로 사용하는 방법

창고 데이터를 DTO 처럼 불변으로 관리하는 방법이다
단점 변경이 있을 때 마다 copyWith을 사용할 때 여러개의 필드값을 넣어주는 반복을 해야 한다
 
1차 유형 riverpod
1차 유형 repo

5. repository 코드 작성

전체코드
import 'package:data_sample/pages/post_vm.dart'; /// - 책임 : 서버 접근, db 접근 /// - 책임 : 데이터 파싱 /// 휴대기기 내부 db, 서버 db에 접근해야하기 때문에 각각 repo 를 만들어야 한다 /// 통신을 위한 repo class PostRepository { // const PostRepository(); // const 는 있는지 찾아야 한다. 싱글톤으로 만들면 바로 찾는다 static PostRepository instance = PostRepository._single(); // 싱글톤 생성 PostRepository._single(); // 이름있는 생성자만 만들면 기본생성자를 호출할 수 없다 Future<Post> findById(int id) async { // 1. 통신 // 자바스크립트의 setInterval Map<String, dynamic> response = await Future.delayed(Duration(seconds: 5), () { return _mockPost; }); // 2. 파싱 x -> 리턴 // 나중에 파싱에 오류 try catch 처리를 하자 Post post = Post.fromMap(response); return post; } Future<Post> update() async { // 1. 통신 코드 Map<String, dynamic> response = await Future.delayed( Duration(seconds: 5), () { return _mockUpdatePost; }, ); Post post = Post.fromMap(response); // 2. 리턴 return post; } } // 가짜 데이터 final Map<String, dynamic> _mockUpdatePost = {"id": 1, "userId": 3, "title": "제목10"}; // 가짜 데이터 final Map<String, dynamic> _mockPost = { "id": 1, "userId": 3, "title": "제목1", "replies": [ {"id": 1, "comment": "댓글1", "owner": "ssar"}, {"id": 2, "comment": "댓글2", "owner": "cos"}, {"id": 3, "comment": "댓글3", "owner": "love"}, ], };

1. repository

1. repository

class PostRepository { // const PostRepository(); // const 는 있는지 찾아야 한다. 싱글톤으로 만들면 바로 찾는다 static PostRepository instance = PostRepository._single(); // 싱글톤 생성 PostRepository._single(); // 이름있는 생성자만 만들면 기본생성자를 호출할 수 없다 ... }
  • 생성자를 const 로 작성해서 단일 repository로 만들어서 공유해서 사용할 수 있다
    • 단점 → heap 메모리에서 해당 데이터를 검색하는 과정이 있다
  • static 으로 싱글톤 방법으로 생성
    • _를 붙여서 private 로 이름이 있는 생성자를 만들고 static 변수를 만들어 바로 초기화 해서 사용한다
    • 이름이 있는 생성자만 존재하면 기본 생성자로 호출 x

findbById()

class PostRepository { ... Future<Post> findById(int id) async { // 1. 통신 // 자바스크립트의 setInterval Map<String, dynamic> response = await Future.delayed(Duration(seconds: 5), () { return _mockPost; }); // 2. 파싱 x -> 리턴 // 나중에 파싱에 오류 try catch 처리를 하자 Post post = Post.fromMap(response); return post; } ... }
  • repository 는 스프링의 repository 처럼 사용하면 된다
  • 서버 혹은 DB 에서 데이터를 요청하는 로직이 있다
  • 데이터를 요청하면 원래는 json을 받지만 dio 라이브러리를 사용하면 Map 으로 변환 해준다
  • dio 로 부터 받은 Map 데이터를 이름 있는 생성자 fromMap 을 사용해 파싱한다
  • 파싱된 객체를 리턴 한다
  • 다트도 싱글 스레드이기 때문에 await 를 사용해 비동기 통신을 해야 한다
  • 비동기를 사용하는 함수의 리턴 값은 Future 타입 이다
  • 임시로 delayed를 사용해 통신하는 것처럼 만들어 사용한다
  • 리턴하는 데이터는 mock 데이터다

update()

Future<Post> update() async { // 1. 통신 코드 Map<String, dynamic> response = await Future.delayed( Duration(seconds: 5), () { return _mockUpdatePost; }, ); Post post = Post.fromMap(response); // 2. 리턴 return post; }
  • 이것도 마찬가지로 통신 로직이다
  • 업데이트 후 받은 데이터를 fromMap 으로 파싱한다
  • 임시로 delayed를 사용해 통신하는 것처럼 만들어 사용한다
  • 리턴하는 데이터는 mock 데이터다

mockData

// 가짜 데이터 final Map<String, dynamic> _mockPost = { "id": 1, "userId": 3, "title": "제목1", "replies": [ {"id": 1, "comment": "댓글1", "owner": "ssar"}, {"id": 2, "comment": "댓글2", "owner": "cos"}, {"id": 3, "comment": "댓글3", "owner": "love"}, ], };
// 가짜 데이터 final Map<String, dynamic> _mockUpdatePost = {"id": 1, "userId": 3, "title": "제목10"};
  • 실제 데이터를 요청할 수 없는 상황에서 사용
  • 이 mock 데이터를 가지고 가짜 통신 코드를 작성한다
  • 나중에 실제 데이터가 존재하면 데이터 부분만 변경하면 된다

6. 창고 코드 작성(모델 분리형)

전체코드
import 'package:data_sample/model/post.dart'; import 'package:data_sample/model/reply.dart'; import 'package:data_sample/repository/post_repository.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // 1. 창고 관리자 final postProvider = NotifierProvider<PostVM, PostModel?>(() { return PostVM(); }); // 2. 창고 (비지니스 로직) class PostVM extends Notifier<PostModel?> { PostRepository postRepository = PostRepository.instance; @override PostModel? build() { init(); return null; } Future<void> updateV2() async { // 통신 (제목10) await postRepository.update(); Post prevPost = state!.post; prevPost.update("제목10"); state = state!.copyWith(post: prevPost); } Future<void> update() async { // 통신 final response = await postRepository.update(); Post nextPost = Post.fromMap(response); state = state!.copyWith(post: nextPost); } Future<void> init() async { final response = await postRepository.findById(1); state = PostModel.fromMap(response); } } // 3. 창고 데이터 타입 class PostModel { final Post post; final List<Reply> replies; // 레코드 // 생성자 PostModel({required this.post, required this.replies}); // fromMap 생성자 PostModel.fromMap(Map<String, dynamic> data) : post = Post.fromMap(data), replies = (data["replies"] as List<dynamic>).map((e) => Reply.fromMap(e)).toList(); // copyWith PostModel copyWith({ Post? post, List<Reply>? replies, }) { return PostModel( post: post ?? this.post, replies: replies ?? this.replies, ); } }

1. 창고 데이터 타입

1. 필드 값

class PostModel { final Post post; final List<Reply> replies; // 레코드 // 생성자 PostModel({required this.post, required this.replies}); ... }
  • 이름을 페이지명+Model 로 작성
  • 내부 데이터는 각각의 공유 사용이 가능한 model 을 가져와 사용한다(컴포지션)
  • final 을 사용해 초기화 후 변경을 금지한다
  • 생성자는 선택적 매개변수로 만들어 이름이 보이도록 만든다

2. fromMap

class PostModel { ... PostModel.fromMap(Map<String, dynamic> data) : post = Post.fromMap(data), replies = (data["replies"] as List<dynamic>).map((e) => Reply.fromMap(e)).toList(); ... }
  • 이름 있는 생성자
  • map 을 가져와서 각각의 model 로 파싱하고 초기화 한다

3. copyWith

class PostModel { ... PostModel copyWith({ Post? post, List<Reply>? replies, }) { return PostModel( post: post ?? this.post, replies: replies ?? this.replies, ); } }
  • 상태 변경을 위한 깊은 복사 메서드
  • 선택적 매개변수를 사용해 필요한 부분만 받는다
  • ?? 연산자를 사용해 왼쪽이 null 이면 오른쪽을 넣는다

4. Post(model)

class Post { int? id; int? userId; String? title; void update(String title) { this.title = title; } // 일반 생성자 Post(this.id, this.userId, this.title); // fromMap 생성자 (initializer list 사용) Post.fromMap(Map<String, dynamic> data) : id = data['id'], userId = data['userId'], title = data['title']; }
  • 공유 가능한 데이터는 따로 분리해서 model 로 사용한다
  • 이 model 은 데이터 변경이 가능하기 때문에 ? 타입을 사용한다
  • 스프링의 엔티티처럼 사용하면 된다
  • 상태 변경 메서드를 추가해도 된다
  • 마찬가지로 이름있는 생성자를 사용한다

5. Reply(model)

class Reply { int? id; String? comment; String? owner; // 일반 생성자 Reply(this.id, this.comment, this.owner); // fromMap 생성자 Reply.fromMap(Map<String, dynamic> data) : id = data['id'], comment = data['comment'], owner = data['owner']; }
  • 공유 가능한 데이터는 따로 분리해서 model 로 사용한다
  • 이 model 은 데이터 변경이 가능하기 때문에 ? 타입을 사용한다
  • 스프링의 엔티티처럼 사용하면 된다
  • 상태 변경 메서드를 추가해도 된다
  • 마찬가지로 이름있는 생성자를 사용한다

7. repository 코드 작성

5번과 동일
// 서버접근 + 파싱 class PostRepository { static PostRepository instance = PostRepository._single(); PostRepository._single(); Future<Map<String, dynamic>> findById(int id) async { // 1. 통신 코드 final response = await Future.delayed( Duration(seconds: 2), () { return _mockPost; }, ); // 2. 리턴 return response; } Future<Map<String, dynamic>> update() async { // 1. 통신 코드 final response = await Future.delayed( Duration(seconds: 2), () { return _mockUpdatePost; }, ); // 2. 리턴 return response; } } // 가짜 데이터 final Map<String, dynamic> _mockUpdatePost = {"id": 1, "userId": 3, "title": "제목10"}; // 가짜 데이터 final Map<String, dynamic> _mockPost = { "id": 1, "userId": 3, "title": "제목1", "replies": [ {"id": 1, "comment": "댓글1", "owner": "ssar"}, {"id": 2, "comment": "댓글2", "owner": "cos"}, {"id": 3, "comment": "댓글3", "owner": "love"}, ], };
 
Share article

jjack1