한 파일, 두 모양 — 개수를 모르는 목록

5 분
목표화면 강좌 마지막 편. conditional 로 화면이 갈라지고, list 가 개수를 모르는 채로 줄을 만든다. 그리고 세 트랙 열여섯 편이 하나로 붙는다.

지금까지의 화면은 값이 바뀌어도 모양은 그대로였다. 실제 화면은 그렇지 않다 — 대기가 없을 때와 셋일 때는 다른 것을 보여줘야 한다.

갈라지는 자리

{ "type": "conditional", "condition": "{{empty}}",
  "then":   { "type": "text", "text": "Nobody is waiting", ... },
  "orElse": { "type": "box", ... } }

condition 이 참이면 then, 아니면 orElse. 둘 다 그냥 노드라서 무엇이든 들어간다.

여기서 {{empty}} 를 쓴 게 중요하다. {{waiting == 0}} 같은 식을 쓰지 않았다. 조건을 정하는 쪽은 화면이 아니라 서버다 — "비었다"의 정의가 바뀌면(예: 예약 대기는 안 세기로 하면) 서버만 고친다.

개수를 모르는 줄

{ "type": "list", "items": "{{rows}}",
  "itemTemplate": { "type": "linear", "direction": "horizontal", "distribution": "spaceBetween",
    "children": [
      { "type": "text", "text": "{{item.no}}", ... },
      { "type": "text", "text": "{{item.since}}", ... } ] } }

items 는 배열을 가리키고, itemTemplate 은 한 줄의 모양이다. 줄이 몇 개인지는 안 적혀 있다.

템플릿 안에서는 {{item.*}} 로 그 줄의 값을 쓴다. 바깥 상태와 이름이 안 섞인다.

{{rows}} 에 셋을 넣으면 셋, 열을 넣으면 열이 생긴다. 파일은 1,234바이트 그대로다.

같은 파일을 두 번 렌더한다

이 편의 주장은 "한 파일이 두 모양이 된다"이다. 캡처 한 장으로는 증명이 안 된다.

await render('step5', {'empty': true, 'waiting': 0, 'rows': <dynamic>[]},
    as: 'step5_empty', ...);

const rows = [
  {'no': '44', 'since': 'waiting 2 min'},
  {'no': '45', 'since': 'waiting 5 min'},
  {'no': '46', 'since': 'waiting 9 min'},
];
await render('step5', {'empty': false, 'waiting': rows.length, 'rows': rows},
    as: 'step5_filled', ...);

그리고 몇 줄이 실제로 만들어졌는지 센다.

final built = [
  for (final r in rows)
    if (find.text(r['no']!).evaluate().isNotEmpty) r['no']!
];
step5_empty   1234 B, 16 lines, type "page" -> step5_empty.png
empty state shows: 1 placeholder, 0 rows
step5_filled  1234 B, 16 lines, type "page" -> step5_filled.png
filled state shows: 0 placeholder, 3 rows (44, 45, 46)

세는 이유는 개수가 틀려도 그림이 그럴듯하기 때문이다. 두 줄만 그려도 카드는 채워져 보인다.

grep -qE '"(44|45|46)"' screens/step5.json && die "step5: the rows are baked in"
grep -q '"itemTemplate"' screens/step5.json || die "step5: no row template"
cmp -s captures/step5_empty.png captures/step5_filled.png && \
  die "step5: the conditional did not change anything"
grep -q 'empty state shows: 1 placeholder, 0 rows' $LOG || \
  die "step5: the empty branch is wrong"
grep -q 'filled state shows: 0 placeholder, 3 rows (44, 45, 46)' $LOG || \
  die "step5: the list did not build one row per item"

{{item.no}} 를 44 로 바꿔서 돌려 보면 첫 줄이 걸린다.

다섯 편

step1  233 B, 4 lines, type "page" -> step1.png
step2  906 B, 12 lines, type "page" -> step2.png
step3  one file, two states, two pictures (27577 B vs 24734 B)
step4  pressed -> the host was asked for: queue.next, queue.take
step5  empty state shows: 1 placeholder, 0 rows
       filled state shows: 0 placeholder, 3 rows (44, 45, 46)
live   ui://desk — 546 B, 6 lines, type "page", title "Desk"
       the server now reports {"waiting":2} after one press

5 steps + the live loop · each checked against its own claim · one host, five screens, no widgets written by hand

마지막 줄이 이 트랙의 요지다. 화면 다섯 개가 서로 다른 것을 하는데 그린 코드는 하나다. 1편 이후 호스트에서 렌더 부분은 안 바뀌었고, 늘어난 건 onToolCall 의 본문과 — 캡처가 못 보는 것을 받아 적는 — 검사용 줄들뿐이다.

세 트랙이 붙는 자리

course-server   여섯 편   도구를 열고, 거절하고, 화면을 자원으로 내주고, 바뀌면 알린다
course-client   다섯 편   붙고, 물어보고, 부르고, 화면을 받고, 구독한다
course-ui       다섯 편   받은 그 화면이 그림이 된다

셋이 한 대기실을 다룬다. 서버의 desk.admit 이 클라이언트의 callTool 이 되고, 서버가 ui://desk 로 내준 JSON 이 이 트랙의 screens/*.json 과 같은 문법이다.

그 말이 맞는지는 마지막 검사가 확인한다. host/test/live_test.dart 가 서버 트랙의 완성본을 실제로 띄우고, ui://desk 를 읽고 — screens/ 에 없는 화면이다 — 그대로 렌더한 뒤, 그 화면이 들고 있던 Admit one 을 누른다.

ui://desk — 546 B, 6 lines, type "page", title "Desk"
nothing about this screen is on disk here; it arrived just now
opened with 3 waiting
pressed Admit one
the server now reports {"waiting":2}

한 번 눌러서 3이 2가 됐다. 그 사이 두 장의 캡처가 서로 다르다는 것까지 검사가 본다. 서버의 화면을 이 폴더로 복사해 오면 [ -e screens/desk.json ] 에서 걸린다 — 복사본을 렌더하면 이 편의 주장이 사라지기 때문이다.

클라이언트 트랙은 모의 서버를 안 쓴다 — 다섯 단계 전부 서버 트랙의 그 서버를 실제로 띄워서 붙는다. 그래서 두 트랙이 어긋나면 검사가 걸린다. 클라이언트 3편에서 실제로 한 번 걸렸고, 서버 쪽 거절 메시지가 잘못 뭉쳐 있던 걸 그때 찾았다.

직접 돌려보기

cd content/sample/course-ui
bash verify.sh
open captures/step5_empty.png captures/step5_filled.png

가져갈 것

  1. conditional — 조건은 상태로 받는다, 식으로 쓰지 않는다
  2. list + itemTemplate — 한 줄의 모양만 적는다
  3. 개수를 센다 — 그림은 개수가 틀려도 그럴듯하다

다음

세 트랙이 다룬 건 한 대기실 하나다. 다음 달부터는 이 화면이 사람이 실제로 서 있는 자리로 내려간다 — 창고, 교대 인수인계, 회선이 끊긴 계산대.

샘플 실행하기

makemind-academy/course_ui/
git clone https://github.com/makemind-academy/course_ui
cd course_ui
bash verify.sh
GitHub에서 열기 →