파일 전송 앱
6e37efd 기준입니다.LocalSend는 같은 로컬 네트워크의 기기끼리 파일과 메시지를 주고받는 무료 오픈소스 앱입니다. 일반적인 로컬 전송에는 인터넷 연결이나 중앙 파일 보관 서버가 필요하지 않습니다. Windows 노트북과 Android 휴대폰처럼 운영체제가 다른 기기를 연결하는 것이 대표적인 쓰임새입니다.
각 기기가 보내는 클라이언트이면서 받는 서버이기도 하므로, 파일은 상대 기기로 직접 이동합니다. 여기서 서버는 거대한 데이터센터가 아니라 앱 안에서 요청을 기다리는 작은 프로그램을 뜻합니다. 받는 기기에 별도 서버 제품을 설치하는 구조가 아닙니다.
먼저 복도에 “저 여기 있어요”라고 알립니다. 이웃이 자기 호수를 알려주면, 그 문을 두드려 물건 목록을 보여줍니다. 상대가 받겠다고 답한 물건만 건넵니다.
복도의 알림이 기기 발견, 문을 두드리는 것이 HTTP 요청, 수령 동의가 승인 단계입니다. 실제 물건인 파일 내용은 동의 뒤에 흐릅니다. 이 네 단계를 구분하면 코드가 훨씬 덜 복잡하게 보입니다.
| 사용자 행동 | 내부 책임 | 읽기 시작할 곳 |
|---|---|---|
| 주변 기기 확인 | UDP 알림과 HTTP 확인으로 상대 찾기 | packages/core/src/discovery/mod.rs |
| 파일 선택 | 파일 이름·크기 등 메타데이터 준비 | app/lib/provider/selection/ |
| 수신 승인 | 사용자가 고른 파일만 전송 허용 | app/lib/provider/network/server/controller/receive_controller.dart |
| 진행률 확인 | 바이트를 저장하며 상태를 화면에 전달 | packages/core/src/http/server/common/save.rs |
앱을 서로 연결하면 상대가 늦게 답하거나, 사용자가 취소하거나, 운영체제가 소켓을 회수할 수 있습니다. LocalSend에서는 이런 상황을 UI 상태, 메시지, 세션, 자원 정리로 나눠 다루는 법을 배울 수 있습니다.
GitHub Trending Dart Weekly #11이라는 순위와 95,312개의 스타는 관심의 크기를 보여줍니다. 특정 게시물이나 업데이트가 이번 주 관심을 일으켰다는 인과관계까지 보여주는 수치는 아닙니다. 프로젝트의 매력은 파일 하나를 옮기려고 계정·클라우드 업로드·공유 링크를 준비하는 수고를 줄인다는 제품의 효용으로 해석할 수 있습니다.
README는 Windows, macOS, Linux, Android, iOS, Fire OS 배포 경로를 안내합니다. 소스에도 app/android/, app/ios/, app/macos/, app/windows/, app/linux/가 있습니다. “크로스플랫폼”은 소개 문구에 머물지 않고 권한·창·파일 선택을 처리하는 플랫폼 코드로 이어집니다.
| 비교 대상 | 잘 맞는 상황 | LocalSend의 장점과 대가 |
|---|---|---|
| AirDrop | Apple 기기 사이의 시스템 공유 | 서로 다른 OS를 연결하는 공통 앱을 제공. 대신 앱 설치와 로컬 네트워크 설정이 필요 |
| 메일 첨부 | 멀리 있는 사람에게 비동기로 전달 | 근처 기기에 서버 업로드 없이 직접 전달. 상대가 함께 연결돼 있어야 함 |
| 클라우드 링크 | 나중에 받을 파일을 서버에 보관 | 일반 로컬 전송에서 외부 파일 보관을 생략. 장기 보관·동기화 서비스와는 목적이 다름 |
| USB 케이블 | 물리적으로 연결해 데이터 이동 | 케이블 없이 공유 가능. 실제 속도는 무선 환경·디스크·암호화 비용의 영향을 받음 |
이 표는 전달 방식의 차이를 비교한 것입니다. 경쟁 제품의 모든 기능이나 전송 성능을 평가한 순위가 아닙니다. LocalSend도 게스트 Wi-Fi의 기기 격리나 방화벽에 막히면 바로 연결되지 않을 수 있습니다.
이 저장소의 학습 가치는 예쁜 공유 화면과 실제 네트워크 실패 처리가 한 흐름으로 연결된다는 데 있습니다. Flutter만 읽어도 상태 관리와 반응형 UI를 배우고, 더 내려가면 Rust의 비동기 I/O, TLS, 스트림, 취소 안전성을 만납니다.
cli/Cargo.toml의 localsend-cli는 같은 Rust 코어를 사용합니다. 화면이 달라도 통신 규칙을 재사용한다는 사실이 구조로 드러납니다. 공용 라이브러리를 분리하는 이유를 “중복을 줄인다”보다 구체적으로 이해하기 좋은 예입니다.
최근 커밋 6e37efd의 제목은 fix(webrtc): less blocking implementation (#3625)입니다. 하지만 app/lib/provider/network/webrtc/signaling_provider.dart에는 const webRTCEnabled = false;가 있습니다. app/lib/config/init.dart의 postInit()도 이 값이 참일 때만 시그널링 연결을 시작합니다.
WebRTC와 별도 서버 구현을 근거로 “현재 앱은 인터넷 어디서나 파일을 보낸다”고 설명하면 안 됩니다. 이 커밋의 기본 동작을 이해할 때는 로컬 HTTP 전송을 중심으로 읽고, WebRTC는 비활성 경로의 별도 학습 대상으로 다루는 것이 정확합니다.
| 항목 | 설정값 | 근거와 읽는 법 |
|---|---|---|
| Flutter | 3.41.9 | .fvmrc와 CI 고정값. 앱의 environment.flutter는 ^3.41.0 |
| Dart SDK | ^3.11.0 | 루트와 앱의 pubspec.yaml에 선언된 허용 범위 |
| 앱 버전 | 1.18.2+64 | app/pubspec.yaml. 배포 버전과 빌드 번호를 함께 표현 |
| Rust | 1.97.1 | rust-toolchain.toml과 isolate 패키지의 도구 설정에 고정 |
| Rust edition | 코어·서버 2021 / CLI·브리지 2024 | 각 Cargo.toml. edition은 컴파일러 버전 번호와 별개 |
| 워크스페이스 | Dart 3개 / Rust 4개 멤버 | 루트 pubspec.yaml과 Cargo.toml |
pubspec.yaml·Cargo.toml은 허용할 버전을 적고, 잠금 파일은 실제 선택한 버전을 기록합니다. Cargo의 axum = "0.8.1"은 정확히 0.8.1만 쓰겠다는 뜻이 아닙니다. 이 커밋의 Cargo.lock에서는 0.8.9가 선택돼 있습니다.| 기술 | 실제 버전 | 이 저장소에서의 역할 |
|---|---|---|
| Refena | refena_flutter 3.5.0 | 설정·기기 목록·전송 상태를 provider와 action으로 관리. Riverpod로 혼동하지 않기 |
| 모델 변환 | dart_mappable 4.8.0 | @MappableClass() 모델과 변환·복사 코드 생성 |
| 유니언 모델 | freezed_annotation 3.1.0freezed 3.2.5 | 여러 형태의 값을 다루는 모델과 생성 코드 |
| 다국어 | slang 4.19.0slang_flutter 4.19.0 | 번역 JSON에서 Dart 접근 코드 생성 |
| 파일·설정 | file_picker 11.0.3shared_preferences 2.5.5 | 파일 선택과 사용자 설정의 영속화 |
| 데스크톱 통합 | window_manager 0.5.2tray_manager 0.5.3 | 창 제어와 트레이 메뉴 |
| 라우팅·생성 | routerino 0.8.1build_runner 2.15.1 | 화면 이동과 모델·리소스 생성 작업 |
위 값은 app/pubspec.yaml의 선언값입니다. Refena, Slang, dart_mappable, file_picker는 pubspec.lock에서도 같은 버전을 확인할 수 있습니다. 앱은 로컬 경로 의존성 localsend_isolates를 통해 네트워크 계층을 사용합니다.
| 기술 | 선언값 → 잠금 버전 | 담당 기능 |
|---|---|---|
| Tokio | 1.53.1 → 1.53.1 | 비동기 실행, 소켓, 채널, 작업 실행 |
| Hyper | 1.11.0 → 1.11.0 | 기기 안에서 HTTP 요청 수신 |
| Reqwest | 0.13.4 → 0.13.4 | 다른 기기에 HTTP 요청·파일 전송 |
| rustls | 0.23.43 → 0.23.43 | TLS 암호화와 인증서 검증 |
| Serde | 1.0.229 → 1.0.229 | 기기 정보·전송 요청의 JSON 변환 |
| socket2 | 0.6.5 선언 | 인터페이스별 멀티캐스트 소켓 설정 |
| WebRTC | 0.14.0 → 0.14.0 | 별도 P2P 통신 경로. 기본 앱에서는 비활성 |
근거는 packages/core/Cargo.toml과 루트 Cargo.lock입니다. 일반 파일 전송 서버는 Axum이 아니라 Hyper 기반입니다. packages/core/src/http/server/mod.rs의 handle_request_inner()에서 메서드와 경로를 직접 분기합니다.
packages/localsend_isolates/pubspec.yaml은 flutter_rust_bridge: ^2.12.0을 선언하고, Rust 플러그인 크레이트는 flutter_rust_bridge = "=2.12.0"으로 정확한 버전을 지정합니다. 양쪽 잠금 파일도 2.12.0입니다. 이 다리가 Dart의 요청과 Rust의 결과를 연결합니다.
| 별도 실행 대상 | 주요 의존성 | 범위 |
|---|---|---|
| 터미널 클라이언트 | clap 4.6 → 4.6.4ratatui 0.30 → 0.30.2 | cli/. 같은 Rust 코어 위에 명령과 TUI 구현 |
| 시그널링 서버 | axum 0.8.1 → 0.8.9 | server/. WebSocket /v1/ws에서 연결 협상 메시지 처리 |
server/src/main.rs의 기본 바인딩은 SERVER_IP=0.0.0.0, SERVER_PORT=3000입니다. 이 서버는 로컬 파일 전송 때마다 파일을 맡기는 중앙 저장소가 아닙니다. 기본 앱에서 꺼져 있는 WebRTC 경로와 관련된 별도 프로그램입니다.
| 설정 | 실제로 하는 일 | 배울 점 |
|---|---|---|
.github/workflows/ci.yml | Dart 포맷·분석·테스트, Rust Clippy·코어 테스트·CLI/플러그인 검사 | 언어가 달라도 한 변경을 함께 검증 |
| 패키징 검사 | 앱·CLI·Inno Setup·MSIX·AppImage 버전 비교 | 배포물 사이의 버전 불일치도 자동 검출 |
| Rust feature | 코어는 default = [], CI 코어 검사에는 --features full | 기능 플래그가 켜진 범위까지 확인 |
| Dockerfile | server/Dockerfile: rust:1.97 빌드 → debian:bookworm 실행 이미지, 3000번 포트 | 컨테이너 대상은 Flutter 앱이 아닌 시그널링 서버 |
| 플랫폼 빌드 | Android APK, Linux DEB/RPM/AppImage, Windows ZIP/EXE, CLI 등의 워크플로 | 공통 코드와 플랫폼별 배포는 서로 다른 책임 |
| FOSS 변형 | remove_proprietary_dependencies.sh가 표시된 구매 의존성을 제거하고 후원 UI 코드를 주석 처리 | 배포 채널에 따라 구성이 달라질 수 있음 |
기술 스택을 읽을 때는 라이브러리 이름뿐 아니라 어느 실행 대상이 그 기술을 사용하는지까지 연결해야 합니다. Flutter 앱, 기기 내부 HTTP 서버, CLI, 외부 시그널링 서버를 모두 “백엔드”라는 한 칸에 넣으면 설계의 핵심이 사라집니다.
그림의 화살표는 책임 사이의 호출·메시지 흐름입니다. 파일 바이트가 매번 Flutter 화면을 통과한다는 뜻이 아닙니다. 받는 쪽은 Dart가 저장 대상을 결정하고, Rust가 실제 파일 내용을 기록하는 경로를 제공합니다.
| 초기화 순서 | 실제 함수 | 왜 먼저 필요한가 |
|---|---|---|
| 1. 기반 준비 | preInit() → initRustLib() | 화면에서 사용할 Rust 라이브러리·설정·번역 준비 |
| 2. 상태 주입 | RefenaContainer 생성 | 설정 저장소와 기기 정보를 provider에 전달 |
| 3. 작업자 준비 | IsolateSetupAction.reduce() | discovery, httpUpload, httpServer 자식 isolate 생성 |
| 4. 화면 연결 | main() → RefenaScope.withContainer() | 같은 상태 컨테이너를 위젯 트리에 연결 |
| 5. 통신 시작 | postInit() → startServerFromSettings() | 수신 서버와 기기 발견 리스너 시작 |
출발점은 app/lib/main.dart와 app/lib/config/init.dart입니다. 시작 코드만 읽어도 앱이 어떤 서비스에 기대는지 보입니다. 모바일 앱이 다시 활성화될 때 ensureRunning()을 호출하는 부분은 운영체제가 중지된 앱의 소켓을 회수하는 상황을 고려한 설계입니다.
packages/core/src/multicast/mod.rs의 기본값은 IPv4 그룹 224.0.0.167, 포트 53317입니다. MulticastHandle::announce()가 기기 정보를 알리면 이를 들은 기기는 HTTP register 요청으로 응답합니다. 파일을 UDP 멀티캐스트로 뿌리는 구조가 아닙니다.
packages/core/src/multicast/socket.rs의 bind_multicast_sockets()는 네트워크 인터페이스별로 소켓을 만듭니다. Wi-Fi·유선·가상 어댑터가 함께 있을 때 한 소켓의 송신 인터페이스에만 의존하지 않기 위해서입니다. 자기 알림은 지문을 비교해 걸러냅니다.
멀티캐스트가 통하지 않을 때는 다른 길도 있습니다. app/lib/provider/network/scan_facade.dart의 StartSmartScan은 알림과 즐겨찾기 확인을 시작하고, 알려진 주소 확인이 끝난 뒤 1초의 유예 시간을 둡니다. 스캔 시작 이후 새 기기나 기존 기기를 한 번도 확인하지 못하면 서브넷 스캔으로 확장합니다. 대상 인터페이스는 최대 3개, Rust 발견 계층의 기본 요청 제한 시간은 500ms, 스캔 동시성 상수는 50입니다.
프로토콜 버전 2.2와 API 경로의 v2는 모순이 아닙니다. 세부 버전과 경로 버전이 구분됩니다. 서버에는 호환용 GET /api/localsend/v1/info도 남아 있으므로, v2 구현이라는 설명을 “v1 문자열을 가진 경로가 전혀 없다”로 확대해서는 안 됩니다.
Dart isolate는 다른 isolate와 같은 변경 가능한 객체를 자유롭게 공유하는 작업자가 아닙니다. 메시지를 보내 일을 요청하고 결과 스트림을 받습니다. 이 저장소는 그 연결에 요청·결과 타입을 붙여 잘못된 메시지를 줄입니다.
// packages/typed_isolates/lib/src/isolate_helper.dart의 핵심 형태
class IsolateConnector<R, S> {
// R: 작업자로부터 받는 메시지, S: 작업자에게 보내는 메시지
final Stream<R> receiveFromIsolate;
final SendPort _sendToIsolate;
final Isolate isolate;
// 생성자는 생략
void sendToIsolate(S message) {
_sendToIsolate.send(message);
}
}
실제 isolate 생성은 TypedIsolates.startIsolate<R, S, P>()가 담당합니다. packages/localsend_isolates/lib/src/isolate/parent/parent_isolate_provider.dart의 IsolateSetupAction에서 그 사용 예를 볼 수 있습니다. 긴 제네릭을 만났다면 먼저 “받는 타입, 보내는 타입, 초기값 타입” 순서로 풀어 읽으세요.
상태도 자동으로 공유되지 않습니다. actions_sync.dart는 새 SyncState를 자식에게 보냅니다. ServerService.startServer()가 서버 시작 작업을 보내기 전에 별칭·포트·프로토콜을 먼저 동기화하는 순서는 중요합니다. 순서를 바꾸면 작업자가 예전 설정으로 시작할 수 있습니다.
화면 담당자는 접수 창구, isolate는 담당 부서입니다. 같은 메모장을 동시에 고치는 대신 정해진 양식의 요청서를 보냅니다. 설정 변경도 구두 전달이 아니라 새 업무 지침을 발송하는 과정이 필요합니다.
Rust 코어는 Flutter 대화상자를 모릅니다. packages/core/src/http/server/v2.rs의 prepare_upload()는 PrepareUpload 이벤트와 decision_tx를 만들고 앱의 답을 기다립니다. 앱은 일반 승인 창을 띄우거나, 설정에 따라 자동 승인할 수 있습니다.
// prepare_upload()의 흐름을 줄인 설명용 코드
let (decision_tx, decision_rx) = oneshot::channel();
let event = ServerEventV2::PrepareUpload {
session_id,
ip,
info,
cert_fingerprint,
files,
decision_tx,
};
event_tx.send(event).await?;
// 실제 구현은 취소 신호와 함께 tokio::select!로 기다립니다.
let decision = decision_rx.await?;
oneshot은 한 번의 답을 위한 채널입니다. 브리지의 RsHttpServer는 Rust 쪽에 응답 채널을 보관하고 respond_prepare_upload()로 답하게 합니다. Dart의 수신 이벤트는 server_isolate.dart를 거쳐 ServerService와 ReceiveController.onPrepareUpload()로 이어집니다.
따라서 ReceiveController는 HTTP 라우트 핸들러가 아니라 통신 계층이 이미 해석한 이벤트를 처리하는 앱의 정책 담당자입니다. 자동 수신도 코어를 고쳐 구현하는 대신 승인 응답을 빨리 보내는 방식으로 구성할 수 있습니다.
코어를 CLI에서도 쓰기 쉬운 이유가 여기에 있습니다. “통신은 이렇게 진행한다”와 “언제 허락할지, 어디에 저장할지”를 나눴기 때문입니다. 다른 UI를 붙여도 프로토콜을 다시 작성할 필요가 줄어듭니다.
packages/core/src/http/server/mod.rs의 세션 슬롯은 Option<SessionStateV2>로 상태를 표현합니다. 비어 있거나, 승인 대기 Pending이거나, 진행 중 Active입니다. prepare_upload()는 슬롯이 차 있으면 HTTP 409로 새 세션을 거절합니다.
세션 하나 안에는 여러 파일이 있을 수 있습니다. “동시에 하나의 수신 세션”을 “항상 파일 하나씩만 처리”로 바꿔 읽으면 안 됩니다. 승인된 파일에는 각각 UUID 토큰이 발급되며, upload()는 세션 ID·송신자 주소·파일 ID·토큰·현재 상태를 검사합니다.
PendingSessionGuard와 UploadGuard는 비정상 종료를 처리합니다. 요청을 처리하던 Future가 중간에 사라져도 Drop에서 정리 작업을 시작해 대기 슬롯을 비우거나 파일 상태를 실패로 정리합니다. 정상 진행 때는 disarm() 또는 finish()로 중복 정리를 막습니다.
tokio::spawn으로 예약되므로 즉시 완료된다는 뜻도, 앱 강제 종료 뒤 디스크의 모든 흔적을 지운다는 뜻도 아닙니다.체크섬 불일치 때의 업로드 시도 상한은 MAX_UPLOAD_ATTEMPTS = 3입니다. 모든 네트워크 오류를 무조건 세 번 재시도한다는 정책은 아닙니다. 어떤 오류가 다시 Pending으로 돌아가는지 finalize_file()의 분기를 읽어야 합니다.
packages/core/src/http/server/common/save.rs의 save_req_to_target()는 HTTP 본문을 청크 단위로 읽습니다. 저장 대상은 FileUploadTarget::Path, Android의 Fd, 앱이 직접 소비하는 Stream으로 나뉩니다. 전체 파일을 하나의 거대한 바이트 배열로 먼저 만드는 경로와는 다릅니다.
| 구체적 장치 | 코드의 값·행동 | 의미 |
|---|---|---|
| 파일 청크 채널 | UPLOAD_CHANNEL_CAPACITY = 16 | 저장 쪽이 느리면 송신 작업이 기다리도록 구성 |
| 쓰기 버퍼 | WRITE_BUFFER_SIZE = 512 * 1024 | 작은 입력 청크를 모아 파일 쓰기 횟수 감소 |
| 진행률 이벤트 | 채널이 차면 알림을 버릴 수 있음 | 중간 표시 하나 때문에 파일 저장을 막지 않음 |
| 기기 등록 이벤트 | register()의 try_send() | 앱이 밀려도 정보성 알림 때문에 등록 응답을 멈추지 않음 |
| 승인 이벤트 | send().await와 응답 채널 | 사용자의 결정은 누락시켜도 되는 진행률과 다르게 취급 |
파일 바이트는 기다려서 전달하고, 반복 가능한 중간 알림은 버릴 수 있게 하는 차이가 앱의 응답성을 지킵니다. 청크 채널의 16은 개수 제한이므로 이를 바로 “메모리 최대 16MB” 같은 수치로 바꿀 수는 없습니다. 청크 크기와 다른 버퍼도 영향을 줍니다.
packages/core/tests/event_backpressure.rs에는 이벤트를 소비하지 않아도 등록 응답과 스캔이 멈추지 않아야 한다는 테스트가 있습니다. 비동기 코드에서 중요한 것은 async를 붙이는 것만이 아니라, 누구의 느림이 누구를 기다리게 만드는가를 정하는 일입니다.
packages/core/src/http/client/server_cert_verifier.rs의 PinnedServerCertVerifier::verify_server_cert()는 상대 인증서의 SHA-256 지문을 기대값과 비교합니다. 이 검사는 요청을 보낸 뒤가 아니라 TLS 핸드셰이크에서 수행됩니다. 지문이 다르면 파일 내용이나 요청 메타데이터를 보내기 전에 연결을 실패시킬 수 있습니다.
서브넷 스캔처럼 상대 지문을 미리 모르는 발견 경로도 있습니다. packages/core/src/discovery/mod.rs의 unpinned_client()는 기대 지문 없이 인증서를 검사하고, HTTPS 연결에서 확인한 지문을 이후 전송에 사용합니다. 이를 최초 사용 시 신뢰, TOFU라는 관점에서 읽을 수 있습니다. 반면 HTTPS로 멀티캐스트 알림에 답할 때는 알림에 담긴 지문을 처음부터 고정합니다. 처음 만난 상대가 의도한 사람인지까지 암호화가 자동으로 증명하지는 않습니다.
기기 인증서의 수명도 살펴볼 만합니다. app/lib/provider/persistence_provider.dart는 보안 컨텍스트가 저장돼 있지 않을 때 generateSecurityContext()로 생성해 저장합니다. README의 “기기에서 인증서를 생성한다”는 설명을 매 전송마다 새 인증서를 만든다는 뜻으로 읽으면 안 됩니다. 저장된 기기 신원을 이후 연결에도 사용하는 설계입니다.
일반 HTTPS 수신 모드에서는 클라이언트 인증서도 요구합니다. 웹 공유 모드에서는 브라우저 접속을 위해 선택 사항으로 바뀝니다. CustomClientCertVerifier와 start_server_with_listener()의 mandatory_client_auth 분기가 이 차이를 보여줍니다.
ReceiveController.onPrepareUpload()는 event.certFingerprint ?? event.info.fingerprint를 사용합니다. 인증서에서 확인한 지문을 우선하고, 인증서 지문이 없으면 요청 본문의 자기 신고값을 사용합니다. HTTP 연결뿐 아니라 클라이언트 인증서 없이 접속하는 HTTPS 웹 업로드도 이 경우에 해당합니다. “상대가 JSON에 적은 지문”과 “연결에서 증명한 신원”을 구분하는 연습에 좋은 코드입니다.
README는 HTTPS 전송을 설명하지만 PersistenceService.isHttps()의 기본값이 true인 한편, 설정과 통신 타입에는 HTTP 경로도 있습니다. 따라서 모든 설정에서 항상 암호화된다고 단정하면 안 됩니다. 체크섬 생성·검증도 기본값은 참이지만 옵션이며, 송신자가 체크섬을 제공했는지도 영향을 줍니다.
체크섬은 내용의 일치 여부, TLS는 통신 보호, 지문 확인과 수신 승인은 상대 신뢰에 관련된 별도 장치입니다. 어느 하나를 나머지 전부의 보증으로 설명해서는 안 됩니다.
화면이 궁금하면 app/lib/pages/, 상태가 궁금하면 app/lib/provider/, 실제 전송 규칙이 궁금하면 packages/core/src/http/로 내려가면 됩니다. 경계 사이가 궁금할 때만 localsend_isolates를 펼치면 읽기 부담이 줄어듭니다.
packages/core/src/http/dto_v2.rs와 앱의 화면 모델은 같은 목적의 파일이 아닙니다. DTO는 네트워크에서 주고받는 모양을 나타내고, 화면 상태는 진행률·표시 이름·사용자 선택처럼 UI가 필요한 정보를 함께 가집니다. 서로 다른 모델을 무조건 하나로 합치면 한쪽 변경이 다른 쪽까지 번지기 쉽습니다.
| 언어 | 전체 추적 파일 / 줄 | 생성·동봉 빌드 도구 제외 |
|---|---|---|
| Dart | 393개 / 149,359줄 | 275개 / 29,703줄 |
| Rust | 110개 / 31,186줄 | 109개 / 22,951줄 |
| Kotlin | 4개 / 885줄 | 동일한 집계 |
| Swift | 11개 / 757줄 | 동일한 집계 |
| C++ 구현 | 9개 / 807줄 | .cc와 .cpp 합계 |
| HTML | 4개 / 757줄 | 저장소에 포함된 웹 페이지 |
| Shell | 9개 / 380줄 | 7개 / 228줄 |
커밋 6e37efd의 Git 추적 파일을 대상으로 한 물리 줄 수이며 빈 줄과 주석·테스트를 포함합니다. 우측 열은 app/lib/gen/, *.g.dart, *.mapper.dart, *.freezed.dart, 이름에 frb_generated가 들어가는 파일, cargokit/ 아래 파일을 제외한 수치입니다. 모든 종류의 생성 코드를 완벽하게 식별한 “순수 수작업 코드” 통계는 아닙니다.
Rust 코어만 보면 packages/core/src/는 10,728줄, packages/core/tests/는 3,857줄입니다. 테스트 분량이 크다는 관찰은 가능하지만 줄 수만으로 보안성이나 테스트 커버리지를 판단할 수는 없습니다. 생성된 번역·브리지 코드가 Dart 전체 규모를 크게 늘린다는 점도 함께 기억하세요.
app/lib/widget/responsive_builder.dart의 SizingInformation은 화면 폭 700 미만을 모바일, 700 이상을 태블릿·데스크톱, 800 이상을 데스크톱으로 판단합니다. 기기 이름을 맞히려는 분류가 아니라 주어진 공간에 맞는 레이아웃을 선택하는 기준입니다.
// 실제 SizingInformation의 폭 기준
const SizingInformation(double width)
: isMobile = width < 700,
isTabletOrDesktop = width >= 700,
isDesktop = width >= 800;
실습 아이디어: 폭 699, 700, 799, 800에서 세 값이 어떻게 달라지는지 표로 적어보세요. 700~799 구간에서는 태블릿·데스크톱용 공간이지만 isDesktop은 거짓입니다. 경계값을 보는 습관이 UI 조건문의 오류를 줄입니다.
LocalSendApp.build()는 settingsProvider.select()로 테마 관련 값만 구독합니다. 모든 설정 변경이 전체 테마 판단에 영향을 주지 않도록 관심 범위를 좁히는 예입니다. 연습: “전송 진행률이 바뀔 때 테마 위젯도 다시 만들 필요가 있을까?”라는 질문으로 구독 지점을 분류해 보세요.
nearbyDevicesProvider는 ReduxProvider이고, RegisterDeviceAction.reduce()가 발견한 기기를 상태에 반영합니다. 기기는 별칭이 아니라 지문을 키로 등록됩니다. 같은 사람이 별칭을 바꾸거나 같은 기기에 여러 주소가 생기는 상황에서 무엇을 식별자로 삼을지 배우는 지점입니다.
실습 아이디어: 같은 지문·다른 별칭인 기기 두 개와, 같은 별칭·다른 지문인 기기 두 개가 차례로 들어오면 목록이 몇 개가 되는지 예상해 보세요. 별칭을 식별자로 삼았을 때와 비교하면 모델 설계의 의미가 선명해집니다.
isolate 경계는 Dart 실행 문맥 사이의 메시지 통신이고, FRB 경계는 Dart와 Rust 사이의 호출·값 변환입니다. 둘을 같은 “백그라운드 처리”라고 뭉뚱그리지 마세요. IsolateHttpServerStartAction부터 start_server()까지 따라가면 두 경계를 차례로 볼 수 있습니다.
packages/localsend_isolates/flutter_rust_bridge.yaml의 rust_input: crate::api, rust_root: rust/, dart_output: lib/rust는 어떤 Rust API에서 Dart 코드를 생성하는지 보여줍니다. 생성 파일을 처음부터 읽기보다 packages/localsend_isolates/rust/src/api/server.rs의 수동 API부터 보세요.
실습 아이디어: “서버 시작 요청”·“서버 시작 완료”·“파일 수신 진행률”이 각각 어느 방향으로 두 경계를 건너는지 화살표로 그려보세요. 실제 파일 바이트도 그 길을 반드시 지나야 하는지 별도로 표시하는 것이 핵심입니다.
FileUploadTarget::Fd는 Android 파일 디스크립터의 소유권을 받아 저장이 끝나면 닫는 경로입니다. File::from_raw_fd() 주위의 안전 조건은 “이 정수를 누가 닫을 책임이 있는가”라는 현실의 문제와 연결됩니다. Rust 소유권은 값의 복사 여부만 다루는 추상 규칙이 아닙니다.
RsHttpServer의 Mutex<Option<mpsc::Receiver<ServerEventV2>>>도 유용합니다. listen()은 take()로 수신기를 가져가므로 한 번만 듣도록 구성됩니다. 두 번째 호출은 오류를 전달합니다. 타입과 작은 상태 변화로 사용 규칙을 표현하는 사례입니다.
실습 아이디어: “두 명이 같은 수신기를 소비하면 누가 어떤 이벤트를 받을까?”를 생각한 뒤, 한 번만 꺼내 쓰는 설계가 어떤 모호함을 없애는지 설명해 보세요. Mutex가 있다고 모든 기능을 여러 번 호출해도 된다는 뜻은 아닙니다.
단계적 발견은 알고리즘의 정답보다 시스템 비용을 생각하게 합니다. 알림 한 번과 알고 있는 주소 확인은 적은 작업으로 끝날 수 있지만, 서브넷 스캔은 여러 주소에 접속합니다. StartSmartScan의 유예 시간과 SCAN_CONCURRENCY는 사용자 대기 시간·트래픽·동시 연결 수를 조절하는 손잡이입니다.
실습 아이디어: 멀티캐스트 허용, 멀티캐스트 차단, 상대가 꺼짐이라는 세 환경에서 어떤 단계가 실행되고 언제 결과가 나올지 순서도를 만드세요. 500ms 제한 시간과 동시성 50만으로 모든 실제 소요 시간을 정확히 계산할 수 없는 이유도 적어보세요.
packages/localsend_isolates/lib/src/task/server/file_saver.dart의 digestFilePathAndPrepareDirectory()는 디렉터리를 포함한 파일 이름을 다룹니다. 경로 이탈 시도와 절대 경로를 거부하는 처리, 기존 파일과 겹치면 이름에 번호를 붙이는 처리를 찾아볼 수 있습니다.
Rust의 packages/core/src/util/filename.rs는 Windows·HFS·FAT·POSIX 규칙을 나누고 MAX_LEN = 255바이트 제한을 둡니다. 글자 수와 UTF-8 바이트 수는 다르므로 한글 파일 이름을 다룰 때 특히 좋은 학습 소재입니다.
실습 아이디어: ../outside.txt, sub/photo.jpg, CON.txt, 긴 한글 파일 이름을 “경로 자체 거부”와 “이름 정리”로 나눠 예상하세요. file_saver_traversal_test.dart는 실제 경로 이탈 입력을, Rust 파일의 테스트는 이름 규칙을 읽는 출발점입니다.
한국어 번역은 app/assets/i18n/ko.json, 누락 번역 목록은 _missing_translations_ko.json입니다. 생성된 app/lib/gen/strings_ko.g.dart를 직접 수정하면 다음 생성 작업에서 변경이 사라질 수 있습니다.
app/test/unit/i18n_test.dart는 번역 접근 코드의 컴파일과 Flutter의 언어 지원을 확인합니다. 실습 아이디어: 수신 승인 문구 하나를 골라 JSON 키 → 생성 접근자 → 화면 사용처를 연결해 보세요. 번역이 표시되는 위치까지 이해하면 문구 길이와 버튼 폭도 함께 고려하게 됩니다.
좋은 학습은 기술 이름을 수집하는 대신, 한 데이터가 경계를 지날 때 책임과 형태가 어떻게 바뀌는지 설명하는 데서 시작됩니다.
| 플랫폼 | README의 최소 버전 | 소스에서 함께 볼 부분 |
|---|---|---|
| Android | 7.0 | minSdkVersion flutter.minSdkVersion으로 SDK에 위임. 앱 설정에 숫자 24를 직접 고정한 형태는 아님 |
| iOS | 13.0 | app/ios/Podfile은 13.0, Xcode 설정에 공유 확장용 14.0도 존재 |
| macOS | 11 Big Sur | app/macos/Podfile은 11.0. 일부 프로젝트 타깃에는 10.15 설정도 남음 |
| Windows | 10 | 구형 OS 관련 의존성 주석만으로 현행 공식 지원을 확대 해석하지 않기 |
| Linux | 단일 최소 버전 미명시 | README는 xdg-desktop-portal과 데스크톱 환경별 GTK/KDE 포털을 안내 |
README는 iOS에서 다른 앱의 공유 기능으로 보내려면 14 이상이 필요하다고 덧붙입니다. 지원 표는 배포 앱의 안내이고, 빌드 파일의 개별 타깃 값은 그 일부입니다. 타깃 하나의 낮은 숫자만 보고 앱 전체의 최소 OS 버전을 바꾸어 설명하면 안 됩니다.
| 자원·연결 | 필요 조건과 해석 |
|---|---|
| CPU / RAM / GPU | README에 정량 최소 사양이 명시돼 있지 않음. 모델 추론용 GPU·VRAM을 요구하는 프로젝트는 아님 |
| 저장 공간 | 받을 파일을 저장할 여유 필요. 갤러리 저장 경로는 먼저 캐시에 기록하므로 최종 저장소만 확인해서는 부족할 수 있음 |
| 네트워크 | 상대 기기로 직접 통신 가능한 로컬 네트워크. Wi-Fi와 유선이 연결된 구성도 경로·방화벽 정책이 허용해야 함 |
| 포트 | 기본 수신 TCP·UDP 53317. TCP 전송과 UDP 발견은 서로 다른 기능 |
| 권한·격리 | 로컬 네트워크 권한, 저장 위치 권한, AP 격리, VPN의 LAN 차단 여부가 동작에 영향 |
인터넷이 필요 없다는 말은 네트워크가 필요 없다는 뜻이 아니며, 같은 Wi-Fi에서도 기기 간 통신이 허용돼야 합니다. 상대가 안 보일 때는 UI 오류부터 의심하기보다 발견 단계와 실제 접속 단계를 나눠 살펴보세요.
| 개발 대상 | 저장소에서 확인되는 조건 |
|---|---|
| 공통 도구 | Flutter 3.41.9, Dart SDK 허용 범위 ^3.11.0, Rust 1.97.1 |
| Android | compileSdkVersion 36, targetSdkVersion 37, Java·Kotlin 대상 17, NDK 버전은 Flutter 설정에 위임 |
| Apple 플랫폼 | 플랫폼별 Xcode 프로젝트와 Podfile 사용. 배포 시 서명·패키징 환경도 별도로 필요 |
| Windows / Linux | 플랫폼별 Flutter 네이티브 빌드 도구와 Rust 라이브러리 연결 필요. Dart 도구만 설치해서 끝나는 구성은 아님 |
| 별도 서버 이미지 | server/Dockerfile은 Rust 1.97 계열 빌드 이미지와 Debian 실행 이미지를 사용 |
compileSdk와 targetSdk는 빌드 설정의 선언값입니다. 이 값만으로 빌드 성공이나 실제 지원 OS 범위를 보증할 수는 없습니다. 정량 RAM·디스크 요구량과 플랫폼별 모든 도구 버전이 한곳에 고정돼 있지는 않습니다. SDK·의존성 캐시·Rust 빌드 산출물은 앱 설치 크기와 별도로 공간을 차지합니다.
상대 이름이 안 보이면 “안내 방송이 들리는가”를, 직접 주소로도 연결되지 않으면 “상대 문까지 갈 수 있는가”를 확인합니다. 문까지 갔는데 저장이 실패하면 그때 저장 공간·권한을 봅니다. 발견·연결·저장의 순서로 범위를 좁히는 것입니다.
첫 두 과제는 소스를 읽고 표·그림을 만드는 것만으로 할 수 있습니다. 나머지는 독립된 연습 코드나 테스트 실험 환경에서 확장할 과제입니다. 기능을 바꾸기 전에 현재 계약을 설명할 수 있는지 확인하세요.
목표: 사용자 동작, 메시지, HTTP 요청을 서로 구별합니다.
할 일: StartSmartScan, register(), prepare_upload(), ReceiveController.onPrepareUpload(), upload(), save_req_to_target()를 순서대로 배치하세요. 발견 과정에서 요청을 보내는 방향이 바뀐다는 점에 주의하세요.
결과물: A·B 기기 두 열로 그린 순서도와, 각 단계의 실제 파일 경로 목록.
완료 기준: UDP가 파일을 전달하는지, 승인 응답 전에 파일 본문을 보내는지, 파일 토큰을 누가 만드는지 세 질문에 답할 수 있습니다.
# 저장소 루트에서 실행하는 읽기용 검색 예시
rg -n 'class StartSmartScan|onPrepareUpload' app/lib
rg -n 'async fn (register|prepare_upload|upload)' packages/core/src/http/server/v2.rs
rg -n 'save_req_to_target|UPLOAD_CHANNEL_CAPACITY' packages/core/src/http/server/common/save.rs
목표: 화면과 기기 목록의 규칙을 입력·출력으로 설명합니다.
할 일: 화면 폭 699·700·799·800의 SizingInformation 값을 계산하세요. 이어 지문 X인 기기의 별칭이 바뀌는 경우, 별칭은 같고 지문 Y가 추가되는 경우를 RegisterDeviceAction에 대입해 보세요.
결과물: 폭별 불리언 표와, 기기 등록 세 번에 따른 목록 변화 표.
완료 기준: 700~799 구간의 두 분류값이 다른 이유, 별칭 중복이 기기 동일성을 뜻하지 않는 이유를 설명할 수 있습니다.
목표: 프로토콜 구현을 바꾸지 않고 승인 정책을 바꿉니다.
할 일: 독립된 연습 코드에서 파일 ID와 크기를 받아 1MiB 이하만 선택하는 순수 함수를 만드세요. 선택된 ID 집합이 PrepareUploadDecisionV2::Accept의 입력에 해당한다고 생각하면 됩니다. 실제 네트워크나 디스크는 필요하지 않습니다.
입력: 0바이트, 정확히 1MiB, 1MiB+1바이트 파일을 포함한 목록. 화면 이름과 내부 파일 ID가 다른 예도 넣으세요.
완료 기준: 경계값을 올바르게 처리하고, “아무 파일도 선택하지 않음”과 명시적 Decline의 의미를 구별합니다. 실제 코어는 승인 결과가 비면 204, 거절이면 403을 반환합니다.
// 과제용 Dart 순수 함수. 실제 서버 API를 호출하는 코드는 아닙니다.
Set<String> selectSmallFiles(Map<String, int> sizes) {
const limit = 1024 * 1024;
return {
for (final entry in sizes.entries)
if (entry.value >= 0 && entry.value <= limit) entry.key,
};
}
목표: 성공 경로보다 중단 경로를 먼저 설명합니다.
읽을 곳: packages/core/tests/v2_server.rs의 test_prepare_upload_aborted_by_sender_disconnect()와 test_second_session_blocked_and_cancel(), 구현의 PendingSessionGuard.
할 일: 첫 요청이 승인 대기 중일 때 두 번째 요청을 보내는 경우와, 첫 요청을 취소한 뒤 새 요청을 보내는 경우의 상태표를 만드세요. 확장 실험에서는 가드를 빠뜨린 작은 모형을 만들어 차이를 관찰합니다.
완료 기준: 중단 뒤 슬롯이 비는지, 예전 승인 응답이 유효한지, 다음 요청이 진행 가능한지에 대한 검증 항목이 있습니다. “오류가 발생했다” 한 문장으로 끝내지 않습니다.
목표: 잃어도 되는 이벤트와 기다려야 하는 데이터를 구분합니다.
읽을 곳: packages/core/tests/event_backpressure.rs의 register_keeps_answering_when_events_are_not_consumed(). 채널 용량 16에 대해 그 네 배의 등록 요청을 보내는 구성을 읽어보세요.
할 일: 독립된 모형에서 용량이 작은 채널과 느린 소비자를 만들고, 기다리는 송신과 즉시 실패하는 송신을 비교하세요. 등록 알림, 진행률, 승인 결정, 파일 청크의 네 종류마다 누락·재시도·대기 정책을 적습니다.
결과물: 응답 시간, 버린 이벤트 수, 파일 바이트 보존 여부를 나눈 관찰표.
완료 기준: try_send()를 모든 채널에 일괄 적용하지 않습니다. 중간 진행률이 빠져도 최종 결과는 전달돼야 한다는 요구를 별도로 검증합니다.
| 과제 | 만들 결과물 | 스스로 확인할 핵심 |
|---|---|---|
| 입문 1 | 두 기기의 요청 순서도 | 발견과 업로드의 방향 차이 |
| 입문 2 | 경계값·목록 상태표 | 표시 이름과 식별자의 차이 |
| 중급 3 | 순수 승인 함수 | 정책과 통신 구현의 분리 |
| 중급 4 | 중단 시나리오 검증표 | 다음 요청이 가능한 상태로 복귀 |
| 고급 5 | 채널 부하 관찰표 | 데이터 종류에 맞는 대기·누락 정책 |
실습의 완료 기준은 기능을 흉내 내는 데서 끝나지 않고, 실패한 뒤에도 다음 작업이 가능한지 설명하는 것입니다.
Dart·Flutter를 처음 접한다면 Rust 코어부터 정독할 필요는 없습니다. 먼저 화면에서 보이는 상태를 이해하고, HTTP의 요청·응답을 익힌 다음 언어 경계를 내려가세요. 아래 일정은 학습 순서의 예시이며 주당 학습량에 따라 늘려도 됩니다.
| 주차 | 기초 개념 | 이 저장소에서 읽기 | 남길 결과물 |
|---|---|---|---|
| 1주 | Dart 타입·불변 값·위젯·조건부 레이아웃 | main.dart, responsive_builder.dart | 위젯 트리와 화면 폭 경계값 표 |
| 2주 | provider·action·상태 구독·영속 설정 | settings_provider.dart, nearby_devices_provider.dart | 설정 변경과 기기 등록의 상태 변화도 |
| 3주 | HTTP·JSON·TCP/UDP·멀티캐스트 | packages/core/src/http/dto_v2.rs, packages/core/src/multicast/mod.rs, scan_facade.dart | 발견·승인·업로드 순서도 |
| 4주 | Future·Stream·isolate·FRB·채널 | parent_isolate_provider.dart, packages/localsend_isolates/rust/src/api/server.rs | 언어와 실행 문맥의 경계 지도 |
| 5주 | Rust 소유권·enum·상태 머신·Drop | packages/core/src/http/server/common/session.rs, v2.rs, file_saver.dart | 취소·거절·재시도 시나리오 검증표 |
| 6주 | TLS 지문·역압·통합 테스트·CI | server_cert_verifier.rs, event_backpressure.rs, ci.yml | 보호 장치와 자동 검사 항목의 연결표 |
이 표의 짧은 파일 이름은 앞 절에서 소개한 경로의 파일을 가리킵니다. 같은 이름의 v2.rs는 특히 HTTP 클라이언트·서버 양쪽에 있으므로, 상태 머신을 읽을 때는 packages/core/src/http/server/v2.rs를 선택하세요.
app/lib/util/native/, Android의 MainActivity.kt, 창·트레이 watcher를 연결해 읽으세요. 파일 선택 결과가 언제 일반 경로이고 언제 플랫폼 URI인지, 앱이 다시 활성화될 때 어떤 서비스를 복구해야 하는지에 집중합니다.
connection_limit.rs, peer_ip.rs, packages/core/tests/accept_resilience.rs로 확장하세요. 열린 연결 수 제한, IPv6 인터페이스 범위, 파일 디스크립터 부족을 다루는 사례입니다. WebRTC는 그 뒤에 기본 로컬 전송과 비교하면서 읽는 편이 이해하기 쉽습니다.
다음 기술로 넘어갈 기준은 문법을 다 외웠는지가 아니라, 지금 읽은 계층의 입력·출력·실패 책임을 설명할 수 있는지입니다.
| 용어 | 쉬운 설명 |
|---|---|
| 로컬 우선 | 내 기기와 가까운 연결에서 핵심 작업을 처리하는 접근. LocalSend의 일반 전송은 외부 파일 보관 서버에 의존하지 않습니다. |
| P2P | 동등한 기기들이 직접 통신하는 방식. 한 기기도 요청을 보내고 받을 수 있습니다. 모든 네트워크에서 자동 연결된다는 뜻은 아닙니다. |
| 클라이언트 / 서버 | 요청하는 역할과 응답하는 역할. LocalSend에서는 한 앱이 두 역할을 모두 맡습니다. |
| 포트 | 같은 기기 안에서 통신 서비스를 구분하는 번호. 기본값 53317은 TCP 전송과 UDP 발견에서 각각 쓰입니다. |
| TCP / UDP | 전송 성질이 다른 통신 방식. 이 구현은 UDP로 기기를 알리고, HTTP 파일 전송에는 TCP 연결을 사용합니다. |
| 멀티캐스트 | 그룹에 참여한 여러 기기에 알리는 방식. 모든 인터넷 사용자에게 보내는 방송은 아닙니다. |
| 서브넷 스캔 | 같은 주소 범위의 여러 상대에게 차례로 접속해 확인하는 방식. 멀티캐스트가 안 되는 환경에서 다른 발견 경로가 됩니다. |
| REST API | HTTP 메서드와 경로로 작업을 나눠 요청하는 인터페이스. 여기서는 등록·전송 준비·업로드·취소 같은 단계가 있습니다. |
| DTO | 통신으로 주고받을 데이터의 모양. 화면의 모든 상태를 그대로 네트워크에 내보내는 것과 다릅니다. |
| TLS / HTTPS | 통신 내용을 보호하는 연결과 그 위의 HTTP. 사용자가 원한 상대인지 판단하는 승인 정책까지 대신하지는 않습니다. |
| 자체 서명 인증서 | 기기가 자기 키로 서명한 인증서. 공인 인증기관의 이름 확인 대신 별도의 상대 확인 설계가 필요합니다. |
| 인증서 지문 | 인증서 바이트를 해시해 만든 식별값. LocalSend는 DER 형식 인증서의 SHA-256을 대문자 16진수로 표현합니다. |
| 인증서 고정 | 기대하는 지문과 실제 연결 상대의 지문을 비교하는 것. 요청 데이터를 보내기 전에 다른 상대를 차단하는 데 쓰입니다. |
| mTLS | 서버뿐 아니라 클라이언트도 인증서를 제시하는 TLS 구성. 이 구현은 웹 공유 여부에 따라 요구 조건이 달라집니다. |
| TOFU | 처음 연결에서 알게 된 신원을 이후 확인에 사용하는 접근. 최초 연결 상대를 어떻게 신뢰할지는 별도 문제입니다. |
| 세션 / 토큰 | 세션은 한 전송 묶음의 상태, 토큰은 승인된 개별 파일 업로드에 필요한 값. 화면에 보이는 별칭과 다른 역할입니다. |
| 체크섬 | 파일 내용에서 계산한 요약값. 받은 내용이 기대한 내용과 일치하는지 확인하며, 상대의 의도까지 보증하지는 않습니다. |
| isolate | 자기 실행 문맥을 가진 Dart 작업 단위. 다른 isolate에는 메시지로 요청과 결과를 전달합니다. |
| FFI / FRB | 다른 언어의 함수를 연결하는 방식과 Dart·Rust 연결을 돕는 도구. FRB가 isolate 개념 자체를 대체하지는 않습니다. |
| Future / Stream | 나중에 얻을 한 결과와, 시간에 걸쳐 오는 여러 결과. 시작 완료와 연속 진행률을 구분할 때 유용합니다. |
| mpsc / oneshot | 여러 송신자의 메시지를 한 수신자로 모으는 채널과, 한 번의 답을 위한 채널. 이벤트 전달과 승인 응답에 쓰입니다. |
| 역압 | 뒤쪽 처리가 느리면 앞쪽도 기다리게 해 데이터가 무한히 쌓이지 않도록 하는 제어입니다. |
| 상태 머신 | 허용한 상태와 이동만 표현하는 모델. 대기 중인 파일을 전송 중·완료·실패로 구분하는 데 쓰입니다. |
| RAII / Drop 가드 | 객체가 사라질 때 필요한 정리를 연결하는 방식. 비정상 중단으로 세션이 영원히 사용 중인 상태로 남는 일을 줄입니다. |
| 파일 디스크립터 | 운영체제가 열린 파일을 가리키는 손잡이. Android의 일부 저장 대상은 일반 경로 대신 이 손잡이로 연결됩니다. |
| SAF | Android Storage Access Framework. 사용자가 허용한 문서·폴더에 접근하는 체계이며 일반 파일 경로와 구별해야 합니다. |
| 경로 이탈 | 외부에서 받은 이름이 상위 디렉터리 등을 가리켜 허용된 저장 범위를 벗어나려는 문제입니다. |
| 시그널링 / STUN | WebRTC 연결 정보를 교환하는 절차와 자기 네트워크 주소를 알아내는 관련 기능. 로컬 HTTP 전송의 필수 서버는 아닙니다. |
| 워크스페이스 | 관련 패키지를 한 묶음으로 관리하는 설정. 이 저장소는 Dart와 Rust에 각각 멤버와 잠금 파일이 있습니다. |
| 잠금 파일 / CI | 의존성 선택을 기록한 파일과 변경을 자동 검사하는 절차. 같은 코드를 일관되게 빌드·배포하는 데 필요한 기반입니다. |
| 항목 | 상태와 해석 |
|---|---|
| 선정 | GitHub Trending Dart Weekly #11 · 2026-10-23 분석 |
| 스타 / 포크 | ⭐95,312 / 5,357 · 2026-10-23 GitHub 기준 |
| 생성일 | 2022-12-16 |
| 주 언어 | Dart. 통신 코어와 CLI·시그널링 서버에는 Rust도 사용 |
| 최신 릴리스 | v1.18.2 · 2026-08-21 |
| 최근 push | 2026-10-21 · GitHub 기준 |
| 최근 커밋 | 6e37efd · 2026-10-21fix(webrtc): less blocking implementation (#3625) |
| 코드 규모 | Dart 약 149,359줄 / Rust 약 31,186줄. 생성 코드·cargokit 제외 시 각각 29,703줄 / 22,951줄 |
| 네이티브 코드 | Kotlin 885줄 / Swift 757줄 / C++ 구현 807줄. 물리 줄 수, 주석·빈 줄 포함 |
| 한국어 지원 | app/assets/i18n/ko.json과 생성된 한국어 번역 코드 존재 |
| 기본 네트워크 | 프로토콜 2.2 · 포트 53317 · IPv4 멀티캐스트 224.0.0.167 |
| WebRTC | 구현 존재. 앱의 webRTCEnabled는 false |
| 라이선스 | Apache-2.0 · 루트 LICENSE |
① 릴리스와 이 글의 코드는 시점이 다릅니다. v1.18.2 릴리스 날짜는 2026-08-21, 분석 대상 커밋은 2026-10-21입니다. 앱 버전 문자열이 같아도 이 글의 모든 내부 구현이 해당 배포 파일에 들어 있다고 가정하면 안 됩니다.
② 로컬 전송은 네트워크 정책에 의존합니다. AP 격리·방화벽·VPN·OS 권한이 연결을 막을 수 있습니다. 별도 WebRTC 코드는 기본 앱에서 꺼져 있으므로 원격 전송의 해결책으로 바로 가정할 수 없습니다.
③ 암호화가 상대의 의도까지 보증하지는 않습니다. 최초 상대 확인, 사용자 승인, HTTP 설정, 자동 수신 설정은 각각 따로 살펴야 합니다. 체크섬도 파일 일치 확인이지 악성 파일 판별 기능은 아닙니다.
④ 소스의 장치와 실제 환경의 결과는 구별해야 합니다. 스트리밍·채널 제한·테스트가 있어도 특정 기기의 전송 속도나 최대 메모리가 자동으로 정해지지는 않습니다. OS 지원은 배포판 안내와 실제 패키지 조건을 함께 확인해야 합니다.
⑤ 통계는 범위를 가진 숫자입니다. 물리 줄 수는 주석·테스트를 포함하고 생성 코드 제외 규칙에도 영향을 받습니다. 특정 커밋의 파일 통계만으로 전체 누적 커밋 수·기여자 활동량·테스트 커버리지를 추정할 수는 없습니다.