flutter - 플러터로 크로스 플랫폼 앱 개발하기(3)

2026. 3. 6. 23:10·dev/app

상태 관리 라이브러리

앱에는 고정된 데이터와 변경되는 데이터가 존재한다. 앱의 주된 사용 목적 중 대부분은 변경되는 데이터의 조회로 이 데이터를 어떻게 관리하고 표현하는지가 앱의 성능, 사용성에 큰 영향을 미친다. 상태 관리 라이브러리는 변경되는 데이터를 클래스 내부에서 관리하고 위젯에 담아 화면에 출력하는 클래스간 데이터 흐름을 총괄한다.

 

상태 관리 라이브러리로는 대표적으로 Provider, Bloc, Riverpod, GetX가 존재한다. GetX는 이제 거의 안 쓰이고, Provider는 일전에 설명한 관계로 Bloc, Riverpod 두 라이브러리를 중심으로 설명한다.

Bloc

Business Logic Component

imperative (명령형) : 외부 자극을 단일 경로를 따라 내부 상태 변화로 매핑하는 상태머신 

 

UI → Event → Bloc → State → UI

Bloc에서의 데이터 흐름은 다음과 같다.

  1. 외부 입력
  2. UI가 Bloc에 Event 전달
  3. Bloc 객체에서 로직 실행
  4. State 변경(emit)
  5. BlocBuilder가 변경 감지
  6. UI 반영(build)
  • state machine : 외부 이벤트를 감지 시 로직을 실행시켜 내부의 상태를 변화시키는 상태 머신
    • (현재 상태 + 이벤트 로직) ———(이벤트)——> (새 상태)
  • Event기반 모델 - 상태 변경을 트리거한 동작을 명시하여 모든 변화를 추적(디버깅, 로깅, 테스트 용이)
add(IncrementEvent);                       //Bloc

ref.read(counter.notifier).increment();    //Riverpod

 

  • 관련 객체
    • BlocProvider: Bloc 객체를 트리에 주입
    • Bloc: Widget에서 받은 Event를 처리하는 로직 수행
    • BlocBuilder: State 구독 후 변경사항 UI에 반영
  • MVVM 패턴 표방 - Widget(View)과 Bloc(Model)의 분리

위젯과 Bloc 코드

//일반 Widget
...
ElevatedButton(
  onPressed: () {
    count++;
    setState(() {}); 
  },
  child: Text("증가"),
);
//UI단에서 상태를 변경하는 로직 직접 실행 -> V+M 강하게 결합

Widget build(BuildContext context) {
  return Text('$count');
}
// setState에 의해 자동 빌드



//Bloc 적용 Widget
...
ElevatedButton(
  onPressed: () {
    context.read<CounterBloc>().add(IncrementEvent());
  },
  child: Text("증가"),
);
//UI단에서는 이벤트를 수신할 Bloc에 적당한 이벤트를 던지고 
//Bloc에서 이벤트 감지 후 로직 실행하도록 분리

BlocBuilder<CounterBloc, int>(
  builder: (context, state) {
    return Text('$state');
  },
);
//Bloc 로직 실행으로 인해 변경 감지 시 빌드되도록 상태 구독

 

//Bloc 내부
on<IncrementEvent>((event, emit) async {
  emit(Loading());
  final result = await apiCall();    //비동기 처리되는 콜백 등록
  if (result) {
    emit(Success(state + 1));       //로직 실행후 변경된 상태 emit
  } else {
    emit(Failure());
  }
});

 

 

전통적인 setState()와 Bloc의 차이

  • 상태의 소유권
    • setState 구조: 상태(State)는 UI(StatefulWidget)와 강하게 결합(존재가 독립적이긴 해도 로직상 의존적)
    • Bloc 구조: 상태는 UI 외부의 Bloc이라는 객체 안에서 Event를 거침으로써 한번 flow가 끊김→ 독립성 강화
  • 상태 변화의 예측 가능성
    • setState 구조: 위젯 내부에 변경 로직이 포함 + setState() 후 자동 빌드로 인해 입출력 흐름이 잘 안보임
      • UI 이벤트 → state 변경 → build 재호출이 한 파일 내에서 암묵적으로 진행
      • setState 호출, count++ 등으로 어느 위치에서든 상태를 자유롭게 수정 가능
      • 변경 경로가 여러 개 존재 가능
    • Bloc: UI와 로직이 완전 분리되어 상태 변화 추적에 용이
      • add(Event)를 통한 입력(UI→Bloc)과 on<Event> 를 통한 출력(Bloc→UI) 흐름이 명시적으로 드러남
      • 외부 이벤트라는 단일 시작점 강제
      • 상태 변경 경로를 하나로 통일

 

Riverpod

Provider 구조를 계승하여 확장했다는 의미에서 애너그램(provider -> riverpod)

declarative (선언형) : 상태를 정의해두면 의존 관계에 따라 자동으로 갱신되는 반응형 시스템

 

UI → Notifier → State → Provider 의존성 전파 → Consumer → UI

Riverpod에서의 데이터 흐름은 다음과 같다.

  1. 외부 입력
  2. UI가 Notifier로 상태 변경 전달 (ref.read)
  3. Notifier에서 로직 실행
  4. State 변경
  5. Notifier를 소유한 Provider가 변경 감지 (ref.watch로 연결)
  6. 의존성 그래프 전파 (Consumer까지 전달)
  7. UI 반영(build)
  • Provider의 상태 저장
  • 상태 변경 감지
  • watch 의존성 관리
  • rebuild 트리거
Riverpod
 ├─ Provider (정의)
 ├─ Notifier (상태 조작)
 ├─ Consumer/ref.watch (구독)
 └─ ProviderContainer (핵심 저장소 + 의존성 관리)

 

Provider

값을 제공하는 선언 단위로 값을 생성하는 규칙(함수)과 의존성을 정의하는 객체

  • 상태를 직접 들고 있거나 다른 provider를 조합, 가공하는 객체 모두 해당
  • ref.watch()로 연결된 의존성 그래프의 모든 노드는 provider
  • 위젯이 State에 접근할 수 있는 유일한 통로(상태에 대한 식별자 제공)
  • 대부분 상태를 직접 들고 있지 않고 Notifier로 접근
  • 상태에 접근할 통로를 어떤 방식으로 제공할지 정의

 

final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);

 

위는 int 타입의 상태 count를 관리하는 CounterNotifier라는 객체에 외부 위젯이 접근할 수 있는 통로를 counterProvider라는 이름으로 정의한 코드이다.

 

Notifier

상태를 직접 관리하는 컨트롤러

  • 실제로 state를 보유한 객체
  • state를 변경하는 로직 포함
  • 위젯에게 노출되는 상태변경 통로
  • 위젯에 의한 상태 변경 흐름
    • 위젯이 Notifier 메서드 호출
    • Notifier가 state를 갱신
    • ProviderContainer가 변경 감지
    • watch 중인 위젯(Consumer)에 최신 state 전달 + rebuild

 

Consumer

상태를 구독하는 위젯

  • provider를 watch하는 위젯으로 Notifier를 통해 상태 변경 로직에 접근 가능
  • 상태가 바뀌면 자동 rebuild

 

ProviderContainer

(provider-consumer)쌍을 저장하고 State의 변경 감지 시, watch 등록한 위젯에게 변경된 상태 전달

  • 앱 전체 상태 저장
    • 모든 provider 인스턴스 관리
    • 모든 Consumer의 watch 정보 추적
  • 모든 State의 변경 감지
    • Notifier가 state 변경 시 ProviderContainer가 이를 감지
    • 변경된 상태가 속한 Provider 확인
    • 그 Provider를 watch 중인 위젯 목록 조회
    • 해당 위젯들에게 state 전달
ProviderContainer   ← 최상위 상태 관리자
   └─ Provider      ← 상태 접근 경로 정의
        └─ Notifier ← 상태 변경 관리 객체
             └─ State ← 실제 상태

 

 

 

Bloc과 Riverpod

Bloc과 Riverpod의 유사점

  • UI → Notifier → State → Provider → Consumer → UI
  • UI → Event → Bloc → State → UI
    • 이벤트 대신 Notifier 함수를 통한 상태 변경의 통제 구조 계승
  • 상태 외부화 - 독립적인 상태를 두고 이를 참조하여 관리하는 객체(Bloc, Notifier)를 분리하여 상태 직접 수정 차단
  • UI와 로직 분리
  • Listen → Watch
    • Bloc : 상태 변경 감지 책임이 수신 측에 존재 (상태를 인자로 받는 위젯 빌더)
    • Riverpod : (송신 - 수신) 쌍을 따로 관리하는 제3자(ProviderContainer)에게 변경 감지 책임 위임(위젯이 Watch로 Provider 의존성 등록), 위젯은 UI 반영 책임만 존재

 

코드상 흐름 차이

Bloc: UI → add(Event) → Bloc → emit(state) → BlocBuilder 리빌드(위젯 리빌드)

Riverpod: UI → notifier.increment() → state 변경 → watch 중인 Provider가 감지 → watch 중인 Consumer가 감지 → 위젯 리빌드

 

 

// Riverpod 적용 위젯
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider); // 상태 구독

... 

        ElevatedButton(
          onPressed: () {
            ref.read(counterProvider.notifier).increment();
          },
          child: Text("증가"),
        );
        // UI단에서는 상태를 변경할 Notifier의 메서드를 호출

        final count = ref.watch(counterProvider);
        // 특정 상태를 구독하고 변경 시 자동 rebuild

        Text('$count');

 

//Provider 내부 (provider + notifier)
final counterProvider =
    NotifierProvider<CounterNotifier, int>(CounterNotifier.new);

class CounterNotifier extends Notifier<int> {
  @override
  int build() => 0;

  void increment() {
    state++;
  }
}

 

 

Bloc Riverpod
add(Event) notifier.method()
emit(state) state =
BlocBuilder ref.watch()
state 인자 final count 변수

 

 

Stream 기반 반응 vs 의존성 추적 기반 반응

상태 변경을 전달받고 반응하는 방식에 있어서 Stream기반의 Bloc과 의존성 추적 기반의 Riverpod 사이에는 본질적인 차이가 존재한다. 

 

Bloc - 상태 변경 자체를 데이터로 표현하여 전달 - 이벤트 중심 아키텍쳐

Riverpod - 변경된 상태를 감지하도록 연관된 객체끼리 연결 - 의존성 중심 아키텍쳐

 

  Stream 기반 의존성 추적 기반
중심 개념 데이터 흐름 의존 관계
연결 방식 구독(listener) 중심 watch 등록 (제3자가 연결 관리)
구조 선형 흐름 그래프 구조
철학 이벤트 드리븐 선언적 의존성

 

 

API 통신

실시간으로 변경되는 데이터를 앱 외부에서 받아오기 위해서는 API 통신이 필수적이다. http, Dio 같은 라이브러리를 사용하여 JSON과 다트 객체 간의 변환과 API 요청 전송 및 응답 처리를 구현할 수 있다.

 

API

Application Programming Interface

서로 다른 소프트웨어 애플리케이션이 서로 어떻게 통신하고 데이터를 공유할 지 미리 정의해둔 규칙이다. API를 통해 클라이언트는 서버의 내부 구조를 알지 못하더라도 약속한 형식에 맞춰 요청을 보내기만 하면 약속한 형식으로 응답을 받아올 수 있다.

  • 어떻게 하는지는 몰라도 무엇을 하는지는 안다

플러터 앱에서 API 호출을 통해 데이터를 받아오는 흐름은 다음과 같다.

1. 사용자 UI 이벤트 처리
    - 버튼 클릭이나 입력 변경에 따른 사용자 행동에 대해 이벤트 생성, 해당 이벤트리스너에 의한 API 통신 프로세스 시작
2. API 요청 전송
    - 이벤트리스너에서 적절한 파라미터를 채운 후 http 클라이언트를 통해 API 요청 전송
3. API 응답 수신
    - 서버로부터 받은 API 응답에 대한 수신 처리(상태 코드 확인, 에러 핸들링)
4. 응답 데이터 처리
    - API 응답으로 받은 데이터(JSON, XML)의 파싱 및 변환 후 앱의 로직에 맞게 조작

 

 

플러터에서 API 관련 기능을 제공하는 대표적인 라이브러리로 http, Dio 2가지가 존재한다.

 

http 패키지

http 패키지는 HTTP(Hypertext Transfer Protocol)기반으로 API 요청을 작성하고 응답을 처리할 수 있도록 일반적인 http 메서드를 제공한다.

 

GET (http.get) 

데이터 조회

//패키지 내부 정의
Future<Response> get(Uri url, {Map<String, String>? headers})

//사용 예시
final response = await http.get(
 'https://api.example.com/users', headers : {'Authorization': 'Bearer token'}
);
  1.  
  • url : 요청을 보낼 목적지 URL
  • header: GET 요청에 필요한 정보(일반적으로 인증 정보 또는 content-type)

 

POST (http.post)

데이터 생성

// 패키지 내부 정의
Future<Response> post(
  Uri url, {
  Map<String, String>? headers,
  Object? body,
  Encoding? encoding,
})

// 사용 예시
final response = await http.post(
  Uri.parse('https://api.example.com/users'),
  headers: {
    'Authorization': 'Bearer token',
    'Content-Type': 'application/json',
  },
  body: '{"name": "John"}',
);

 

  • body: 서버에서 POST 요청에 대한 처리로 새 데이터를 생성할 때 필요한 정보

 

PUT (http.put)

전체 수정

// 패키지 내부 정의
Future<Response> put(
  Uri url, {
  Map<String, String>? headers,
  Object? body,
  Encoding? encoding,
})

// 사용 예시
final response = await http.put(
  Uri.parse('https://api.example.com/users/1'),
  headers: {
    'Authorization': 'Bearer token',
    'Content-Type': 'application/json',
  },
  body: '{"name": "Updated"}',
);

 

 

PATCH (http.patch)

부분 수정

// 패키지 내부 정의
Future<Response> patch(
  Uri url, {
  Map<String, String>? headers,
  Object? body,
  Encoding? encoding,
})

// 사용 예시
final response = await http.patch(
  Uri.parse('https://api.example.com/users/1'),
  headers: {
    'Authorization': 'Bearer token',
    'Content-Type': 'application/json',
  },
  body: '{"name": "Partial Update"}',
);

 

 

DELETE (http.delete)

데이터 삭제

// 패키지 내부 정의
Future<Response> delete(
  Uri url, {
  Map<String, String>? headers,
  Object? body,
  Encoding? encoding,
})

// 사용 예시
final response = await http.delete(
  Uri.parse('https://api.example.com/users/1'),
  headers: {'Authorization': 'Bearer token'},
);

 

 

멱등성

멱등성은 어떤 연산을 여러 번 수행해도 결과가 달라지지 않는 성질로 네트워크로 전달되는 API 통신 특성상 로직 설계에 중요한 고려 사항이 된다. 서버에서는 http의 각 메서드 특징을 고려하여 로직을 설계해야한다.

 

메서드 멱등성 반복 요청 시 행동 예시
GET O 상태 변화 없이 같은 결과 반환 GET /users/1
POST X 호출마다 새 데이터 생성 POST/users/
PUT O 받은 값으로 덮어쓰기(결과 동일)
PUT /users/1
{name: "John"}
PATCH △ 상황에 따라 결과 달라질 수 있음
PATCH /users/1
{count: count + 1}
DELETE O 해당하는 데이터 존재할 경우에 삭제 DELETE /users/1

 

  • 같은 요청을 여러 번 보내도 한 번 보낸 결과와 동일하면 멱등
  • 응답이 아닌 최종 상태 기준으로 판단

 

API 응답 데이터 처리

API 응답 데이터는 주로 다음과 같은 순서로 처리되어 UI에 반영된다.

JSON → Map → Model(다트 객체) → State → UI 

1. API 응답 (JSON)
2. jsonDecode() 호출: JSON → Map
    - jsonDecode() 메서드가 JSON 문자열 파싱 후 Dart 기본 타입 Map<String, dynamic> 객체로 역직렬화
3. fromJson() 호출: Map → 모델
    - 미리 정의해둔 Model.fromJson() 메서드가 Map<String, dynamic> 객체를 Model 객체로 변환
4. State 변경
5. UI 표시

 

다음과 같은 구조로 API를 처리하도록 설계된 애플리케이션에서 각 컴포넌트의 역할과 예시 코드는 다음과 같다.

UI
↓
Notifier (상태관리)
↓
Repository (비즈니스 로직)
↓
ApiService (HTTP + 응답 처리)
↓
http client

 

Service = 실제 네트워크 통신 수행

Model = 외부 데이터를 앱 내부에서 사용할 수 있도록 구조화한 Dart 객체 (응답의 최종 변환 결과)

Repository = 데이터 변환 및 비즈니스 로직 (데이터 조합, 조건 처리, 캐싱 전략)
Notifier = 상태 관리 및 변경 흐름(success, loading, error) 제어

Provider = 변경 전파를 위한 의존성 연결 및 진입점 제공
UI =  변경된 상태를 화면에 표시

 

Service

class ApiService {
  final String baseUrl;

  ApiService(this.baseUrl);

  Future<Map<String, dynamic>> get(String path) async {
    final response = await http.get(	//http 메서드 get 호출
      Uri.parse('$baseUrl$path'),
      headers: {
        'Authorization': 'Bearer token',
        'Content-Type': 'application/json',
      },
    );

    return _handleResponse(response);
  }

  Map<String, dynamic> _handleResponse(http.Response response) {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      return jsonDecode(response.body);
    } else {
      throw Exception('API Error: ${response.statusCode}');
    }
  }
}

 

 

Model

class User {
  final int id;
  final String name;

  User({required this.id, required this.name});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
    );
  }
}

 

Repository

class UserRepository {
  final ApiService api;

  UserRepository(this.api);

  Future<User> getUser() async {
    final data = await api.get('/users/1');	//서비스의 메서드 get 호출
    return User.fromJson(data);
  }
}

 

Notifier

class UserNotifier extends StateNotifier<User?> {
  final UserRepository repository;

  UserNotifier(this.repository) : super(null);

  Future<void> fetchUser() async {
    state = await repository.getUser(); //상태 변경
  }
}

 

Provider

final apiServiceProvider = Provider(
  (ref) => ApiService('https://api.example.com'),
);

final userRepositoryProvider = Provider(
  (ref) => UserRepository(ref.read(apiServiceProvider)),
);

final userProvider = StateNotifierProvider<UserNotifier, User?>(
  (ref) => UserNotifier(ref.read(userRepositoryProvider)),
);

 

UI

...
    final user = ref.watch(userProvider);

    ElevatedButton(
      onPressed: () {
        ref.read(userProvider.notifier).fetchUser();
      },
      child: Text('Load User'),
    );

    Text(user?.name ?? 'Loading...');

 

 

Dio 라이브러리

Dio는 플러터 애플리케이션 전용으로 최적화된 네트워킹 및 파일 입출력을 제공하는 라이브러리이다. http 패키지 위에 설계된 Dio의 유연한 http 기능 및 인터셉터, 타임아웃을 활용하여 API 요청 및 응답 처리 로직을 간편하게 구성할 수 있다. 다음은 Dio에서 추가로 제공하는 기능이다.

  • http 클라이언트 기본 옵션 설정: http 클라이언트의 기본 헤더, 기본 URL, 연결 시간 제한 등의 옵션을 쉽게 설정 가능
  • 인터셉터 지원: 인터셉터를 통해 모든 요청 및 응답에 네트워크 통신의 공통 로직 적용 가능 (토큰 자동 갱신, 공통 헤더 추가, 공통 응답 처리, 로깅, 전역 오류 처리 등)
  • 사용자 정의: 사용자 지정 형식 처리나 직렬화 로직 등 자유도 높은 요청 및 응답 변환기 구현 가능
  • 파일 업로드 및 다운로드: FormData, MultipartFile 등 대용량 파일 업로드/다운로드 기능 지원

 

Dio API 요청

Dio를 통해 http 요청을 보내기 위해서는 클라이언트 기본 설정을 적용한 Dio 인스턴스를 만들어야한다.

final dio = Dio(
  BaseOptions(
    baseUrl: 'https://api.example.com',
    headers: {
      'Authorization': 'Bearer token',
      'Content-Type': 'application/json',
    },
    connectTimeout: Duration(seconds: 5),
    receiveTimeout: Duration(seconds: 5),
  ),
);

 

GET (dio.get)

dio.get(String)

// 내부 정의
Future<Response<T>> get<T>(
  String path, {
  Map<String, dynamic>? queryParameters,
  Options? options,
  CancelToken? cancelToken,
  ProgressCallback? onReceiveProgress,
});

// 사용 예시
Future<void> fetchUser() async {
  try {
    final response = await dio.get('/users/1'); //파라미터로 엔드포인트 url 전달

    print(response.data);

  } on DioException catch (e) {
    // Dio 전용 에러 처리
    print('Dio error: ${e.message}');

    if (e.response != null) {
      print('StatusCode: ${e.response?.statusCode}');
      print('Error Data: ${e.response?.data}');
    }

  } catch (e) {
    // 기타 에러
    print('Unknown error: $e');
  }
}

 

POST (dio.post)

dio.post(String, Map)

// 내부 정의
Future<Response<T>> post<T>(
  String path, {
  Object? data,
  Map<String, dynamic>? queryParameters,
  Options? options,
  CancelToken? cancelToken,
  ProgressCallback? onSendProgress,
  ProgressCallback? onReceiveProgress,
});

// 사용 예시
Future<void> createUser() async {
  try {
    final response = await dio.post(
      '/users', //첫번째 파라미터 = 엔드포인트 url
      data: {'name': 'John'}, //두번째 파라미터 = body
    );

    print('Created: ${response.data}');

  } on DioException catch (e) {
    if (e.response?.statusCode == 400) {
      print('잘못된 요청');
    } else if (e.response?.statusCode == 401) {
      print('인증 필요');
    } else {
      print('기타 서버 에러');
    }

  } catch (e) {
    print('Unexpected error: $e');
  }
}

 

PUT (dio.put)

dio.put(String, Map)

//내부 정의
Future<Response<T>> put<T>(
  String path, {
  Object? data,
  Map<String, dynamic>? queryParameters,
  Options? options,
  CancelToken? cancelToken,
  ProgressCallback? onSendProgress,
  ProgressCallback? onReceiveProgress,
});

// 사용 예시
Future<void> updateUser() async {
  try {
    final response = await dio.put(
      '/users/1', //첫번째 파라미터 = 엔드포인트 url
      data: {'name': 'Updated Name'}  //두번째 파라미터 = body
      ,
    );

    print('Updated: ${response.data}');

  } on DioException catch (e) {
    print('Error: ${e.message}');

    if (e.response != null) {
      print('StatusCode: ${e.response?.statusCode}');
      print('Error Data: ${e.response?.data}');
    }

  } catch (e) {
    print('Unexpected error: $e');
  }
}

 

DELETE (dio.delete)

dio.delete(String)

//내부 정의
Future<Response<T>> delete<T>(
  String path, {
  Object? data,
  Map<String, dynamic>? queryParameters,
  Options? options,
  CancelToken? cancelToken,
});

//사용 예시
Future<void> deleteUser() async {
  try {
    final response = await dio.delete(
      '/users/1',
    );

    print('Deleted: ${response.data}');

  } on DioException catch (e) {
    print('Error: ${e.message}');

    if (e.response != null) {
      print('StatusCode: ${e.response?.statusCode}');
    }

  } catch (e) {
    print('Unexpected error: $e');
  }
}

 

Dio 라이브러리 내부 http 메서드 정의

파라미터 의미
path 요청 경로
data body (POST/PUT/DELETE 가능)
queryParameters URL 쿼리
options 헤더 등 추가 설정
cancelToken 요청 취소
onSendProgress 업로드 진행률
onReceiveProgress 다운로드 진행률

 

POST, PUT 요청에서 대용량 파일 전송

http 요청에서 body에 해당하는 두번째 파라미터(Object)에 Map 형식으로 전송

final formData = FormData.fromMap({
  'name': 'john',
  'file': await MultipartFile.fromFile(
    '/path/image.png',
    filename: 'image.png',
  ),
});  //텍스트와 파일 데이터를 담은 Map 정의

final response = await dio.post(
  '/upload',
  data: formData,  //data 파라미터로 정의해둔 Map 전송
);

 

'dev > app' 카테고리의 다른 글

flutter - 플러터로 크로스 플랫폼 앱 개발하기(4)  (0) 2026.02.28
flutter - 플러터로 크로스 플랫폼 앱 개발하기(2)  (0) 2026.02.27
flutter - 플러터로 크로스 플랫폼 앱 개발하기(1)  (0) 2026.02.22
'dev/app' 카테고리의 다른 글
  • flutter - 플러터로 크로스 플랫폼 앱 개발하기(4)
  • flutter - 플러터로 크로스 플랫폼 앱 개발하기(2)
  • flutter - 플러터로 크로스 플랫폼 앱 개발하기(1)
cusum26
cusum26
  • cusum26
    CUSUMlog
    cusum26
  • 전체
    오늘
    어제
    • 분류 전체보기 (18) N
      • dev (15)
        • blockchain (1)
        • ai (6)
        • web (0)
        • infra (4)
        • app (4)
      • cs (1) N
        • blockchain (1) N
      • scalability (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    group metadata
    kafka ui
    Merkle Trie
    consumer offset
    acks
    비동기
    Kafka
    lazy fanout
    도메인 이벤트
    fanout-on-write
    fanout-on-read
    FanoutTask
    min.insync.replicas
    KafkaConfig
    kafkaListenerContainerFactory
    codex skill
    컨슈머 오프셋
    msa
    __consumer_offsets
    bccprm
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
cusum26
flutter - 플러터로 크로스 플랫폼 앱 개발하기(3)
상단으로

티스토리툴바