Docs · Cocode ADE · 분석 · 설계 문서

풀스택 코딩 에디터 · E02

flutter_ide 기술 분석

오픈소스 Flutter IDE 의 구조를 패턴 학습 관점에서 해부하고 재사용을 판정

목차

파이프라인 fullstack-code-editor Discovery 단계의 부속 산출물. 본 문서는 docs/discovery-fullstack-code-editor.md §4.1 의 전문(全文)이다. 분석 대상 저장소에는 LICENSE 파일이 없다(GitHub API license: null, pubspec.yaml 에 license 키 없음 — README 본문의 "MIT" 문구만 존재). 따라서 이 분석은 코드 복사가 아니라 패턴 학습을 목적으로 하며, 재사용 판정(§8)도 그 전제 위에 있다. 저장소 활동 실측(GitHub API, 2026-09-16): 커밋 22건 · 기여자 2명(19 + 3) · ★35 · 포크 6 · 열린 이슈/PR 6건(#1 설정 화면, #6 Dart LSP, #7 전역 검색↔Monaco, #8 AI 사이드바, #4 README 스크린샷, PR #9 검색 결과 내비게이션) · 최초 커밋 2025-12-11 · 마지막 push 2026-01-05.


flutter_ide (ankurg132) 기술 분석

대상: ankurg132/flutter_ide HEAD 63807f3 (shallow clone — HEAD 1커밋만 조회 가능) · 21 Dart 파일 / 5,030 LOC · 분석일 2026-09-16 방법: lib/·test/ 전 파일, pubspec.yaml/pubspec.lock, README.md, analysis_options.yaml, .metadata, macos/Runner/*.entitlements 정독. 보조 검증: Dart SDK 3.47.3 process_patch.dart(runInShell 인용 처리), pub.dev 메타데이터(flutter_monaco / flutter_pty / webview_flutter 플랫폼). 인용은 파일:행.

1. 개요

  • 무엇인가: VS Code 외형을 모방한 단일 창 코드 에디터. Monaco(WebView) 편집기 + 파일 트리 + 탭 + 자동저장 + PTY 터미널 + pub.dev 검색 + 내장 브라우저 탭 + git 사이드바. 저장소 생성 2025-12-11, 마지막 push 2026-01-05(약 4주 활동), 주 저자 1명(커밋 22건 중 19건 · 기여자 2명), 35★.
  • 성숙도: 데모/사이드 프로젝트. 근거 — pubspec description: "A new Flutter project."(pubspec.yaml:2), 앱 타이틀 'Flutter Demo'와 템플릿 주석 잔존(main.dart:15,17-31), 유일한 테스트는 템플릿 카운터 테스트의 본문만 지운 빈 껍데기(test/widget_test.dart:8-17), CI 없음(.github/에는 FUNDING.yml뿐), 린트는 기본 flutter_lints 그대로(analysis_options.yaml:10,23-25), README 가 존재하지 않는 flutter_idedemo/ 디렉토리를 안내(README.md:74-77,96), 선언만 하고 import 하지 않는 의존성 6종(§4).
  • 플랫폼 디렉토리(실측): linux/, macos/, windows/, web/ 존재 · android/, ios/ 없음. README 의 "Runs on macOS, Windows, Linux, iOS, and Android"(README.md:39)는 사실과 다름. .metadata 의 migration 항목은 root·web 뿐이라 데스크톱 디렉토리는 나중에 수동 추가된 흔적. macOS 는 Debug/Release 모두 com.apple.security.app-sandbox=false(macos/Runner/DebugProfile.entitlements, Release.entitlements) — PTY·임의 파일 접근에 필요하지만 Mac App Store 배포 불가. Podfile osx 10.15.
  • 실제로 동작하는 타깃은 macOS 하나. Windows: Monaco 는 WebView2 로 뜨지만 브라우저 탭(webview_flutter)은 Windows 구현이 없어 WebViewController() 생성 시 예외(§4), 단축키가 Meta 키 전용(§3-16). Linux: flutter_monaco 가 Linux 미지원 → 편집기 자체가 없음. Web: flutter_pty(dart:ffi)를 무조건 import(output_panel.dart:5) → 웹 컴파일 자체가 불가, FileService 웹 구현도 스텁(§2).

2. 아키텍처

2.1 위젯 트리

main.dart:14  MaterialApp(theme: ColorScheme.fromSeed(deepPurple) ← 라이트 템플릿 테마, main.dart:32)
                     └ EditorScreen (editor_screen.dart:33)  StatefulWidget · 1,079행 · 메서드 33개 · 상태 필드 16개
                        └ Scaffold > Column (599-709)
                           ├ Expanded > Row
                           │  ├ _buildActivityBar() (712)      ActivityBarItem ×4 + 터미널 토글 (activity_bar.dart:3)
                           │  ├ [idx0] _buildSidebar() (752) → FileTree (file_tree.dart:6)
                           │  │ [idx1] FlutterSidebar (flutter_sidebar.dart:6)
                           │  │ [idx2] PubDevSidebar (pubdev_sidebar.dart:4)   폭 = _sidebarWidth+50 (623)
                           │  │ [idx3] GitSidebar (git_sidebar.dart:18)        폭 = _sidebarWidth+50 (632)
                           │  ├ HorizontalResizeHandle (640; resize_handles.dart:3)
                           │  └ Expanded > Column
                           │     ├ _buildTabBar() (841) → EditorTab / WebEditorTab (editor_tabs.dart:5,98) + Run/토글 버튼 (890-908)
                           │     ├ _buildBreadcrumbs() (915)
                           │     ├ Expanded: _buildWebView() | WelcomeScreen | _buildEditor()→MonacoEditor (660-668, 956)
                           │     ├ VerticalResizeHandle (673)
                           │     └ OutputPanel (682) → xterm TerminalView + flutter_pty (output_panel.dart:197,105)
                           └ _buildStatusBar() (1023)
                    

2.2 상태 관리 — 전부 setState + 전역 가변 포인터

  • 상태 관리 라이브러리 없음. setState 85회(editor_screen 25, git_sidebar 21, pubdev 10 …). 모든 앱 상태(_rootNode, _openFiles, _activeFile, _currentCode, _webTabs, _webViewControllers, _fileWatchers, _recentlySavedFiles, _pendingCommand, 레이아웃 치수 등)가 _EditorScreenState 의 인스턴스 필드(editor_screen.dart:41-67,225)에 살고, 자식에게는 생성자 파라미터 + 콜백으로 prop-drilling 된다(예: FileTree 가 콜백 6개를 받음 file_tree.dart:7-12).
  • 전역 함수 포인터: void Function(String)? _globalRunTerminalCommand(editor_screen.dart:26)를 initState 에서 _runTerminalCommand 로 세팅(74), dispose 에서 null(132). 공개 API runTerminalCommand()(29-31)는 이 포인터를 호출 — README 가 "Global Command API" 로 홍보(README.md:29,125-136). 사실상 top-level 서비스 로케이터.
  • 정적 싱글턴: final FileService fileService = FileServiceImpl();(file_service.dart:7) — import 시점에 생성되는 top-level 인스턴스. GitService 는 GitSidebar 상태가 직접 new(git_sidebar.dart:33), PubClient 도 마찬가지(pubdev_sidebar.dart:19). DI·인터페이스 주입 지점 없음 → 테스트 대역 삽입 불가.
  • 명령 전달이 prop-diff 부수효과: PubDevSidebar.onRunCommand → _runTerminalCommand(327-332)가 _pendingCommand 문자열과 _isOutputVisible 만 세팅 → OutputPanel.didUpdateWidget 이 initialCommand != oldWidget.initialCommand 를 감지(output_panel.dart:68-71) → post-frame 에 pty.write('$command\n')(81,86) → onCommandExecuted 로 _pendingCommand=null(692-696). 즉 "명령"이 문자열 동등성으로 식별되고 위젯 rebuild 에 편승한다.

2.3 플랫폼 추상화 — 조건부 import 한 곳, 나머지는 우회

  • file_service.dart:2 import 'file_service_io.dart' if (dart.library.html) 'file_service_web.dart'; + 8-메서드 인터페이스(file_service_interface.dart:3-12). 조건이 구식 dart.library.html(wasm 대응은 dart.library.js_interop).
  • 그러나 추상화가 우회된다: FlutterSidebar 는 dart:io 로 디스크를 직접 읽고(flutter_sidebar.dart:2,111-113,166,222,336), 파일 감시는 File.watch 직접 호출(editor_screen.dart:146-147), GitService·OutputPanel 은 Process/Pty. 21개 중 5개 파일이 dart:io 를 import.
  • 웹 구현은 스텁: create/delete/rename 은 null/false 반환(file_service_web.dart:61-98), saveFile 은 메모리 맵에만 기록(76-80), pickDirectory 는 다중 파일 선택을 'Uploaded Files' 가상 루트로 평탄화(15-47). File System Access API 미사용.

2.4 계층화

  • 없음. lib/ 루트에 화면·위젯(file_tree.dart, output_panel.dart, *_sidebar.dart)이 섞여 있고 widgets/editor, widgets/git 만 하위 폴더. models/ 2파일(31행), services/ 2서비스. import 스타일도 상대 7 : 패키지 2 로 혼재.
  • 모델은 가변 + 미사용 필드: FileNodeDirectory.isExpanded(file_system_entity.dart:10)와 FileNodeFile.content(21)는 어디서도 읽지 않고, 펼침 상태는 FileTree._expandedPaths(file_tree.dart:30)와 FlutterSidebar._expandedFolders(flutter_sidebar.dart:23) 두 곳에 독립적으로 존재. children 기본값 const [](15)인데 targetDir.children.add(...)(editor_screen.dart:237,253)로 변이 — 현재는 모든 생성 경로가 growable 리스트를 넘겨 잠복 상태.

2.5 핵심 데이터 흐름 — 파일 열기 → 편집 → 저장 → 외부 변경

  1. 트리 클릭 → FileTree.onFileSelected(file_tree.dart:161) → _openFile(editor_screen.dart:514): _openFiles 추가 + _watchFile 등록(515-521) → _activeFile 동기 교체(524-526) → fileService.readFile(529) → _currentCode 갱신(530-532) → setValue(534) + setLanguage(536-537).
  2. 편집 내용은 Monaco(WebView) 버퍼에만 존재한다. Flutter 쪽은 변경 이벤트를 구독하지 않는다(onChanged류 사용 0건).
  3. 2초 폴링(_startAutoSave 258-289): getValue()(266) → _currentCode 와 문자열 비교(268) → 다르면 _recentlySavedFiles 마킹(276) → fileService.saveFile(277) → 500ms 뒤 마킹 해제(279-281).
  4. 외부 변경: File.watch modify(147-149) → 마킹돼 있으면 무시(151-153), 아니면 _reloadFileContent(154) → 활성 파일이면 setValue(176) + SnackBar(179-184).
  5. 결론: "진실의 원천"이 세 곳(Monaco 버퍼 / _currentCode / 디스크)이고, 동기화 수단은 폴링·타이머·문자열 비교뿐이다. §6-b 의 손실 경로는 전부 여기서 파생된다.

2.6 객체 생성·배선 지점 (주입 가능한 곳 없음)

객체 생성 위치 수명 테스트 대역 주입
fileService top-level final(file_service.dart:7) 프로세스 전체 ✗
_globalRunTerminalCommand editor_screen.dart:26, set 74 / clear 132 EditorScreen ✗
MonacoController onReady 콜백(969-970) 편집기 위젯 마운트 동안(조건부 자식 660-668) ✗
WebViewController URL 키 캐시(77-104) 탭 닫힐 때 map 에서만 제거(313) ✗
StreamSubscription<FileSystemEvent> 파일별(147-161) 탭 닫기·dispose ✗
Pty OutputPanel._startPty(output_panel.dart:105) 패널 State(숨겨도 유지, 폴더 변경 시 kill 132-137) ✗
GitService git_sidebar.dart:33 사이드바 State — 뷰 전환마다 재생성(§6-b S4) ✗
PubClient pubdev_sidebar.dart:19, close 37 사이드바 State — 뷰 전환마다 재생성 ✗

3. 기능 인벤토리

# 기능 구현 위치 실제 동작 / 한계
1 Monaco 편집기 MonacoEditor(editor_screen.dart:956-973), 옵션 dart/vsDark/automaticLayout(964-968), 컨트롤러는 onReady(969-970) 내용 주입 setValue(534), 회수 getValue 폴링(266). 변경 이벤트 구독 없음. 언어 매핑 10종(569-596), 미지정 확장자는 dart(594). 편집기가 조건부 자식(660-668)이라 웹탭 전환/전체 닫기 때마다 WebView 가 파괴·재생성되고 onReady 가 다시 뜬다(→ §6-b3).
2 탭 _openFiles/_webTabs(43,49), EditorTab/WebEditorTab(editor_tabs.dart:5,98), 가로 ListView(851) 두 탭 위젯이 ~90행 복붙. dirty 표시·재정렬·미들클릭 닫기·"다른 탭 닫기" 없음. 활성 탭 재클릭이 디스크 재읽기(860-865→529).
3 자동저장 _startAutoSave(258-289): Future.doWhile + 2초 Future.delayed(259-261), getValue() 와 _currentCode 비교(266-268), 빈 내용 보호(270-272), _recentlySavedFiles 500ms 마킹(276-281) 취소 불가(Timer 아님), onReady 마다 새 루프(971). 탭 전환·닫기·종료 시 flush 없음, 수동 ⌘S 없음.
4 파일 감시 _watchFile(141-165) File.watch(events: modify)(147), _reloadFileContent(167-191) → setValue+SnackBar, 닫을 때 해제(193-196), dispose(134-137) 열린 파일만 감시, 디렉토리 감시 없음(트리 자동 갱신 없음), rename/delete 이벤트 미처리.
5 Quick Open ⌘P → _showQuickOpen(357-373), 트리 순회 _collectAllFiles(340-355) → QuickOpenDialog(quick_open_dialog.dart:6): substring 필터(52-55), 정렬 build/ 뒤·.dart 앞·알파벳(62-82), 화살표/Enter/Esc(91-109) fuzzy 아님, 선택 항목 스크롤 추적 없음, 최근 파일 가중치 없음, KeyboardListener(focusNode: FocusNode()) 를 build 에서 매번 생성(153-155).
6 파일 트리 CRUD FileTree(file_tree.dart:6) 재귀 평탄 리스트(72-148), 호버/선택 _HoverableItem(246-317), 우클릭 메뉴 Rename/Delete(290-316). 생성 _createNewFile/_createNewFolder(editor_screen.dart:227-256), 삭제(375-437), 이름변경(439-486). IO: _buildDirectoryNode 가 전체 트리를 listSync 로 한 번에(file_service_io.dart:20-54), 숨김파일만 제외(37) 삭제 후 await fileService.pickDirectory(); 를 다시 호출하고 결과를 버림(419) → OS 폴더 선택창이 튀어나오고 트리는 안 바뀜. 이름변경은 활성 탭 경로만 갱신(455-465), 트리·감시자 미갱신("ideally we'd update in place" 467). build/·node_modules 미제외, UI 아이솔레이트에서 동기 순회. DnD·복사·다중선택·Finder 열기 없음.
7 Flutter 프로젝트 사이드바 FlutterSidebar(flutter_sidebar.dart:6): lib(109-131)/test(133-161)/pubspec(249-262)/PLATFORMS(264-317)/OTHER(319-393) build() 안에서 existsSync/listSync(113,137,166,222,253,294,336) — FileService 우회 + 리빌드마다 디스크 I/O. 파일 열기 시 새 FileNodeFile(name, path) 생성(451).
8 pub.dev 사이드바 PubDevSidebar(pubdev_sidebar.dart:4), PubClient(19). "Popular" = search('flutter', page:1)(48). 검색은 submit 시(167,173). 상세 packageInfo(92) → 설명, "View on Pub.dev" 웹탭(339-342), flutter pub add <name> 실행(377-381,463), 메타 링크(396-426) 목록은 패키지명만(256-263), 디바운스·페이지네이션 없음. 'License' 행에 latestPubspec.name, 'Published' 행에 version 을 표시(428-429) — 라벨 버그. 상세 로드 실패는 무음(97-101).
9 터미널 OutputPanel(output_panel.dart:8): Terminal(maxLines:10000)(31), Pty.start(shell, cols/rows, workingDirectory, environment: Platform.environment)(105-111), 셸 = Windows cmd.exe / 그 외 $SHELL ?? /bin/bash(96-98), 출력→터미널(113-116), 입력→PTY(119-121), 리사이즈(124-126) 명령 실행 = 셸에 문자열 타이핑(81,86). 작업 폴더 변경 시 PTY kill·재시작(62-66,132-137). "Close" 는 숨김 + clear 타이핑(187-190), 프로세스는 유지. 터미널 1개, 분할·탭·프로세스 종료 버튼 없음. _runCommand 는 PTY 미준비 시 2프레임 후 포기(77-84).
10 WebView 탭 WebTab(web_tab.dart:2), URL 키 컨트롤러 캐시(editor_screen.dart:77-104), JavaScriptMode.unrestricted(82), 첫 onPageFinished 이후 모든 내비게이션을 차단하고 새 탭으로(88-97), 제목 휴리스틱(106-126), 읽기전용 URL 바(999-1007) 페이지 내 이동·뒤로가기·새로고침 불가. macOS 외 데스크톱에서 구현 없음(§4).
11 git 사이드바 GitSidebar(git_sidebar.dart:18) + GitService(git_service.dart:54, Process.run('git', args, runInShell:true) 59-64). 조회: log 200건 %H|%an|%ad|%s(81-122), branch %(HEAD)|%(refname:short)(124-140), status --porcelain=v1(142-155). 액션: stage/unstage/revert(git_sidebar.dart:94-171), Fetch/Pull/Push/Sync(509-539→253-312, upstream·dirty 가드 260-279), Commit/Commit&Push/Commit&Sync 스플릿 버튼(681-763→327-383) 커밋 전 무조건 git add -A(339) → 스테이징 UI 는 장식. diff 뷰 없음(git show 를 터미널로 812). 체크아웃도 터미널에 원문 타이핑(880,883) — GitService.checkoutBranch/checkoutNewBranch(git_service.dart:221-233)는 미사용. 브랜치 생성·stash·충돌 UI 없음, 갱신마다 프로세스 3개(235-239).
12 상태바 _buildStatusBar(editor_screen.dart:1023-1045) 'Spaces: 2', 'UTF-8' 하드코딩, onPressed: () {}(1037-1041). 커서 위치·브랜치·진단 없음.
13 액티비티 바 _buildActivityBar(712-750), ActivityBarItem/SidebarIconButton(activity_bar.dart:3,60) 4개 뷰 + 터미널 토글.
14 리사이즈 핸들 HorizontalResizeHandle/VerticalResizeHandle(resize_handles.dart:3,38) delta 드래그, 커서 지정(14,49) 사이드바 150–500(643), 터미널 100–500(676). 4px 히트영역, 더블클릭 리셋·영속화 없음.
15 웰컴 화면 WelcomeScreen(welcome_screen.dart:86), 버튼 Open Folder ⌘O / New File ⌘N(126-139) "Recent" 는 현재 열린 루트 하나만(143-181) — 최근 목록 영속화 없음.
16 단축키 HardwareKeyboard.instance.addHandler(editor_screen.dart:72) → _handleGlobalKeyEvent(198-214) 실제 배선: Meta+P(Quick Open, 203), Meta+O(폴더 열기, 208) — isMetaPressed 만 검사(201)하므로 Windows/Linux 의 Ctrl 조합은 동작하지 않음(README 171-178 의 "Cmd/Ctrl" 과 불일치). 플레이스홀더: ⌘N 은 버튼 라벨뿐(welcome_screen.dart:137), ⌘+클릭 정의로 이동 은 Listener.onPointerDown 의 빈 if 블록(951-955). ⌘S/⌘W/⌘F/⌘⇧F/⌘B/⌘` 없음. 핸들러가 전역이라 다이얼로그 입력 중에도 발화. flutter_monaco 문서상 WebView 가 포커스를 쥐면 Flutter 키 이벤트가 오지 않을 수 있음(§4).

4. 의존성 표

패키지 (선언 → lock) 역할 플랫폼·제약
flutter_monaco ^1.1.1 → 1.1.1 (2025-11-16; 최신 3.4.3 2026-08, 2 메이저 뒤, 최신은 SDK ≥3.12·webview_flutter_windows ^1.0) Monaco 호스팅 Monaco 는 WebView 안에서 실행: macOS/iOS = WKWebView(webview_flutter), Android = WebView, Windows = WebView2(webview_windows 0.4.0 — lock 에 전이 의존으로 존재, WebView2 런타임 필요), Linux 미지원(패키지 README 명시). Monaco 에셋 ~30MB 번들, 첫 실행 시 app-support 디렉토리로 추출(1–2초) → 오프라인 OK, 번들 크기 +30MB. 인스턴스당 별도 WebView(30–100MB). Dart↔JS 는 JSON 엔벨로프 브리지 — 이 앱은 2초마다 getValue() 로 문서 전체를 IPC 로 회수. 데스크톱 플랫폼뷰의 포커스/First Responder/키보드는 별도 처리 필요(문서 경고) → Flutter 레벨 단축키·IME 는 WebView 엔진에 위임됨. (웹 지원 표기는 3.x README 기준, 1.1.1 은 미확인.)
webview_flutter ^4.10.0 → 4.13.0 내장 브라우저 탭 플러그인 플랫폼 = android/ios/macos 만(webview_flutter_wkwebview 3.23.5). Windows/Linux 구현 없음 → WebViewController()(editor_screen.dart:81) 가 "platform implementation has not been set" 로 실패.
webview_flutter_web ^0.2.3 → 0.2.3+4 웹 구현 직접 import 0건이지만 웹 플러그인은 의존성 선언만으로 자동 등록되므로 의도는 맞음. iframe 기반이라 NavigationDelegate·JS 모드 등 대부분 미구현 → "링크→새 탭" 로직(88-97) 이 웹에선 무의미. 어차피 웹 빌드 불가(아래).
file_picker ^10.3.7 → 10.3.7 폴더 선택 6개 플랫폼 지원. 데스크톱 getDirectoryPath(file_service_io.dart:12-13); 웹은 디렉토리 선택 불가 → pickFiles(withData:true) 평탄 목록(file_service_web.dart:19-22).
xterm ^4.0.0 → 4.0.0 터미널 에뮬레이터 UI 순수 Dart, 웹 포함 전 플랫폼. TerminalView(output_panel.dart:197).
flutter_pty ^0.4.0 → 0.4.2 (약 20개월 전, README 스스로 "experimental") PTY dart:ffi + 네이티브(Android/iOS/Linux/macOS/Windows), 웹 없음 → output_panel.dart:5 무조건 import 로 웹 컴파일 차단. macOS 는 sandbox off 필요(현재 off).
pub_api_client ^2.5.0 → 2.7.1 pub.dev API HTTP; 네트워크만 있으면 동작. oauth2, http 전이.
path ^1.9.1 경로 조작 5개 파일 사용. 단 breadcrumb 은 '/' 로 직접 split(editor_screen.dart:916) → Windows 오동작.
font_awesome_flutter ^10.12.0 아이콘(dartLang, codeBranch) 2개 파일.
json_rpc_2 ^4.0.0 미사용(import 0) LSP(JSON-RPC) 를 염두에 둔 흔적으로 추정 — README 로드맵 LSP 미완(README.md:201).
http, path_provider, flutter_svg, meta, cupertino_icons 미사용(import 0) path_provider 는 flutter_monaco 가 에셋 추출에 전이 사용.
flutter_lints ^6.0.0 (dev) 린트 규칙 커스텀 없음.

전이 의존 중 주목: webview_windows 0.4.0, freezed_annotation, flutter_helper_utils(flutter_monaco), zmodem(xterm), ffi.

5. 강점 (차용 가치 있는 패턴)

  1. 조건부 import 심(seam) + 좁은 인터페이스: file_service.dart:1-7 + file_service_interface.dart:3-12. 웹 구현이 스텁이어도 경계 자체는 올바른 자리에 있다.
  2. 명령 결과 값 객체: GitServiceResult { exitCode, stdoutText, stderrText, ok, bestMessage }(git_service.dart:34-52). 인자 배열 명시, --porcelain=v1, --pretty=format:%H|%an|%ad|%s --date=iso-strict, %(HEAD)|%(refname:short) 로 파싱 가능한 출력 강제(81-140), subject 의 | 를 재결합(102).
  3. git 안전 가드: pull/sync 전 upstream 확인(git_sidebar.dart:260-269), dirty tree 차단(271-279), pull --ff-only(git_service.dart:206), revert 확인 다이얼로그(138-155), await 뒤 mounted 체크(101,223,241…), try/finally 로 로딩 해제(109-111).
  4. PTY 배선 레시피: Pty.start ↔ Terminal.onOutput/onResize 연결(output_panel.dart:105-126), 첫 프레임 이후 시작(43-46), 실패를 터미널 자체에 출력(127-129).
  5. 자체 저장 억제 + 빈 내용 보호 의도: _recentlySavedFiles(editor_screen.dart:67,276-281), 빈 내용 저장 차단(270) — 구현은 타이밍 의존이지만 문제 인식은 정확.
  6. 링크 → 새 탭 인터셉트: 첫 로드 후 모든 내비게이션을 탭으로 승격(88-97) — 문서 브라우저 UX 아이디어.
  7. Quick Open 랭킹: .dart 우선·build/ 후순위(quick_open_dialog.dart:62-82).
  8. 앵커드 팝업 메뉴: RenderBox→Overlay 좌표로 showMenu 위치 계산(git_sidebar.dart:721-732).
  9. 작은 크롬 위젯: ActivityBarItem, SidebarIconButton, StatusBarItem, 리사이즈 핸들의 SystemMouseCursors.resizeColumn/Row(resize_handles.dart:14,49), _TopGitAction/_CommitAction enum + exhaustive switch(git_sidebar.dart:5-16,314-325).

6. 약점 / 기술 부채

(a) 구조

  • God widget: _EditorScreenState 1,079행, 메서드 33개, 상태 필드 16개, setState 25회 — FS CRUD 다이얼로그·탭·자동저장·감시자·WebView 컨트롤러·git/pubdev 배선·_build* 7개가 한 클래스. GitSidebar 도 890행/setState 21회.
  • 복붙: _getFileIcon 3벌(editor_screen.dart:1047, file_tree.dart:193, quick_open_dialog.dart:111, 아이콘 집합도 서로 다름), EditorTab↔WebEditorTab(editor_tabs.dart:5-96 vs 98-187), isStagedXY 2벌(git_sidebar.dart:55-59,65-69), bool _isHovered 보일러플레이트 7회, 펼침 상태 2벌(§2.4).
  • 테마 없음: Color(0x…) 리터럴 118개(고유 26색). MaterialApp 은 라이트 deepPurple 템플릿(main.dart:32) → AlertDialog/showMenu/SnackBar/TextField(_showNameDialog 488-512, _deleteNode 380-399)가 다크 UI 안에서 Material 라이트로 렌더링.
  • 전역 가변 함수 포인터(26), top-level 싱글턴(file_service.dart:7), build() 내 동기 I/O(flutter_sidebar.dart 8곳), 미사용 의존성 6종, 상대/패키지 import 혼재.

(b) 데이터 손실·경쟁 조건 (편집기로서 치명)

  1. 탭 전환/닫기/종료 시 flush 없음: _openFile(514-548)이 저장 없이 _currentCode 를 덮고 setValue(534) → 최대 2초 분량 편집이 결정론적으로 소실. _closeFile(550-567)·dispose(129-139)도 동일.
  2. 교차 파일 쓰기 경쟁: 폴링 루프는 getValue() 이후 _activeFile!.path 를 읽는다(266,275). _openFile 은 _activeFile 을 동기 교체(524-526)하고 _currentCode 는 await readFile 뒤(529-532)에 갱신 → 그 창에서 폴링이 떨어지면 A 파일 버퍼가 B 파일 경로에 저장된다.
  3. 자동저장 루프 증식: _startAutoSave 가 onReady 마다 호출(971)되고 Future.doWhile 은 취소 불가(258-289). 편집기는 조건부 자식(660-668)이라 웹탭 전환/전체 닫기 N번 → 루프 N개가 2초마다 각각 문서 전체를 IPC 회수.
  4. 감시자 억제가 타이밍 기반: 500ms 창(279-281) 밖에서 FS 이벤트가 오면 setValue(176) → 커서·undo 스택 초기화 + 그 사이 입력 덮어쓰기.
  5. 활성 파일 재클릭(트리/탭 860-865) → 디스크 재읽기로 미저장 편집 폐기.
  6. 빈 내용 보호(270-272) 때문에 파일을 의도적으로 비우는 편집은 영구히 저장되지 않음.
  7. 삭제 후 폴더 선택창 재호출·결과 폐기(419); 이름변경 후 트리·감시자 미갱신(467-472); FileTree._expandedPaths 가 initState 에서만 채워져(file_tree.dart:34-40) 다른 폴더를 새로 열면 접힌 채 표시.
  8. 커밋이 스테이징을 무시(git_sidebar.dart:339); 체크아웃 명령을 셸에 타이핑(880,883)하므로 flutter run 이 포그라운드인 동안 그 프로세스의 stdin 으로 들어감(output_panel.dart:86).
  9. _closeFile 이 setState 콜백 안에서 async _openFile 호출(559); QuickOpenDialog 의 build 내 FocusNode() 생성·미해제(quick_open_dialog.dart:154); breadcrumb 이 '/' split + 루트 이름 매칭(916-923).

재현 시나리오(코드 경로 기준)

  • S1 편집 소실: A 열기 → 한 글자 입력 → 2초 안에 B 클릭. _openFile(B) 가 flush 없이 setValue(534) → A 의 입력은 디스크·_currentCode 어디에도 남지 않는다. _closeFile(550-567)·앱 종료(129-139)도 동일 경로.
  • S2 교차 저장: A 편집 → 폴링 직전에 B 클릭. _activeFile=B(524) 인 채 readFile(B)(529) 를 기다리는 동안 getValue()(266) 가 A 버퍼를 반환 → content != _currentCode(A 원본)(268) → saveFile(_activeFile=B, A 버퍼)(275-277). 창의 폭은 readFile 지연 시간이라 대용량 파일·느린 디스크에서 넓어진다.
  • S3 루프 증식: 파일 열기 → pub.dev "View on Pub.dev"(웹탭, pubdev_sidebar.dart:339-342) → 파일 탭 복귀 → MonacoEditor 재마운트·onReady(969) → _startAutoSave 2회째. N회 반복 후 2초마다 getValue() N회, 이전 루프는 mounted 가 참이라 영원히 산다(260,262).
  • S4 사이드바 상태 소실: 탐색기에서 폴더 여러 개 펼침 → git 뷰 전환 → 탐색기 복귀 → FileTree State 가 재생성돼 루트만 펼쳐진다(file_tree.dart:34-40). git 뷰 복귀마다 initState→_refresh()(git_sidebar.dart:75-78) 로 프로세스 3개(235-239), pub.dev 복귀마다 인기 목록 재요청(pubdev_sidebar.dart:29-32). 원인: 사이드바가 if (_selectedActivityIndex == n) 조건부 자식(editor_screen.dart:610-637)이라 전환 때마다 dispose 된다.

(c) 오류 처리 — 사실상 부재

  • catch 22개 중 16개가 무음: 주석만 있는 빈 본문 6개(editor_screen.dart:162,188,283,538, file_service_io.dart:45,95) + 로그 없이 null/false 반환 10개(file_service_io.dart:68,84,109,124,147, file_service_web.dart:44, flutter_sidebar.dart:168,224,346, pubdev_sidebar.dart:97) + 스트림 onError 무시(editor_screen.dart:157-159). 코드베이스 전체에 print/debugPrint/log 호출 0건.
  • 최악은 저장 실패(file_service_io.dart:95-97)와 자동저장 실패(editor_screen.dart:283-285)가 사용자에게 전혀 보이지 않는다는 점. readFile 은 try 없이 readAsString(file_service_io.dart:57-59) → 바이너리를 열면 예외(이건 541-546 에서 SnackBar 로 표면화되긴 함).

(d) 보안

  • 셸 인젝션(실재): runTerminalCommand('git checkout ${b.name}')(git_sidebar.dart:880,883) → pty.write('$command\n')(output_panel.dart:86). git ref 이름은 ; $ ( ) & | 백틱을 허용하므로 클론한 저장소의 악성 브랜치명이 클릭 한 번에 실행된다. 안전한 GitService.checkoutBranch(git_service.dart:228-233)가 있는데도 미사용.
  • Process.run(..., runInShell: true)(git_service.dart:59-64): POSIX 에서는 Dart 가 인자마다 작은따옴표 인용(SDK 3.47.3 process_patch.dart _getShellArguments 확인)하므로 커밋 메시지·경로 인젝션은 불가. Windows 는 cmd.exe /c 에 공백/탭/따옴표가 있을 때만 인용하므로 msg&calc 같은 인자는 메타문자 위험. 애초에 runInShell 이 불필요(PATH 탐색은 기본 동작).
  • 파일/폴더 이름 검증 없음: p.join(parentPath, name)(file_service_io.dart:64,137)에 ../../x 가 통과 → 프로젝트 밖 쓰기.
  • git add -A(339)가 .env 등 비밀을 통째로 스테이징. WebView 는 임의 패키지의 homepage/repository/issue_tracker URL 을 JavaScriptMode.unrestricted(82)로 IDE 프로세스 안에서 로드(macOS sandbox off). PTY 는 전체 환경변수 상속(110) — 터미널로서는 정상.

(e) 플랫폼

  • README 5플랫폼 주장(README.md:39) vs 실제 §1. dart.library.html 조건(file_service.dart:2)은 wasm 비호환. 셸을 비로그인으로 띄우므로(output_panel.dart:96-98) Finder 에서 실행 시 .zprofile 의 PATH(Homebrew·fvm)가 없어 통합 터미널에서 flutter 를 못 찾을 수 있음.

(f) 품질·테스트·문서

  • 테스트 0건(빈 템플릿 테스트로 flutter test 가 공허하게 통과, test/widget_test.dart:15-17), GitService 파서·FileService 단위 테스트 없음, CI 없음, i18n 없음, 접근성 없음(GestureDetector 11곳에 Semantics 없음, 4px 핸들), 영속화 없음(최근 폴더·레이아웃·설정). README 의 존재하지 않는 디렉토리, pub.dev 라벨 버그(§3-8).

(g) 편집기 기능 공백

  • LSP/진단/완성 없음(Monaco 내장 언어 서비스는 TS/JSON/CSS/HTML 뿐, Dart 는 토크나이저만), 전역 검색/치환 없음(로드맵 README.md:202), 분할 뷰 없음(206), undo 이력은 Monaco 버퍼뿐이며 setValue 때마다 리셋, dirty 표시·인코딩/EOL 처리(상태바 하드코딩)·문제 패널·실행 구성(Run = flutter run 타이핑 894)·디버거·디바이스 선택·확장/테마(204-205)·멀티루트·대용량 트리 가드·바이너리 처리·.gitignore 반영 모두 없음.

7. '풀스택 코딩 에디터' 관점의 격차

영역 필요 역량 flutter_ide 근거
편집기 코어(탭·자동저장·외부변경 감지·Quick Open) 안정적 버퍼 모델, dirty 추적, save-on-switch 부분 §3-1~5, §6-b 결함
다중 언어 LSP(Dart analysis server, TS, Python, SQL, YAML…) 진단·완성·호버·정의·참조·rename 없음 json_rpc_2 미사용; ⌘클릭 빈 블록(951-955). 최신 flutter_monaco 3.x 는 LSP stdio/WebSocket 연동을 문서화하나 고정된 1.1.1 에 사용 흔적 없음
문제 패널·Quick Fix·리팩터링(심볼 rename, 파일 이동 시 import 수정, organize imports) 없음 파일 rename 이 import 를 고치지 않음(439-486)
디버거(DAP)·브레이크포인트·핫리로드 컨트롤·디바이스 선택 없음 Run 버튼 = 셸에 flutter run 타이핑(894)
실행 구성·태스크 러너(프론트/백엔드/테스트/watch 동시 실행) 없음 단일 셸·문자열 명령
백엔드 프로세스 관리(서버 start/stop/restart, 프로세스별 로그, 포트 맵, 헬스) 없음 단일 PTY, 포그라운드 프로세스 인지 못함(§6-b8)
다중 터미널·분할·프로세스 kill·링크 감지 부분 output_panel.dart:31-33 터미널 1개
데이터베이스 브라우저(연결·스키마·쿼리 콘솔·결과 그리드·마이그레이션) 없음 .sql 하이라이트뿐(589)
HTTP/API 클라이언트(요청 컬렉션·환경변수·OpenAPI·응답 뷰어) 없음 WebView 탭은 문서 열람만
환경/시크릿 관리(.env per env, 마스킹, 커밋 차단) 없음 git add -A 가 비밀을 스테이징(339)
Docker/devcontainer/compose UI 없음 —
배포(CI 상태·클라우드 배포·로그 스트리밍) 없음 —
AI 어시스트(채팅·인라인 완성·에이전트 실행·diff 적용) 없음 훅 없음(flutter_monaco 의 completion/action 요청 채널 미사용)
협업(동시 편집·코멘트·프레즌스) 없음 —
확장/플러그인 API 없음 로드맵(204)
테마·설정·키맵 없음 하드코딩 118색, 라이트 테마 불일치, 설정 저장소 없음
워크스페이스 검색/치환(regex·파일 필터) 없음 Quick Open 은 파일명 substring 만
테스트 UI(탐색기·단일 테스트 실행·커버리지) 없음 —
git 고급(diff 뷰어·hunk 스테이징·blame·PR·충돌 해결·stash) 부분 status/stage/commit/push/log/branch 목록만; diff 는 터미널 git show(812)
프로젝트 트리(디렉토리 감시·gitignore·DnD·멀티루트·지연 로딩) 부분 일회성 스냅샷 + 수동 CRUD(§3-6)
내장 브라우저/프리뷰(localhost 프리뷰·devtools) 부분 macOS 한정, localhost 배선 없음
패키지 관리(pub·npm·pip·cargo, outdated, 의존성 트리) 부분 pub.dev 검색 + flutter pub add 만
세션 복원(열린 탭·레이아웃·최근 항목) 없음 welcome_screen.dart:143-181
워크스페이스 신뢰·명령 allowlist·샌드박싱 없음 §6-d 인젝션
접근성·i18n 없음 —
크로스플랫폼 동등성(Win/Linux/Web) 부분/없음 §1, §4

8. 재사용 판정

모듈 판정 한 줄 근거
main.dart 폐기 템플릿 잔재(라이트 테마·'Flutter Demo').
editor_screen.dart 폐기 (아이디어만) god widget + 데이터 손실 경쟁(§6-b) + 전역 포인터. 링크→탭 인터셉트, 자체 저장 억제 의도 만 재설계(해시+세대 카운터)로 승계.
models/file_system_entity.dart 폐기 가변·미사용 필드·동등성 없음 → sealed/immutable 트리 모델로 교체.
models/web_tab.dart 폐기 2필드 클래스, 동등성 없음(교체 비용 0).
services/file_service.dart + _interface.dart 패턴만 차용 조건부 import 심 + 좁은 인터페이스는 유지; 조건을 js_interop 으로, 반환은 Result 타입으로, 비동기·지연 목록화·ignore 규칙 추가.
services/file_service_io.dart 패턴만 차용 디렉토리 우선 정렬·숨김 제외(26-37)만; 동기 재귀 listSync·무음 catch·경로 검증 부재는 폐기.
services/file_service_web.dart 폐기 스텁; File System Access API 로 다시 써야 함.
services/git_service.dart 패턴만 차용 (거의 그대로) 결과 객체·porcelain/pretty-format 파서(34-155)는 그대로 쓸 만함; runInShell 제거, 타임아웃/취소 추가, checkoutBranch 를 실제로 사용.
file_tree.dart 패턴만 차용 재귀 평탄 리스트 + 호버/우클릭 메뉴 구조는 OK; SingleChildScrollView+Column(63-69)은 가상화·키보드 내비·지연 로딩으로 교체.
flutter_sidebar.dart 폐기 build 내 I/O·서비스 우회·중복 펼침 상태. "프로젝트 구조 뷰" 는 트리 모델 위의 필터로 재구현.
pubdev_sidebar.dart 패턴만 차용 검색→상세→추가 플로우만; 라벨 버그(428-429)·디바운스·페이지네이션·캐시 필요.
output_panel.dart 패턴만 차용 xterm+flutter_pty 배선 레시피(105-126)는 표준; 다중 터미널 서비스 + 프로세스 모델로 재작성, 포그라운드 프로세스에 명령 타이핑 금지.
widgets/editor/activity_bar.dart 패턴만 차용 구조는 단순·정상이나 색 리터럴·GestureDetector 무 Semantics — 디자인 시스템 토큰으로 재작성.
widgets/editor/editor_tabs.dart 폐기 복붙 쌍·dirty 상태 없음; 호버/닫기 어포던스 패턴만.
widgets/editor/quick_open_dialog.dart 패턴만 차용 다이얼로그+키보드 내비+랭킹 골격; fuzzy 스코어러·가상화·FocusNode 누수 수정.
widgets/editor/resize_handles.dart 그대로 차용 → ⚠️ 라이선스 부재로 재작성(패턴만) 71행, 커서·delta 콜백만 있는 순수 위젯 — 재작성 비용 0. 히트영역 확대·영속화만 덧붙이면 됨.
widgets/editor/status_bar.dart 패턴만 차용 아이템/구분자 구조; 내용은 전부 하드코딩.
widgets/editor/welcome_screen.dart 패턴만 차용 시각 골격만; Recent 미구현.
widgets/git/git_sidebar.dart 패턴만 차용 가드·앵커드 메뉴·스플릿 버튼은 승계; 인젝션 경로(880,883)·stageAll(339) 폐기.
test/widget_test.dart 폐기 빈 테스트.
의존성 선택 조건부 xterm·flutter_pty·file_picker·pub_api_client 는 유지 후보. flutter_monaco 는 결정 지점: macOS/Windows 한정·Linux 공백·+30MB·WebView 포커스/키보드 경고·2초 폴링 IPC 를 감수할지, 아니면 네이티브 Flutter 편집기(+LSP) 로 갈지 — 풀스택 에디터라면 후자 검토 권장.

차용 시 최소 수정 체크리스트 (패턴 차용 모듈)

  • git_service.dart: runInShell: true 제거(59-64) · Process.run 에 타임아웃/취소 · getLog 구분자 | 를 %x00 등으로 교체(작성자명에 | 가 있으면 열이 밀려 날짜 파싱 실패, 96-102) · 결과 객체에 실행 명령·소요시간 필드 · 파서 단위 테스트(porcelain XY, %(HEAD)).
  • resize_handles.dart: 히트영역 4px → 8px+ · 더블클릭 리셋 · onDragEnd 로 치수 영속화 콜백 · 키보드 조절.
  • output_panel.dart 레시피: Pty 를 위젯 State 에서 떼어 TerminalSession 서비스로(다중 세션·수명 분리) · 명령은 idle 세션에만 전달(포그라운드 프로세스 감지) · 로그인 셸 PATH 보강($SHELL -l 또는 path_helper) · 종료/강제종료 UI.
  • file_service 심: 조건 dart.library.js_interop · 목록화는 Directory.list() 스트림 + 깊이/개수 상한 + .gitignore/build/ 제외 · 디렉토리 감시 추가 · 모든 실패를 Result<T, FsError> 로 반환 · 이름 검증(경로 구분자·.. 거부).
  • quick_open_dialog.dart: FocusNode 를 State 필드로 · fuzzy 스코어러 · 선택 항목 ensureVisible · 최근 파일 가중치.
  • git_sidebar.dart: 체크아웃은 GitService.checkoutBranch 로(880,883 폐기) · _commitFlow 의 stageAll(339) 제거 · 새로고침은 FS/.git 감시 기반.

총평: "Flutter 로 IDE 셸을 만들 수 있다" 를 4주 만에 보여준 데모로서 가치가 있고, git 서비스 파서·PTY 배선·리사이즈 핸들 정도는 바로 참고할 만하다. 그러나 편집 버퍼의 소유권이 불명확(Monaco 버퍼 vs _currentCode vs 디스크)해서 데이터 손실이 구조적으로 발생하고, 오류가 전부 삼켜지며, 플랫폼 지원은 macOS 하나뿐이다. 풀스택 에디터의 기반으로는 아키텍처를 새로 세우고 패턴만 선별 차용하는 것이 맞다.

부록 A. 파일별 메트릭 (스크립트 실측)

파일 LOC StatefulWidget setState( catch ( Color(0x…) dart:io
lib/editor_screen.dart 1079 1 25 6 17 Y
lib/file_tree.dart 317 2 4 0 12
lib/flutter_sidebar.dart 477 1 6 3 10 Y
lib/main.dart 37 0 0 0 0
lib/models/file_system_entity.dart 24 0 0 0 0
lib/models/web_tab.dart 7 0 0 0 0
lib/output_panel.dart 237 1 0 1 4 Y
lib/pubdev_sidebar.dart 540 1 10 3 15
lib/services/file_service.dart 7 0 0 0 0
lib/services/file_service_interface.dart 12 0 0 0 0
lib/services/file_service_io.dart 152 0 0 7 0 Y
lib/services/file_service_web.dart 99 0 0 1 0
lib/services/git_service.dart 233 0 0 1 0 Y
lib/widgets/editor/activity_bar.dart 105 2 4 0 2
lib/widgets/editor/editor_tabs.dart 187 2 8 0 11
lib/widgets/editor/quick_open_dialog.dart 282 1 3 0 11
lib/widgets/editor/resize_handles.dart 71 0 0 0 2
lib/widgets/editor/status_bar.dart 69 1 2 0 1
lib/widgets/editor/welcome_screen.dart 187 1 2 0 8
lib/widgets/git/git_sidebar.dart 890 1 21 0 25
test/widget_test.dart 18 0 0 0 0
합계 5030 14 85 22 118 5 파일

LOC 는 wc -l 기준(개행 수). git_service.dart·git_sidebar.dart 는 파일 끝 개행이 없어 물리 행은 1 더 많다.

부록 B. 검증 방법과 한계

  • 정적 정독만 수행했고 빌드·실행은 하지 않았다(대상 디렉토리 무수정 원칙). 플랫폼 지원 판단은 pubspec.lock 의 해석 결과 + 각 플러그인 pubspec 의 platforms 선언 + pub.dev README(flutter_monaco 는 최신 3.4.3 README 기준, 1.1.1 과 차이 가능)에 근거한다.
  • 저장소가 shallow clone 이라 커밋 이력·저자 수·활동 기간은 조회 불가 — 팀 리드가 제공한 GitHub 메타데이터(생성 2025-12-11, 마지막 push 2026-01-05; API 실측 커밋 22건 · 기여자 2명 — 주 저자 19건)를 인용했다.
  • runInShell 인용 처리는 로컬 Dart SDK 3.47.3 의 process_patch.dart(_getShellArguments) 를 직접 읽어 확인했다. 셸 인젝션(§6-d)은 코드 경로 분석이며 실제 실행으로 재현하지는 않았다.
  • 경쟁 조건 S2 는 타이밍 의존이라 재현 확률이 낮을 수 있으나, 코드상 방어 장치가 전혀 없다는 점은 확정적이다.