노닥 스터디 2주차 과제 · 챗사피엔스 운영 DB(Supabase/Postgres) → 에어테이블 거울 복제 · 2026-10-14 18:00 KST 스냅샷
v2_)을 같은 에어테이블 안에 14개 세워서 나란히 비교했습니다. 다만 지금 당장 손댈 건 이름 바꾸기와 안 쓰는 칸 지우기뿐입니다. 큰 수술은 사람이 수백 명 될 때.students(회원 명단), recording_orders(주문 내역)처럼 시트가 12장 있습니다.CS-20261002-1531-7HJZ8H 처럼 사람이 읽는 주문번호. 컴퓨터가 내부에서 쓰는 번호(1531)와는 다른 것입니다. 이 둘을 같은 이름으로 불렀다는 게 핵심 3번 문제입니다.이 문서의 표 그림은 그림이 아니라 실제 에어테이블 베이스에서 API로 꺼내온 레코드입니다(개인정보는 김**·****1234로 마스킹된 상태로 복제했습니다). 에어테이블은 로그인이 필요해 스크린샷을 못 찍으므로, 같은 데이터를 그리드 모양으로 재현했습니다.
문제 이야기를 하기 전에, 무대를 한 번만 봅시다. 어려운 건 없습니다. 시트 12장이고, 그중 두 장(students·recording_orders)에 거의 모든 사건이 모여 있습니다.
| 시트(표) | 줄 수 | 무슨 줄이 들어 있나 |
|---|---|---|
recording_orders | 133 | 주문 한 건 = 한 줄. 누가 뭘 얼마에 샀는지 |
students | 71 | 로그인한 회원 한 명 = 한 줄 |
recording_tokens | 22 | 녹화본 시청용 열쇠 한 개 = 한 줄 |
products | 18 | "어느 강의를 어느 가격으로 파는가" 한 가지 = 한 줄 |
reviews | 12 | 후기 한 개 = 한 줄 |
instructors | 7 | 강사 한 명 = 한 줄 |
recordings | 6 | 강의 한 개 = 한 줄 (이름은 '녹화본'인데 라이브·오프라인도 여기 있습니다) |
materials | 5 | 교보재 파일 한 개 = 한 줄 |
questions / question_replies | 2 / 1 | 질문과 답변 |
payouts | 2 | 강사 정산 한 건 = 한 줄 |
event_logs | 100 | "누가 어느 페이지를 눌렀다" 기록. 실제로는 1,467줄 |
행 수는 2026-10-14 18:00 KST 기준. 에어테이블 무료 요금제가 1,000행까지만 담기므로 event_logs는 최신 100건만 복제했습니다.
빨간 칸 25개가 event_logs = 1,467줄 (84%). 회색 5개가 주문·회원·강의 등 사업에 쓰는 데이터 전부 = 280줄.
"DB를 백업하자 / 분석하자 / 옮기자"고 할 때마다 작업량의 8할이 클릭 기록입니다. 정작 사업 데이터는 280줄뿐인데요. 이건 구조 결함은 아니지만, 이번에 에어테이블로 옮기면서 가장 먼저 몸으로 부딪친 벽이었습니다 (1,000행 한도에 걸려 로그를 잘라내야 했습니다).
이 그림의 네모 하나 ≒ 58줄.
찾은 문제는 12건인데, 그중 설명할 가치가 큰 4건만 끝까지 보겠습니다. 각 건마다 ① 한 줄 요약 → ② 실제 데이터 → ③ 뭐가 위험한가 → ④ 어떻게 고치나 순서입니다. 나머지 8건은 4장에 한 줄씩 모았습니다.
관리자가 4명인데, "관리자입니까?" 칸은 71명 전원 아니오라고 적혀 있습니다.
회원 표에는 권한을 나타내는 칸이 두 개 있습니다. role(역할)과 is_admin(관리자 여부). 옛날에 is_admin으로 시작했다가, 나중에 강사까지 생기면서 role로 갈아탄 흔적입니다.
| Akey | Aname | #phone | ⌄role | ☑is_admin |
|---|---|---|---|---|
| P458 이** | 이** | ****4360 | user | ☐ |
| P6 김** | 김** | ****0489 | admin | ☐ ← 관리자인데 체크가 비어 있습니다 |
전수 집계: role='admin' 4명 · is_admin=true 0명 (71명 전원 false).
비어 있는 칸은 아무도 안 씁니다. 그런데 이 칸은 그럴듯한 이름을 달고 그럴듯한 값(false)을 들고 있습니다. 반년 뒤의 내가, 또는 AI에게 "관리자만 들어오게 해줘"라고 시켰을 때 — is_admin이라는 이름이 role보다 먼저 눈에 들어옵니다. 그 코드를 배포하는 순간 관리자 4명 전원이 관리자 페이지에서 잠깁니다.
비유하면 — 고장난 걸 아는 스위치는 안 누릅니다. 위험한 건 멀쩡해 보이는데 아무 데도 연결 안 된 스위치입니다.
칸을 지웁니다. 권한은 role 하나만 씁니다(user / instructor / admin). 같은 이유로 강의 표의 content_status도 지웁니다(③에서 설명).
이게 이번 작업에서 가장 먼저 실제 운영 DB에 적용할 항목입니다. 데이터를 옮길 필요가 없고, 이 칸을 읽는 코드가 하나도 없다는 걸 확인했기 때문입니다.
order_id라는 이름이 어떤 표에서는 글자, 다른 표에서는 숫자입니다.
주문 표의 order_id는 사람이 읽는 주문코드입니다. 그런데 후기 표의 order_id는 "몇 번째 주문줄"을 가리키는 내부 번호입니다.
참고로 같은 연결을 제대로 부른 곳이 한 군데 있습니다 — recording_tokens.recording_order_id. 이름만 보면 뭘 가리키는지 바로 압니다. 이게 원래 모양입니다.
두 칸을 헷갈려서 연결하면 에러가 안 납니다. 글자와 숫자를 맞춰보면 그냥 "일치하는 게 없음"이 되어서, 화면에는 "후기 0건"이라고 조용히 뜹니다.
에러는 고칩니다. 0건은 그냥 믿습니다. "아 아직 후기를 안 썼구나" 하고 넘어가는 종류의 버그라서, 몇 달을 모르고 지낼 수 있습니다.
이름으로 종류가 보이게 바꿉니다. 사람이 읽는 글자는 order_code(→ 개선안에서는 payment_code), 연결은 전부 …_id + 가리키는 표 이름. 즉 order_id라는 애매한 이름을 DB에서 없앱니다.
단, 교재에 나온 함정이 정확히 이 지점입니다 — 칸 이름을 손으로 바꾸면, 그 이름으로 값을 찾던 코드가 조용히 멈춥니다. 그래서 "이름 바꾸기"가 아니라 "이름과 코드를 같은 커밋에서 같이 바꾸기"로 계획을 잡았습니다.
칸이 3개면 공개 여부를 바꿀 때 어디를 고쳐야 하는지 알 수 없습니다. 지금 강의 6개는 전부 "작성 중"으로 적힌 채 실제로 팔리고 있습니다.
| Aslug | ⌄format | ☑published | ☑listed | ⌄content_status | 실제로 팔리나? |
|---|---|---|---|---|---|
| offline-01 | offline | ☑ | ☑ | draft | ✅ 판매 중 (결제 4건) |
| v01 | online | ☑ | ☑ | draft | ✅ 종료 (판매됨) |
| v02 | online | ☑ | ☑ | draft | ✅ 종료 (판매됨) |
| v03 | online | ☑ | ☑ | draft | ✅ 판매 중 (결제 15건) |
| v04 | online | ☑ | ☐ | draft | 🚫 홍보 전 (목록에 안 뜸) |
| offline-02 | offline | ☑ | ☐ | draft | 🚫 홍보 전 (목록에 안 뜸) |
세로로 읽어보면 바로 보입니다. content_status는 6줄 전부 같은 값(draft)이고, published도 6줄 전부 같은 값(true)입니다. 줄마다 값이 다른 칸은 listed 하나뿐입니다.
칸이 항상 같은 값이면, 그 칸은 아무것도 결정하지 않습니다. 실제로 노출을 조절한 건 listed 하나였고, content_status='draft'는 "작성 중"이라고 써 있으면서 아무것도 막지 않는 장식이었습니다.
현장에서 생기는 문제는 이렇습니다. 강의를 내리고 싶을 때 — 어느 칸을 끄면 내려가는지 아무도 확신할 수 없습니다. 결국 셋 다 끄게 되고, 다시 올릴 때 하나를 빼먹습니다.
체크박스 3개를 단계가 있는 칸 1개로 접습니다.
visibility = hidden 아무도 못 봄 → link_only 주소 아는 사람만 (결제는 가능, 목록엔 없음) → listed 목록에 뜸
켜고 끄는 칸이 여러 개면 조합이 생깁니다. 3개면 8가지 상태인데, 그중 실제로 쓰는 건 2가지였습니다. 단계 하나면 상태가 딱 3개고, 셋 다 의미가 있습니다.
하나만 사면 1줄, 둘을 같이 사면 2줄. 2줄을 묶는 값으로 첫 줄의 주문코드를 씁니다.
어떤 분이 지난 특강 녹화본(20,000원)과 교보재(5,000원)를 한 번에 결제했습니다. 카드는 한 번 긁혔고 25,000원이 빠져나갔습니다. DB에는 이렇게 들어갑니다.
| Akey | Aorder_id | ⌄price_track | #amount | Aorder_group_id |
|---|---|---|---|---|
| #1531 이** | CS-20261002-1531-7HJZ8H | vod_nonattendee | 20,000 | CS-20261002-1531-7HJZ8H |
| #1532 이** | CS-20261002-1532-G954PF | material | 5,000 | CS-20261002-1531-7HJZ8H |
노란 칸을 보세요. 1531번 줄의 자기 주문코드가, 1532번 줄에서 "묶는 값"으로 쓰이고 있습니다. 두 줄 중 누가 대표인지는 "번호가 작은 쪽"이라는 암묵적 규칙으로만 압니다. 이런 묶음 결제가 14건 있습니다.
order_group_id가 비어 있습니다. 그래서 "결제 1건"을 가리키는 값이 — 단건이면 order_id, 묶음이면 order_group_id — 칸 자체가 두 개입니다.영수증처럼 2층으로 나눕니다. 위층은 "결제 1건"(총액·결제수단·결제시각), 아래층은 그 안의 "품목"들. 결제는 무조건 1줄이고, 품목은 1개든 5개든 아래에 붙습니다.
그러면 결제 세는 방법이 한 가지가 되고, 환불은 위층 1줄만 건드리면 되고, 품목이 3개로 늘어도 구조가 안 바뀝니다. 다음 장에서 이 변화를 나란히 보겠습니다.
위에 나온 그 결제(이** 님, 25,000원, 녹화본+교보재)를 현재 구조와 개선안에 각각 넣어봤습니다. 같은 사건인데 모양이 이렇게 다릅니다. 왼쪽이 지금, 오른쪽이 개선안입니다.
| order_id | amount | group |
|---|---|---|
| …-1531-7HJZ8H | 20,000 | …-1531-7HJZ8H |
| …-1532-G954PF | 5,000 | …-1531-7HJZ8H |
총액 25,000원은 어디에도 안 적혀 있습니다. 더해서 알아내야 합니다.
| payment_code | ⌄status | amount_total |
|---|---|---|
| …-1531-7HJZ8H | paid | 25,000 |
↓
| 품목 | payment | amount |
|---|---|---|
| 녹화본 | PAY …1531 | 20,000 |
| 교보재 | PAY …1531 | 5,000 |
결제는 항상 1줄. 품목이 몇 개든 구조가 같습니다.
| order_id | ☑material_granted | _at |
|---|---|---|
| …-1532-G954PF | ☑ | 10-02 11:50 |
결제 기록이 나중에 고쳐집니다. 그러면 이 줄은 "결제했을 때의 기록"이 아니라 "지금 상태"가 돼서, 나중에 보면 그때 무엇을 샀는지 확인할 수 없습니다. 권한 종류가 늘면 칸이 또 늘어납니다(attended_live, material_granted, …).
| key | ⌄kind | person | 왜 생겼나 |
|---|---|---|---|
| E material P458 | material | P458 이** | #1532 교보재 |
기록은 쌓이기만 합니다. 권한 종류가 늘어도 칸이 아니라 줄이 늘어납니다.
13:00~13:15 오프닝 — 에이전트 작동 시연 13:15~13:45 에이전트 개념 — 챗봇과 무엇이… …(중략)… 17:40~18:00 체크리스트 + Q&A [준비물] 노트북 필수 · 해외결제 카드 필수 [별도 비용] 클라우드 서버 연 10달러 [장소] 독산역 디지털엠파이어 빌딩 2층 [운영] 강사 1명 + 코치 2명
장소가 칸이 아니라 글 속 한 줄입니다. "장소가 정해진 수업만 보여줘"가 안 되고, 문자에 장소를 넣으려면 글에서 잘라내야 합니다.
| Avenue | Aitems_to_bring | Aextra_cost_note |
|---|---|---|
| 독산역 디지털엠파이어 2층 | 노트북 · 해외결제 카드 | 서버 연 10달러 |
문자 발송·안내 페이지·검색이 전부 칸 하나 읽기로 끝납니다. 커리큘럼은 커리큘럼만 담습니다.
| 세어보면 | 현재 | 개선안 |
|---|---|---|
| 표 개수 | 12 | 14 |
| 한 사람의 정보가 적혀 있는 곳 | 2곳 | 1곳 |
| "결제 1건"을 가리키는 방법 | 2가지 | 1가지 |
| 공개 여부를 정하는 칸 | 3개 | 1개 |
| 거래 기록을 나중에 수정하는 칸 | 5개 | 0개 |
| 값이 틀렸거나 아무도 안 쓰는 칸 | 2개 | 0개 |
| 같은 값을 또 적는 칸 | 4개 | 0개 (1개는 의도적 유지) |
표 개수가 늘어난 게 핵심입니다. 좋은 구조는 표가 적은 게 아니라, 한 표가 한 가지만 담는 것이었습니다.
같은 방식으로 찾은 나머지입니다. 전부 실제 데이터로 숫자를 확인했습니다.
price_track(가격 종류)은 product_id(상품)만 보면 알 수 있는 값입니다. 상품 표에서 꺼내 읽으면 되는데 주문 줄에 또 적어 뒀습니다.주문 133건 전수 대조 — 불일치 0건. 즉 100% 중복이고, 언젠가 한쪽만 고쳐지면 그날이 사고입니다instructor_id는 "담당 강사"로 읽히는데, 뜻이 정반대입니다이름만 보면 "이 회원의 담당 강사"처럼 읽힙니다. 실제 뜻은 "이 회원이 바로 그 강사"였습니다.71명 중 채워진 건 4명 = 강사 본인들. 이름만 보고 "담당 강사"로 쓰면 틀린 코드가 나옵니다[태그]로 들어 있습니다(3장 사건 3). 반대로 최소인원 칸은 온라인 강의 4개에도 달려 있어서 의미가 없습니다.칸이 있어야 할 것은 글 안에, 없어도 될 것은 칸으로 — 정확히 반대였습니다pending은 "입금 대기"가 아니라 "결제창에서 그냥 나감"입니다이름만 보면 돈이 들어오기를 기다리는 상태로 읽힙니다. 실제로는 결제창까지 갔다가 그냥 나간 흔적이라, 아무리 기다려도 입금은 오지 않습니다.주문 133건 중 42건(32%)이 이 상태. 개선안에서는 initiated(시작됨)로 개명 — 지우지는 않습니다. "살 뻔한 사람" 명단이라서요문제를 12개 찾았다고 12개를 고치는 게 아닙니다. 지금 돌아가는 사이트라서, 고치다 멈추면 결제가 멈춥니다. 난이도로 3단계로 나눴습니다.
is_admin 삭제 (아무도 안 읽는 것 확인됨)content_status 삭제order_id → 글자는 order_code, 연결은 recording_order_idinstructor_id → linked_instructor_idpending → initiatedprice_track을 상품 표에서 읽어오기로 전환visibility 1개솔직한 질문을 스스로 던졌습니다 — 이 12건, 에어테이블 없이 그냥 들여다봐도 알 수 있었나? 답은 반반입니다.
"is_admin이 true인 사람 몇 명?" "price_track과 상품이 다른 줄 있나?" 같은 질문은 원래 DB에서도 1분이면 세어 봅니다. 도구가 알려준 게 아닙니다.
① 칸 161개 전부에 설명을 써야 했습니다. 그러니까 칸마다 "이거 왜 있지?"를 한 번씩 물은 셈입니다. 그러다 설명이 안 써지는 칸이 나왔습니다 — is_admin, content_status. 설명을 쓸 수 없는 칸이 곧 죽은 칸이었습니다.
② 연결 19개를 손으로 하나씩 걸어야 했습니다. "이게 뭘 가리키나"를 적다 보니 글자 order_id와 숫자 order_id가 나란히 놓였습니다. 평소 쿼리를 쓸 때는 쓰는 쪽만 보니까 둘을 같이 볼 일이 없었습니다.
③ 에어테이블은 연결 칸에 상대 표의 첫 칸 값을 보여줍니다. 제 DB의 첫 칸은 전부 1, 133 같은 번호라서, 연결을 걸어 놓으니 아무것도 읽을 수 없었습니다. 그래서 표마다 key 칸을 새로 만들었습니다. 기계가 식별하는 값과 사람이 식별하는 값은 다른 것 — 이 질문을 해 본 적이 없었습니다.
④ 무료 1,000행 한도에 부딪혀서 알았습니다 — 전체 1,747줄 중 84%가 클릭 기록이었습니다. 한도라는 제약이 비율을 보게 만들었습니다.
쿼리는 의심이 가는 곳만 들여다봅니다. 옮기기는 161칸 전부를 지나가야 끝납니다. 평소에 코드를 읽을 때는 쓰는 칸만 봅니다. 안 쓰는 칸은 영원히 안 보입니다. 그 차이에서 4건이 나왔습니다.
교재가 "필드마다 설명 쓰기"를 유난히 강조한 이유를 여기서 알았습니다. 설명은 남을 위한 친절이 아니라, 내 구조를 검사하는 도구였습니다.
"잘 돌아가는 것"과 "잘 설계된 것"은 다릅니다. 이 사이트는 결제도 되고 문자도 가고 정산도 됩니다. 그런데 안에는 값이 전부 틀린 칸과, 두 가지 뜻으로 쓰이는 이름이 있었습니다. 돌아가니까 안 봤던 것입니다.