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

한 팀만 쓰는 도구를 처음 세운 날, 기능보다 먼저 “누가 들어오고 무엇을 지키나”를 정해야 했다. 그날 내린 판단들을 모았다.


아무나 들어오면 안 되는 도구였다

콘티온은 한 팀만 쓰는 도구라 누구나 가입해서 쓰게 둘 수 없었다. 로그인은 구글 계정으로 하되(OAuth — 비밀번호를 우리가 보관하지 않고 구글이 “이 사람이 맞다”고 확인해 주는 방식), 로그인했다고 바로 쓰게 하지 않고 관리자가 승인해야 들어오는 승인제로 하기로 했다. 오래 안 들어온 계정은 잠기게 해서, 떠난 사람의 계정이 열린 채 남지 않게 했다.

이렇게 두면 앱이 비밀번호를 아예 갖지 않는다. 저장한 비밀번호가 없으니 샐 비밀번호도 없다. 대신 문을 지키는 일이 구글 계정으로 넘어간다. 이게 뒤의 급소 이야기로 이어진다.


여럿이 고치다 보니 “이게 최종이야?”가 흐려졌다

콘티(곡 순서표)를 여럿이 만지다 보면 어느 게 확정본인지 흐려진다. 그래서 콘티에 초안과 승인 두 상태를 두고, 승인권이 있는 사람이 확정하면 그 콘티는 잠기게 했다. 잠긴 콘티는 PPT 내보내기만 되고, 다시 고치려면 승인권자가 잠금을 풀어야 한다.

여럿이 쓰는 문서에는 “지금 고쳐도 되는 상태인가”가 눈에 보여야 한다. 상태가 없으면 확정했다는 사실이 사람들 기억에만 남고, 누군가 무심코 한 곡을 바꿔도 알 길이 없다.


지운 걸 되돌릴 수 있어야 했다

곡이나 콘티를 지우면 정말 없어지게 두지 않기로 했다. 여럿이 쓰는 도구에서는 누군가 실수로 지우는 일이 반드시 생기고, 그때 되돌릴 길이 없으면 그 곡을 처음부터 다시 입력해야 한다.

그래서 지우기를 소프트 삭제(soft delete — 데이터를 실제로 없애지 않고 “지운 시각”만 표시해 목록에서 빼는 방식)로 했다. 쓰는 사람 눈에는 사라진 것처럼 보이지만 데이터는 그대로 있어서, 휴지통에서 표시만 지우면 돌아온다.

이와 별개로 쓰는 데이터베이스(Cloudflare D1)는 최대 30일 전 시점으로 통째로 되돌리는 기능(Time Travel)을 기본으로 준다. 한 곡을 잘못 지운 일상적인 실수는 휴지통으로, 데이터베이스 전체가 잘못된 사고는 시점 복원으로 층을 나눴다. 되돌리기에도 크기가 있다.


두 사람이 같은 곡을 동시에 고치면

같은 곡을 두 사람이 동시에 열어 고치면 나중에 저장한 쪽이 앞사람의 수정을 덮어쓴다. 그래서 누가 곡 편집을 열면 그 곡을 잠가, 다른 사람은 “지금 누가 편집 중”이라는 안내를 보고 못 열게 하기로 했다.

그런데 잠근 사람이 저장하지 않고 브라우저를 그냥 닫으면 그 곡은 영영 잠긴다. 그래서 잠금에 만료 시간을 붙였다. 몇 분 동안 아무 신호가 없으면 풀린 것으로 보고, 편집 화면을 열어 둔 동안에는 1분마다 “아직 보고 있다”는 신호(하트비트 heartbeat — 살아 있음을 알리려고 주기적으로 보내는 신호)를 보내 살아 있는 편집은 안 풀리게 했다.

잠금은 거는 것보다 풀리는 조건을 먼저 정해야 한다. 사람이 잠금을 풀 거라고 믿으면, 그 사람이 창을 닫는 순간 아무도 못 쓰는 곡이 생긴다.


곡 구분을 관리자가 늘릴 수 있게

곡의 구분(카테고리) 목록은 처음에 코드에 적어 두었다. 구분이 하나 늘 때마다 코드를 고쳐 다시 배포해야 하니, 관리자가 화면에서 직접 추가·수정하게 데이터베이스로 옮기기로 했다.

옮기고 보니 짝을 맞춰야 할 곳이 생겼다. 곡들은 구분을 이름으로 들고 있어서, 구분 이름만 바꾸면 곡들은 옛 이름을 든 채 남는다. 그래서 이름을 바꾸면 그 구분에 속한 곡들의 값도 같이 바꾸고, 구분을 지우면 곡은 지우지 않고 “없음”으로 옮기게 했다.

목록을 코드 밖으로 빼면 바꾸는 사람이 바뀐다. 그 대신 그 목록을 이름으로 가리키던 곳들이 같이 따라가게 챙겨야 한다.


팀원 표가 한 줄에 안 들어갔다

관리자 화면의 팀원 표에 이름·이메일·부서·권한·상태·마지막 로그인·작업 버튼까지 다 넣었더니 한 줄에 안 들어가 줄이 꺾였다. 칸을 줄이는 대신 목록과 상세로 나눴다. 목록에는 이름·부서·권한 표시만 두고, 사람을 누르면 상세 화면에서 권한을 바꾸고 그 사람의 접속 기록까지 본다.

이런 구성을 마스터-디테일(master-detail — 목록에서 하나를 고르면 그 항목의 전체 내용을 따로 보여주는 배치)이라 부른다. 관리자 도구에서 흔한 모양인 데는 이유가 있다. 표는 칸이 늘수록 한 줄이 무너지고, 한 사람에 대해 할 일이 많아질수록 그 일은 그 사람을 고른 뒤의 화면에 모으는 편이 낫다.


실명을 어디서 가리나

콘티 기록·검수 목록·휴지통처럼 팀 전체가 보는 화면에는 실명 대신 가운데를 가린 이름을 보여주기로 했다(홍길동 → 홍*동). 실명은 관리자 화면에서만 보인다.

가리는 자리는 화면이 아니라 서버다. 화면에서만 가리면 보기엔 가려져 있어도 서버가 내려준 데이터에는 실명이 그대로 들어 있어서, 브라우저 개발자도구(웹페이지가 주고받는 데이터를 열어 볼 수 있는 브라우저 내장 도구)로 열면 다 보인다. 그래서 팀이 보는 데이터는 서버가 가린 채로 내보내고, 실명이 담긴 데이터는 관리자에게만 준다.

“안 보이게 했다”와 “안 보냈다”는 다르다. 보내 놓고 가린 건 가린 척일 뿐이다.


뚫린다면 어디부터 뚫릴까

해킹당한다면 원인이 뭘지 물었더니, 답은 코드 버그가 아니었다. 급소는 계정과 시크릿(비밀키 — 서버만 알고 있어야 하는 값)이었다.

  • 로그인을 구글에 맡겼으니 관리자의 구글 계정이 사실상 이 앱의 마스터키다. 그 계정이 털리면 앱도 열린다. 그래서 1순위 방어는 코드가 아니라 관리자 계정의 2단계 인증(2FA — 비밀번호에 더해 휴대폰 확인을 한 번 더 거치는 것)이었다.
  • 서버는 “이 사람은 로그인했다”는 표식에 비밀키로 서명한다. 이 비밀키가 새면 누구로든 로그인한 척할 수 있다. 그래서 비밀키는 코드·커밋·로그 어디에도 남기지 않고, 실수로 저장소 기록에 들어간 적이 없는지도 점검해 두었다.

코드 쪽에서 흔히 노리는 공격은 기본 방어가 이미 되어 있었다.

  • 주입(injection) — 입력창에 명령문을 섞어 넣어 데이터베이스가 그걸 명령으로 착각하게 만드는 수법. 사용자가 넣은 값을 명령문에 이어 붙이지 않고 “값”으로 따로 넘기면 막힌다.
  • XSS — 누가 이름 칸에 화면 조작 코드를 적어 넣으면 그게 다른 사람 화면에서 실행되는 공격. React(화면을 만드는 도구)는 사용자가 넣은 글자를 코드가 아니라 글자로만 그려서 기본으로 막는다.
  • CSRF — 로그인된 사람이 낚시 링크를 누르면 본인도 모르게 우리 서버로 요청이 날아가게 만드는 수법. 다른 사이트에서 시작된 요청에는 로그인 표식이 따라가지 않게 막아 두었다.

정리하면 이 도구의 보안은 팀 구글 계정들의 2단계 인증과 비밀키 관리, 이 두 가지로 모였다. 지킬 곳이 분명해지니 “지금 더 조일 곳은 없다”는 판단도 설 수 있었다. 어디를 지킬지 모르면 모든 곳을 조금씩 걱정하게 되고, 정작 급소는 비어 있기 쉽다.


자동 배포를 붙이려니 안 됐다

Cloudflare Pages(정적 사이트와 작은 서버 기능을 함께 올려 주는 호스팅)에 올리는 방식은 두 가지다. 직접 업로드(direct upload — 내 컴퓨터에서 명령으로 올리는 것)와 Git 연동(저장소에 push하면 알아서 빌드·배포되는 것). 처음엔 직접 업로드로 만들었는데, 나중에 push하면 자동으로 배포되게 붙이려 하니 안 됐다. 이 선택은 프로젝트를 만들 때 정해지고 서로 바꿀 수 없어서, 바꾸려면 프로젝트를 새로 만들고 시크릿도 다시 넣어야 했다.

그래서 자동 배포는 접고 둘을 떼어 운영하기로 했다. push는 코드 백업(GitHub), 배포는 따로 명령으로. push해도 사이트는 안 바뀌고, 배포해도 GitHub은 안 바뀐다. 배포가 잦지 않은 도구라 이 편이 오히려 단순했다.

나중에 못 바꾸는 선택은 처음 만들 때 조용히 지나간다. 무엇이 되돌릴 수 없는 선택인지 만들기 전에 한 번 물어 두는 편이 싸다.


요약

  • 닫힌 팀 도구는 구글 로그인에 승인제를 얹어 문을 닫는다. 비밀번호를 안 가지니 샐 비밀번호도 없고, 대신 문지기가 구글 계정이 된다.
  • 여럿이 고치는 문서는 초안·승인 상태를 두고 확정본을 잠가야 “이게 최종인가”가 흐려지지 않는다.
  • 지우기는 소프트 삭제로 되돌릴 수 있게 한다. 일상 실수는 휴지통, 큰 사고는 데이터베이스 시점 복원으로 층을 나눈다.
  • 편집 잠금은 풀리는 조건(만료 시간)부터 정한다. 창을 닫고 떠난 사람 때문에 영영 잠기는 곡이 없어야 한다.
  • 목록을 코드에서 데이터베이스로 빼면 관리자가 직접 바꾼다. 대신 그 이름을 가리키던 곡들도 같이 따라가게 한다.
  • 표의 칸이 넘치면 칸을 줄이지 말고 목록과 상세(마스터-디테일)로 나눈다.
  • 실명은 서버에서 가린다. 보내 놓고 화면에서 가리면 개발자도구로 다 보인다.
  • 이런 도구의 급소는 코드가 아니라 계정(관리자 구글 계정의 2FA)과 시크릿(서명 비밀키)이다. 지킬 곳이 분명해야 “더 조일 곳이 없다”는 판단도 선다.
  • Cloudflare Pages의 직접 업로드와 Git 연동은 만들 때 정해지고 못 바꾼다. 되돌릴 수 없는 선택은 만들기 전에 묻는다.

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 습관만들기 Pawbit TIL 1 27 insertOnConflictUpdate vs DoUpdate(target) 28 세션 필터 29 아코디언(Accordiaon) UI를 펼친상태로 만들기 30 input의 step 31 습관만들기 Pawbit TIL 2 32 습관만들기 Pawbit TIL 3 33 Word Cloud 34 Google Sheets를 데이터 버스로(with AppSheet) 35 Django 모델 텍스트 필드 자동 수집 패턴 36 localStorage로 섹션 토글 상태 유지 37 순차 ID 생성(`select_for_update()` + `max()` 조합) 38 역참조 검색과 distinct() 39 xlsx 다운로드와 로딩 오버레이 충돌 40 Android 파일 공유 MIME 타입 41 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 42 크레센도 Phaser + TypeScript TIL 1 43 크레센도 Phaser + TypeScript TIL 2 44 크레센도 Phaser + TypeScript TIL 3 45 크레센도 Phaser + TypeScript TIL 4 46 크레센도 Phaser + TypeScript TIL 5 47 여둘까 Office Layout TIL 1 48 습관만들기 Pawbit TIL 4 49 여둘까 Office Layout TIL 2 50 여둘까 Office Layout TIL 3 51 크레센도 Phaser + TypeScript TIL 6 52 UTF-8 BOM과 PowerShell 파일 쓰기 53 소리꽃 KeyBloom TIL 1 54 습관만들기 Pawbit TIL 5 55 여둘까 Office Layout TIL 4 56 소리꽃 KeyBloom TIL 2 57 소리꽃 KeyBloom TIL 3 58 메트로놈 Simple Metronome TIL 1 59 소리꽃 KeyBloom TIL 4 60 메트로놈 Simple Metronome TIL 2 61 소리꽃 KeyBloom TIL 5 62 습관만들기 Pawbit TIL 6 63 크레센도 Phaser + TypeScript TIL 7 64 메트로놈 Simple Metronome TIL 3 65 소설 타임라인 관리 도구 Lumior TIL 1 66 습관만들기 Pawbit TIL 7 67 정적 블로그 SEO 정비와 Pagefind 검색 도입 68 크레센도 Phaser + TypeScript TIL 8 69 메트로놈 Simple Metronome TIL 4 70 습관만들기 Pawbit TIL 8 71 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 72 메트로놈 Simple Metronome TIL 5 73 모래게임 Sandrop TIL 1 74 습관만들기 Pawbit TIL 9 75 메트로놈 Simple Metronome TIL 6 76 모래게임 Sandrop TIL 2 77 모래게임 Sandrop TIL 3 78 습관만들기 Pawbit TIL 10 79 습관만들기 Pawbit TIL 11 80 메트로놈 Simple Metronome TIL 7 81 모래게임 Sandrop TIL 5 82 모래게임 Sandrop TIL 4 83 습관만들기 Pawbit TIL 12 84 모래게임 Sandrop TIL 6 85 소리꽃 KeyBloom TIL 6 86 모래게임 Sandrop TIL 7 87 모래게임 Sandrop TIL 8 88 소리꽃 KeyBloom TIL 7 89 모래게임 Sandrop TIL 9 90 소리꽃 KeyBloom TIL 8 91 모래게임 Sandrop TIL 10 92 소리꽃 KeyBloom TIL 9 93 콘티온 ContiOn TIL 1 읽는 중 94 소리꽃 KeyBloom TIL 10 95 콘티온 ContiOn TIL 2 96 습관만들기 Pawbit TIL 13 97 콘티온 ContiOn TIL 3 98 모래게임 Sandrop TIL 11 99 소리꽃 KeyBloom TIL 11 100 여둘까 Office Layout TIL 5 101 모래게임 Sandrop TIL 12 102 모래게임 Sandrop TIL 13 103 콘티온 ContiOn TIL 4 104 모래게임 Sandrop TIL 14 105 소리꽃 KeyBloom TIL 12 106 습관만들기 Pawbit TIL 14 107 모래게임 Sandrop TIL 15 108 모래게임 Sandrop TIL 16 109 소리꽃 KeyBloom TIL 13 110 온실 GreenHouse TIL 1 111 콘티온 ContiOn TIL 5 112 모래게임 Sandrop TIL 17 113 소리꽃 KeyBloom TIL 14 114 온실 GreenHouse TIL 2 115 콘티온 ContiOn TIL 6 116 모래게임 Sandrop TIL 18 117 콘티온 ContiOn TIL 7 118 모래게임 Sandrop TIL 19 119 소리꽃 KeyBloom TIL 15 120 모래게임 Sandrop TIL 20 121 모래게임 Sandrop TIL 21 122 소리꽃 KeyBloom TIL 16 123 소리꽃 KeyBloom TIL 17 124 소리꽃 KeyBloom TIL 18 125 여둘까 Office Layout TIL 6 126 소리꽃 KeyBloom TIL 19 127 소리꽃 KeyBloom TIL 20 128 콘티온 ContiOn TIL 8 129 콘티온 ContiOn TIL 9 130 로그스톤 상표 셀프 출원 TIL 1 131 소리꽃 KeyBloom TIL 21 132 로그스톤 샵 LogStone Shop TIL 1 133 소리꽃 KeyBloom TIL 22 134 로그스톤 샵 LogStone Shop TIL 2 135 소리꽃 KeyBloom TIL 23 136 모래게임 Sandrop TIL 22 137 모래게임 Sandrop TIL 23 138 소리꽃 KeyBloom TIL 24 139 로그스톤 샵 LogStone Shop TIL 3 140 모래게임 Sandrop TIL 24 141 소리꽃 KeyBloom TIL 25 142 모래게임 Sandrop TIL 25 143 MiniMacro TIL 1 144 모래게임 Sandrop TIL 26 145 콘티온 ContiOn TIL 10 146 Astro가 무엇인지, 왜 옮기는지 147 모래게임 Sandrop TIL 27 148 소리꽃 KeyBloom TIL 26 149 MiniMacro TIL 2 150 로그스톤 샵 LogStone Shop TIL 4 151 메트로놈 Simple Metronome TIL 8 152 모래게임 Sandrop TIL 28 153 여둘까 Office Layout TIL 7 154 메트로놈 Simple Metronome TIL 9 155 블로그 Astro 이관 TIL 156 여둘까 Office Layout TIL 8 157 로그스톤 샵 LogStone Shop TIL 5 158 모래게임 Sandrop TIL 29 159 오픈데이 Openday TIL 1 160 로그스톤 샵 LogStone Shop TIL 6 161 모래게임 Sandrop TIL 30 162 오픈데이 Openday TIL 2 163 모래게임 Sandrop TIL 31 164 Unreal Engine MCP TIL 1 165 모래게임 Sandrop TIL 32 166 소리꽃 KeyBloom TIL 27 167 콘티온 ContiOn TIL 11 168 오픈데이 Openday TIL 3 169 로그스톤 샵 LogStone Shop TIL 7 170 모래게임 Sandrop TIL 33 171 소리꽃 KeyBloom TIL 28 172 오픈데이 Openday TIL 4 173 여둘까 Office Layout TIL 9 174 오픈데이 Openday TIL 5 175 ScorePlayer TIL 1 176 모래게임 Sandrop TIL 34 177 BuildingHub TIL 1 178 ScorePlayer TIL 2 179 여둘까 Office Layout TIL 10 180 BuildingHub TIL 2 181 ScorePlayer TIL 3 182 Unreal Engine MCP TIL 2 183 궁 미로 PalaceMaze TIL 1 184 메트로놈 Simple Metronome TIL 10 185 메트로놈 Simple Metronome TIL 11 186 오픈데이 Openday TIL 6 187 BuildingHub TIL 3 188 Obsidian vault에서 코드만 빼기 — directory junction 189 ScorePlayer TIL 4 190 여둘까 Office Layout TIL 11 191 오픈데이 Openday TIL 7 192 URL은 바뀔 수 있는 것에 묶지 않는다 193 도구는 기능이 아니라 내 일에 맞는지로 고른다 194 돌이키기 힘든 변경은 작게 먼저 확인한다 195 궁 미로 PalaceMaze TIL 2 196 궁 미로 PalaceMaze TIL 3 197 소리꽃 KeyBloom TIL 29 198 오픈데이 Openday TIL 8 199 BuildingHub TIL 4 200 ScorePlayer TIL 5 201 로그스톤 샵 LogStone Shop TIL 8 202 여둘까 Office Layout TIL 12 203 오픈데이 Openday TIL 9 204 BuildingHub TIL 5 205 MiniMacro TIL 3 206 소리꽃 KeyBloom TIL 30 207 오픈데이 Openday TIL 10 208 여둘까 Office Layout TIL 13 209 소리꽃 KeyBloom TIL 31 210 궁 미로 PalaceMaze TIL 4 211 로그스톤 샵 LogStone Shop TIL 9 212 로그스톤 샵 LogStone Shop TIL 10 213 소리꽃 KeyBloom TIL 32