파이프라인
fullstack-code-editorDiscovery 단계의 부속 산출물. 본 문서는docs/discovery-fullstack-code-editor.md§4.1 의 전문(全文)이다. 분석 대상 저장소에는 LICENSE 파일이 없다(GitHub APIlicense: 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_ideHEAD63807f3(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.3process_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 배포 불가. Podfileosx 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 + 전역 가변 포인터
- 상태 관리 라이브러리 없음.
setState85회(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). 공개 APIrunTerminalCommand()(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:2import '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 핵심 데이터 흐름 — 파일 열기 → 편집 → 저장 → 외부 변경
- 트리 클릭 →
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). - 편집 내용은 Monaco(WebView) 버퍼에만 존재한다. Flutter 쪽은 변경 이벤트를 구독하지 않는다(
onChanged류 사용 0건). - 2초 폴링(
_startAutoSave258-289):getValue()(266) →_currentCode와 문자열 비교(268) → 다르면_recentlySavedFiles마킹(276) →fileService.saveFile(277) → 500ms 뒤 마킹 해제(279-281). - 외부 변경:
File.watchmodify(147-149) → 마킹돼 있으면 무시(151-153), 아니면_reloadFileContent(154) → 활성 파일이면setValue(176) + SnackBar(179-184). - 결론: "진실의 원천"이 세 곳(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. 강점 (차용 가치 있는 패턴)
- 조건부 import 심(seam) + 좁은 인터페이스:
file_service.dart:1-7+file_service_interface.dart:3-12. 웹 구현이 스텁이어도 경계 자체는 올바른 자리에 있다. - 명령 결과 값 객체:
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). - 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). - PTY 배선 레시피:
Pty.start↔Terminal.onOutput/onResize연결(output_panel.dart:105-126), 첫 프레임 이후 시작(43-46), 실패를 터미널 자체에 출력(127-129). - 자체 저장 억제 + 빈 내용 보호 의도:
_recentlySavedFiles(editor_screen.dart:67,276-281), 빈 내용 저장 차단(270) — 구현은 타이밍 의존이지만 문제 인식은 정확. - 링크 → 새 탭 인터셉트: 첫 로드 후 모든 내비게이션을 탭으로 승격(88-97) — 문서 브라우저 UX 아이디어.
- Quick Open 랭킹:
.dart우선·build/후순위(quick_open_dialog.dart:62-82). - 앵커드 팝업 메뉴:
RenderBox→Overlay좌표로showMenu위치 계산(git_sidebar.dart:721-732). - 작은 크롬 위젯:
ActivityBarItem,SidebarIconButton,StatusBarItem, 리사이즈 핸들의SystemMouseCursors.resizeColumn/Row(resize_handles.dart:14,49),_TopGitAction/_CommitActionenum + exhaustive switch(git_sidebar.dart:5-16,314-325).
6. 약점 / 기술 부채
(a) 구조
- God widget:
_EditorScreenState1,079행, 메서드 33개, 상태 필드 16개,setState25회 — FS CRUD 다이얼로그·탭·자동저장·감시자·WebView 컨트롤러·git/pubdev 배선·_build*7개가 한 클래스.GitSidebar도 890행/setState21회. - 복붙:
_getFileIcon3벌(editor_screen.dart:1047,file_tree.dart:193,quick_open_dialog.dart:111, 아이콘 집합도 서로 다름),EditorTab↔WebEditorTab(editor_tabs.dart:5-96vs98-187),isStagedXY2벌(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(_showNameDialog488-512,_deleteNode380-399)가 다크 UI 안에서 Material 라이트로 렌더링. - 전역 가변 함수 포인터(26), top-level 싱글턴(
file_service.dart:7),build()내 동기 I/O(flutter_sidebar.dart8곳), 미사용 의존성 6종, 상대/패키지 import 혼재.
(b) 데이터 손실·경쟁 조건 (편집기로서 치명)
- 탭 전환/닫기/종료 시 flush 없음:
_openFile(514-548)이 저장 없이_currentCode를 덮고setValue(534) → 최대 2초 분량 편집이 결정론적으로 소실._closeFile(550-567)·dispose(129-139)도 동일. - 교차 파일 쓰기 경쟁: 폴링 루프는
getValue()이후_activeFile!.path를 읽는다(266,275)._openFile은_activeFile을 동기 교체(524-526)하고_currentCode는await readFile뒤(529-532)에 갱신 → 그 창에서 폴링이 떨어지면 A 파일 버퍼가 B 파일 경로에 저장된다. - 자동저장 루프 증식:
_startAutoSave가onReady마다 호출(971)되고Future.doWhile은 취소 불가(258-289). 편집기는 조건부 자식(660-668)이라 웹탭 전환/전체 닫기 N번 → 루프 N개가 2초마다 각각 문서 전체를 IPC 회수. - 감시자 억제가 타이밍 기반: 500ms 창(279-281) 밖에서 FS 이벤트가 오면
setValue(176) → 커서·undo 스택 초기화 + 그 사이 입력 덮어쓰기. - 활성 파일 재클릭(트리/탭 860-865) → 디스크 재읽기로 미저장 편집 폐기.
- 빈 내용 보호(270-272) 때문에 파일을 의도적으로 비우는 편집은 영구히 저장되지 않음.
- 삭제 후 폴더 선택창 재호출·결과 폐기(419); 이름변경 후 트리·감시자 미갱신(467-472);
FileTree._expandedPaths가initState에서만 채워져(file_tree.dart:34-40) 다른 폴더를 새로 열면 접힌 채 표시. - 커밋이 스테이징을 무시(
git_sidebar.dart:339); 체크아웃 명령을 셸에 타이핑(880,883)하므로flutter run이 포그라운드인 동안 그 프로세스의 stdin 으로 들어감(output_panel.dart:86). _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) →_startAutoSave2회째. N회 반복 후 2초마다getValue()N회, 이전 루프는mounted가 참이라 영원히 산다(260,262). - S4 사이드바 상태 소실: 탐색기에서 폴더 여러 개 펼침 → git 뷰 전환 → 탐색기 복귀 →
FileTreeState 가 재생성돼 루트만 펼쳐진다(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) 오류 처리 — 사실상 부재
catch22개 중 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.3process_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_trackerURL 을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 없음, 접근성 없음(GestureDetector11곳에 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 는 타이밍 의존이라 재현 확률이 낮을 수 있으나, 코드상 방어 장치가 전혀 없다는 점은 확정적이다.