빌드

화면은 파일이다 — 네 줄로 시작하는 UI 런타임

작성: makemind · 2026년 5월 13일

클라이언트 강좌 4편에서 화면이 도착했다. ui://desk 를 읽으면 546바이트가 왔고, 클라이언트는 그걸 그냥 문자열로 받아서 길이만 세고 끝냈다.

이번 트랙은 그 문자열이 그림이 되는 쪽이다.

네 줄

{ "type": "page", "title": "Step 1",
  "content": { "type": "center",
    "child": { "type": "text", "text": "The whole screen is this file",
               "style": { "fontSize": 34, "fontWeight": "bold", "color": "#111827" } } } }

233바이트. 이게 화면 전부다. 노드가 셋 있다.

  • page — 한 화면. 제목을 가진다.
  • center — 자식 하나를 가운데에 둔다.
  • text — 글자.

트리다. page 안에 content 가 하나, center 안에 child 가 하나. 여러 개를 담는 노드는 2편에서 나온다.

이걸 그리는 쪽

final rt = MCPUIRuntime(enableDebugMode: false);
// screen = jsonDecode(파일 내용)
await tester.runAsync(() => rt.initialize(screen));
await tester.pumpWidget(RepaintBoundary(
  key: key,
  child: MaterialApp(
    debugShowCheckedModeBanner: false,
    home: Builder(
      builder: (c) => rt.buildUI(
        context: c,
        onToolCall: (tool, params) async => fired.add(tool),
      ),
    ),
  ),
));

initialize 에 맵을 넘기고, buildUI 가 위젯을 돌려준다. 그 사이에 화면에 대한 판단은 없다.

이 호스트가 테스트 안에서 도는 건 캡처를 뜨기 위해서다. 앱이라면 pumpWidget 자리에 runApp 이 온다 — 위아래 두 줄은 그대로다.

onToolCall 은 4편에서 쓴다. 지금은 이름을 받아 적기만 한다.

무엇이 없는지가 요점이다

이 트랙의 호스트는 host/test/capture_test.dart 파일 하나다. 다섯 편이 끝날 때까지 이 파일은 거의 안 바뀐다 — 다섯 화면 전부 같은 호스트가 그린다.

그래서 이 강좌에서 가장 중요한 검사는 "있는지"가 아니라 "없는지" 다.

# 화면에 보이는 글자가 프로그램 안에 있으면 실패한다.
grep -rq 'The whole screen is this file' $SRC && \
  die "step1: the words on the screen are inside the host"

# 위젯을 손으로 짓고 있으면 실패한다.
grep -rqE '\bText\(|\bColumn\(|\bRow\(|\bElevatedButton\(' $SRC && \
  die "step1: the host builds widgets by hand"

$SRChost/test host/pubspec.yaml 이다. host/build 는 뺐다 — 컴파일 산출물에 문자열이 그대로 박혀 있어서, 거기까지 훑으면 이 검사가 영원히 통과한다. 실제로 4편 검사를 처음 붙였을 때 그 이유로 잘못 걸렸다.

이 콘텐츠는 개발자 이상이 필요합니다

로그인 후 플랜을 업그레이드하면 계속 읽을 수 있습니다.

플랜 보기
Twitter