본문으로 건너뛰기
5분 AI.
GITHUB TRENDING DART MONTHLY #10 딥다이브 · 2026-10-23 분석

conduit 딥다이브
— 여러 AI를 잇는 모바일 앱의 내부 설계

모바일 앱

빠른 판단

무엇
Open WebUI·직접 모델 API·Hermes 에이전트를 공통 채팅 화면에 연결하는 iOS·Android 클라이언트
이럴 때
자체 AI 서버나 선택한 모델 API를 휴대폰에 연결해 대화·첨부·에이전트 작업을 한 앱에서 다룰 때
도입 전 확인
모델 실행 서버가 아니라 기존 서버·API에 접속하는 클라이언트다
내 Open WebUI 서버, OpenAI 호환 API, Ollama, Hermes 에이전트를 휴대전화에서 사용하는 Flutter 클라이언트입니다. 채팅 화면 뒤에는 연결 어댑터, 끊기는 네트워크를 견디는 저장·동기화, 답변을 부드럽게 보여 주는 스트리밍 처리가 있습니다. 이 글은 그 부품을 작은 단위로 읽으며 실제 제품의 경계를 설계하는 법을 공부합니다. 저장소 cogwheel0/conduit · ⭐2,361 · 포크 261 · Dart · GPL-3.0 · 2026-10-23 GitHub 기준.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

내가 고른 AI 서버를 휴대전화에 연결하는 독립 클라이언트

conduit는 iOS·Android용 AI 채팅 앱입니다. Open WebUI에 로그인해 서버 기능을 이용하거나, Direct 연결로 모델 API에 바로 요청하거나, Hermes 서버의 에이전트와 대화할 수 있습니다. 이 저장소에서 배울 핵심은 서로 다른 AI 서비스를 하나의 모바일 경험으로 묶는 방법입니다.

한 문장 비유

여러 방송국에 맞춰 쓸 수 있는, 내 손안의 라디오

모델 서버가 방송국이라면 conduit는 수신기입니다. 방송국을 바꾸어도 화면과 조작은 익숙하게 유지됩니다. 다만 방송국마다 제공하는 기능과 신호 형식이 달라, 내부에는 이를 해석하는 연결 부품이 필요합니다.

수신기에 최근 청취 목록이 남는다고 방송국까지 수신기 안에 들어 있는 것은 아닙니다. 마찬가지로 대화를 로컬에 저장하는 일과 모델을 로컬에서 실행하는 일은 별개입니다.

세 가지 기본 연결 경로

연결실제 상대여기서 공부할 것
Open WebUI사용자가 운영하거나 접근 권한을 가진 서버인증, 서버 권한, 대화 동기화, HTTP·소켓 전환
DirectOpenAI 호환 API, Ollama, OpenRouter 등공통 인터페이스와 제공자별 프로토콜 변환
Hermes사용자가 연결한 에이전트 서버실행 이벤트, 도구 승인, 일정과 서버 기능 확인

README에는 Apple On-Device와 Apple Private Cloud Compute(PCC)도 Direct 제공자로 소개되어 있습니다. 일반 iOS 설치 조건과 별도로 OS·기기·권한 조건이 붙습니다. 따라서 모든 iPhone에서 오프라인 모델이나 PCC를 쓸 수 있다고 읽으면 안 됩니다.

용어 풀이
클라이언트와 백엔드
클라이언트는 사용자의 입력을 받아 서버에 요청하고 결과를 보여 주는 쪽입니다. 백엔드는 모델 실행, 서버 기록, 계정·권한 등을 처리하는 상대입니다. conduit의 핵심 코드는 클라이언트에 있고, Open WebUI·Ollama·Hermes 자체의 서버 구현은 이 앱의 실행 구성 요소가 아닙니다.

한 번의 질문에 일어나는 일

  1. 선택한 모델과 대화의 저장 위치를 확인합니다.
  2. 기존 메시지·첨부·생성 설정으로 요청을 구성합니다.
  3. 해당 연결의 전송 방식을 통해 답변 조각이나 실행 이벤트를 받습니다.
  4. 공통 상태를 갱신하고 Markdown 등으로 화면에 표시합니다.
  5. 저장 정책에 따라 로컬 기록을 보존하고 필요한 서버 동기화를 진행합니다.

이 순서는 이해를 위한 요약입니다. 구현에는 임시 대화, 여러 모델 비교, 중단·재개, 첨부 업로드와 계정 전환 같은 분기가 더 있습니다. 처음부터 모든 분기를 읽기보다 DirectProviderAdapter처럼 작은 경계부터 시작하는 편이 좋습니다.

2왜 주목받는가

트렌딩 순위와 제품의 유용성을 구분해서 읽기

2026-10-23 GitHub Trending Dart Monthly #10에 선정되었고, 같은 날 스타는 2,361개입니다. 순위만으로 유입 원인을 확정할 수는 없습니다. 아래는 README와 코드에서 드러나는 관심을 끌 만한 제품적 이유에 대한 해석입니다.

2-1. 셀프호스팅의 불편이 휴대전화에 모인다

데스크톱에서 잘 쓰던 AI 서버라도 모바일에서는 키보드, 사진 첨부, 공유 메뉴, 화면 잠금, 프록시 인증이 문제가 됩니다. README는 이러한 모바일 사용의 빈틈을 출발점으로 설명합니다. 실제로 lib/platform/에는 공유 수신, 홈 위젯, 앱 생명주기, 백그라운드 실행을 담당하는 구현이 따로 있습니다.

모델을 더 똑똑하게 만드는 대신, 이미 있는 모델을 매일 쓰기 편하게 만드는 데 집중한 프로젝트입니다.

2-2. 한 종류의 서버에만 묶이지 않는 연결

Direct의 OpenAiCompatibleAdapter와 OllamaAdapter는 같은 인터페이스를 구현합니다. Open WebUI 계정 없이도 Direct 연결을 구성할 수 있다는 README의 설명과 맞닿아 있습니다. 다만 노트·채널·터미널 같은 서버 기능까지 모든 연결에 똑같이 생기는 것은 아닙니다.

접근 방식강점conduit가 더하는 것 / 선택의 대가
브라우저의 Open WebUI서버의 웹 UI를 바로 사용conduit는 공유·위젯·플랫폼 UI를 앱에 연결함. 대신 서버 API 변화도 따라가야 함
특정 서비스 전용 앱그 서비스에 맞춘 일관된 기능여러 서버·제공자를 고를 수 있음. 대신 제공자별 기능 차이를 이해해야 함
직접 만든 API 데모요청과 응답 구조를 빨리 실험저장, 재시도, 계정 분리, 렌더링까지 실제 앱의 주변 문제를 배울 수 있음

이 표는 설계 방향의 비교입니다. 응답 속도·배터리·장애율의 측정 결과나 개별 경쟁 앱의 기능 순위는 아닙니다.

2-3. “로컬 우선”을 데이터 구조로 구현한다

packages/conduit_core/lib/database/에는 대화·메시지·노트·동기화 작업을 저장하는 테이블이 있습니다. packages/conduit_core/lib/sync/outbox_drainer.dart는 보류한 작업을 다시 처리합니다. 네트워크 요청 함수 하나를 넘어, 이동 중 연결이 불안정해지는 상황을 데이터 모델에 반영한 점이 좋은 학습 재료입니다.

학습 관점
기능의 수보다 기능 사이의 경계를 보자

인증, 스트리밍, DB, UI가 각각 존재한다는 사실보다, 이들 사이에 어떤 값을 넘기고 누가 수명을 책임지는지에 주목하세요. 작은 채팅 데모가 제품으로 커질 때 필요한 사고가 그 경계에 드러납니다.

3기술 스택 전체 지도

선언된 버전 범위와 실제 잠금 버전을 함께 읽기

루트 pubspec.yaml의 앱 버전은 4.2.1+152입니다. Dart SDK 조건은 >=3.13.0 <4.0.0, Flutter 조건은 >=3.47.0입니다. 아래의 “선언”은 pubspec.yaml, “잠금”은 pubspec.lock 기준입니다.

용어 풀이
버전 범위와 잠금 파일
^5.9.0은 호환 범위의 업데이트를 허용하는 선언입니다. 실제로 선택된 버전은 잠금 파일에 기록됩니다. 이 저장소의 Dio는 선언이 ^5.9.0이고 잠금은 5.11.1입니다. “무엇을 허용했는가”와 “무엇으로 묶였는가”를 나누어 봅니다.

3-1. 프론트엔드: Flutter 화면과 상태 관리

역할선언 → 잠금코드에서 만나는 곳
상태 관리flutter_riverpod ^3.4.3 → 3.4.3
riverpod ^3.4.3 → 3.4.3
화면과 순수 Dart 로직이 같은 제공자 그래프를 사용
화면 이동go_router ^18.0.1 → 18.0.1lib/core/router/app_router.dart
Markdownmarkdown ^7.3.0 → 7.3.1공통 파서와 Flutter 렌더러를 분리
문서·미디어pdfrx ^2.4.7, flutter_tex ^5.2.6 — 선언값PDF·수식 등을 채팅 내용에 표시
플랫폼 저장소flutter_secure_storage ^11.1.1 → 11.1.1키·토큰 저장 기능을 플랫폼 어댑터로 제공

material_ui ^1.0.0, cupertino_ui ^1.0.0도 선언되어 있습니다. Material·Cupertino와 네이티브 브리지를 사용하는 앱이지, 웹사이트 전체를 WebView 하나에 넣은 구조는 아닙니다. 다만 Chart.js 등 일부 콘텐츠에는 WebView가 사용됩니다.

3-2. 백엔드와 맞닿는 클라이언트 로직

packages/conduit_core는 Flutter에 의존하지 않는 Dart 패키지입니다. 인증, API, 채팅 상태, DB, 동기화, Direct 연결을 담당합니다. 이름에 core가 붙었다고 서버가 되는 것은 아닙니다. 화면과 분리된 로직도 휴대전화 안에서 실행되는 클라이언트 코드일 수 있습니다.

역할선언 → 잠금실제 책임
HTTPdio ^5.9.0 → 5.11.1요청, 인증 인터셉터, 스트림 응답, 취소
모델 APIopenai_dart ^8.1.0 → 8.1.0
ollama_dart ^2.4.0 → 2.6.1
제공자 모델·형식 지원. 어댑터에는 직접 처리하는 전송·파싱도 존재
소켓socket_io_client ^3.1.2 → 3.1.6Open WebUI 이벤트 수신
도구 연결mcp_dart ^2.4.0 → 2.4.2Direct 모델의 MCP 도구 연결 관련 구현
관계형 저장drift ^2.34.2 → 2.35.0
drift_flutter ^0.3.1 → 0.3.1
SQLite 스키마·질의와 Flutter의 파일 열기 분리
설정·이전 저장층hive_ce ^2.14.0 → 2.20.0
shared_preferences ^2.3.2 → 2.5.5
설정과 기존 데이터 이전 관련 경로. 대화 저장 전체를 Hive라고 설명하면 부정확

3-3. 공통 패키지: 재사용의 단위를 따로 만든다

패키지역할넘지 않아야 할 경계
conduit_core인증·전송·DB·동기화·채팅 상태Flutter 플러그인을 직접 붙이지 않고 포트를 이용
conduit_markdown전처리, 인용·추론·도구 호출 파싱브라우저용 JS로도 컴파일 가능한 순수 Dart
conduit_theme팔레트와 테마 레지스트리화면 프레임워크와 색상 데이터를 분리
conduit_ddgs공개 검색엔진에 대한 기기 측 검색검색이 로컬에서 시작되어도 외부 엔진에 질의가 전송됨

네 패키지의 선언 버전은 모두 0.1.0이고 publish_to: 'none', resolution: workspace를 사용합니다. 루트의 workspace: 목록과 하나의 잠금 파일로 의존성을 함께 해석합니다.

3-4. 인프라·빌드: 서버 배포보다 모바일 배포가 중심

항목소스의 설정값학습 포인트
AndroidAGP 9.2.1 · Kotlin 플러그인 2.4.0 · Gradle 9.4.1 · compileSdk 37android/settings.gradle.kts, Gradle wrapper, 앱 빌드 설정을 함께 읽기
Android 네이티브Java 17 · NDK 29.0.14206865 · ARM ABI기본 비분할 빌드는 armeabi-v7a·arm64-v8a로 제한
iOS배포 대상 16.0 · Swift Package Manager 활성화Xcode 프로젝트 설정과 Flutter 설정을 함께 확인
코드 생성build_runner ^2.15.1 → 2.16.1
freezed ^4.0.1 → 4.0.1
모델·직렬화·DB·제공자 코드를 생성
제공자 생성riverpod_generator ^4.0.9 → 4.0.9선언을 실제 제공자 코드로 연결
호환성 예외record: 7.1.1, objective_c: 9.6.0 강제 지정루트의 dependency_overrides와 사유 주석을 함께 읽기

.github/workflows/ci.yml은 Flutter stable을 사용하며 SDK 버전을 고정하지 않습니다. 모바일 분석·테스트, Android 네이티브 테스트, 패키지 검사, 웹 안전성, 번역 검사가 나뉩니다. 모바일 분석은 --no-fatal-warnings --no-fatal-infos이고 공통 패키지는 --fatal-infos입니다. “CI가 있다”에서 멈추지 말고 어떤 진단이 실제 실패 조건인지 읽어야 합니다.

.github/workflows/release.yml은 v* 태그에서 릴리스 노트를 검증하고 Android APK·AAB와 iOS 빌드를 처리합니다. 모델을 올리는 Docker 배포가 이 저장소의 주된 배포 단위는 아닙니다.

문서와 설정의 차이
빌드 안내의 숫자를 설정 파일로 교차 확인하세요

docs/BUILDING.md에는 AGP 9.1.0, Gradle 9.3.1, Android SDK 36이 적혀 있습니다. 커밋 3f95de1의 실제 설정은 각각 9.2.1, 9.4.1, compileSdk 37입니다. 또한 안내의 디렉토리 설명 중 lib/core/database 등은 공통 패키지로 이동한 현재 위치와 다릅니다.

4아키텍처 심화 분석

네 가지 설계 패턴으로 연결·상태·저장의 흐름 따라가기

전체 구조: 코드의 경계와 데이터의 이동

[휴대전화: conduit] Flutter 화면: lib/features/ + lib/shared/ | v Riverpod 상태 / 채팅 흐름 packages/conduit_core/lib/ | | | v v v Open WebUI API Direct 어댑터 레지스트리 Hermes 연결 HTTP / Socket.IO OpenAI 호환 / Ollama 등 실행·승인 이벤트 | | | +-----------------+---------------------+ | 사용자가 선택한 연결 =========================|========================== 네트워크 경계 v Open WebUI / 모델 제공자 / Ollama / Hermes [휴대전화 내부의 별도 경계] core -- 포트 --> lib/platform/ --> OS·Flutter 플러그인 | +-- Drift --> SQLite: 대화·메시지·노트·Outbox +-- Markdown 파싱 --> Flutter 콘텐츠 렌더러 +-- 보안 저장 포트 --> Keychain / Android 보안 저장소

도식의 연결은 선택 가능한 경로입니다. 모든 메시지가 Open WebUI를 거쳐 Direct 제공자로 가는 구조가 아닙니다. 또 추론 요청의 목적지와 대화 기록의 동기화 목적지는 서로 다를 수 있습니다.

4-1. 포트와 어댑터: “무엇이 필요한가”와 “어떻게 하는가”

packages/conduit_core/lib/ports/에는 플랫폼 기능의 계약이 있습니다. 예를 들어 DatabaseOpenerPort는 DB 실행기를 여는 역할을 맡고, SecureKeyValueStore는 키·값 읽기와 쓰기 역할을 정의합니다. Flutter 구현은 lib/platform/flutter_database_opener.dart와 lib/platform/flutter_secure_key_value_store.dart에 놓입니다.

lib/main.dart의 main()은 ProviderContainer를 만들 때 실제 구현을 주입합니다. 다음은 그중 DB와 갱신 스케줄러 부분의 발췌입니다.

// lib/main.dart의 ProviderContainer 설정 중 발췌
final providerContainer = ProviderContainer(
  overrides: [
    databaseOpenerProvider.overrideWithValue(
      const FlutterDatabaseOpener(),
    ),
    flushSchedulerProvider.overrideWithValue(
      const FlutterFlushScheduler(),
    ),
  ],
);

로직은 “DB를 열 수 있는 대상”을 요구할 뿐, 앱 문서 디렉토리를 얻는 Flutter 플러그인을 직접 알 필요가 없습니다. 실제 FlutterDatabaseOpener.open()이 driftDatabase()와 getApplicationSupportDirectory를 연결합니다. 테스트에서는 이 계약을 가짜 구현으로 바꿀 수 있습니다.

비유

전자제품이 콘센트 규격만 알면 되는 것과 같습니다. 발전소의 종류까지 제품 안에 박아 넣지 않습니다. 포트는 콘센트 규격이고, 플랫폼 어댑터는 실제 전원을 연결하는 부품입니다.

패키지 주석에는 conduitd나 데스크톱 호스트의 재사용 의도도 등장합니다. 그러나 이 커밋의 루트에는 해당 앱 디렉토리가 없습니다. 확장을 위한 계약이 있다는 사실과 완성된 데스크톱 제품이 있다는 주장은 구분해야 합니다.

4-2. 전략 패턴과 레지스트리: 연결은 달라도 호출 모양은 같다

packages/conduit_core/lib/features/direct_connections/services/direct_provider_adapter.dart의 계약은 작습니다. 아래 코드는 메서드 선언을 그대로 옮기고 주석을 줄인 것입니다.

abstract interface class DirectProviderAdapter {
  String get key;

  Future<DirectConnectionProbe> probe(
    DirectConnectionProfile profile,
  );

  Future<List<DirectRemoteModel>> listModels(
    DirectConnectionProfile profile,
  );

  DirectCompletionRun startCompletion(
    DirectConnectionProfile profile,
    DirectCompletionRequest request,
  );
}

OpenAiCompatibleAdapter와 OllamaAdapter는 이 계약을 구현합니다. DirectProviderAdapterRegistry.register()는 문자열 키로 구현을 등록하며, 앞뒤 공백을 제거한 뒤 빈 키·내부 공백이 든 키·기본적으로 중복인 키를 거절합니다. require()는 등록되지 않은 키에 오류를 냅니다.

프로필은 구현 클래스 이름 대신 문자열 키를 저장합니다. 새 어댑터를 등록하는 확장은 프로필 스키마 변경을 줄여 줍니다. 여기서 전략 패턴은 “어떤 방식으로 모델과 통신할지 갈아 끼우는 것”, 레지스트리는 “그 구현을 키로 찾는 명부”입니다.

호출중요한 계약초보자가 놓치기 쉬운 점
probe()실제 연결 가능 여부를 제공자 방식으로 검증모델 ID를 수동 입력했어도 살아 있는 연결인지 확인해야 함
listModels()선택 가능한 모델 목록 제공directManualModels()는 수동 목록이 있으면 탐색 네트워크를 생략
startCompletion()공통 요청을 제공자 형식으로 바꾸고 실행 반환공통 계약이 모든 모델의 이미지·도구 지원을 보장하지는 않음

packages/conduit_core/lib/features/direct_connections/models/direct_completion.dart의 DirectCompletionRequest는 모델 ID, 메시지, 생성 매개변수를 모읍니다. DirectTextPart·DirectImagePart·DirectFilePart처럼 콘텐츠 종류를 타입으로 나누므로, 화면이 공급자마다 다른 JSON을 직접 만들 필요를 줄일 수 있습니다.

4-3. 생산자와 소비자 분리: 네트워크 속도와 화면 속도는 다르다

먼저 packages/conduit_core/lib/services/chat_completion_transport.dart의 ChatCompletionTransport는 httpStream, taskSocket, jsonCompletion을 구분합니다. ChatCompletionSession은 바이트 스트림, 작업 ID, JSON 결과, 중단 함수를 전송 모드에 맞게 담습니다.

이는 README의 “WebSocket 스트리밍”이라는 소개보다 넓은 구현입니다. Open WebUI 응답에 따라 HTTP 스트림이나 JSON 경로도 있으며 Direct에는 별도의 제공자 스트림이 있습니다. 따라서 conduit의 모든 답변이 WebSocket으로 온다고 설명하면 틀립니다.

첫 번째 경계: 네트워크 조각을 완성된 이벤트로

packages/conduit_core/lib/services/sse_frame_scanner.dart의 SseFrameScanner.addChunk()는 문자열 조각을 받아 빈 줄로 끝난 SSE 프레임을 내보냅니다. 여러 data: 줄은 줄바꿈으로 합치며, 조각 사이에서 끊긴 CRLF도 처리합니다. 네트워크 조각 하나와 JSON 하나가 일치한다고 가정하지 않습니다.

// 학습용 예시: 두 번 도착한 조각이 이벤트 하나가 된다
final scanner = SseFrameScanner();
final first = scanner.addChunk('data: {"x":').toList();
final second = scanner.addChunk('1}\n\n').toList();

// first.length == 0
// second.single.data == '{"x":1}'
// 실제 바이트의 UTF-8 해독은 이 스캐너에 들어오기 전의 책임

기본 한도는 한 줄과 한 프레임의 데이터 각각 4 * 1024 * 1024 UTF-16 코드 단위입니다. 바이트 한도라고 읽지 마세요. close()는 빈 줄로 끝나지 않은 마지막 이벤트를 버립니다. 잘린 JSON을 완성된 데이터처럼 넘기지 않기 위한 경계입니다.

두 번째 경계: 이벤트가 올 때마다 전체 화면을 다시 그리지 않기

packages/conduit_core/lib/features/chat/providers/chat_messages_notifier.dart의 _scheduleStreamingContentFrame()은 이미 프레임이 예약되었는지 확인합니다. _flushStreamingContentUpdate()는 버퍼 버전과 마지막으로 반영한 버전을 비교한 뒤 streamingContentProvider를 갱신합니다.

제공자 응답 조각: A B C D E \ | / \ / 버퍼에 모으기: ABC DE | | 화면 갱신 기회: [한 번 반영] [한 번 반영] 실제 구현에는 갱신 간격, 전경·배경 분기, 종료 시 반영도 포함된다.

FlutterFlushScheduler.scheduleFlush()는 SchedulerBinding.instance.scheduleFrameCallback()을 씁니다. 프레임 끝의 콜백이 아니라 프레임 시작에 갱신을 붙여 같은 프레임에서 화면 변경을 처리하려는 선택입니다. 백그라운드에서는 보이는 프레임을 기다리지 않고 반영하는 분기도 있습니다.

데이터를 빠짐없이 받는 일과 화면을 매번 갱신하는 일은 서로 다른 문제로 설계해야 합니다.

4-4. Outbox 패턴: 먼저 저장하고, 보낼 일을 남긴다

packages/conduit_core/lib/database/tables/outbox.dart의 OutboxOps는 단순 실패 로그가 아닙니다. 작업 종류, 대상 대화, payload, 상태, 시도 횟수, 다음 시각, 마지막 오류를 가진 앱 재시작 후에도 남는 작업 목록입니다. 상태 제약은 pending·inFlight·failed, 시도 횟수 제약은 attempts >= 0입니다.

사용자 입력 | v SQLite 트랜잭션 +-- 대화·메시지 저장 +-- 필요한 Outbox 작업 저장 | v OutboxDrainer -- 성공 --> 처리 완료 | +-- 재시도 가능 오류 --> 다음 시각 기록 --> 다시 처리 +-- 재시도 불가 오류 / 생성 실패 한도 --> 별도 실패 처리

packages/conduit_core/lib/database/daos/chats_dao.dart의 insertLocalChatWithCreateOp()와 appendMessagesWithUpdateOp()는 데이터 쓰기와 필요한 큐 기록을 같은 트랜잭션에 묶습니다. 둘 중 하나만 남으면 “화면에는 있는데 보낼 일이 없음” 또는 “보낼 일은 있는데 데이터가 없음”이 생기기 때문입니다.

packages/conduit_core/lib/sync/outbox_drainer.dart의 OutboxDrainer는 작업자 풀 크기 2를 사용합니다. requestCompletion은 실패 횟수가 5에 도달하면 실패 상태로 보류해 수동 재시도 대상으로 만듭니다. 오프라인이나 일시 보류(OutboxDeferralException)는 이 실패 횟수를 늘리지 않습니다. 순수 동기화 작업은 같은 횟수 제한을 일괄 적용하지 않습니다.

// packages/conduit_core/lib/sync/backoff.dart의 기본 설정과 재시도 창
baseMs = 2000
capMs  = 300000
window = min(capMs, baseMs * 2^attempt)
delay  = floor(window * jitter)

// attempt 0: 0 이상 2초 미만
// attempt 1: 0 이상 4초 미만
// 상한에 도달하면: 0 이상 5분 미만

위 식은 Dart 코드가 아닌 계산 설명입니다. Backoff.delayMsForAttempt()는 무작위 함수를 주입받아 테스트 가능하게 합니다. 실제 drainer는 지연을 초로 변환하고 최소 1초 뒤로 예약합니다. 지연이 0인 작업을 같은 처리 순환에서 계속 잡는 일을 피합니다.

packages/conduit_core/lib/database/chat_database_repository.dart의 persistNewDirectChat()와 persistDirectMessages()도 읽어 볼 만합니다. Direct 기록을 Open WebUI에 동기화하더라도 requestCompletion을 다시 넣지 않습니다. 이미 모델 제공자가 수행한 생성을 서버에서 중복 실행하지 않기 위해서입니다.

설계 읽기의 기준
추상화는 실패와 수명의 책임까지 정해야 한다

포트는 플랫폼 책임을, 어댑터는 프로토콜 책임을, 버퍼는 갱신 시점을, Outbox는 재시도할 일을 분리합니다. 이 네 경계를 이해하면 거대한 채팅 파일을 한 줄씩 처음부터 읽지 않아도 전체 설계에 접근할 수 있습니다.

5디렉토리 구조 해부

화면을 찾을 때와 로직을 찾을 때 출발점을 달리하기
conduit/ ├── pubspec.yaml / pubspec.lock 앱·워크스페이스·의존성 ├── lib/ │ ├── main.dart 플랫폼 기능 연결과 앱 시작 │ ├── core/ 앱 시작·라우팅·네이티브 연결 일부 │ ├── features/ 채팅·인증·노트 등 Flutter 화면 │ ├── platform/ OS·Flutter 플러그인 어댑터 │ ├── shared/ 공통 위젯·테마·Markdown 렌더러 │ └── l10n/ 14개 언어의 ARB 입력 ├── packages/ │ ├── conduit_core/lib/ │ │ ├── auth/ 세션·인증 인터셉터·프록시 │ │ ├── database/ Drift 테이블·DAO·FTS │ │ ├── features/ 채팅·Direct·Hermes 등 로직 │ │ ├── ports/ 플랫폼 기능 계약 │ │ ├── providers/ 공통 상태·의존성 제공 │ │ ├── services/ API·소켓·SSE·저장 서비스 │ │ └── sync/ 당겨오기·Outbox·재시도 │ ├── conduit_markdown/ 공통 파서와 전처리 │ ├── conduit_theme/ 팔레트 데이터 │ └── conduit_ddgs/ 기기 측 웹 검색 ├── test/ 앱·위젯·회귀 테스트 ├── android/ / ios/ 네이티브 코드·빌드 설정 ├── pigeons/ 플랫폼 통신 계약 ├── tool/ 코드 생성·구조·번역 검사 도구 ├── .github/workflows/ CI와 릴리스 ├── docs/ 빌드 안내와 화면 자료 ├── third_party/mermaid/ 렌더링용 서브모듈 ├── third_party/katex/ 수식 자산용 서브모듈 ├── openwebui-src/ API 참조용 서브모듈 └── hermes-src/ 에이전트 API 참조용 서브모듈

lib/features/와 packages/conduit_core/lib/features/에 비슷한 기능 이름이 반복됩니다. 중복 기능이라고 바로 판단하지 마세요. 앞쪽은 Flutter 표현을, 뒤쪽은 플랫폼과 분리된 상태·서비스를 찾는 출발점입니다.

궁금한 것먼저 열 파일찾아볼 식별자
연결 종류 확장packages/conduit_core/lib/features/direct_connections/services/direct_provider_adapter.dartDirectProviderAdapterRegistry
대화 저장 위치packages/conduit_core/lib/database/chat_database_repository.dartChatStorageKind, DirectChatSyncPreference
응답 조각 처리packages/conduit_core/lib/services/sse_frame_scanner.dartaddChunk(), close()
화면 갱신 시점lib/platform/flutter_flush_scheduler.dartscheduleFlush()
DB 업그레이드packages/conduit_core/lib/database/app_database.dartschemaVersion, migration

줄 수로 보는 학습 범위

범위Dart 파일 / 물리적 줄 수읽는 방법
앱 lib/447개 / 187,853줄화면·플랫폼 구현 중심
packages/conduit_core/lib/547개 / 197,701줄핵심 상태·전송·저장 중심
공통 작은 패키지Markdown 3,239줄 · 테마 610줄 · 검색 1,014줄한 주제를 작게 끝내며 읽기 좋음
전체 Dart1,705개 / 722,962줄도구·테스트·입력 계약 등 포함
이름이 _test.dart인 파일653개 / 324,963줄성공 경로보다 경계 조건을 먼저 찾기

커밋 3f95de1에서 Git이 추적하는 실제 파일의 물리적 행 수입니다. 마지막 개행이 없는 행도 한 줄로 셉니다. 빈 줄·주석을 포함하며 테스트도 포함합니다. 서브모듈 내부와 생성 시 생기는 파일은 제외합니다. Swift는 32개·29,655줄, Kotlin은 21개·12,060줄입니다. 줄 수가 곧 기능 수나 품질 점수는 아닙니다.

용어 풀이
서브모듈과 생성 코드
서브모듈은 다른 저장소의 특정 커밋을 가리키는 연결입니다. 경로만 있고 내용이 없을 수 있습니다. 생성 코드는 모델·테이블 같은 선언으로부터 도구가 만드는 파일입니다. 이 저장소의 Mermaid 경로 의존성은 서브모듈 내용이 필요하고, 여러 *.g.dart·*.freezed.dart는 생성 단계가 필요합니다.

.gitmodules와 docs/BUILDING.md를 함께 보면 역할 차이가 분명해집니다. Mermaid·KaTeX는 빌드에 필요한 자산이고, openwebui-src·hermes-src는 API 참조용입니다. 참조 저장소를 포함한다고 그 서버가 앱과 함께 실행되거나 배포되는 것은 아닙니다.

6학습 포인트 (기술별)

기술 이름을 외우기보다 하나의 문제를 해결하는 방식 배우기

6-1. Dart: 타입과 비동기를 함께 읽는다

DirectCompletionRequest는 메시지 목록과 매개변수를 수정 불가능한 컬렉션으로 보관하고, 빈 모델 ID를 거절합니다. “누구나 어떤 값이나 넣을 수 있는 Map”보다 잘못된 상태를 줄이는 모델링입니다.

Future<T>는 나중에 한 번 얻을 결과, Stream<T>은 여러 번 들어오는 값의 흐름입니다. 모델 목록은 전자에, 연속 답변 조각은 후자에 어울립니다. async*·yield·await for를 packages/conduit_core/lib/features/direct_connections/services/ollama_stream_parser.dart에서 함께 찾아보세요.

용어 풀이
불변 객체와 sealed 타입
불변 객체는 만든 뒤 내부 값을 함부로 바꾸지 못하게 한 값 묶음입니다. sealed class DirectContentPart는 텍스트·이미지 등 허용된 하위 타입을 한곳에서 정리합니다. “이 데이터는 어떤 경우가 가능한가”를 코드로 드러내는 방식입니다.

작은 연습: 텍스트와 이미지만 받는 메시지 모델을 설계해 보세요. 이미지 URL을 일반 문자열에 섞는 설계와 DirectImagePart로 분리한 설계를 비교하면 검증 위치가 달라집니다.

6-2. Riverpod: 상태 관리와 의존성 주입을 한 그래프로

ref.watch()는 의존하는 값의 변화를 구독하고, ref.read()는 해당 시점의 값을 읽는 데 사용됩니다. conduit에서는 화면 상태뿐 아니라 DB 열기·워커·생명주기 같은 기능도 제공자로 연결합니다. 관련 개념은 Riverpod 공식 시작 문서에서 확인할 수 있습니다.

lib/main.dart에서 “무엇을 어떤 구현으로 바꾸는가”를 표로 적어 보세요. databaseOpenerProvider와 flushSchedulerProvider 두 개만 따라가도 앱 시작과 테스트 대체의 관계가 보입니다.

작은 연습: 실제 프레임 대신 콜백 목록을 저장하는 가짜 FlushScheduler를 구상하세요. 콜백 실행 전에는 상태가 유지되고, 실행 뒤에만 갱신되는지 설명할 수 있으면 의존성 주입의 이점을 이해한 것입니다.

6-3. 네트워크: “HTTP 성공” 다음에도 계약이 있다

SSE는 빈 줄로 프레임을 나누고 Ollama의 전용 스트림은 줄 단위 JSON을 처리합니다. 두 방식 모두 네트워크의 수신 조각과 논리적 데이터의 경계가 다릅니다. SseFrameScanner와 parseOllamaNdjson()을 비교해 경계가 어디서 확정되는지 찾아보세요.

packages/conduit_core/lib/services/streaming_response_controller.dart의 StreamingResponseController.cancel()은 중복 취소를 막고 구독을 해제합니다. 반면 서버 작업 중단은 packages/conduit_core/lib/features/chat/services/chat_transport_dispatch.dart의 stopActiveTransport()가 전송 메타데이터를 보고 HTTP 취소와 작업 중단 경로를 선택합니다.

작은 연습: “화면이 더 이상 구독하지 않음”과 “서버가 생성을 멈춤”을 각각 화살표로 그려 보세요. 이 둘을 한 동작으로 가정하면 화면을 닫아도 서버 비용이 계속 생기는 상황을 놓칠 수 있습니다.

6-4. Drift·SQLite: 캐시를 넘어 상태의 근거로

AppDatabase.schemaVersion은 10입니다. 같은 파일의 설명 주석 일부에는 버전 8이 남아 있습니다. 실제 마이그레이션 분기는 9·10까지 이어지므로, 현재 동작을 읽을 때는 실행되는 getter와 분기를 우선 확인합니다.

packages/conduit_core/lib/database/fts/fts_query.dart의 toFtsMatchQuery()는 사용자의 검색어를 공백 단위로 나누고 따옴표로 감싼 접두 검색어로 바꿉니다. 검색 문자열을 SQL 매개변수로 넘기는 일과 FTS 검색 문법을 안전하게 구성하는 일은 별개입니다.

// toFtsMatchQuery()의 결과를 이해하는 예시
입력:  flutter stream
출력:  "flutter"* "stream"*

입력:  AND
출력:  "AND"*

// 빈 입력은 빈 문자열. 호출자는 검색을 생략해야 한다.

이것은 전용 한국어 형태소 분석기라는 뜻이 아닙니다. packages/conduit_core/lib/database/fts/fts_ddl.dart의 토크나이저는 unicode61 remove_diacritics 2입니다. 한글의 조사·띄어쓰기까지 기대한 대로 찾는지는 별도의 검색 사례로 검토해야 합니다.

작은 연습: 빈 검색어, 따옴표가 든 검색어, AND만 있는 검색어의 예상 결과를 적어 보세요. 관련 DB 개념은 Drift 공식 문서의 테이블·트랜잭션·마이그레이션 순서로 읽으면 좋습니다.

6-5. 인증: 저장 장소와 전송 범위를 따로 지킨다

packages/conduit_core/lib/auth/api_auth_interceptor.dart의 ApiAuthInterceptor.onRequest()는 대상이 선택한 서버와 같은 origin이거나 같은 호스트의 HTTP:80 → HTTPS:443 전환인지 확인하고, 이 조건을 벗어나면 서버 자격 증명을 제거합니다. 보안 저장소에 넣었다는 사실만으로 다른 서버에 잘못 전송하는 문제까지 해결되지는 않습니다.

DirectConnectionProfile.secureUpdate()는 origin이 바뀌고 새 비밀 값의 확인이 없으면 withoutSecrets()로 비밀 값을 지웁니다. DirectConnectionProfileStore.upsert()는 변경을 직렬화하며, expectedPrevious를 전달한 경우 저장된 값과 비교해 동시 편집 충돌을 감지합니다.

비유

열쇠를 금고에 보관하는 일과 올바른 문에만 사용하는 일은 다릅니다. 보안 저장소는 보관 문제를, origin 검사와 프로필 갱신 규칙은 사용 대상 문제를 다룹니다.

6-6. 렌더링·국제화·테스트: UI 바깥의 품질

lib/shared/widgets/markdown/markdown_compile_service.dart에는 동기 컴파일 임계값 384, 전처리 임계값 768(둘 다 문자열 길이의 UTF-16 코드 단위), 워커 유휴 정리 시간 30초가 있습니다. MarkdownCompileService.handleMemoryPressure()는 캐시를 비우고 유휴 워커 정리를 요청합니다. 이 수치를 모든 앱의 정답으로 외우기보다 작은 입력과 긴 입력을 다르게 다루는 이유를 배웁니다.

l10n.yaml은 영어 ARB를 템플릿으로 두고 한국어 ko를 포함한 14개 로케일을 나열합니다. lib/l10n/app_ko.arb는 번역 입력입니다. 생성된 Dart를 손으로 수정하는 대신 ARB와 자리표시자 계약을 관리합니다.

좋은 학습 단위는 파일 전체가 아니라 입력·출력·실패 조건을 함께 설명할 수 있는 작은 계약입니다.

작은 연습: packages/conduit_core/test/services/sse_frame_scanner_test.dart를 읽고 각 테스트를 “상황 → 기대 결과” 한 줄로 번역해 보세요. 테스트 개수나 줄 수만으로 커버리지가 충분하다고 단정하지는 않습니다.

7하드웨어 / 시스템 요구사항

앱 사용, 앱 빌드, 모델 실행의 자원을 구분하기

7-1. 앱을 쓰는 기기

대상확인되는 조건해석
iOS 기본 앱Xcode 프로젝트의 배포 대상 16.0Apple 모델 기능의 요구 OS와는 별도
Android 기본 앱빌드 안내는 Android 7.0+ / API 24실제 minSdk는 flutter.minSdkVersion에 위임. 최종 병합 설정은 SDK·플러그인 영향도 받음
CPU·RAM일반 앱의 최소 코어 수·RAM 수치가 명시되어 있지 않음긴 대화·문서·차트·음성 사용량에 따라 메모리 부담이 달라짐
모델 연결접근 가능한 Open WebUI·API·Ollama·Hermes 중 선택원격 추론일 때 휴대전화에 별도 추론용 GPU는 필요하지 않음
기기 권한마이크·카메라·사진·위치 등 기능별 권한권한을 거절하면 해당 기능의 대체 동작도 확인해야 함

7-2. Apple 모델은 별도 조건

README와 docs/BUILDING.md 기준으로 On-Device는 iOS 26과 Apple Intelligence 지원 기기가 필요하며, 개발에는 Xcode 26을 요구합니다. 로컬 모델 문맥 창은 4K로 설명되어 있고 이미지·추론 제어·도구 호출은 지원하지 않는다고 적혀 있습니다.

같은 문서에서 PCC는 iOS 27, Xcode 27, Apple Intelligence와 Apple의 관리형 PCC entitlement를 추가 조건으로 제시합니다. 오래된 SDK에서는 PCC를 컴파일에서 제외하면서 기본 배포 대상은 iOS 16으로 유지한다고 설명합니다. 이 조건은 저장소의 기능 안내이며 기기별 실제 사용 가능 여부나 권한 발급을 보장하지 않습니다.

7-3. 소스 빌드에 필요한 환경

환경필요한 것초보자에게 중요한 점
공통Flutter 3.47.0 이상 · Dart 3.13.0 이상서브모듈과 생성 코드가 빠지면 소스가 있어도 바로 빌드되지 않음
Android 개발Java 17 · compileSdk 37 · Gradle/AGP 도구 모음기본 ABI가 ARM이므로 안내도 ARM 에뮬레이터나 실기기를 권장
iOS 개발macOS · Xcode · 서명 설정안내의 시뮬레이터 설정은 Apple Silicon 대상으로 x86_64를 제외
저장 공간SDK·빌드 캐시·시뮬레이터·의존성 공간저장소의 소스 용량과 전체 개발 환경 용량은 다름. 공식 최소 GB 수치는 제시되지 않음

docs/BUILDING.md는 build_runner를 필수 단계로 설명합니다. Pigeon 생성기는 분석기 버전 범위가 달라 tool/pigeon_codegen/에 별도 도구 의존성을 둡니다. 누락된 생성 파일 오류를 실제 구현 버그로 오해하지 않는 것도 빌드 학습의 일부입니다.

7-4. 모델을 돌리는 서버와 네트워크

Direct Ollama 연결에서 모델은 보통 Ollama가 실행 중인 기기에서 돌아갑니다. 그 기기의 RAM·VRAM은 모델 크기, 양자화, 문맥 길이, 동시 요청 수에 좌우됩니다. conduit 자체의 요구사항으로 “GPU 몇 GB”를 일괄 지정할 근거는 없습니다.

휴대전화에서 localhost를 입력하면 개발 PC가 아니라 그 휴대전화 자신을 가리킵니다. 실기기에서 PC의 모델 서버를 사용하려면 접근 가능한 주소, 서버의 수신 설정, 방화벽과 네트워크 경로를 함께 확인해야 합니다.

Open WebUI 스트리밍 문제에 대해 빌드 안내는 서버의 ENABLE_WEBSOCKET_SUPPORT="true" 설정을 확인하도록 안내합니다. 프록시도 인증 쿠키·헤더와 지속 연결을 전달해야 합니다. 앱이 여러 전송 방식을 처리한다는 사실이 서버 설정 오류까지 없애 주지는 않습니다.

8직접 해볼 수 있는 실습 과제

별도의 연습 프로젝트에서 작은 계약 하나씩 재현하기

아래 과제는 실제 계정이나 유료 모델 없이도 설계하고 검증할 수 있습니다. 원본 코드와 테스트를 읽은 뒤, 네트워크 입력은 가짜 데이터로 대체해 보세요. 각 과제의 완료 기준을 충족한 다음 범위를 넓히면 좋습니다.

과제 1 · 입문

버전 경고 판단표 만들기 문자열·분기

출발점: packages/conduit_core/lib/utils/server_version_compat.dart의 ServerVersionCompat.isSupported()와 대응 테스트.

할 일: 0.12.3, v0.12.3, 0.12.4, 0.12.3-rc1, 빈 문자열, null을 표에 넣고 결과와 이유를 적으세요.

완료 기준: 앞의 두 값과 rc1은 지원, 0.12.4는 경고 대상, 빈 값과 null은 경고를 띄우지 않는 이유를 설명합니다. 모르는 버전에 관대한 정책과 실제 호환성 보장은 다릅니다.

과제 2 · 입문

SSE 경계 조건 카드 만들기 파서·테스트 읽기

출발점: packages/conduit_core/lib/services/sse_frame_scanner.dart와 packages/conduit_core/test/services/sse_frame_scanner_test.dart.

할 일: 정상 프레임, 두 조각으로 나뉜 프레임, CRLF 경계, 여러 data 줄, 빈 data 줄, 데이터 없는 event, 끝나지 않은 EOF의 기대 결과를 적으세요.

완료 기준: 명시적인 빈 data 이벤트는 하나의 프레임이지만, 데이터 없는 event와 빈 줄 없이 끝난 미완성 이벤트는 내보내지 않는 차이를 설명합니다.

// 과제 2의 판별 입력. 역슬래시 표기는 실제 제어 문자를 뜻한다.
A: 'data: hello\n\n'                // 1개, hello
B: 'event: ping\n\n'                // 0개
C: 'data:\n\n'                     // 1개, 빈 문자열
D: 'data: {"unfinished":' + EOF     // 0개
과제 3 · 중급

가짜 Direct 제공자 설계하기 인터페이스·대체 구현

출발점: DirectProviderAdapter, DirectCompletionRun, DirectProviderAdapterRegistry.

할 일: 고정 모델 하나를 반환하고, “안녕”·“하세요”를 순서대로 내보내는 가짜 어댑터를 독립적으로 구현하세요. 먼저 실제 실행 타입의 이벤트·취소 계약을 읽고 필요한 부분만 재현합니다.

완료 기준: 등록·조회가 가능하고 중복 키 등록은 실패하며 취소 뒤에는 새 이벤트가 나오지 않습니다. probe()와 listModels()의 역할을 각각 설명합니다.

과제 4 · 중급

재시도 시계 만들기 시간·무작위성 주입

출발점: packages/conduit_core/lib/sync/backoff.dart와 packages/conduit_core/test/sync/backoff_test.dart.

할 일: jitter: () => 0.5로 고정하고 시도 0, 1, 2, 8의 지연을 계산하세요. 실제 현재 시각 대신 조절 가능한 가짜 시각을 사용해 다음 실행 시각도 표시합니다.

완료 기준: 지연은 1,000·2,000·4,000·150,000ms입니다. jitter가 0일 때 계산값은 0이지만 drainer 예약은 최소 1초 뒤라는 계층 차이까지 설명합니다.

과제 5 · 고급

오프라인 대화 저장을 상태도로 설계하기 트랜잭션·복구·중복 방지

출발점: ChatDatabaseRepository.persistDirectMessages(), ChatsDao.insertLocalChatWithCreateOp(), OutboxDrainer와 관련 테스트.

할 일: 기기 전용 Direct 대화와 Open WebUI에 기록을 동기화하는 Direct 대화를 나눕니다. 저장 직전, 큐 기록 직전, 전송 직후에 앱이 종료되는 상황을 상태도로 그리세요.

완료 기준: 기기 전용 대화는 Open WebUI 큐를 만들지 않고, 기록 동기화는 모델 생성을 재요청하지 않아야 합니다. 서버 성공 뒤 앱 종료로 생기는 재전송 가능성과 로컬 ID가 서버 ID로 바뀌는 문제를 별도로 표시합니다.

실습의 목표는 기능을 많이 붙이는 것이 아니라 실패해도 지켜야 할 조건을 말과 코드로 표현하는 것입니다.

9관련 기술 심화 학습 로드맵

한 주에 개념 하나, 읽을 코드 하나, 남길 결과물 하나

아래는 기초 문법을 조금 아는 독자를 위한 6주 순서입니다. 시간은 개인차가 크므로 주차보다 완료 기준을 우선하세요. 첫 주부터 앱 전체를 빌드하는 것보다 순수 Dart 함수의 계약을 읽는 편이 범위를 잡기 쉽습니다.

주차배울 주제이 저장소에서 읽을 것남길 결과물
1주차Dart null 안전성·컬렉션·타입packages/conduit_core/lib/utils/server_version_compat.dart, packages/conduit_core/lib/features/direct_connections/models/direct_completion.dart버전 판단표와 메시지 타입 관계도
2주차Future·Stream·취소·프레이밍packages/conduit_core/lib/services/sse_frame_scanner.dart, packages/conduit_core/lib/features/direct_connections/services/ollama_stream_parser.dart입력 조각별 이벤트 결과표
3주차Flutter 위젯·Riverpod·의존성 주입lib/main.dart, packages/conduit_core/lib/ports/flush_scheduler.dart가짜 스케줄러로 설명하는 상태 갱신 흐름
4주차SQLite·Drift·트랜잭션·이전packages/conduit_core/lib/database/app_database.dart, packages/conduit_core/lib/database/tables/outbox.dart, packages/conduit_core/lib/database/daos/chats_dao.dart대화·메시지·작업 큐의 관계도
5주차재시도·소유권·계정 경계packages/conduit_core/lib/sync/backoff.dart, packages/conduit_core/lib/sync/outbox_drainer.dart, packages/conduit_core/lib/auth/api_auth_interceptor.dart실패·계정 전환 시나리오표
6주차렌더링·국제화·CIlib/shared/widgets/markdown/markdown_compile_service.dart, l10n.yaml, .github/workflows/ci.yml긴 답변·번역·검사 경계를 정리한 설계 메모

공식 문서와 코드를 왕복하는 순서

  1. Dart 비동기 문서로 한 번의 결과와 연속된 값의 차이를 익힌 뒤 SSE 스캐너를 읽습니다.
  2. Flutter 앱 아키텍처 문서에서 의존성 주입·테스트 가능성을 공부하고 플랫폼 포트와 비교합니다.
  3. Riverpod 시작 문서를 읽고 ProviderContainer의 overrides를 추적합니다.
  4. Drift 문서에서 트랜잭션을 익힌 뒤 데이터와 Outbox를 함께 쓰는 이유를 설명합니다.
  5. Ollama API 문서로 서버와 클라이언트의 책임을 확인한 뒤 전용 어댑터와 비교합니다.
다음 단계로 넘어갈 기준
설명할 수 있으면 작게 재현해 본다

포트·어댑터·Outbox라는 이름을 외우는 것보다 “이 경계를 없애면 어떤 오류가 생기는가”를 설명해 보세요. 답이 구체적이면 같은 문제를 작은 메모 앱이나 다운로드 앱에 옮겨 볼 준비가 된 것입니다.

10핵심 키워드 사전

이 글의 용어를 코드 속 역할로 다시 연결하기
용어쉬운 설명
FlutterDart로 화면과 앱을 만드는 도구 모음. conduit의 iOS·Android 화면을 담당합니다.
위젯화면을 구성하는 부품. 버튼·입력창부터 채팅 화면까지 작은 부품을 조합합니다.
Riverpod 제공자상태나 서비스를 필요로 하는 곳에 연결하는 통로. conduit에서는 플랫폼 기능도 연결합니다.
의존성 주입객체가 필요한 도구를 직접 고정해서 만들지 않고 외부에서 받게 하는 방식입니다.
포트로직이 필요로 하는 기능의 계약. 예를 들어 DB를 여는 기능입니다.
어댑터계약을 실제 환경에 맞게 구현하는 부품. Flutter 플러그인이나 모델 API의 차이를 흡수합니다.
전략 패턴같은 목표를 수행하는 여러 방법을 공통 인터페이스로 교체하는 설계입니다.
레지스트리이름으로 구현을 찾는 명부. Direct 연결 키로 제공자 어댑터를 찾습니다.
Future / Stream나중에 한 번 얻는 값 / 시간에 따라 여러 번 들어오는 값의 흐름입니다.
SSE서버가 HTTP 응답으로 이벤트를 이어 보내는 형식. 빈 줄이 이벤트 경계를 만듭니다.
NDJSON한 줄에 JSON 하나를 두는 형식. Ollama 스트림 파서가 줄 경계를 처리합니다.
WebSocket / Socket.IO지속 연결 방식 / 그 위에서 이벤트·재연결 등을 다루는 통신 계층. 같은 이름의 프로토콜은 아닙니다.
버퍼·일괄 반영들어온 조각을 잠시 모으고 한 번에 상태에 반영하는 것. 화면 갱신 부담을 줄입니다.
isolateDart에서 메모리를 분리해 계산을 수행하는 실행 단위. 긴 Markdown 처리의 워커에 사용됩니다.
Drift / DAODart의 관계형 저장 라이브러리 / 특정 데이터의 읽기·쓰기 동작을 모은 객체입니다.
트랜잭션여러 DB 변경이 함께 성공하거나 함께 취소되게 묶는 단위입니다.
Outbox나중에 서버로 보낼 일을 DB에 남긴 목록. 앱이 종료되어도 처리할 일을 복구할 수 있습니다.
백오프·지터재시도 간격을 늘리는 정책 / 요청이 동시에 몰리지 않도록 간격에 넣는 무작위성입니다.
로컬 우선로컬 저장을 사용 경험의 기반으로 삼는 방향. 모든 추론·동기화가 오프라인이라는 뜻은 아닙니다.
origin주소의 scheme·host·port 조합. 자격 증명을 어디까지 보낼지 판단하는 기준입니다.
보안 저장소키·토큰 등 민감한 값을 OS 기능으로 보관하는 곳. 대화 DB 전체의 암호화와는 구분합니다.
FTS5SQLite의 전문 검색 기능. 검색어 문법과 토크나이저가 결과에 영향을 줍니다.
마이그레이션앱 업데이트로 DB 구조가 바뀔 때 기존 데이터를 새 구조로 옮기는 절차입니다.
ARB / 로케일번역 문자열·설명을 담는 입력 형식 / 언어·지역 설정입니다.
서브모듈다른 Git 저장소의 특정 커밋을 가리키는 연결. 일반 파일 복사와 다릅니다.
MCP모델 애플리케이션이 외부 도구·자원에 연결할 때 쓰는 규약. 연결했다고 모든 실행이 자동 허용되는 것은 아닙니다.

11참고 링크

프로젝트 현황과 소스 근거를 분리해 확인하기

공식 저장소와 프로젝트 문서

핵심 소스 바로 가기 — 커밋 3f95de1 기준

관련 기술의 공식 문서

현재 상태 (2026-10-23 확인)

항목상태
선정GitHub Trending Dart Monthly #10
스타 / 포크2,361 / 261 — 2026-10-23 GitHub 기준
주 언어Dart
생성일2025-08-09
최신 릴리스v4.2.1 — 2026-10-16
앱 선언 버전4.2.1+152 — pubspec.yaml
최근 push2026-10-18 — GitHub 기준. 개별 커밋 날짜와는 다른 시각
분석 기준 커밋3f95de1 — 2026-10-17
Open WebUI 0.12.3 서버에서 버전 경고를 표시하지 않도록 지원한 변경 (#850)
버전 경고 기준ServerVersionCompat.maxSupportedVersion = '0.12.3'
이를 넘는 해석 가능한 버전에 경고. 알 수 없는 버전은 경고하지 않음
코드 규모Dart 722,962줄 / 1,705파일 · Swift 29,655줄 · Kotlin 12,060줄
테스트 규모*_test.dart 653파일 / 324,963줄 — 전체 Dart 줄 수에 포함
규모 집계 범위Git 추적 파일의 물리적 줄 수. 빈 줄·주석 포함, 서브모듈 내부·미생성 파일 제외
라이선스GPL-3.0 — 배포·수정·재사용 조건은 LICENSE 원문 확인
한국어 지원lib/l10n/app_ko.arb와 ko 로케일 설정 존재. 전체 번역 품질을 보증하는 수치는 아님
쓰기 전에 알아둘 한계
연결·저장·호환성의 조건을 확인하세요

① 모든 연결의 기능이 같지 않습니다. Open WebUI의 노트·채널·터미널과 Direct 모델의 채팅 기능은 다릅니다. Hermes도 서버가 보고하는 기능에 좌우됩니다. Apple 모델은 별도 OS·기기 조건이 있고 PCC에는 추가 entitlement가 필요합니다.

② 로컬 저장은 외부 전송이 없다는 뜻이 아닙니다. 원격 추론에 쓰는 질문은 선택한 서버나 모델 제공자에게 갑니다. Direct 기록은 설정에 따라 Open WebUI에 동기화될 수 있으며, 기기 측 웹 검색도 질의를 검색엔진에 보냅니다. 기존 대화와 새 대화에 적용되는 기록 위치 정책도 구분해야 합니다.

③ 보안 저장소와 대화 DB는 다릅니다. 키·토큰의 보안 저장 경로는 존재하지만, FlutterDatabaseOpener의 일반 SQLite 연결을 대화 전체의 기본 암호화로 해석하면 안 됩니다. AppDatabase는 SQLCipher를 나중에 도입할 수 있는 경계라고 설명합니다.

④ 문서와 실행 설정이 함께 움직이지 않을 수 있습니다. README의 Open WebUI 0.11 표기와 달리 이 커밋의 경고 상한은 0.12.3입니다. Android 도구 버전과 DB 설명 주석에도 차이가 있습니다. 코드상의 경고 정책은 모든 구버전 서버·프록시 조합의 동작을 보장하지 않습니다.

⑤ 이 글의 근거는 소스와 설정입니다. 실제 기기에서 측정한 배터리·메모리·응답 속도나 장애 복구 성공률을 제시하는 성능 검증 자료는 아닙니다. 스토어 배포 바이너리와 분석 커밋도 구분해서 보세요.