💬말로 만드는 AI▶ 유튜브
🗂️ 뒷이야기 · 02

02 SJ FLOW v2 ① — 기반 공사: 브라우저마다 따로 놀던 도구를 클라우드로

이 페이지 목차목적만든 순서1 · 새 저장소와 첫 업로드 (7/1 09:25 ~ 09:43)2 · 로그인을 진짜 계정으로 (7/1 17:20)3 · 데이터를 Firestore 로 (7/1 20:27 · 21:36)4 · 폰에서 로그인이 안 되던 날 (7/3 14:21 ~ 16:07, 7/4 08:18)5 · 사내 메신저와 NCR·CAR — 하루 60커밋 (7/3)6 · 사고 ① — index.html 이 옛 판으로 되돌아갔다 (7/3 13:02)7 · 사고 ② — 등록한 NCR 이 곧바로 사라졌다 (7/4)8 · 한 주에 도구 다섯 (7/6 ~ 7/12)💡 설명함정과 배운 것출처

뒷이야기 · 2 / 17단계 · 2026-07-01 ~ 07-12 · 갈래 SJ FLOW
이전 단계: 01 프롤로그 — EPC Control 과 1차 저장소 · 다음 단계: 03 SJ FLOW v2 ② — AI 게이트웨이와 AI 비서 1~3기

핵심 요약

항목내용
이 단계에서 한 일7/1 새 저장소를 열고 같은 날 로그인을 진짜 계정(구글 인증)으로, 데이터를 클라우드 DB(Firestore)로 옮겼다. 이어 사내 메신저·NCR/CAR·OKR·측정기구·자산 대장·전자결재를 붙였다. 도중에 사고 둘(옛 판으로 되돌아감 · 등록하자마자 사라지는 NCR)을 겪고 안전망을 넣었다.
기간2026-07-01 ~ 07-12 (12일)
규모커밋 267개(병합 커밋 9개 따로, 합 276). 하루 최다 7/3 의 60개(병합 포함 65). HTML·JS 파일 6개 13,390줄(7/1 첫 업로드) → 15개 28,103줄(7/12 마감, 아침 브리핑 화면 1개 886줄 포함). 모듈 폴더 5 → 11개
다룬 도구GitHub Pages · Firebase 인증(구글) · Firestore · iframe 모듈 · 구글 캘린더 · 엑셀/PDF 출력 · QR
결과전: 로그인 = 브라우저 안의 가짜 계정, 데이터 = 브라우저별 → 후: 사내 구글 계정 로그인 · 실시간 공유 DB · 도구 모듈 11폴더 + 단일 파일 1개 · push 직전 상태를 남기는 백업 태그
공개 범위사내(로그인 뒤) — 🔒 항목은 넣지 않음

한 줄 요약 — 하루 만에 "내 화면엔 있는데 남 화면엔 없다"를 끝냈고, 열이틀 만에 모듈 일곱 개(폴더 6 + 파일 1)를 늘렸다. 그 대가로 사고 둘을 겪었고, 둘 다 같은 것을 여럿이 건드려서 생겼다.

2단계 · SJ FLOW v2 ① — 기반 공사

목적

1단계에서 그릇만 만들고 멈췄다. 이 단계의 질문은 하나였다. "한 사람이 만든 도구를 어떻게 전 직원이 같은 데이터로 쓰게 하나." 7/1 커밋 메시지 둘이 문제를 적는다. 로그인과 계정이 브라우저 localStorage 에만 있는 가짜 인증이라 다른 기기에서는 같은 계정으로 로그인이 안 된다. 핵심 데이터도 브라우저별 localStorage 에만 있어 사람마다 기기마다 다른 데이터를 보고 있었다. 1단계 설계서가 정한 "Phase 1 미완 과제"(Firebase 연동)가 그대로 첫 일이 됐다.

SJ FLOW 는 crazy4eu(cwkim83)가 이끌어 이 저장소(sejong-platform-v2)에서 만들어 온 사내 플랫폼이다. 이 단계는 로그인·공용 DB의 기초를 놓고, 품질관리부 도구들이 올라타던 열이틀이다. 도구 일부는 같은 부서 동료들이 같은 저장소에 직접 올렸다.

sjflow v1 architecture · 크게 보기 ↗

만든 순서

1 · 새 저장소와 첫 업로드 (7/1 09:25 ~ 09:43)

7/1 09:25 에 sejong-platform-v2 가 만들어졌다(Initial commit). 2분 뒤 09:27 에 파일 6개 13,390줄이 한 번에 올라갔다. index.html 3,491줄, WBS 4,148줄, ITP 빌더 1,195줄, 공수(맨데이) 추적 1,167줄, 모바일 검사 1,236줄, QA 문서 생성기 2,153줄이다. 1단계와 같이 GitHub 웹 화면의 업로드였다(커미터 GitHub). 09:43 에는 회사 도메인을 연결하는 CNAME 파일이 올라갔다.

이 파일 안의 WBS 는 1단계 40일 사이에 자란 것이다. 코드 주석에 v28.2(워크오더 자동 등록)와 v28.3(자동 가져오기)이 이미 들어 있었다. 다만 저장 기능은 꺼져 있었다. 이날 저녁 커밋 메시지가 밝힌다. WBS 화면은 저장이 "데모 모드"로 일부러 비활성화돼 있었고, 플랫폼 본체의 일정 데이터와 연결되지 않은 별개의 사본이었다.

2 · 로그인을 진짜 계정으로 (7/1 17:20)

이날 12:56 에 "v1.0 릴리스" 이름표를 달았지만 로그인은 여전히 가짜였다. 4시간 24분 뒤인 17:20 에 로그인을 Firebase 인증(구글 로그인)으로 바꿨다.

  • 구글 로그인 한 가지로 통일했다. 도메인 힌트(hd) 파라미터에 더해 이메일 도메인을 다시 확인해 사내 계정만 통과시킨다.
  • 최초 로그인 때 이름·부서·직급만 한 번 입력받아 users/{uid} 문서를 만든다. 비밀번호라는 개념 자체가 사라졌다.
  • 앱 시작을 로그인 상태 변화(onAuthStateChanged) 한 곳에서 처리하게 바꿨다. 로그인 성공 때 users 컬렉션 전체를 state.users 에 채워서, 코드 전체 90여 곳이 쓰던 동기 참조 방식은 건드리지 않았다.
  • 관리자가 남의 비밀번호를 초기화하거나 계정을 발급하는 기능은 브라우저 코드만으로는 불가능(서버 쪽 관리 도구인 Admin SDK 가 필요)해서 빼고, 부서·직급 수정과 계정 비활성화로 바꿨다.

변경은 index.html 한 파일, 168줄 추가 · 238줄 삭제였다. 배운 것: 90여 곳을 고치는 대신 한 지점(state.users 를 채우는 곳)만 갈아 끼우니 하루 안에 끝났다.

3 · 데이터를 Firestore 로 (7/1 20:27 · 21:36)

로그인이 진짜가 되자 데이터가 문제로 남았다. 조사해 보니 사용자가 실제로 만지는 WBS 화면은 localStorage 에도 저장되지 않는 임시 데이터였다. 20:27 에 projects·tasks·wbsData 를 Firestore 로 옮겼다.

  • 로그인 성공 후 세 컬렉션을 onSnapshot(바뀌면 바로 알려 주는 실시간 구독)으로 구독한다. 프로젝트 만들기·수정·삭제·PM 지정, 업무 추가 같은 쓰기를 모두 setDoc/updateDoc/deleteDoc 으로 바꿨다.
  • WBS 는 프로젝트마다 문서 하나(wbsData/{pid})로 저장한다. 이미 있던 "렌더링마다 0.4초 디바운스 자동 저장" 훅을 재사용해 편집 동작마다 저장 코드를 새로 심지 않았다.
  • 자기 쓰기가 되돌아와 무한 반복하지 않도록 hasPendingWrites(아직 서버에 안 올라간 내 쓰기 표시)로 걸러냈다.
  • 21:36 에는 화면에 박혀 있던 데모 데이터를 전부 지우고 빈 상태로 시작했다.

보안 규칙(누가 읽고 쓸 수 있는지 정하는 규칙)은 코드가 아니라 Firebase 콘솔에서 사용자가 직접 게시했다. 이 규칙 파일이 저장소 안으로 들어온 것은 7/18 이다(3단계). 변경은 두 파일, 174줄 추가 · 184줄 삭제. 17:20 로그인에서 20:27 DB 까지 3시간 7분이었다. 결정의 이유는 이력 문서가 한 문장으로 적는다. 공유가 시스템의 존재 이유였으므로 최우선으로 처리했다.

4 · 폰에서 로그인이 안 되던 날 (7/3 14:21 ~ 16:07, 7/4 08:18)

7/3 에 아이폰 사파리·크롬에서 구글 로그인이 계속 'missing initial state' 오류로 실패했다. 구글 인증 페이지를 다녀오는 동안 브라우저가 임시 저장소(sessionStorage)를 지워 버려서였다. 두 시간이 안 되는 사이(14:21~16:07)에 방식을 네 번 바꿨다.

  1. 14:21 — 인증 도메인을 앱과 같은 사이트의 하위 도메인으로 바꿨다(다른 도메인을 거치면 저장소가 지워지는 정책을 피하려고).
  2. 15:34 — 구글 계정 서비스(GIS)의 토큰 클라이언트 방식으로 바꿔 페이지 이동 왕복을 없앴다.
  3. 15:51 — 조용한 토큰 갱신을 건너뛰고 매번 계정 선택 화면을 보여 줬다.
  4. 16:07 — 팝업도 저장소도 쓰지 않는 순수 페이지 이동 방식으로 바꿨다. 구글 인증 주소로 페이지 자체를 옮기고, 돌아올 때 주소 해시에서 토큰을 읽는다.

다음 날 7/4 08:18 에 리디렉션 뒤 로그인 화면이 깜빡이고 다시 눌러야 하던 문제를 고쳤다. 이 방식이 끝은 아니었다. 7/24 에는 팝업 로그인을 빠르게 만드는 최적화가 들어갔고, 7/31 코드에는 팝업과 토큰 로그인 호출이 함께 있다. 배운 것: 브라우저 저장소와 팝업에 기대는 로그인은 모바일 사파리에서 깨진다. 같은 사이트 도메인과 페이지 이동이 가장 덜 깨졌다.

5 · 사내 메신저와 NCR·CAR — 하루 60커밋 (7/3)

7/3 은 7월 최다인 60커밋(병합 5개 포함 65)이었다. 작성자 이름만 사람 셋과 git 계정이 설정되지 않은 작업 세션으로 갈린다. 이날 올라온 것은 이렇다.

  • 메신저 (10:45): 기존 메신저는 화면뿐이었고 localStorage 에만 저장돼 "로컬 메모장 수준"이었다. channels/messages 를 Firestore 에 실시간 저장하고, 보낸 메시지를 서버 응답 전에 먼저 보여 주는 방식(낙관적 전송)과 채널별 읽음 표시를 넣었다. 새 프로젝트와 신규 직원마다 채널을 자동 만든다. 파일 첨부는 Storage 요금제 문제로 보류돼 비활성 상태였다. 사이드바 글자와 접기 화살표를 키우면서 "고령 사용자 가독성"을 이유로 적은 커밋이 두 개 있다.
  • 캘린더 (11:12 · 12:00): 개인 구글 캘린더를 읽고 쓰게 하고, 12:00 에 삭제까지 반영해 양방향이 됐다.
  • NCR·CAR (12:42 ~ 16:53): 품질관리부 대리가 NCR(부적합 보고) 모듈을 크게 고쳤고(12:42), 14:10 에 NCR·CAR 를 iframe 모듈로 분리했다. 16:20 CAR(시정조치)를 새로 만들고, 16:49 에는 NCR·CAR 데이터를 실시간 동기화하는 품질종합분석표를 붙였다. 16:53 에는 데이터 정합성 자동 검사가 들어갔다. 12:42 에서 16:49 까지 4시간 7분이었다.

iframe 모듈이란 본체 화면 안에 다른 HTML 파일을 끼워 넣는 방식이다. 정적 사이트(빌드 도구 없음) 안에서 모듈을 따로 만들고 따로 배포하려는 현실적인 선택이었고, 같은 사이트라 로그인 세션이 그대로 이어진다. 이 선택이 다음 두 사고의 무대가 된다.

6 · 사고 ① — index.html 이 옛 판으로 되돌아갔다 (7/3 13:02)

12:44 에 NCR 개선과 캘린더 수정을 합치는 병합(merge) 커밋이 들어갔다. 충돌을 잘못 풀어서 index.html 이 로그인 화면까지 옛 판(로컬 데모 계정)으로 돌아갔다. 수십 개 커밋 분량의 기능이 통째로 사라진 것이다. 13:02 에 마지막 정상 상태로 복구했다. 1,289줄 추가 · 1,693줄 삭제의 큰 되돌림이었다. 그 병합이 가져온 NCR 개선도 함께 유실돼 다시 합쳐야 한다고 메시지에 적혀 있다.

13:15, 복구 13분 뒤(병합 31분 뒤)에 안전망을 넣었다. main(모두가 함께 쓰는 기본 줄기)에 push 가 들어올 때마다 GitHub Actions(저장소 자동화)가 push 직전 상태를 backup/<시각>-<해시> 태그로 남기고, 최근 30개만 유지한다. 누가 push 하든 적용된다. 복구는 이 태그에서 파일을 꺼내 새 커밋으로 올리는 방식이고, 이후 개발 규칙(CLAUDE.md)에 "force push(서버의 기록을 내 것으로 강제로 덮어쓰기) 절대 금지"로 박혔다. 사람이 셋 이상 같은 main 에 직접 올리는 환경이라 병합 사고는 언젠가 날 일이었다.

7 · 사고 ② — 등록한 NCR 이 곧바로 사라졌다 (7/4)

증상은 이랬다. iframe 의 NCR 모듈에서 새 항목을 발행하면 목록에 잠깐 나타났다가 사라졌다. 원인은 경쟁(race)이었다.

  1. 사용자가 NCR 을 발행하면 iframe 이 localStorage 에 새 NCR 을 포함해 저장한다.
  2. 마우스를 움직이거나 키를 누르면 본체(index.html)의 세션 갱신 코드가 20초마다 돈다. 이때 본체의 save() 가 실행된다.
  3. 본체는 NCR 컬렉션을 구독하지 않아 페이지를 연 시점의 옛 NCR 목록을 메모리에 들고 있다. save() 가 그 옛 값으로 localStorage 를 통째로 덮어쓴다.
  4. iframe 이 다음 화면을 그리며 localStorage 를 다시 읽으면 새 NCR 이 없다.
ncr vanish race · 크게 보기 ↗

시간표는 이렇다. 09:53 본체의 onSnapshot 처리기에 save() 를 더하는 커밋이 들어갔고, 첫 수리 커밋이 이것을 원인으로 지목했다. 13:41 첫 수리는 그 처리기 네 곳만 고쳤다. 14:01 진짜 수리는 저장 함수 자체를 고쳤다. 소유권을 나눠서, NCR·CAR 관련 키는 본체가 쓰기 직전에 디스크 값을 읽어 채택하게 했다. 본체의 save() 호출 20곳이 어디서 불려도 덮어쓸 수 없다. 14:24 에는 로컬 배열에 없는 문서를 "삭제"로 오판하던 동기화 로직을 없앴고, 삭제를 24시간 대기 후 승인제로 바꿨다. 그날 15:20 에는 연속 등록 때 번호가 겹치지 않게 하는 보완도 들어갔다.

배운 것: 첫 수리는 증상 쪽 일부(4곳)만 고쳤고 같은 뿌리의 나머지 경로 20곳이 남아 있었다. 같은 문제를 쓰는 곳이 많으면 쓰는 곳이 아니라 저장 함수를 고친다. 재현 시나리오를 로컬에서 돌려 원인을 확정한 뒤 고쳤다는 점도 메시지에 남아 있다.

8 · 한 주에 도구 다섯 (7/6 ~ 7/12)

이 주에 올라온 도구는 다섯이다. 목표(OKR) 정식 구현(7/7 09:04) · WBS 개정(Rev) 관리와 결재 흐름(7/7 11:23) · 측정기구 반입출 관리(7/10 15:41, 새 파일 1,461줄) · 총무부 자산 관리 대장(7/10 16:21, 377줄) · 다단계 전자결재(7/11 05:09). 측정기구와 자산 대장은 40분 간격이었다. 7/12 에는 품질 3자검사 템플릿, ITP 결재 상신, WBS 변경 자동 상신이 이어졌다.

도구가 빨리 붙은 배경에는 규칙 하나가 있다. 7/8 에 "새 Firestore 컬렉션 이름은 t_ 로 시작한다"는 공지에 맞춰 NCR·CAR(09:46)와 ITP·QA(10:23)의 컬렉션 이름을 바꿨다. t_ 로 시작하면 사내 계정에 읽기·쓰기를 열어 주는 보안 규칙이 이미 있어서, 새 도구를 만들 때마다 규칙을 손대지 않아도 된다. 이 넓은 규칙은 9월에 민감한 컬렉션(경비·자산·NCR·CAR 판 기록 등)을 하나씩 예외로 떼어 내며 조여진다(13단계). 이 시기에는 속도를 위한 결정이었다. 같은 7/8 09:46 커밋은 그때까지 localStorage 만 쓰던 모바일 검사도 Firestore 로 옮겼고, 사진은 NCR·CAR 처럼 700KB 조각으로 나눠 저장하게 했다.

그리고 대가가 있었다. 이름을 바꾸기 전에 NCR·CAR 이 쓰던 컬렉션 이름은 보안 규칙에 없었다. 커밋 메시지(7/16)는 그래서 당시 등록분의 클라우드 저장이 전부 실패하고 등록한 사람의 브라우저에만 남았다고 적는다. NCR·CAR 동기화는 7/3 16:49 에 시작됐고 이름은 7/8 09:46 에 바뀌었으니 그 사이 등록분이 해당했을 것이다(두 커밋 시각으로 읽은 추정 · 몇 건인지는 확인 못 함 · 3단계 1번에서 발견).

이 12일 동안 제목이 "모듈 주소 뒤의 BUILD 번호 올림"인 커밋이 18개였고, 그중 10개는 GitHub Pages 배포가 실패하거나 늦어서 다시 올리려는 것이었다(재시도 커밋은 모두 11개). 정적 사이트는 배포도 캐시도 직접 챙겨야 했다.

결과로 7/12 마감 HTML·JS 파일은 15개 28,103줄(아침 브리핑 화면 1개 886줄 포함), 모듈 폴더 11개와 단일 파일 모듈 1개(자산 대장)가 됐다. 7/1 의 6파일 13,390줄에서 열이틀 만에 2.1배다.

💡 설명

  • Firebase 인증(구글 로그인): 구글이 로그인을 맡아 주는 서비스. 우리는 비밀번호를 보관하지 않는다.
  • Firestore: 구글 클라우드의 문서형 데이터베이스. 한 사람이 저장하면 다른 사람 화면에 바로 반영된다.
  • onSnapshot(실시간 구독): DB 가 바뀌면 브라우저에 바로 알려 주는 연결.
  • localStorage: 브라우저 안에 작은 글자를 남기는 저장소. 그 PC 의 그 브라우저에서만 보인다.
  • iframe 모듈: 본체 화면 안에 다른 HTML 파일을 끼워 넣은 도구 화면.
  • 병합(merge)과 충돌: 두 사람이 같은 파일을 따로 고친 것을 하나로 합치는 일. 같은 줄을 고치면 충돌이 나고, 잘못 풀면 한쪽 변경이 사라진다.
  • 백업 태그: 저장소의 특정 시점에 붙인 이름표. 나중에 그 시점으로 파일을 되찾을 수 있다.
  • 경쟁(race): 두 곳이 동시에 같은 데이터를 쓰다가 늦게 쓴 쪽이 이기는 현상.
  • 보안 규칙: Firestore 가 읽기·쓰기 요청을 받을 때 허용 여부를 정하는 규칙. 콘솔에 따로 게시한다.
  • BUILD 번호(캐시버스터): 파일 주소 뒤에 붙이는 번호. 올리면 브라우저가 옛 파일을 버리고 새 파일을 받는다.
  • 디바운스: 입력이 멈춘 뒤 잠깐 기다렸다가 한 번만 저장하는 방식.
  • NCR · CAR · OKR: 부적합 보고서 · 시정조치 요구서 · 분기 목표 관리.

함정과 배운 것

  • 로그인 개편은 90여 곳을 고치는 대신 한 지점만 갈아 끼우는 쪽이 안전했다.
  • 보안 규칙을 콘솔에서만 관리하면 코드와 규칙이 어긋난다. 어긋나면 에러 없이 저장만 조용히 실패한다(7/3~7/8 NCR·CAR). 규칙은 저장소에 넣고 코드와 같이 바꾼다(3단계에서 실행).
  • 부모와 iframe 이 같은 저장소를 쓰면 마지막에 쓴 쪽이 이긴다. 키마다 주인을 정하고, 저장 함수 한 곳에서 지킨다.
  • 첫 수리는 증상의 일부만 잡는다. "이 데이터를 쓰는 곳이 몇 곳인가"를 먼저 센다.
  • 병합 사고는 사람이 여럿 직접 main 에 올릴 때 반드시 난다. 올리기 직전 상태를 자동으로 남기는 안전망이 값싼 보험이다. 이 습관은 9/26 에도 필요했다(충돌 표시가 남은 파일이 배포된 사고).
  • 모바일 사파리 로그인은 저장소와 팝업에 기대면 깨진다. 같은 사이트 도메인과 페이지 이동을 쓴다.
  • 정적 사이트는 배포 실패와 옛 캐시를 직접 챙긴다(재배포 11번, BUILD 번호 18번).

출처

  • 커밋: b38dee0 · a31fb9b · d5a4f28 · 6ab566b · 3e8e4ac · 338fe6d · 0e46cd9 · 7c6d9f0 · ddbf823 · 9ab4960 · 8a1d6d1 · d3df847 · b0c5d39 · a2c3a5a · 68d91d5 · 891f13d · 0b63a39 · 031a6e9 · 5569954 · af27d5a · 0d9410c · 4783d4b · 6d474df · c941d7b · d156e0b · ff5e118 · e9deee8 · 3ed3fa4 · bb40623 · d3cfdf3 · c5e0730 · 628b0ad · 02ecfe9 · 1ad919c · a2a38c2 · 5213799 · 1b2f721 · 680a531 · 5280c9d
  • 문서: sejong-platform-v2 PROJECT-HISTORY.md WEEK 1~2 · §2-1 결정 · §2-2 사고, .github/workflows/backup-before-main-push.yml(6d474df 시점), CLAUDE.md
  • 기억 노트: merge-push-trap · ncr-car-sync-rewrite · firestore-rules-publish

← 01 프롤로그 — EPC Control 과 1차 저장소 · 03 SJ FLOW v2 ② — AI 게이트웨이와 AI 비서 1~3기 →