AX 프로젝트 추진 사례
업무관리시스템을 10회차로 완성하기
과제 정의부터 배포·오픈베타·효과 정량화까지, 실제로 만들어진 순서 그대로 따라갑니다.
전체 그림 — 무엇을 만들었고, 왜 이 순서였나
- 우리가 어떤 업무를 자동화했고, 결국 어떤 시스템으로 끝났는가.
- 10회차 프로세스의 각 회차가 우리에게 무엇으로 나타났는가.
- 이 문서를 읽는 방법 — 결정 로그·화면 캡처·측정값을 어떻게 따라갈 것인가.
출발점 — 업무가 여러 도구에 나뉘어 있었다
우리 회사는 교육·컨설팅이 주업무입니다. 고객 문의가 오면 영업 → 견적 → 수주 → 일정 배정 → 강의 수행 → 계산서 발행 → 수금 → (외부강사) 정산까지 한 줄기로 흘러갑니다. 그런데 그 한 줄기가 여러 개의 도구에 나뉘어 있었습니다.
| 도구 | 담당하던 것 | 문제 |
|---|---|---|
| 구글 시트 회사 캘린더 | 누가 언제 어디서 강의하는지 — 월별 블록 그리드 | 일정만 있고 돈이 없다. 셀에 적힌 정보는 사람만 읽을 수 있다 |
| 에어테이블 프로젝트 목록 | 계약·금액·단계·계산서 | 돈만 있고 일정이 없다. 날짜가 비어 있어도 강의를 안 한 것이 아니다 |
| 엑셀 외부인건비 지급대장 | 외부강사 원천징수·실지급 | 앞의 두 곳과 아무 연결이 없다. 수식은 회계법인이 준 그대로 |
영업이 성사되면 시트와 에어테이블 양쪽에 이중 등록해야 했고, 외부 강사에게 줄 돈은 세 번째 파일에서 따로 계산했습니다. 그리고 어느 쪽도 "이 프로젝트에서 우리가 얼마를 청구했고 얼마가 아직 안 들어왔는지"를 한 화면에서 답하지 못했습니다. 10회차의 목표는 이 셋을 하나로 합치는 것이었습니다.
(고객사·프로젝트·계산서·강의·멤버·일정·지급·견적)
(캘린더·프로젝트·강의·계산서·견적서·고객사·멤버·지급·대시보드·관리자)
(기존 시트·에어테이블·엑셀에서)
(D1~D46, 무엇을 왜 그렇게 정했나)
10회차 대응표 — 우리는 각 회차에서 무엇을 했나
10회차 프로세스가 회차마다 던지는 핵심 질문에 우리가 어떻게 답했는지를 먼저 펼쳐 둡니다. 이 문서의 각 회차는 이 표의 한 줄을 자세히 푼 것입니다.
| 회차 | 핵심 질문 | 우리의 답 |
|---|---|---|
| 1회차 | 무엇을 자동화하는가? | 흩어진 3개 도구를 하나로. 프로젝트 : 계산서 : 강의 = 1 : N : N 3층 구조를 정하고 개발계획서를 씀 |
| 2회차 | 자동화가 정말 가능한가? | 캘린더 한 화면을 심장으로 잡고 구현. 목업 대신 실데이터를 먼저 이관해 검증(개발 순서를 바꿈) |
| 3회차 | 아웃풋 품질은 몇 점인가? | 강의–프로젝트 연결률 24% → 89%. 원천징수 계산을 지급대장 36행과 100% 대조 |
| 4회차 | 기획한 기능이 전부 동작하는가? | 7개 테이블을 공용 DataTable로. 엑셀처럼 쓰는 인라인 편집까지 붙임 |
| 5회차 | 어디까지 확장할 것인가? | 내부·외부 강사 테이블 물리 통합(재배정을 한 필드로), 지급대장 개편, 견적서·메일 발송 연계 |
| 6회차 | 누구나 접속할 수 있는가? | 로그인·가입 승인제. 배포 어댑터가 막혀 게이팅 방식을 통째로 갈아엎고 서버 액션 46개에 인증 삽입 |
| 7회차 | 낯선 사용자에게 맡겨도 되는가? | 동시 요청 부하 시험 에러 0, 보안 헤더, 문서 4종 정비 |
| 8회차 | 실제로 쓰이는가? | DB를 운영용으로 전환하고 파일 저장소를 옮김. 로컬↔운영 동기화 도구를 만들어 실사용 시작 |
| 9회차 | 데이터가 시키는 개선은? | 쓰면서 드러난 용어 혼동을 DB 단위로 리네임, 날짜 시간대 버그 수정, 화면 필터 재설계 |
| 10회차 | 무엇이 얼마나 좋아졌는가? | 배포처를 바꿔 완주. 결정 로그 46건이 그대로 인수인계 문서가 됨 |
이 문서를 읽는 방법
진행 방식회차별로 ①무엇을 하는 회차인지 → ②우리가 실제로 한 것 → ③그때 내린 결정 → ④결과 화면·숫자 → ⑤산출물 순으로 읽습니다. 완료 기준: 각 회차 끝의 산출물 목록을 보고 "우리 과제라면 이 자리에 무엇이 놓일지" 말할 수 있는가.
1결정 로그를 눈여겨보십시오
파란 D번호 블록은 우리 개발 노트에서 그대로 가져온 것입니다. 잘한 결정만이 아니라 틀려서 되돌린 결정도 그대로 실려 있습니다 — 그게 이 기록의 값어치입니다.
2숫자를 눈여겨보십시오
"잘 됐다"가 아니라 연결률 89%, 36행 100% 일치, 동시 80요청 에러 0처럼 측정값으로 적혀 있습니다. 3회차·10회차가 요구하는 것이 바로 이 형태입니다.
3화면은 최종본입니다
캡처는 완성된 시스템에서 찍었습니다. 각 회차 본문은 "이 화면이 그때 어디까지였는지"를 글로 적어 두었으니, 화면과 설명의 시점 차이를 감안하고 보십시오.
1회차 — 과제 정의와 개발계획서
- 흩어진 실물 자료에서 SIPOC과 등장인물(테이블)을 뽑아내는 과정을 따라갈 수 있다.
- 사람이 하던 프로세스를 그대로 옮기지 않고 재설계한 지점을 짚을 수 있다.
- 3층 데이터 구조(프로젝트:계산서:강의)가 왜 필요했는가.
기존 업무 방식 — 실제로 쓰던 파일 세 개
과제 정의는 "실물을 보는 것"에서 시작합니다. 우리가 쓰던 파일 세 개를 그대로 펼쳐 놓고 시작했습니다. 아래 캡처는 실제 원본 파일이고, 고객사명·인명·계좌·금액만 가명으로 바꾼 것입니다.
① 구글 시트를 엑셀로 내려받은 회사 캘린더
이 한 장에서 1회차의 문제가 전부 보입니다. ① 일정은 있는데 금액이 없습니다. ② "이 칸의 약칭이 어느 회사인가"는 사람 머릿속에만 있습니다. ③ 정작 쓸모 있는 정보(시간·장소·연락처)는 셀 값이 아니라 노트에 숨어 있어서, 값만 읽는 프로그램에는 존재하지 않는 것과 같습니다. ④ 달이 바뀌면 블록이 끊겨 "지난주와 이번 주"를 나란히 볼 수 없습니다.
② 에어테이블에서 내보낸 프로젝트 목록
③ 외부 인건비 지급대장 엑셀
IF와 ROUNDDOWN으로 계산됩니다. 회계법인이 준 이 수식이 업무 규칙의 원본이고, 3회차에서 이걸 코드로 옮긴 뒤 전 행과 대조하게 됩니다.STEP 1 · 업무 범위와 SIPOC
우리 SIPOC은 다음과 같이 정리됐습니다. 중요한 것은 I(인풋)에 "실물 파일 이름"이 적혀 있다는 점입니다. "일정 정보"가 아니라 회사캘린더.xlsx의 일정표(2026) 시트라고 적어야 그다음 단계가 굴러갑니다.
| 구분 | 내용 |
|---|---|
| S 공급자 | 영업 담당(계약·금액), 강의를 수행하는 내부 멤버·외부 강사(일정·실적), 회계법인(원천징수 기준), 고객사(발주·수금) |
| I 인풋 | 에어테이블 내보내기 프로젝트 목록 CSV(계약·금액·단계·계산서), 구글시트 회사캘린더 xlsx(월별 블록 그리드), 외부인건비 지급대장 xlsx(지급내역·계좌 시트) |
| P 프로세스 | ① 견적 작성·발송 → ② 프로젝트 등록 → ③ 캘린더 일정 배정(강의↔프로젝트 연결) → ④ 강의 수행 → ⑤ 계산서 발행 → ⑥ 수금 추적 → ⑦ 외부강사 지급행 생성 → ⑧ 지급 |
| O 아웃풋 | 캘린더·프로젝트·계산서·지급 화면, 견적서 PDF와 발송 이력, 대시보드(계약총액·발행완료·미청구·미수금) |
| C 고객 | 전 직원(조회·입력), 관리자(개인별 실적), 외부 강사(정산 수령), 고객사(견적서 수신) |
STEP 2 · 휴먼 프로세스를 프로그램 프로세스로
가장 중요한 재설계는 "셀 하나에 사람이 알아서 적던 것"을 프로그램이 다룰 수 있는 형태로 바꾸는 일이었습니다. 구글 시트에서는 한 칸에 고객사·시간·장소·담당자 연락처가 자유 문장으로 섞여 있었습니다.
휴먼 — 구글 시트 셀
- 날짜 × 사람 행의 위치로 "누가 언제"를 표현
- 칸 안의 텍스트는 고객사 약칭(사람만 해석 가능)
- 시간·장소·연락처는 셀 노트에 숨어 있음
- 같은 회사를 부르는 이름이 여러 개(약칭·경유처 표기)
x표시 = "이 날 잡지 말 것" 신호
프로그램 — 강의 테이블
- 강의 1건 = 하루 × 강사 1명이라는 최소 단위 확정
- 고객사는 별도 테이블의 id로 연결(이름은 바뀌므로)
- 시간·장소·메모를 구조화 필드로 분리
- 약칭↔정식명 매핑 규칙을 이관 코드에 명시
x는 이관 시 "삭제" 분류로 처리
"강의 1건 = 하루 × 강사 1명"이라는 정의가 우리 시스템 전체를 지탱합니다. 이 단위가 있어야 개인별 실적(일수·시간·강사료)이 집계되고, 외부강사 지급행이 자동으로 생기고, 캘린더 칩이 그대로 정산 근거가 됩니다. 단위를 "프로젝트 단위"나 "회차 단위"로 잡았다면 전부 다시 만들어야 했을 것입니다.
STEP 3 · 데이터 정의 — 세 단위가 다르다는 발견
진행 방식실물 자료에서 등장인물을 뽑아 테이블로 만듭니다. 완료 기준: 테이블마다 필드·연결·관계 종류가 적혀 있고, "이 구조로 답해야 할 질문"이 함께 적혀 있는가.
우리 설계의 핵심 판단은 프로젝트·계산서·강의가 서로 다른 단위라는 것을 인정한 데 있습니다. 한 프로젝트에 계산서가 2장 나가고 강의가 5건 있는 경우가 실제로 있었습니다. 이걸 한 테이블에 욱여넣으면 어느 쪽 기준으로도 집계가 안 됩니다.
고객사 (clients)
└─ 프로젝트 (projects) 사업·계약 단위. 계약 총액(부가세 별도) 보유
├─ 계산서 (invoices) 전자세금계산서 발행 단위 [N:1]
└─ 강의 (lectures) 하루 × 강사 1건 [N:1]
멤버 (members) ─→ 강의 내부 멤버 · 외부 강사
멤버 ─→ 지급 (payments) 외부 인건비 지급대장
일정 (events) 미팅 · 휴가 · 기타 · 공휴일 (강의 외)
계산서와 강의를 직접 연결하지 않는다
둘 다 프로젝트에만 매답니다. 계산서 1장이 강의 3건에 대응하는지 5건에 대응하는지는 업무상 정해져 있지 않고, 억지로 연결하면 헷갈리기만 합니다. 대신 집계 규칙을 이렇게 정의했습니다.
| 지표 | 정의 | 쓰이는 곳 |
|---|---|---|
| 회사 매출 | 발행완료 계산서의 공급가액 합 | 대시보드 · 월/연 매출 |
| 개인 실적 | 강의 강사료의 합 | 관리자 화면 · 개인별 일수·시간·강사료 |
| 미청구 | 프로젝트 계약총액 − 발행완료 계산서 합 | 대시보드 · "발행 누락" 추적 |
STEP 4~5 · 화면 기획과 개발계획서
백엔드는 위의 8개 테이블, 프론트엔드는 화면 목록으로 정리했습니다. 이 목록이 그대로 10회차 뒤의 좌측 네비게이션이 됩니다.
| 화면 | 보여주는 것 | 할 수 있는 것 |
|---|---|---|
| 캘린더 | 주 단위 연속 그리드 · 사람별 행 · 강의 음영 | 일정 추가, 드래그로 날짜·담당자 이동 |
| 프로젝트 | 계약·금액·단계·수행자·계산서 수 | 검색·필터·그룹(서브토탈), 상세에서 강의·계산서 편집 |
| 강의 | 고객사·프로젝트·강사·날짜·시간·장소·강사료 | 엑셀식 인라인 편집 |
| 계산서 | 발행상태·수금상태·공급가액·세액 | 발행 요청·상태 변경 |
| 지급 | 외부강사 share·원천징수·실지급 | 소득구분 선택 시 자동 계산 |
| 대시보드 / 관리자 | 매출·미청구·미수금 / 개인별 실적 | 기간 조회 (관리자 화면은 권한 분리) |
계획 문서에는 한 버전 아래로 적어 두었지만, 스캐폴드 도구가 설치한 최신 조합을 그대로 썼습니다. 기능이 동일하고 문제가 없었기 때문입니다. 계획서와 실제가 다르면 실제를 기록에 반영하는 것이 결정 로그의 역할입니다.
1회차 산출물
- 개발계획서 — SIPOC · 3층 데이터 모델 · 테이블 정의 · 화면 목록 · 로드맵 · 미결 항목
- 이관 규칙 초안 — 약칭↔정식명, 삭제/미팅/기타 분류, 강사료 배분 원칙
1회차를 끝낼 때 확인한 것
- 인풋이 "실물 파일 이름 + 시트명"까지 적혀 있다.
- 자동화의 최소 단위(무엇 1건 = 무엇)가 한 문장으로 정의돼 있다.
- 테이블 간 관계(1:N)와 "연결하지 않기로 한 것"이 명시돼 있다.
- 이 구조로 답해야 할 질문 목록이 있다.
- 만들지 않을 것 목록이 있다.
2회차 — MVP: 캘린더 한 장을 관통시킨다
- 전체 기획 중 무엇을 MVP의 심장으로 골랐고, 기준은 무엇이었는가.
- 목업 대신 실데이터를 먼저 넣기로 한 판단과 그 이유.
- MVP가 답해야 했던 "자동화 가능성 질문"은 무엇이었는가.
핵심 흐름 선정 — 왜 캘린더였나
화면 10개 중 캘린더를 먼저 만든 이유는 세 가지 기준이 전부 거기서 겹쳤기 때문입니다.
| 기준 | 질문 | 우리의 답 |
|---|---|---|
| 가치의 중심 | 약속한 효과가 실제로 발생하는 기능은? | 구글 시트를 대체하는 순간 이중 등록이 사라진다 |
| 흐름의 척추 | 인풋이 아웃풋이 되기까지 반드시 거치는 최단 경로는? | 시트 셀 → 강의 1건 → 프로젝트 연결 → 집계 |
| 불확실성 | "정말 될까?" 가장 자신 없는 구간은? | 셀 위치(날짜×사람행)만으로 강의를 정확히 복원할 수 있는가 |
개발 순서를 바꾼 결정 — 목업보다 실데이터 먼저
진행 방식MVP 범위를 확정하고 구현합니다. 완료 기준: 인풋 → 아웃풋 한 사이클이 실제 데이터로 관통되는가.
원래 계획은 화면용 테이블 컴포넌트를 먼저 만들고 목업 데이터로 확인하는 순서였습니다. 그런데 목업으로 테이블을 만들 바에는 실데이터를 먼저 확보해서 모든 화면을 처음부터 실데이터로 개발·검증하는 편이 낫다고 판단해 순서를 바꿨습니다.
이유: 목업과 실데이터가 어긋나면 화면을 두 번 만들게 됩니다. 그리고 우리 과제에서는 데이터 정확성이 곧 성패였으므로 이관 스크립트를 먼저 검증해야 했습니다.
그래서 2회차의 실제 작업은 "캘린더 화면 만들기"가 아니라 "이관 스크립트 + 캘린더 화면"이 되었습니다. 첫 이관 결과는 다음과 같았습니다.
즉 강의 756건 중 185건(24%)만 프로젝트에 붙었습니다. 나머지 571건은 "(미연결 강의)"라는 임시 프로젝트에 격리했습니다. 중요한 것은 버리지 않았다는 점입니다 — 전량을 DB에 적재하되 연결만 보류했습니다.
고객사 매칭이 부분일치만 지원해서 약칭·경유처 표기·회차별 강사 배분을 아직 반영하지 못한 상태였습니다. 여기서 완벽을 추구하는 대신 "데이터 유실 0, 연결 정확도는 측정 대상"으로 정의하고 3회차로 넘겼습니다.
2회차 산출물
- MVP 범위표 — 만들 것(캘린더·이관) / 미룰 것(나머지 화면·인증·정산)
- MVP 코드 + OVERVIEW.md — 실행 방법, 현재 동작 범위
- 자동화 가능성 실험 기록 — 연결 185/756 = 24%, 미연결 571건 격리
- 결정 로그 시작 — D1~D13
3회차 — 하네스 엔지니어링: 품질을 숫자로 만든다
- 우리가 품질 지표를 무엇으로 정의했는가.
- 현수준 측정 → 원인분석 → 개선 → 재측정 사이클의 실제 사례를 따라갈 수 있다.
- 계산 로직을 원본과 전수 대조해 검증한 방법.
품질 지표 정의 — 무엇을 몇 점으로 잴 것인가
우리 아웃풋은 "화면"이 아니라 "옮겨진 데이터"였습니다. 그래서 지표도 데이터 정확도로 잡았습니다.
| 지표 | 정의 | 현수준 | 판정 기준 |
|---|---|---|---|
| 강의–프로젝트 연결률 | 연결된 강의 ÷ 전체 강의 | 24% | 미연결분은 앱에서 수동 배정 가능해야 함 |
| 강사료 배분 정확도 | 회차별 강사료 합 = 견적 공급가액 | 미측정 | 마진 0 — 합이 정확히 일치해야 함 |
| 원천징수 계산 일치율 | 계산 결과 = 지급대장 엑셀 값 | 미측정 | 100% 외에는 의미 없음(돈이므로) |
| 숨은 정보 복원율 | 셀 노트에서 살려낸 시간·장소·메모 건수 | 0건 | 많을수록 좋음 |
원인분석 → 개선 → 재측정 ①: 연결률 24% → 89%
진행 방식측정값이 낮은 원인을 찾아 고치고, 같은 방법으로 다시 잽니다. 완료 기준: Before/After가 같은 지표로 비교되는가.
원인은 사람만 알던 규칙이 코드에 없다는 것이었습니다. 구체적으로 세 가지였습니다.
| 원인 | 사람은 어떻게 알았나 | 코드에 넣은 규칙 |
|---|---|---|
| 고객사 약칭 | "이 약칭은 저 회사"라고 다 알고 있음 | 별칭 매핑 표를 이관 코드에 명시 |
| 경유처 표기 | 교육 플랫폼을 통해 들어온 건은 회사명이 두 개 | 정식명 + 경유처 접미 형태로 통일 |
| 회차별 강사 배분 | 같은 프로젝트라도 회차마다 메인·보조가 다름 | 회차별 배분 규칙을 금액까지 코드화 |
규칙을 덧붙이는 대신 이관 스크립트를 다시 썼습니다. 그리고 규칙이 가장 복잡한 고객사 한 곳을 검증 표본으로 삼아 회차별 건수·인당 금액·강사료 합계를 전부 대조했습니다 — 전부 일치.
결과: 강의 555건 중 연결 493건(89%), 미연결 62건. 강사료는 429건이 채워졌고 그중 88건은 명시 규칙, 341건은 공급가액 ÷ 강의수 균등 배분입니다.
원인분석 → 개선 → 재측정 ②: 원천징수 36행 전수 대조
외부 강사에게 지급할 때는 소득세와 지방세를 떼야 합니다. 이 계산은 틀리면 안 되는 종류입니다. 처음 구현은 "원 단위 내림"이었고, 틀렸습니다.
실제 수식은 10원 단위 절사이고, 게다가 순차 계산이었습니다. ① 소득세 = 내림(share × 세율, 10원 단위) → ② 지방세 = 내림(① × 10%, 10원 단위) → ③ 원천징수 = ① + ② → ④ 실지급 = share − ③. 지방세를 share에서 직접 계산하면 값이 달라집니다.
검증: 지급대장에서 값이 채워진 전 행(36행)과 대조해 100% 일치를 확인했습니다.
| 소득구분 | 소득세 | 지방세 | 실지급 |
|---|---|---|---|
| 사업소득 | 3% | 소득세의 10% | share − 원천징수 (합계 3.3%) |
| 기타소득 | 8% | 소득세의 10% | share − 원천징수 (합계 8.8%) |
| 전자세금계산서 | 원천징수 없음 | share × 1.1 (부가세 가산) | |
버려질 뻔한 정보를 되찾다 — 셀 노트 1,022개
측정을 하다 보면 "없다고 생각했던 것이 사실 있었다"는 발견이 나옵니다. 우리에게 그 순간은 셀 노트였습니다 — 1회차에서 본 캘린더 캡처의 붉은 삼각형이 그것입니다.
내려받은 xlsx 안에 셀 노트 1,022개가 주석으로 그대로 보존돼 있었습니다. 노트는 정형 템플릿(시간 / 장소 / 주제 / 담당자 연락처 / 특이사항)이었습니다.
노트 파서를 붙이고 시간 표기를 정규화(예: "1시~3시", "오후3시" → 24시간 HH:MM, 애매하면 원문을 메모에 보존)한 결과, 강의 555건 중 시간 209 · 장소 142 · 메모 181건을 복원했습니다.
3회차 산출물
- 품질 지표 정의서 — 4개 지표와 판정 기준
- 측정·개선 리포트(Before/After) — 연결률 24%→89%, 원천징수 36행 100% 일치, 노트 복원 532건
- 재사용 가능한 검증 스크립트 — 이후 회차에서 데이터가 바뀔 때마다 재실행
3회차를 끝낼 때 확인한 것
- 지표가 "몇 점"으로 표현되고, 측정 방법이 재현 가능하다.
- 개선 전후를 같은 지표·같은 방법으로 비교했다.
- 돈·법 관련 계산은 원본과 전수 대조했다.
- 자동화가 못 미친 잔여분을 사람이 처리할 통로가 화면에 있다.
4회차 — MVP를 넘어: 기능 목록 100% 구현
- MVP 이후 추가 기능을 어떤 순서로 붙였는가.
- 같은 조작감을 여러 화면에 퍼뜨리기 위해 무엇을 공용화했는가.
- 기능 확인과 데이터 확인을 어떤 기준으로 했는가.
무엇부터 붙였나 — 화면을 늘리지 않고 "테이블"을 늘렸다
남은 화면은 프로젝트·강의·계산서·고객사·멤버·지급이었습니다. 이들은 겉보기엔 다르지만 전부 "목록 + 검색·필터·정렬·그룹 합계 + 상세 편집"이라는 같은 뼈대였습니다. 그래서 화면을 하나씩 만드는 대신 공용 DataTable을 만들고 컬럼 설정 파일만 화면마다 따로 두었습니다.
강의 화면에 엑셀식 편집을 넣기 위해 전용 스프레드시트 라이브러리를 검토했지만 채택하지 않았습니다. 이유: 그 라이브러리는 이미 만들어 둔 공용 툴바(검색·그룹·필터·정렬)와 통합되지 않아 강의 화면만 이질적이 되기 때문입니다.
대신 이미 쓰고 있던 테이블 라이브러리의 컬럼 순서·폭 기능과 드래그 라이브러리를 공용 DataTable에 흡수시켰습니다. 모든 테이블이 같은 조작감을 유지합니다.
이 결정 덕분에 이후 회차에서 화면이 늘어나도 사용자가 새로 배울 것이 없었습니다. 한 테이블에서 익힌 조작이 일곱 테이블에서 그대로 통합니다.
3층 구조가 한 화면에서 만나는 곳 — 프로젝트 상세
기능 확인과 데이터 확인 — 두 가지는 다른 일이다
진행 방식기능 목록 대비 구현 여부와, 화면에 뜬 값의 정확성을 따로 점검합니다. 완료 기준: 두 점검이 각각 기록으로 남았는가.
| 점검 | 질문 | 우리가 실제로 한 것 |
|---|---|---|
| 기능 확인 | 기획한 화면·동작이 전부 있는가 | 화면 목록 대비 전 페이지 응답 확인 + 타입·린트 0건을 회차마다 반복 |
| 데이터 확인 | 화면에 뜬 값이 원본과 같은가 | 계약총액 합·계산서 합·강의 건수를 이관 원본과 대조. 합계행을 화면에 상시 노출 |
필터를 걸면 필터된 전체의 합이 표 하단에 고정 표시되고, 헤더도 스크롤에 고정됩니다. 검증을 위해 따로 리포트를 뽑을 필요 없이 쓰면서 검산이 됩니다.
4회차 산출물
- 기능 확인 결과표 — 화면 목록 대비 구현·응답 점검
- 데이터 검증 보고 — 합계 대조, 잔여 불일치의 화면 표시 방식
- 기능 완성 코드 + 갱신된 OVERVIEW.md
5회차 — 고도화와 인접 시스템 연계
- 쓰다 보니 드러난 구조적 한계를 어떻게 되돌려 고쳤는가.
- 위험한 스키마 변경을 안전하게 수행한 절차.
- 시스템 밖(메일·PDF)으로 확장한 연계 기능.
고도화 ① — 테이블 두 개를 하나로 합친 대공사
1회차 설계에서는 내부 멤버와 외부 강사를 별도 테이블로 두었습니다. 성격이 다르니 자연스러운 판단이었습니다. 그런데 쓰다 보니 문제가 드러났습니다 — 퇴사자입니다. 내부 직원이 퇴사하면 외부 강사가 되고, 그 사람의 과거 강의는 그대로 남아 있어야 합니다.
합치기 전
- 강의에 두 개의 외래키(내부용·외부용)
- 내부↔외부 재배정 = 한 키를 지우고 다른 키를 채움
- 강사 지정 값에 접두사로 구분을 심어야 함
- 집계 쿼리가 두 갈래로 갈라짐
합친 뒤
- 강의에 강사 필드 하나
- 재배정 = 필드 값 하나 교체
- 구분은 멤버 테이블의 속성(내부/외부)
- 집계가 한 갈래
이미 데이터가 들어 있는 테이블을 합치는 작업이라, 한 번에 하지 않고 나눴습니다. ① 새 구조를 추가만 하고(기존 유지) → ② 데이터를 옮기고 양쪽이 맞는지 검증 → ③ 그 뒤에야 옛 컬럼·테이블 제거. 작업 전 DB 백업.
검증 결과: 내부 7명·외부 20명 통합, 강의 배정 483건·미배정 68건, 지급 111건 전부 연결 유지.
함정도 있었습니다 — 기존 내부 멤버들의 새 컬럼이 빈 문자열이라 조회 시 파싱 오류가 났고, 빈 목록으로 채워 넣어 해결했습니다.
연계 ② — 시스템 밖으로: 견적서 PDF와 메일 발송
진행 방식인접 시스템(메일·파일·알림)으로 확장합니다. 완료 기준: 시스템 안에서 만든 산출물이 밖으로 나가 실제 업무를 대체하는가.
프로세스의 맨 앞(견적)이 아직 시스템 밖에 있었습니다. 5회차에 이걸 안으로 들였습니다.
| 기능 | 내용 |
|---|---|
| 견적서 작성 | 고객사·품목·수량·단가 입력 → 화면에서 바로 서식 미리보기 |
| PDF 생성 | 화면 그대로 PDF로 변환. 견적서 화면과 프로젝트 등록 화면이 같은 생성 함수를 공용 |
| 메일 발송 | 제목·본문 편집(기본값 자동 생성) + PDF 첨부. 발송 시각을 기록해 이력으로 남김 |
| 프로젝트 전환 | 수주되면 견적서에서 프로젝트를 바로 생성 — 재입력 없음 |
발행 요청 알림(주기·시각·담당자)을 설정 화면까지만 만들고, 실제 자동 발송은 배포 환경의 스케줄러에 붙이기로 했습니다. 로컬에서 동작하지 않는 기능을 억지로 흉내 내지 않고 경계를 명시한 것입니다.
5회차 산출물
- 고도화 내역서 — 멤버 테이블 통합(3단계 절차와 검증 결과)
- 연계 설계서 — 견적서 PDF·메일 발송, 알림 설정과 미구현 경계
- 고도화·연계가 반영된 코드 + 갱신된 OVERVIEW.md
6회차 — 배포: 로그인과 보안 설정
- 사내 도구에 맞는 인증 방식을 고른 기준.
- 배포 환경 제약 때문에 구조를 갈아엎은 사례를 따라갈 수 있다.
- "정문만 잠그면 안 되는" 이유 — 우리가 겪은 뒷문.
인증 방식 선택 — 큰 프레임워크를 쓰지 않기로 했다
이유 두 가지입니다. ① 우리가 쓰는 프레임워크 버전은 변경이 많은 최신 버전이라, 내부 API에 깊이 의존하는 무거운 라이브러리는 충돌 위험이 컸습니다. ② 사용자가 사내 소수라 서명된 토큰 쿠키 + 비밀번호 해시만으로 충분·안전했습니다.
대신 이 업무에 꼭 필요한 것을 직접 넣었습니다 — 가입 승인 플로우. 가입하면 '대기' 상태로 생성되고, 관리자가 승인해야 로그인이 됩니다. 이건 범용 프레임워크의 기본 기능이 아닙니다.
배포가 막혀 구조를 갈아엎다 — 그리고 발견한 보안 구멍
진행 방식배포 대상 환경에서 실제로 빌드·동작하는지 확인합니다. 완료 기준: 로컬이 아니라 배포 환경 기준으로 통과했는가.
처음에는 요청을 가로채는 미들웨어 계층에 인증 게이트를 두었습니다. 로컬에서 7가지 시나리오 전부 통과했습니다. 그런데 배포용 빌드가 "이 방식은 지원하지 않는다"며 실패했습니다.
조사 결과 배포 어댑터의 알려진 제약이었고 우회할 방법이 없었습니다. 그래서 인증 게이팅을 레이아웃/페이지 계층으로 옮겼습니다 — 프레임워크가 공식적으로 권장하는 보안 패턴이기도 합니다. 재검증 10/10 통과, 빌드 성공.
그런데 이 이전이 보안 구멍을 만들었습니다. 미들웨어가 없으면 서버 액션(폼 제출·데이터 변경) 요청이 레이아웃의 인증 검사보다 먼저 실행됩니다. 즉 로그인하지 않아도 데이터를 바꿀 수 있는 상태였습니다.
조치: 데이터를 변경하는 서버 액션 46개 전부의 첫 줄에 인증 확인을 삽입했습니다. 수작업이 아니라 코드 변환 스크립트로 처리하되, 함수 시그니처를 잘못 건드리지 않도록 괄호 짝을 세어 정확한 위치를 찾았습니다.
6회차 산출물
- 운영 접속 경로와 계정 발급 절차 — 가입 → 관리자 승인
- 보안 점검 보고 — 비로그인 차단·권한 구분·위조 쿠키 차단·서버 액션 46개 가드
- 배포 구성 문서 — 환경변수, 빌드 제약과 우회 결정
6회차를 끝낼 때 확인한 것
- 비로그인 상태에서 화면 접근이 차단된다.
- 비로그인 상태에서 데이터 변경 요청도 차단된다.
- 권한(일반/관리자) 구분이 실제로 동작한다.
- 위조된 인증 정보가 거부된다.
- 관리자가 스스로를 잠글 수 없다(마지막 관리자 보호).
7회차 — 안정화와 문서 체계
- 부하 시험 결과를 어떻게 해석했는가.
- 시스템 설명서와 사용자 매뉴얼의 역할 차이.
- 개발 중에 쌓은 기록이 어떻게 문서가 되었는가.
부하 시험 — 숫자보다 해석이 중요하다
배포 전 상태로 동시 요청을 부어 봤습니다.
에러는 0인데 동시 요청 지연이 2.8초로 튀었습니다. 여기서 "느리니 최적화하자"로 가지 않은 것이 이 회차의 핵심입니다. 원인을 먼저 봤습니다 — 당시 로컬 DB가 요청을 한 줄로 세워 처리하는 종류였기 때문이고, 운영 환경의 DB로 바꾸면 사라지는 성질의 지연이었습니다.
함께 넣은 것은 기본 보안 헤더입니다 — 서버 종류를 알리는 헤더 제거, 콘텐츠 타입 추측 금지, 다른 사이트에 프레임으로 삽입 금지, 참조 주소 전달 제한. 코드 몇 줄이지만 안 하면 지적받는 항목입니다.
문서 4종 — 개발 중에 쌓인 기록에서 그대로 나왔다
진행 방식시스템 설명서와 사용자 매뉴얼을 작성해 사용자가 찾을 위치에 둡니다. 완료 기준: 다음 사람이 이 문서만 보고 인수할 수 있는가.
| 문서 | 담는 것 | 독자 |
|---|---|---|
| OVERVIEW.md | 시스템 소개, 설치·실행 방법, 알려진 한계 | 다음 개발자·인수자 |
| 설계 문서 | 배경·목적, 데이터 모델, 화면·기능, 권한, 이관 규칙, 미결 항목 | 구조를 이해해야 하는 사람 |
| 배포 가이드 | 환경변수, 배포 절차, 스키마 변경 순서, 제약 | 운영자 |
| 결정 로그 | 무엇을 왜 그렇게 정했는가 — 되돌린 결정 포함 | "왜 이렇게 돼 있지?"를 묻는 모든 사람 |
7회차 산출물
- 안정화 점검 보고 — 부하 결과와 해석, 후속 과제 4건
- 시스템 설명서 3종 — OVERVIEW.md · 설계 · 배포 가이드
- 사용자 매뉴얼 — 화면 캡처와 업무 시나리오 위주
8회차 — 오픈 베타: 운영 환경으로 옮기고 실제로 쓰기 시작한다
- 로컬 전용 구현이 운영 환경에서 왜 깨졌는가.
- 로컬과 운영 데이터를 안전하게 오가기 위해 만든 것.
- 실사용 전환에 앞서 무엇을 추적 가능하게 만들어 두었는가.
로컬에서만 되던 것들을 고친다
진행 방식운영 환경으로 옮기고 실사용을 시작합니다. 완료 기준: 로컬 가정에 기대던 코드가 전부 걷혔는가.
실사용으로 넘어가면서 두 가지가 걸렸습니다. 둘 다 "로컬에서는 됐는데" 유형입니다.
| 로컬 가정 | 운영에서 깨지는 이유 | 조치 |
|---|---|---|
| DB가 파일 하나 | 동시 접속·확장이 안 되고, 배열·열거형 등 표현력 제약 | 운영급 DB로 통일 — 로컬도 같은 종류로 맞춰 "배포 시 무변경" |
| 첨부파일을 서버 폴더에 저장 | 배포 환경은 파일을 쓸 수 있는 디스크가 없음 | 객체 스토리지로 이전. 비공개 버킷 + 서명된 임시 URL로만 열람 |
"로컬은 가볍게, 운영은 제대로"라는 이중 구성을 유지할지 고민하다가 통일을 택했습니다. 이중 구성은 스키마 파일과 접속 코드가 갈라지고, 배포할 때마다 바꿔야 합니다. 통일하면 배포 시 변경 사항이 0이 됩니다.
데이터 이전은 내보내기 → 가져오기로 1,382행 전부 옮겼고, 건수·날짜 경계·금액 합계·구조화 필드를 대조해 검증했습니다.
첨부파일 저장소 이전은 왕복 검증까지 했습니다 — 버킷 생성(비공개) → 업로드 → 서명 URL로 조회 성공 → 서명 없이 직접 접근하면 차단 → 정리. 마지막 항목이 핵심입니다.
로컬 ↔ 운영 동기화 — 실수를 막는 기본값
실사용이 시작되면 운영 데이터가 진짜가 됩니다. 그런데 대량 수정은 로컬에서 하는 게 편합니다. 그래서 세 개의 명령을 만들었습니다.
diff 운영 ↔ 로컬 차이(추가·삭제·변경 건수)만 보여 줌 — 읽기 전용 pull 운영 → 로컬로 가져오기 push 로컬 → 운영으로 올리기 ※ --force 를 붙이지 않으면 세 명령 모두 "미리보기"만 실행된다. 실제로 쓰는 동작은 명시적으로 요청해야만 일어난다.
8회차 산출물
- 오픈베타 전환 계획·공지 — 전환일, 기존 도구 사용 중단 시점
- 운영 전환 기록 — DB 이전 1,382행 검증, 저장소 이전 왕복 검증
- 동기화 도구와 사용 규칙 — diff/pull/push, 기본값=미리보기
9회차 — 실사용 데이터가 시키는 개선
- 실사용에서만 드러나는 종류의 문제를 예로 들 수 있다.
- 용어 혼동을 DB 단위로 정리한 방법.
- 날짜·시간대처럼 조용히 틀리는 버그를 어떻게 잡았는가.
개선 ① — 이름이 헷갈리면 데이터도 헷갈린다
진행 방식실사용에서 나온 개선 요청을 데이터로 우선순위를 매겨 처리합니다. 완료 기준: 개선의 근거가 "요청받아서"가 아니라 "이렇게 쓰이고 있어서"인가.
쓰다 보니 같은 단어가 두 뜻으로 읽히는 문제가 드러났습니다. '수금유형'을 뜻하는 필드 이름이 '지급'으로도 읽혀서, 화면과 코드 양쪽에서 혼선이 났습니다.
화면 라벨만 고치는 대신 테이블명·컬럼명·경로까지 바꿨습니다. 화면만 고치면 코드를 읽는 사람이 계속 헷갈리기 때문입니다.
방법: 단일 트랜잭션 안에서 이름만 변경(데이터 이동 없음)해 로컬과 운영에 동일하게 적용하고, 적용한 SQL을 파일로 남겨 기록했습니다. 데이터는 그대로 보존됩니다.
개선 ② — 하루가 밀리는 버그
캘린더에서 어떤 강의가 전날 칸에 나타나는 일이 있었습니다. 원인은 시간대였습니다 — 날짜를 저장할 때 한국 시각 자정이 세계 표준시로는 전날이 됩니다. 서버가 표준시로 계산하면 하루가 밀립니다.
날짜를 다루는 유틸리티 전체를 한국 시간대 기준으로 고정하고, 비교 기준도 마찬가지로 맞췄습니다. 특히 월별 집계 경계(7월 1일 강의가 6월로 잡히는 문제)에서 "가짜 중복"처럼 보이는 착시가 있었습니다 — 데이터가 잘못된 게 아니라 비교 기준이 잘못된 것이었습니다.
9회차 산출물
- 개선 백로그 — 실사용에서 나온 항목과 우선순위 근거
- 개선 리포트 — 용어 리네임(적용 SQL 보관), 시간대 고정, 화면 필터 재설계
- 지표 추세 — 미청구·미수금이 실제로 줄고 있는가
10회차 — 완성, 효과 정량화, 그리고 발표
- "최종 완성"을 이슈 0이 아니라 어떤 상태로 정의했는가.
- 실패한 선택을 인정하고 갈아탄 판단.
- 개발 중 기록이 어떻게 최종 발표 자료가 되었는가.
끝까지 안 되는 것은 갈아탄다
진행 방식최종 시스템을 완성하고 효과를 정량화합니다. 완료 기준: 남은 이슈가 "무엇인지 아는 상태"로 정리됐는가.
6회차에서 게이팅 구조까지 바꿔 가며 맞췄던 배포 어댑터가 끝내 안정적으로 빌드되지 않았습니다. 10회차에 다른 플랫폼으로 전환했습니다.
전환은 제거와 유지를 명확히 나눴습니다 — 제거: 이전 플랫폼 전용 설정·스크립트·의존성(패키지 259개 감소)과 주석 속 언급까지. 유지: DB 접속 어댑터(새 환경에서도 그대로 동작). 앱이 환경변수로만 설정을 읽고 있었기 때문에 애플리케이션 코드는 변경 불필요했습니다.
결정 로그에 이렇게 적었습니다 — "이후 배포는 새 플랫폼 기준. 다음 작업 때 헷갈리지 말 것."
효과 정량화 — 무엇이 얼마나 좋아졌는가
발표에서 답해야 할 질문은 하나입니다. "무엇이 얼마나 좋아졌는가." 우리의 답은 세 갈래였습니다.
| 구분 | Before | After |
|---|---|---|
| 사라진 작업 | 영업 성사 시 시트·에어테이블 양쪽에 이중 등록 | 한 곳에 입력하면 캘린더·프로젝트·집계에 자동 반영 |
| 사라진 작업 | 외부강사 지급 시 엑셀에서 수식 복사·행 추가 | 강의가 생기면 지급행 자동 생성, 소득구분 선택 시 원천징수 자동 계산 |
| 새로 가능해진 것 | "이 프로젝트 미청구액"을 알 방법이 없음 | 계약총액 − 발행완료 = 미청구가 상시 표시. 발행 누락이 드러남 |
| 새로 가능해진 것 | 개인별 강의 실적을 손으로 세야 함 | 일수·건수·시간·강사료가 자동 집계(관리자 권한) |
| 품질 | 셀 노트 속 시간·장소는 사람만 읽음 | 구조화되어 검색·집계 가능 (복원 532건) |
(시트·에어테이블·엑셀 → 시스템)
(나머지는 화면에서 배정)
(지급대장 전 행 대조)
(되돌린 결정 포함)
10회차 산출물
- 최종 코드 + 최종 OVERVIEW.md — 알려진 한계 포함
- 효과 평가서 — Before/After와 측정값
- 개발 히스토리 — 결정 로그 D1~D46을 시간순으로
- 문서·매뉴얼 개정본 · Lessons Learned
- 최종 발표 자료
Lessons Learned — 우리가 남긴 다섯 문장
| # | 교훈 | 어디서 나왔나 |
|---|---|---|
| 1 | 최소 단위를 먼저 정하라 | "강의 1건 = 하루 × 강사 1명"이 개인 실적·정산·캘린더를 전부 지탱했다 1회차 |
| 2 | 목업보다 실데이터가 빠르다 | 순서를 바꿔 이관을 앞당긴 덕에 화면을 두 번 만들지 않았다 2회차 |
| 3 | 돈 계산은 원본과 전수 대조하라 | 원 단위 내림으로 잘못 구현했던 원천징수를 36행 대조로 잡았다 3회차 |
| 4 | 정문을 옮기면 뒷문을 다시 보라 | 게이팅 이전 후 서버 액션이 무방비였다 — 46개에 가드 삽입 6회차 |
| 5 | 결정을 그때그때 적어라 | 7회차의 문서 4종이 몰아쓰기 없이 나왔다. 되돌린 결정이 특히 값졌다 전 회차 |
부록 — 결정 로그로 본 10회차
우리 개발 노트에 남은 결정 46건을 회차에 대응시킨 표입니다. 어느 과제든 이 형태로 쌓아 두면, 10회차의 문서·발표가 저절로 만들어집니다.
| 회차 | 결정 번호 | 대표 결정 |
|---|---|---|
| 1~2회차 | D1 ~ D13 | 스택 확정 · 배열/열거형을 단순 타입으로 저장 · 개발 순서 변경(실데이터 우선) · 이관을 초안 뼈대로 끊기 · 드래그 이동은 낙관적 갱신 |
| 3회차 | D6 · D16 · D17 | 원천징수 수식 재구현·36행 대조 · 이관 전면 재작성으로 연결률 89% · 셀 노트 1,022개 복원 |
| 4회차 | D14 ~ D25 | 데이터 페이지 동적 렌더 · 스프레드시트 라이브러리 미채택(UX 일관성) · 인라인 편집 · 헤더/합계 고정 |
| 5회차 | D26 ~ D42 | 멤버 테이블 물리 통합(3단계) · 프로젝트 상세를 저장 버튼 1회 방식으로 · 지급대장 양식 전면 개편 · 견적서·메일·PDF |
| 6회차 | D43 | 경량 커스텀 인증 · 가입 승인제 · 미들웨어 → 레이아웃 게이팅 전환 · 서버 액션 46개 인증 삽입 |
| 7회차 | D43(4단계) | 부하 시험(에러 0, 지연은 로컬 DB 특성) · 보안 헤더 · 후속 과제 4건 명시 |
| 8회차 | D44 | 로컬·운영 DB 통일 · 첨부파일 객체 스토리지 이전(왕복 검증) · 동기화 도구(기본값=미리보기) |
| 9회차 | D45 | 용어 리네임(DB 단위) · 날짜 한국 시간대 고정 · 캘린더 필터·되돌리기 · 대시보드 개편 |
| 10회차 | D46 | 배포 플랫폼 전환 — 실패 인정, 흔적까지 제거, 기준을 로그에 못박음 |
다른 과제에 옮겨 볼 것
- 우리 과제의 "1건 = 무엇"에 해당하는 최소 단위를 한 문장으로 쓸 수 있다.
- 서로 단위가 다른 데이터를 억지로 한 테이블에 넣고 있지 않다.
- 품질 지표가 "100%여야 하는 것"과 "높을수록 좋은 것"으로 나뉘어 있다.
- 결정 로그를 이미 쓰기 시작했다(회차마다 한 줄이라도).
- 자동화가 못 미친 잔여분을 사람이 화면에서 처리할 통로가 있다.