이 글은 Today I Learn 시리즈의 72번째 기록입니다. (총 125개)

설치해서 쓰는 앱에는 광고가 안 뜬다. 광고 회사가 승인한 웹사이트에서만 광고를 내주는데 설치형 앱은 그 조건에 안 맞아서, 화면 한구석이 계속 비어 있었다. 그 자리를 내 다른 앱과 블로그를 알리는 데 쓰기로 하고 온실을 만들었다. 만들기 전에 정해야 했던 것들을 남긴다.


앱에 고정할 것과 밖에 둘 것

광고 문구는 계속 바뀐다. 새 앱을 내면 배너가 늘고, 접은 앱은 내려야 한다. 그런데 그 목록이 앱 안에 들어 있으면 문구 한 줄 고치는 데 앱을 다시 만들어 올리고 사용자가 업데이트할 때까지 기다려야 한다. 광고 하나 바꾸자고 치르기엔 큰 비용이다.

그래서 앱에는 “어떻게 그릴지”만 남기고 “무엇을 보여줄지”는 밖으로 뺐다. 앱은 켜질 때 정해진 주소에서 광고 목록을 받아와 그린다. 목록 파일만 고쳐 올리면 이미 깔려 있는 앱들이 다음 실행 때 새 배너를 받는다.

대가도 있다. 내용이 밖에 있으니 인터넷이 없으면 못 받아온다. 그때는 배너 자리를 그냥 비워 두고 앱은 평소대로 돌아가게 했다 — 광고를 못 받았다고 앱이 멈추면 주객이 뒤바뀐다.

무엇을 밖으로 뺄지 고르는 기준은 둘이었다. 자주 바뀌는가, 바뀔 때 사람 손이 얼마나 드는가. 둘 다 크면 밖으로 뺀다.


데이터를 남이 가져가게 하려면 허락 표시가 필요하다

광고 목록 파일과 그걸 읽는 앱은 서로 다른 주소에 산다. 브라우저는 이런 경우 기본적으로 막는다. 아무 사이트나 남의 주소에서 데이터를 긁어가면 곤란하니 파일을 내주는 쪽이 “누가 가져가도 된다”고 표시해야 통과시킨다. 이 허락 표시를 CORS라고 부른다.

우리 쪽은 파일 옆에 규칙 두 줄을 두는 것으로 끝났다.

/ads.json
  Access-Control-Allow-Origin: *

여기서 배운 건 문법이 아니라 실패하는 방식이다. 이 표시가 빠지면 요란한 오류가 뜨는 게 아니라 광고만 조용히 안 나온다. 앱은 멀쩡히 돌아가고 배너 자리만 비어 있으니, 원래 그런 건지 고장 난 건지 화면만 봐서는 구분이 안 된다.

그래서 배포한 뒤에 표시가 실제로 붙었는지를 눈으로 확인하는 절차를 넣었다. 조용히 실패하는 것들은 “돌려보고 이상 없으면 통과” 방식으로는 절대 안 걸린다.


어디에 얹느냐가 규칙을 데려온다

원래 계획은 이 광고 데이터를 블로그에 같이 두는 것이었다. 새 사이트를 만들 필요가 없어 간단해 보였다.

그런데 블로그는 글을 웹페이지로 굽는 도구를 쓰고, 그 도구에는 밑줄로 시작하는 파일을 결과물에서 빼는 규칙이 있다. 하필 앞의 허락 표시를 담는 파일 이름이 밑줄로 시작한다. 블로그에 얹었다면 설정을 따로 손봐야 했고, 안 했으면 허락 표시가 통째로 빠진 채 배포됐을 것이다. 조용히 실패하는 그 방식으로.

남의 집에 얹으면 그 집 규칙이 전부 따라온다. 빌드 규칙만이 아니라 배포에 걸리는 시간, 검색에 뜨는지 여부, 커밋 이력이 섞이는 것까지. 따로 세우면 처음에 십여 분이 더 들지만 그 뒤로는 안 얽힌다.

비교 축을 이렇게 놓으니 답이 단순해졌다. 얹는 쪽의 이득은 초기 설정 몇 분을 아끼는 한 번뿐이고, 분리하는 쪽의 이득은 운영하는 내내 계속된다. 한 번짜리 이득과 계속되는 이득이 맞붙으면 계속되는 쪽이 이긴다.


서버 없이 어디까지 되나

광고 목록을 손으로 짜기는 번거로워서 편집기 페이지를 만들었다. 폼에 제목·주소·노출 기간을 넣으면 목록 파일이 나오는 화면이다. 그런데 저장 버튼을 누르면 어디에 저장되나. 이 사이트에는 서버가 없다.

브라우저만으로 되는 것이 생각보다 많았다. 편집한 결과를 파일로 만들어 내려받게 하는 것, 클립보드로 복사하는 것, 편집 중인 내용을 브라우저에 남겨 새로고침해도 살아 있게 하는 것까지 다 됐다.

안 되는 것은 진짜 저장 하나다. 내가 만든 파일을 받아서 서비스에 반영해 주는 일은 그걸 받아 줄 상대가 있어야 한다. 지금은 그 자리를 사람이 메운다 — 편집기에서 복사해 넘기면 반영된다.

그래서 서버 없이 시작하기로 했다. 판단 근거는 남는 불편의 크기였다. 붙여넣기 한 번이 전부라면 서버를 붙이느라 드는 품과 새로 생기는 위험이 더 크다. 저장 창구를 열면 반드시 잠가야 하는데, 안 잠근 창구는 누구나 내 모든 앱에 광고를 심을 수 있는 구멍이 된다.

대신 나중에 붙일 자리는 열어 뒀다. 편집기는 그대로 두고 “결과를 어디로 보내는가”만 갈아끼우면 되게 만들었다. 이렇게 해 두면 지금 만든 게 나중에 버려지지 않는다.

하나 더 배운 건 브라우저가 공짜로 주는 편의 기능도 거절당할 수 있다는 것이다. 클립보드 복사는 환경이나 권한에 따라 막히는데, 막히면 아무 일도 안 일어난 것처럼 보인다. 그래서 복사가 안 되면 다운로드로 가라고 알려 주는 길을 같이 뒀다. 편의 기능에는 항상 대안 경로를 하나 붙인다.


나만 쓰는 도구라도 열려 있으면 공개 페이지다

편집기는 주소를 아는 사람이면 누구나 열 수 있다. 실제로 쓰는 사람은 나뿐이지만 자물쇠가 걸린 건 아니다.

그래서 나만 쓴다는 이유로 건너뛰고 싶은 것들을 그냥 지켰다. 입력한 내용을 화면에 넣을 때는 글자로만 넣어서 그 안에 섞인 태그가 코드로 해석되지 않게 하고, 검색에 안 잡히게 막고, 비밀번호나 열쇠에 해당하는 값은 아예 두지 않았다.

“어차피 나만 쓴다”로 예외를 만들기 시작하면 그 예외가 습관이 된다. 나중에 진짜 남이 쓰는 화면을 만들 때 그 습관이 그대로 따라간다.


Series: Today I Learn

1 C++ 자료형(Data Type) 2 MD5 vs pHash 3 C++에서 함수의 선언과 정의 4 Tkinter padx, pady 5 메모리와 포인터 변수 6 Call by Value, Call by Reference, Call by Pointer 비교 7 const 8 Gemfile — Jekyll 프로젝트의 의존성 파일 9 kramdown-parser-gfm — Jekyll의 GFM 파서 10 파서(Parser) 11 AHU vs OHU 12 I might try it vs I'll try it 뉘앙스 차이 13 SESSION_EXPIRE_AT_BROWSER_CLOSE=True 14 configuration key 15 Git stash vs discard 16 subprocess.Popen으로 Windows 탐색기에 명령어를 전달 17 Post 잔디 분석하기 18 Google Sheets Sync 최적화 19 DSL (Domain Specific Language)과 GPL (General Purpose Language) 20 마크다운 표 그리는 방법 21 쿼리 파라미터(Query Parameter). 기존 QR코드 재활용 22 Django 보안 취약점 점검 및 수정 23 OOP Object-Oriented Programming 객체 지향 프로그래밍 24 Fernet 대칭 암호화 25 Jekyll 코드블록 안의 Liquid 태그 26 insertOnConflictUpdate vs DoUpdate(target) 27 세션 필터 28 아코디언(Accordiaon) UI를 펼친상태로 만들기 29 input의 step 30 Word Cloud 31 Google Sheets를 데이터 버스로(with AppSheet) 32 Django 모델 텍스트 필드 자동 수집 패턴 33 localStorage로 섹션 토글 상태 유지 34 순차 ID 생성(`select_for_update()` + `max()` 조합) 35 역참조 검색과 distinct() 36 xlsx 다운로드와 로딩 오버레이 충돌 37 Android 파일 공유 MIME 타입 38 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 39 UTF-8 BOM과 PowerShell 파일 쓰기 40 소리꽃 KeyBloom TIL 1 41 소리꽃 KeyBloom TIL 2 42 소리꽃 KeyBloom TIL 3 43 메트로놈 Simple Metronome TIL 1 44 소리꽃 KeyBloom TIL 4 45 메트로놈 Simple Metronome TIL 2 46 소리꽃 KeyBloom TIL 5 47 메트로놈 Simple Metronome TIL 3 48 정적 블로그 SEO 정비와 Pagefind 검색 도입 49 메트로놈 Simple Metronome TIL 4 50 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 51 메트로놈 Simple Metronome TIL 5 52 모래게임 Sandrop TIL 1 53 메트로놈 Simple Metronome TIL 6 54 모래게임 Sandrop TIL 2 55 모래게임 Sandrop TIL 3 56 메트로놈 Simple Metronome TIL 7 57 모래게임 Sandrop TIL 4 58 모래게임 Sandrop TIL 5 59 모래게임 Sandrop TIL 6 60 소리꽃 KeyBloom TIL 6 61 모래게임 Sandrop TIL 7 62 모래게임 Sandrop TIL 8 63 소리꽃 KeyBloom TIL 7 64 모래게임 Sandrop TIL 9 65 소리꽃 KeyBloom TIL 8 66 모래게임 Sandrop TIL 10 67 소리꽃 KeyBloom TIL 9 68 소리꽃 KeyBloom TIL 10 69 소리꽃 KeyBloom TIL 11 70 소리꽃 KeyBloom TIL 12 71 소리꽃 KeyBloom TIL 13 72 온실 GreenHouse TIL 1 읽는 중 73 소리꽃 KeyBloom TIL 14 74 온실 GreenHouse TIL 2 75 소리꽃 KeyBloom TIL 15 76 소리꽃 KeyBloom TIL 16 77 소리꽃 KeyBloom TIL 17 78 소리꽃 KeyBloom TIL 18 79 소리꽃 KeyBloom TIL 19 80 소리꽃 KeyBloom TIL 20 81 로그스톤 상표 셀프 출원 TIL 1 82 소리꽃 KeyBloom TIL 21 83 소리꽃 KeyBloom TIL 22 84 소리꽃 KeyBloom TIL 23 85 소리꽃 KeyBloom TIL 24 86 MiniMacro TIL 1 87 Astro가 무엇인지, 왜 옮기는지 88 MiniMacro TIL 2 89 메트로놈 Simple Metronome TIL 8 90 메트로놈 Simple Metronome TIL 9 91 블로그 Astro 이관 TIL 92 오픈데이 Openday TIL 1 93 오픈데이 Openday TIL 2 94 Unreal Engine MCP TIL 1 95 콘티온 Conti On TIL 11 96 오픈데이 Openday TIL 3 97 오픈데이 Openday TIL 4 98 오픈데이 Openday TIL 5 99 ScorePlayer TIL 1 100 BuildingHub TIL 1 101 ScorePlayer TIL 2 102 BuildingHub TIL 2 103 ScorePlayer TIL 3 104 Unreal Engine MCP TIL 2 105 궁 미로 PalaceMaze TIL 1 106 메트로놈 Simple Metronome TIL 10 107 메트로놈 Simple Metronome TIL 11 108 오픈데이 Openday TIL 6 109 BuildingHub TIL 3 110 Obsidian vault에서 코드만 빼기 — directory junction 111 ScorePlayer TIL 4 112 오픈데이 Openday TIL 7 113 URL은 바뀔 수 있는 것에 묶지 않는다 114 도구는 기능이 아니라 내 일에 맞는지로 고른다 115 돌이키기 힘든 변경은 작게 먼저 확인한다 116 궁 미로 PalaceMaze TIL 3 117 궁 미로 PalaceMaze TIL 2 118 소리꽃 KeyBloom TIL 29 119 오픈데이 Openday TIL 8 120 BuildingHub TIL 4 121 ScorePlayer TIL 5 122 BuildingHub TIL 5 123 MiniMacro TIL 3 124 소리꽃 KeyBloom TIL 30 125 궁 미로 PalaceMaze TIL 4