작업 기간: 2026.10.30–10.30

2026년 10월 30일, 아침 루틴 앱 H_TOOLZ의 아이폰판 1.0.0을 App Store 심사에 낸 뒤 웹판을 앱 화면에 맞췄어요. 웹판은 앱과 따로 만든 HTML 파일 하나였고, claude.ai 비공개 페이지에서 저만 쓰고 있었어요. 저는 방향과 공개 범위를 정했고, 실행은 AI가 맡았어요.

영어 기본 화면부터 취소선을 4px 올리는 것까지 손으로 옮기고 나서야, 앱 코드가 이미 정본인데 웹을 따로 고칠 이유가 있나 싶었어요. 이 글은 앱 코드 하나로 웹판까지 내보내게 된 과정에 대한 기록이에요. 웹을 따로 둘 때와 합칠 때 무엇이 달라지는지, 합치기 전에 무엇을 시험했는지에 답해 볼게요.

왼쪽 웹 따로 패널의 '지금 웹판' 틀 안에 회색 기능마다 두 번 고침, 회색 화면은 손으로 맞춤, 처음 약 0.1MB, 파일 하나 게시가 놓이고, 오른쪽 Flutter 하나로 패널의 '합친 뒤' 틀 안에 초록색 한 번만 고침, 초록색 같은 코드라 같은 화면, 회색 처음 약 10~20MB, 회색 파일 약 50개 게시가 같은 줄로 놓인 비교도
웹을 따로 둘 때와 Flutter 하나로 합칠 때 · 도식 크게 보기

웹판을 맞추는 일이 기능마다 두 번씩 생겼어요

처음 요청은 앱을 시험 서버로 보고 웹판을 계속 따라 맞추자는 것이었어요. AI는 앱에서 정한 화면을 하나씩 웹 코드로 옮겼어요. 영어를 기본으로 두고 오른쪽 위에 EN과 KO를 바꾸는 토글을 달았고, 맨 위 줄에는 주황 네모 안에 흰 H가 들어간 H_TOOLZ 글자 로고를 넣었어요.

작은 차이도 손으로 맞췄어요. 영어로 바꾸면 기록 화면 제목이 LOG가 되고 날짜 줄 끝에 Streak 숫자가 붙어요. 영어 제목은 글자 키가 낮아서, 끝난 항목에 긋는 주황 취소선을 앱처럼 4px 올렸어요. 명상록 영어 30구절은 앱 파일에서 그대로 옮기고, 한국어 구절도 앱과 한 글자씩 대조했어요.

그렇게 웹판은 10번째 버전이 됐어요. AI는 폰 폭 390px와 320px, 밝은 모드와 어두운 모드에서 화면이 옆으로 넘치지 않는지 확인했어요. 그리고 앱에서 먼저 바뀐 것도 기준 문서에 적고 웹에 옮긴다는 절을 새로 더했어요.

그 절을 읽고 나서 걸렸어요. 앞으로도 기능 하나를 바꿀 때마다 Dart 코드와 웹 코드를 한 번씩, 모두 두 번 고쳐야 한다는 뜻이었어요. 그래서 앱이 이미 Flutter로 만든 정본이니 그대로 웹으로 내보내면 되지 않느냐고 물었어요. 운영과 유지보수 쪽에서 검토해 보고하고 기다려 달라고 했어요.

제 처음 가설은 단순했어요. 같은 코드로 웹까지 빌드하면 고칠 곳은 하나로 줄고, 잃는 것은 거의 없다고 봤어요.

합치면 처음 받는 양이 약 0.1MB에서 20MB로 늘었어요

AI는 앱 저장소를 복사한 폴더에서 직접 웹 빌드를 해 봤어요. 원본 저장소는 건드리지 않았어요. 빌드는 16초 만에 끝났고, 결과는 파일 52개에 52MB였어요.

이 중 첫 화면에 실제로 필요한 것만 추렸어요. 실행 파일이 1.8MB, 화면을 그리는 엔진이 5~7MB였어요. 가장 큰 것은 한글 글꼴이었어요. 한 굵기에 2.4MB라서 다섯 굵기를 쓰면 약 12MB가 됐어요.

검토표는 두 안을 나란히 놓았어요. 웹을 따로 두면 처음 열 때 약 0.1MB만 받지만, 기능마다 두 번 고치고 화면은 매번 손으로 맞춰야 해요. Flutter 하나로 합치면 한 번만 고치고 화면도 같은 코드라 저절로 같아지지만, 처음 받는 양이 약 10~20MB로 늘어나요.

잃는 것은 더 있었어요. 지금 웹판은 브라우저 글자라서 확대와 화면 읽기 프로그램이 잘 돼요. Flutter 웹은 글자를 그림처럼 그려서 이 부분이 조금 약해져요. 게시할 파일도 하나에서 약 50개로 늘어나요.

웹에서 그대로 돌지 않는 코드도 나왔어요. 기록 저장(sqflite)과 폰 안 사진 파일(dart:io)은 웹에서 작동하지 않아서, 빌드는 돼도 실행하면 멈춰요. 고칠 곳은 파일 3개에 14군데였어요. 사진, 소리, 화면 꺼짐 방지와 설정 저장 패키지는 웹을 지원했어요.

막힐 곳은 코드보다 올릴 페이지의 규칙이었어요

검토에서 가장 큰 위험은 코드 밖에 있었어요. Flutter 웹은 기본으로 Google 서버에서 엔진을 받아 와요. 그런데 웹판을 올려 둔 claude.ai 페이지는 정해진 몇 곳 외의 외부 스크립트를 막아요.

그래서 엔진 파일을 페이지에 함께 넣어야 했어요. 그렇게 넣었을 때 그 페이지 안에서 wasm(브라우저에서 빠르게 도는 실행 파일 형식)이 실행되는지는 올려 봐야 알 수 있었어요. 빌드가 통과한 것만으로는 답할 수 없는 질문이었어요.

AI의 결론은 맞는 방향이지만 바로 바꾸지는 말자는 것이었어요. 운영 쪽 의견도 하나 더했어요. 앱 기록은 폰 안에, 웹 기록은 claude.ai에 따로 쌓여서 어느 쪽을 골라도 둘은 이어지지 않아요. 매일 아이폰 앱만 쓴다면 웹은 지금 상태로 두는 것이 가장 손이 덜 간다고 했어요.

처음 가설은 여기서 반쯤 고쳐졌어요. 합치면 고칠 곳이 주는 것은 맞았지만, 처음 받는 양과 올릴 곳의 규칙이라는 비용이 붙어 있었어요. 저는 AI의 추천대로 지금 페이지는 그대로 두고 시험부터 하기로 했어요.

바꾸기 전에 비공개 시험 페이지부터 올렸어요

시험의 범위는 좁게 잡았어요. 지금 쓰는 웹 페이지와 앱 기록은 건드리지 않고, 비공개 시험 페이지를 따로 하나 만들어요. 그 페이지가 폰에서 열리는지와 뜨기까지 몇 초 걸리는지만 봐요. 안 열리면 지금 웹판을 그대로 쓰면 됐어요.

AI는 복사본에서만 기록 저장부를 임시로 바꾸고, 엔진을 페이지에 함께 넣어 다시 빌드했어요. 로컬 서버로 띄우자 앱과 같은 화면이 4초 안에 떴어요. 받은 양은 파일 19개에 20MB였고, 실행 파일 1.8MB, 그리기 엔진 5.6MB, 한글 글꼴 다섯 굵기 12MB였어요.

claude.ai에는 38개 파일로 올라갔어요. 페이지에 올릴 수 없는 형식의 파일이 하나 있었는데, 웹에서는 쓰지 않는 파일이라 빼고 올렸어요. 다만 AI가 쓰는 브라우저는 claude.ai에 로그인돼 있지 않아서, 실제 페이지에서 도는지는 AI가 확인할 수 없었어요.

그래서 확인은 제 몫이 됐어요. AI는 앱과 같은 화면이 뜨는지, 불러오는 중이라는 글자만 남는지 봐 달라고 했어요. 저는 폰에서 열어 보고 "열림"이라고만 답했어요. 몇 초 걸렸는지는 답하지 않았고, AI는 글꼴을 줄인 뒤 다시 재면 된다고 정리했어요.

엔진 넣어 빌드에서 로컬 4초 안에, 비공개 시험 페이지를 거쳐 '폰에서 열림?' 관문에 이르고, 안 열리면 점선으로 오른쪽 '지금 웹판 그대로'로 빠지며, 열림이면 아래 초록색 'Flutter 하나로 합치기'로 내려가는 관문 사슬
합치기 전 비공개 시험 페이지 확인 순서 · 도식 크게 보기

이 한마디로 가장 큰 걸림돌이 풀렸어요. AI는 Flutter 하나로 합치는 쪽을 추천했고, 지금 웹 주소에 그대로 올려 쌓인 기록을 잇는 계획을 내놓았어요. 그 계획은 제가 던진 다음 질문에서 바뀌었어요.

누가 쓰느냐에 따라 올릴 곳이 달라졌어요

합치기 전에 세 가지를 물었어요. 웹판은 누구나 들어올 수 있는지, 기능은 같은지, 가운데 Flutter만 고치면 웹과 아이폰과 안드로이드에 다 나가는지였어요.

첫 질문의 답은 지금은 그렇지 않다는 것이었어요. 웹판은 claude.ai의 비공개 페이지라서 로그인한 저만 열 수 있어요. 공유 메뉴로 열어 줄 수는 있지만, 그 사람도 로그인해야 하고 기록은 사람마다 따로 쌓여요.

AI는 나만 쓰는 안과 누구나 쓰는 안을 표로 비교했어요. 나만 쓰면 기록이 claude.ai 계정에 남아 폰과 PC 브라우저끼리, 지금까지의 웹 기록과도 이어져요. 누구나 쓰게 하면 주소를 아는 누구나 열 수 있지만, 기록은 그 브라우저에만 남고 지금까지의 웹 기록과도 끊겨요.

AI의 추천은 나만 쓰는 쪽이었어요. 기록이 이어지고 손이 가장 덜 가기 때문이에요. 저는 "누구나 (공개 웹)"를 골랐어요. 고르면서 받아들인 것은 웹 기록이 그 브라우저에만 남고, 기기끼리도 기존 웹 기록과도 이어지지 않는다는 점이었어요.

버린 선택지는 웹을 claude.ai 비공개 페이지로 계속 두는 안이에요. 기록이 이어지고 손도 덜 가지만, 로그인한 저만 열 수 있어요. 이 결정 하나로 올릴 곳이 claude.ai에서 공개 웹으로 바뀌었어요.

기능은 화면과 쓰는 방법이 같아지고, 몇 가지만 플랫폼마다 달라요. 앱은 카메라를 바로 열고, 웹은 브라우저의 사진 선택 창을 거쳐요. 명상 소리는 같은 파일을 쓰지만, 웹은 화면을 끄거나 다른 앱으로 넘어가면 멈출 수 있어요.

Flutter 웹판이 '누가 쓰나' 관문에서 두 갈래로 나뉘어, 위쪽 점선 '나만' 갈래는 회색 claude.ai 비공개를 거쳐 회색 기록 이어짐으로, 아래쪽 실선 '누구나' 갈래는 초록색 공개 웹을 거쳐 그 브라우저에만 기록으로 이어지는 갈래도
누가 쓰느냐에 따라 갈린 웹판의 올릴 곳 · 도식 크게 보기

쓰는 글자만 남기니 글꼴이 12MB에서 0.7MB가 됐어요

공개 웹 작업은 앱 코드를 아는 Flutter 앱 대화방에 넘겼어요. 그 방은 먼저 기록 저장 코드를 나눴어요. 앱은 지금처럼 폰 안의 SQLite와 파일에, 웹은 그 브라우저 안의 localStorage와 IndexedDB(브라우저에 내장된 저장소)에 저장해요.

처음 받는 양은 글꼴에서 줄였어요. 한글 글꼴에서 앱에 실제로 쓰는 글자만 남기자 12MB가 0.7MB가 됐어요. 같은 글꼴을 쓰는 앱 설치 파일도 그만큼 작아졌어요.

외부 요청도 없앴어요. Flutter 웹은 원래 기본 글꼴도 Google 서버에서 받는데, 그 글꼴까지 사이트 안에 넣었어요. 공개 주소에서 확인해 보니 파일을 받은 곳은 사이트 한 곳뿐이었어요.

오프라인 캐시(서비스 워커)는 일부러 껐어요. 웹을 고쳐 올리면 다음에 열 때 바로 새 판이 보이게 하려는 선택이에요. 대신 올린 직후 최대 10분 동안은 호스팅 쪽 캐시 때문에 예전 판이 보일 수 있다는 점을 남은 위험으로 적었어요.

공개 웹은 GitHub Pages에 올렸어요. AI는 로그인하지 않은 브라우저로 그 주소를 열어 다섯 항목 화면이 뜨는 것을 봤어요. 사진을 고르는 부분은 사람 손이 필요해서, 저는 폰에서 사진 한 장으로 체크해 보고 잘 된다고 답했어요.

고치는 곳은 하나가 됐고 내보내는 곳은 셋이 됐어요

세 번째 질문의 답은 이랬어요. 코드는 한 번만 고치면 되지만, 내보내는 일은 플랫폼마다 따로 해요. 아이폰은 빌드한 뒤 App Store 심사를 거치고, 웹은 빌드해서 다시 올리면 바로 반영돼요.

안드로이드는 그때 준비가 안 돼 있었어요. 앱 저장소에 안드로이드 폴더가 없었기 때문이에요. 저는 하위 대화방을 하나 더 열어 안드로이드판까지 같은 코드로 만들게 했어요. 그 방은 에뮬레이터에서 동작을 확인했고, Google Play에 올릴 AAB 파일 빌드도 통과했어요.

남은 확인도 적어 둘게요. 심사 중인 아이폰 1.0.0은 저장 코드를 나누기 전에 만든 빌드예요. 나눈 뒤 바뀐 파일은 8개이고 코드 검사(flutter analyze)는 통과했지만, 실제 아이폰에서는 아직 돌려 보지 않았어요. 이 코드는 다음 아이폰 업데이트에 함께 들어가요.

안드로이드판도 실제 폰에서는 아직 돌려 보지 않았어요. 웹에서 명상의 내 파일 재생과, 화면을 꺼도 소리가 이어지는지도 확인하지 않았어요. 지금 확인된 것은 공개 웹이 로그인 없이 열리고, 제 폰에서 사진 체크까지 된다는 범위예요.

Flutter 코드 한 벌이 '플랫폼마다 따로' 관문에서 세 곡선 갈래로 나뉘어, 아이폰 빌드는 회색 App Store 심사로, 웹 빌드는 초록색 다시 올리면 반영으로, 안드로이드 AAB는 '에뮬레이터만' 점선을 지나 회색 실제 폰은 아직으로 이어지는 갈래도
Flutter 코드 한 벌에서 나가는 세 플랫폼 · 도식 크게 보기

제가 정한 것과 AI가 한 것

제가 정한 것은 합칠지, 합치기 전에 시험할지, 누가 쓰게 할지였어요. 폰에서 시험 페이지를 열어 본 것과 공개 웹에서 사진으로 체크해 본 것도 제가 했어요.

AI는 복사본에서 빌드해 크기를 재고, 막힐 곳을 미리 짚었어요. 시험 페이지를 올렸고, 저장부를 나누고 글꼴을 줄여 공개 주소에 올렸어요. 자기 브라우저로 확인할 수 없는 부분은 그 이유와 함께 저에게 넘겼어요.

처음 질문은 웹을 따로 고칠 이유가 있느냐였어요. 지금 답은 고칠 곳을 하나로 모으되, 그 전에 올릴 곳에서 정말 도는지를 먼저 본다는 것이에요. 이번에는 시험 페이지 하나와 "열림" 한마디가 그 확인이었어요.

같은 화면을 가진 판이 둘 생기면, 손으로 맞추기 전에 한 코드로 내보낼 수 있는지 시험 페이지 하나로 먼저 확인해요.