탐구

화면 층만 녹는다 — 설치 없는 앱, 그리고 컴파일이 얼려 둔 것

작성: makemind · 2026년 7월 10일

창간호에 「설치 없는 앱의 시대」를 실었다. 십 분짜리 수정이 사용자 화면에 닿기까지 무슨 일이 벌어지는지 — 빌드, 심사, 배포, 그리고 사용자가 업데이트를 누를 때까지의 기다림 — 그 사슬을 그린 글이었다.

다음 호에 「우리는 왜 컴파일을 버렸나」를 실었다. 그 사슬의 첫 고리를 물성으로 다시 읽은 글이었다. 컴파일은 그 순간의 모든 결정을 하나의 단단한 덩어리로 얼려 넣는 일이라는 문장이, 이 매거진이 여섯 달 동안 쓴 것 중 가장 좋은 개념 압축이었다.

두 글은 같은 산등성이의 앞뒤 면이었다. 하나는 사슬이 사라지면 무엇이 달라지는가, 다른 하나는 왜 애초에 그랬는가. 그래서 계보도, 우회 시도 세 종류도, 반론 문답도 두 번씩 세웠다. 이번에 하나로 합친다.

그리고 합치면서 고칠 것이 하나 더 있다. 두 글 다 숫자 없이 숫자 이야기를 했다.

"화면 정의는 작다. 무거운 코드 번들이 아니라, '버튼 하나, 텍스트 하나'라는 가벼운 명세다."

작다고 적었는데 몇 바이트인지 재지 않았다. "한쪽에선 사흘이 걸리고 한쪽에선 십 초"라고 적었는데 십 초를 재지 않았다. 그때는 잴 것이 없었다. 지금은 있다.

얼음 — 컴파일이 하는 일

먼저 살릴 것부터. 컴파일을 물성으로 읽은 그 대목은 그대로 간다.

우리가 앱을 만든다는 건, 그 순간의 모든 결정을 하나의 덩어리로 얼리는 일이었다. 버튼이 어디 있을지, 무슨 색일지, 누르면 무엇이 일어날지 — 그 결정들이 컴파일을 거쳐 기기 안에 굳는다. 굳은 다음에는 한 글자도 못 바꾼다. 바꾸려면 다시 얼려서, 다시 심사받고, 다시 배포해서, 사용자가 받아 주기를 기다려야 한다.

배포의 고통은 게으름이나 낡은 도구 탓이 아니었다. 화면이라는 결정이 기기 안에 얼어 있다는 구조의 필연이었다.

여기서 물음이 하나 선다. 원래 글이 정식화한 그 물음이다. 능력과 얼음은 정말 분리될 수 없는가? 네이티브 능력을 쓰려면 반드시 얼려야 하는가, 아니면 화면 층만 녹이고 능력 층은 얼린 채 둘 수 있는가.

여섯 달 동안 그걸 실제로 만들어 봤다. 답부터 적는다. 분리된다. 그리고 얼린 채 두어야 하는 것이 생각보다 많다.

정의는 얼마나 작은가 — 실측

원래 글의 가장 약한 문장부터 갚는다. "정의는 작다"에 숫자가 없었다.

지금은 실제로 재 볼 수 있다. 아래는 전부 돌아가는 것에서 받은 크기다.

무엇크기
STM32H723 보드가 건넨 ui://app (페이지 하나)656 B
ESP32가 건넨 ui://app (라우트를 가진 애플리케이션)158 B
그 애플리케이션의 첫 화면 ui://page/main1,894 B
매장 서버의 키오스크 화면1,241 B
같은 서버의 POS 화면1,170 B
같은 서버의 주방 화면1,144 B
무인매장 번들 전체 (JSON 4개)6 KB

한 화면이 1~2 킬로바이트다. 작다는 말은 맞았다. 다만 그때는 그게 맞는지 모르고 적었고, 지금은 안다.

숫자가 생기니 원래 글이 못 한 이야기가 하나 붙는다. 매장 서버가 세 화면을 각각 내주는데 그 셋의 합이 3,555 바이트다. 옛 방식이라면 태블릿 앱·POS 앱·주방 앱 세 개를 만들어 각각 심사받고 각각 배포했을 자리다. 세 개의 앱이 3.5 킬로바이트로 대체된 게 아니라, 세 개의 앱이 있어야 할 이유가 없어진 것이다.

(측정 출처: 칩 하나에 붙이면, 붙인 기기가 단말이 된다 · 보드가 자기 화면을 건넨다 · 폴더 하나가 앱이다)

십 분짜리 수정은 얼마나 걸리나 — 실측

원래 글이 그린 사슬의 반대편이다. 얼리지 않으면 정말 즉시 도달하는가.

무인매장 앱의 화면 JSON을 고치고 다시 돌렸다. 로그다.

[+ 1415ms] edited ui/pages/main.json — 2940 B -> 2934 B, no compiler ran
[+ 1419ms] route "/" -> ui://pages/main  ("Unmanned Store")
[+ 1498ms] captured 04_edited_json.png

편집부터 화면 반영까지 83 밀리초, 컴파일러는 실행되지 않았다. 라우트 이동은 4 밀리초였다. 화면 전환이 파일 조회라서 그렇다.

"십 초"라고 적었던 자리에 이제 실제 숫자가 들어간다. 그리고 여기서 원래 글이 못 본 게 하나 드러난다 — 83 밀리초는 빌드가 없어서가 아니라 배포가 없어서다. 빌드가 0.1초로 줄었다 해도 심사와 배포와 사용자의 업데이트 수락이 남아 있으면 사슬은 그대로다. 녹는 것은 컴파일이 아니라 사슬 전체다.

무엇이 여전히 얼어 있어야 하는가

여기가 두 글 중 「컴파일을 버렸나」가 가진 최고의 절이고, 실제로 만들어 보니 그 절이 옳았다. 다만 원래 글은 그것도 서술로만 적었다.

실시간 루프는 얼린 채 남는다. LCD 패널 샘플에서 온도 제어 루프는 C 펌웨어 안에 있다.

/* 1차 열역학: dT/dt = k·(target − T). 목표로 수렴, 약간의 센서 노이즈. */
static void thermal_update(void) {
    /* ... */
    double alpha = 1.0 - exp(-k * dt);
    g_temp += (g_target - g_temp) * alpha;
}

이건 화면 정의로 내려올 수 없다. 밀리초 단위로 도는 것이 네트워크 너머에 있으면 안 되기 때문이다.

안전 범위도 얼린 채 남는다. 그리고 이건 더 강하다 — 위에서 무엇을 시키든 하드웨어가 자기를 넘지 않는다.

/* The relay clamps its own range. Whatever the rule upstream
 * decided, the hardware still refuses to exceed itself. */
if (v < 0) v = 0;
if (v > 100) v = 100;

원래 글은 "실시간·드라이버·보안은 얼린 채, 화면 층만 액체로"라고 적었다. 만들어 보니 그 목록에 한 줄을 더해야 했다. 자기 한계를 스스로 지키는 것. 규칙이 녹아 있다는 건 규칙이 틀릴 수도 있다는 뜻이고, 그때 마지막으로 붙잡는 것은 얼어 있는 쪽이어야 한다.

결제 승인 경로도 얼린 채 남는다. 카드 단말의 보안 영역은 열지 않는다. 그건 못 열어서가 아니라 안 여는 편이 낫기 때문이고, 그 이유는 내놓지 않은 것이 앱을 지킨다에 따로 적었다.

(펌웨어 경계는 LCD 업체가 HMI를 30분에, 릴레이 클램프는 센서를 갈아 끼워도 제어는 그대로)

규약의 이름 — 두 글이 끝까지 안 부른 것

두 글 다 이 규약을 끝까지 무명으로 뒀다. "약속된 언어", "열린 공통 언어". 열려 있어야 성립한다고 주장하는 글이 그 규약의 지시 대상을 흐리면, 독자는 검증하러 갈 데가 없다.

이름은 MCP(Model Context Protocol)다. 그리고 이 시리즈의 모든 샘플이 그것으로 말한다. 실제 바이트는 이렇게 생겼다.

=> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18", ...}}
<= {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26",
     "capabilities":{"tools":{},"resources":{}},
     "serverInfo":{"name":"WeAct H723 MCP Node","version":"0.1.0"}}}

이건 실제 STM32 보드가 USB 시리얼로 답한 줄이다. 그리고 열려 있다는 주장도 이제 근거가 있다 — 같은 클라이언트 코드로 STM32(UART)와 ESP32(Wi-Fi TCP)에 붙었다. 두 보드는 서로를 모르고, 다른 회사 칩이고, 전송이 다르다. 그래도 같은 규약을 쓰기에 통했다.

원래 글이 "브라우저가 어느 회사 서버의 HTML이든 그려 내는 원리와 정확히 같다"고 적은 그 문장은, 이제 비유가 아니라 두 보드의 로그다.

반론에 다시 답한다 — 이번엔 숫자로

두 글이 각각 다섯 문답을 세웠고 넷이 겹쳤다. 한 번만 세우되, 답할 수 있게 된 것부터 고쳐 답한다.

"매 화면을 서버에서 받으면 느리지 않나?" — 화면 정의는 1~2 KB다(위 표). 그리고 왕복은 링크에 달렸다. 한 실행에서 UART 쪽 도구 왕복이 1~11 ms, Wi-Fi TCP 쪽이 21~69 ms였고, 연결부터 첫 화면까지는 46 ms 대 189 ms였다. 그러니 정확한 답은 이렇다 — 느리지 않다가 아니라, 링크에 따라 자릿수가 다르고 그 차이가 UI 설계를 가른다. 한 자릿수 쪽은 "누르면 바로"를 기대해도 되고, 수십 밀리초 쪽은 대기 상태를 화면에 그려야 한다. 원래 글은 이 갈라짐을 몰랐다.

여기서 하나 덧붙인다. 시간 값은 재현되지 않는다. 크기는 파일이라 몇 번을 재도 같지만 지연은 실행마다 흔들렸고, 어떤 실행에서는 Wi-Fi 쪽이 위 값의 두 배를 넘었다. 그래서 이 글은 정확한 밀리초가 아니라 자릿수만 주장한다.

"오프라인에선?" — 이건 아직 못 갚는다. 원래 글은 "정의는 캐시된다"고 적었는데, 이 시리즈의 어떤 샘플도 캐시와 오프라인 동작을 시험하지 않았다. 재연결 동작도 재지 않았다. 미검증으로 남긴다.

"보안은?" — 런타임이 정의를 불신하고 정해진 조합만 허용한다는 원래 답은 유효하다. 거기에 만들어 보고 알게 된 것을 더한다. 경계는 하나가 아니라 셋이고(받는 쪽 런타임·내보내는 쪽 설계·자기 한계를 지키는 하드웨어), 도구 목록에 없는 능력은 사람이 눌러서도 모델이 골라서도 닿지 않는다. 실증은 별도 편에 있다. 다만 "그러므로 안전하다"는 이 시리즈가 증명하지 않았다 — 인증·권한·전송 보안은 전부 밖이다.

"앱스토어 정책은?" — 원래 글의 답을 그대로 둔다(경계는 임의 네이티브 코드를 심느냐에 있다). 그리고 원래 글이 스스로 단 단서도 그대로 둔다 — 이건 정책 해석이지 법률 자문이 아니다. 여기에 하나를 덧붙인다. 두 글 다 근거 가이드라인을 한 줄도 인용하지 않았다. 인용 없는 해석은 해석이 아니라 짐작이므로, 이 항목은 근거를 붙이기 전까지 미해결로 표시한다.

"서버 비용은?" — 원래 답 유지. 다만 이 시리즈가 만든 서버들은 전부 로컬 프로세스라 운영 비용을 잴 위치에 있지 않았다.

이 샘플이 안 한 것

원래 두 글의 정직 절은 유지한다. 컴파일은 사라지는 게 아니라 자리를 옮기고, 기존 시스템이 하루아침에 변신하지 않고, 디자인이라는 노동은 남는다. 만들어 보니 셋 다 맞았다.

여기에 이번에 알게 된 것을 더한다.

두 글의 사고실험은 사고실험이었다. 매장 기기 장면도, 창간호의 장면도 "~라고 하자"로 시작하는 상상인데 구체적으로 그려져 관측처럼 읽혔다. 이 글이 인용한 것은 전부 실행 로그와 실측이고, 각각 어느 편에서 왔는지 링크로 달았다. 상상과 관측을 같은 서체로 쓰지 않는다.

측정은 좁다. 위 숫자들은 한 대의 맥, 한 대의 공유기, 한 방 안에서, 대부분 한 번씩 잰 값이다. 표본이 적고 환경이 하나다. 크기 값은 결정적이지만(파일이 그만큼이다) 지연 값은 실행마다 달라진다 — 이 글을 쓰는 동안에도 앞서 인용했던 지연 수치가 재실행으로 바뀌어, 실린 로그와 맞추려고 한 번 고쳤다. 시간을 인용하는 글은 그 로그와 함께 늙는다.

세 가지는 아직 못 갚았다. 오프라인·캐시 동작, 앱스토어 정책의 근거 인용, 그리고 운영 비용. 원래 글이 답한 자리에 지금도 실측이 없다.

그래서, 다시 의심하라

원래 글이 그렇게 닫았다. 의심하라고, 다음 글들이 실물로 보일 거라고.

여섯 달이 지났고 그 다음 글들이 나왔다. 이 글이 인용한 숫자는 전부 그 편들에서 나온 것이고, 각 편에는 빌드 로그와 실행 로그와 헤드리스로 찍은 화면이 붙어 있다. 재현하고 싶으면 폴더를 떼어 가서 돌리면 된다.

그리고 여전히 의심할 자리를 남겨 둔다. 오프라인은 안 재 봤고, 정책은 근거를 안 달았고, 비용은 잴 위치에 있지 않았다. 그 셋은 아직 그때와 같은 상태다.

화면 층은 녹았다. 그건 이제 656 바이트와 83 밀리초로 말할 수 있다. 무엇이 얼어 있어야 하는지도 알게 됐고, 그 목록은 처음 생각보다 길었다. 남은 것은 아직 재지 않은 것들이고, 그건 재고 나서 적는다.


makemind.dev 「탐구」 — 그때 재지 않고 적은 숫자를 실측으로 바꿨다.

Twitter