사내 신사업 TF에 합류했다.

공사현장 사진을 자동으로 정리하고, 공정 진행을 한 화면에서 관리하는 서비스를 만드는 일이었다.

그동안 나는 화면을 만드는 사람이었다.

API는 받아서 썼고, DB는 남이 설계했고, 배포는 누군가 해줬다.

이번엔 그 전부를 내가 하게 됐다.

기획 회의부터 들어가서, 스키마를 그리고, 화면을 만들고, 서버에 올리고, 버전을 관리하고, 장애를 처리했다.

만들면서 배운 것보다 운영하면서 배운 것이 더 많았다.

사용자가 개발자가 아니다

이 서비스에는 사용자가 세 종류 있다.

현장에서 사진을 찍어 올리는 협력사 반장님, 사무실에서 검수하는 우리 PM, 그리고 진행 상황을 확인하는 발주처 주무관.

이 중에 반장님이 제일 어려웠다.

처음엔 당연하게 로그인 화면부터 그렸다.

회원가입하고, 비밀번호 만들고, 앱 설치하고.

그런데 생각해보니 현장에서 장갑 낀 손으로 휴대폰을 꺼내는 분에게 회원가입을 요구하는 건 말이 안 됐다.

그 단계에서 이탈하면 사진은 영원히 안 올라온다.

그래서 전부 없앴다.

PM이 보낸 문자 링크를 누르면 바로 업로드 화면이 뜬다.

앱 설치 없음, 회원가입 없음, 비밀번호 없음.

사진 여러 장 골라서 올리면 끝이고, "어느 현장 사진인지"는 시스템이 알아낸다.

사진의 EXIF에서 GPS를 뽑고, VWorld 지오코딩으로 좌표를 주소에 매칭해서 어느 지점에서 찍은 사진인지 자동 판별한다.

위치 정보가 없으면 목록에서 고르거나 "모르겠어요"를 누르면 되고, 나머지는 PM이 정리한다.

이 결정을 하면서 느낀 게 있다.

사용자를 개발자 기준으로 상상하면 안 된다.

나에게 회원가입은 3초짜리 절차지만, 현장 반장님에게는 서비스를 안 쓸 이유다.

결정 1. 사진을 옮기지 않는다

사진이 올라오면 "어느 지점, 어느 공정 단계"에 배치된다.

PM이 검수 보드에서 드래그로 재배정하기도 한다.

처음엔 단순하게 생각했다. 배치가 바뀌면 파일을 그 폴더로 옮기면 되지.

그런데 그렇게 하면 문제가 생긴다.

파일을 옮기는 중에 실패하면 사진이 어디에도 없는 상태가 될 수 있다.

그리고 나중에 클라우드에서 사내 서버로 이전할 때 폴더 구조가 통째로 발목을 잡는다.

그래서 원본 파일은 한 번 저장되면 절대 움직이지 않게 했다.

배치는 DB 메타데이터로만 표현한다. 일종의 가상 폴더다.

드래그로 재배정하면 파일은 그대로 있고 DB의 컬럼 하나가 바뀐다.

이 결정이 나중에 크게 도움이 됐다.

프로토타입은 Supabase로 만들었는데, 사내 정책상 온프레미스 PostgreSQL로 옮겨야 했다.

스토리지 접근을 어댑터로 분리해두고 파일 위치가 고정이었던 덕에, 화면 코드는 거의 손대지 않고 이전을 끝냈다.

결정 2. 삭제를 DB가 금지하게 했다

이 서비스의 사진은 준공 증빙이다.

공사가 끝나고 몇 년 뒤에 감사가 들어오면 그때 필요한 자료다.

그러니까 실수로든 고의로든 지워지면 안 된다.

처음엔 "삭제 버튼을 안 만들면 되지"라고 생각했다.

그런데 그건 UI 레벨의 약속이라 언제든 깨진다.

내가 나중에 급하게 관리 기능을 만들다가 delete를 쓸 수도 있고, 다른 개발자가 합류해서 모르고 지울 수도 있다.

그래서 DB 제약으로 물리 삭제를 막았다.

사진과 기록은 지워지지 않고, 교체하거나 비활성화만 된다.

대표 사진을 바꿔도 기존 사진은 버려지지 않고 "여분"으로 남는다.

사진 보정도 같은 원칙으로 만들었다.

회전, 수평 미세조정, 확대를 지원하는데 원본 파일은 건드리지 않는다.

보정값을 따로 저장해두고 화면과 엑셀 출력에서 일괄로 적용한다.

중요한 규칙은 사람의 약속이 아니라 시스템의 제약으로 만들어야 한다.

이건 코드 리뷰 컨벤션을 만들 때도 같은 생각을 했었는데, 스키마 레벨에서 하니 훨씬 강력했다.

운영에서 배운 것 1. 배포에는 순서가 있다

여기서 실수를 했다.

DB 마이그레이션이 포함된 배포였다.

스키마를 바꾸고, 그 스키마를 쓰는 새 코드를 올려야 하는 상황.

그런데 순서를 헷갈려서 DB를 적용하기 전에 서버를 재시작해버렸다.

새 코드가 아직 없는 컬럼을 찾으면서 500이 떨어졌다.

다행히 무중단으로 DB를 사후 적용해서 복구했지만, 그 몇 분 동안 심장이 내려앉았다.

이걸 겪고 나서 배포 런북에 신호 규칙을 추가했다.

DB가 낀 배포와 번들만 바꾸는 배포를 문서에서 명확히 구분하고, 순서를 단계별로 못 박았다.

같은 실수를 다시 하지 않으려면 기억에 의존하면 안 되고 절차로 남겨야 한다.

프론트만 할 때는 배포가 그냥 "머지하면 되는 것"이었다.

지금은 배포가 되돌릴 수 없는 행위라는 걸 안다.

운영에서 배운 것 2. 기능은 서로 얽혀 있다

버전을 올릴수록 기능이 늘었고, 어느 순간부터 한 기능을 고치면 엉뚱한 데가 깨졌다.

공정 구성을 바꾸면 공정률 계산이 영향을 받고, 공정률은 프로젝트 목록의 한 줄 요약에 나오고, 그건 또 주무관 화면에도 나온다.

사진 배치를 바꾸면 검수 보드, 노선도, 사진대지 엑셀이 전부 따라 움직인다.

머릿속으로만 알고 있으니 시간이 지나면 잊어버렸다.

그래서 기능 체이닝 지도를 문서로 만들었다.

어떤 기능이 어떤 데이터를 건드리고, 그게 어느 화면까지 파급되는지 축별로 정리한 문서다.

그리고 작업 착수 전에 이 문서를 먼저 확인하고, 기능을 바꾸면 문서도 갱신하는 걸 규칙으로 정했다.

이걸 만들고 나서 "이거 고치면 어디 깨지지?"에 답하는 시간이 확 줄었다.

혼자 개발하는 프로젝트여도 문서는 필요했다.

미래의 나는 남이나 마찬가지였다.

운영에서 배운 것 3. 배포 산출물도 봐야 한다

한번은 배포 번들 크기가 이상하게 커졌다.

확인해보니 개발하면서 쌓인 테스트 사진 1GB가 번들에 통째로 딸려 들어가고 있었다.

Next.js의 파일 트레이싱이 데이터 폴더까지 포함시킨 것이었다.

outputFileTracingExcludes로 제외하고, 배포 스크립트에서도 한 번 더 지우게 했다.

번들이 39MB로 줄었다.

로컬에서 잘 돌아간다고 끝이 아니었다.

만들어진 결과물이 무엇을 담고 있는지도 확인해야 했다.

정리하면

프론트엔드만 할 때는 "잘 만드는 것"이 목표였다.

풀스택으로 하나를 끝까지 책임져보니 목표가 달라졌다.

  • 만드는 것보다 잘못될 수 있는 경로를 막는 것이 중요했다
  • 규칙은 약속이 아니라 제약으로 만들어야 지켜졌다
  • 배포는 코드를 올리는 게 아니라 순서가 있는 절차였다
  • 혼자 하는 프로젝트에도 문서가 필요했다

아직 서툴다.

DB 설계는 선임에게 리뷰를 받아 고쳤고, AI 분류 서버는 다른 분이 만들고 있다.

그래도 하나의 서비스가 기획에서 시작해 실제 서버에서 돌고, 사용자가 쓰고, 버전이 올라가는 과정을 처음부터 끝까지 본 건 처음이다.

화면만 볼 때는 안 보이던 것들이 보인다.

이 경험이 다음 프로젝트에서 어떻게 쓰일지 궁금하다.