20년 가까이 개발해 온 내가, AI와 함께 실제로 사용하는 사내 서비스를 만들기까지.
이 글은 AI가 작성한 코드를 얼마나 빨리 만드는지가 아니라, 무엇을 결정하고 어디까지 확인해야 하는지를 배워간 기록이다.
실제 운영 화면이 아닌, 로컬 개발 환경에서 샘플 데이터로 구성한 예시 화면입니다.
AI가 작성한 코드를 믿지 않던 개발자
나는 2000년대 초반 플래시 액션스크립트로 개발을 시작했다. 게임 클라이언트 개발을 거쳐 프론트엔드 개발까지, 20년 넘게 여러 기술의 변화를 겪으며 개발자로 일해 왔다. 그렇게 AI 시대를 맞았다.
사실 불과 몇 개월 전까지만 해도 나는 AI가 작성한 코드를 별로 믿지 않았다. 코드를 만들어 달라고 요청하더라도 결과물을 그대로 사용하지는 않았다. 처음부터 끝까지 직접 읽으며 문제가 없는지, 내가 의도한 방식으로 작성됐는지 확인했다. AI가 만든 코드를 다시 모두 검수해야 한다면 얼마나 도움이 되는 걸까 싶기도 했다.
그러다 올해 4월, 10년 조금 넘게 다니던 회사를 퇴사했다. 그 후에는 그동안 충분히 시도하지 못했던 것들을 만들어 보며 AI를 마음껏 활용했다. 단순히 코드 몇 줄을 요청하는 것이 아니라, 만들고 싶은 서비스를 처음부터 끝까지 함께 구현해 보는 경험이었다.
그 과정에서 생각이 많이 바뀌었다.
중요한 건 AI가 코드를 얼마나 잘 짜느냐만이 아니라, 내가 만들고 싶은 것과 지켜야 할 조건을 얼마나 구체적으로 설명할 수 있느냐는 것이었다.
목적과 기능, 구현 범위, 예외 상황까지 명확하게 전달할수록 결과물도 좋아졌다. 예전처럼 모든 코드를 한 줄씩 검수하지는 않게 됐다. 대신 요구한 기능이 실제로 동작하는지, 실수나 장애가 발생했을 때 어떻게 처리되는지, 운영 환경에서도 문제가 없는지를 더 꼼꼼히 확인하게 됐다.
물론 코드 리뷰가 필요 없어졌다는 뜻은 아니다. 특히 인증과 권한, 데이터 삭제, 배포처럼 영향이 큰 부분은 별도의 검증이 필요하다. 다만 내 일의 중심이 코드를 직접 작성하고 읽는 것에서, 무엇을 만들지 정하고 결과를 검증하는 일로 이동하고 있음을 느꼈다.
그리고 5월, 새로운 회사에 입사했다. 이곳에서 직원들이 실제로 사용할 인트라넷을 AI와 함께 처음부터 만들어 보기로 했다.
회사의 업무 흐름을 먼저 정해야 했다
처음 목표는 흩어져 있던 업무 정보를 하나로 연결하는 것이었다. 직원, 근태, 연차, 일정, 프로젝트, 일감, 투입 시간, 업무보고와 자료를 한 웹 서비스에서 관리하도록 했다.
기능을 하나씩 구현하는 것보다 어려웠던 건 기능 사이의 규칙을 정하는 일이었다. 일감이 끝나면 프로젝트도 완료되는 걸까? 제출한 업무보고를 다시 수정해야 한다면 누가 허용해야 할까? 실수로 퇴근 버튼을 누른 직원은 어떻게 해야 할까?
퇴근 취소가 대표적인 예였다. 직원에게는 실수를 수정할 경로가 필요하지만, 요청만으로 이미 제출된 업무보고의 잠금이 풀려서는 곤란했다. 그래서 직원이 취소를 요청하고 관리자가 승인하도록 나눴다. 승인 전에는 기존 퇴근 상태와 보고서 잠금을 유지하고, 승인할 때만 두 상태를 하나의 트랜잭션으로 변경하도록 했다. 본인 요청을 스스로 승인하거나 같은 요청을 중복 처리하는 상황도 막도록 했다.
화면 역시 실제 사용 흐름에 맞춰 다듬었다. 반복되는 버튼과 팝업, 상태 표시는 공통 컴포넌트로 정리하고, 여러 줄의 업무보고를 입력할 수 있게 했다. 팝업을 이동할 때 화면이 깜빡이거나 입력 내용이 사라지는 작은 불편도 손봤다.
기능 목록보다 먼저 정해야 할 것은, 직원들이 실제로 어떻게 일하고 실수했을 때 어떻게 되돌릴 수 있는지였다.
개발 PC에서 회사 NAS로: 동작하는 것과 운영되는 것의 차이
서비스는 회사 NAS에서 Docker와 PostgreSQL을 이용해 실행하고, Synology의 리버스 프록시를 통해 접근하도록 구성했다. 이때부터는 개발 PC에서 잘 동작했다는 사실만으로 안심할 수 없었다. 환경변수와 데이터 저장 위치, 포트 공개 범위, 운영 이미지에 포함해야 할 도구를 다시 확인했다.
예를 들어 데이터베이스 비밀번호에 특수문자가 있으면 연결 URL을 단순히 이어 붙이는 방식이 오류를 일으킬 수 있었다. 연결 정보를 적절히 인코딩하도록 바꾸고, 운영 환경에서도 Prisma migration에 필요한 도구와 설정을 사용할 수 있도록 정리했다.
Cloudflare와 HTTPS를 연결한 뒤에는 원본 NAS로 직접 접근할 수 있는 경로가 남아 있는지도 살폈다. 개발용 Compose 설정으로 DB 포트가 외부에 열릴 위험이 있었고, 웹 포트 역시 여러 네트워크 인터페이스에 노출된 구성을 발견했다. 개발 설정을 분리하고 운영 DB의 호스트 포트 공개를 없앴다. 이후 웹 접근은 내부 프록시와 독립된 gateway 경로로 제한했다.
NAS 방화벽은 회사 내부망과 관리 경로를 유지하면서 외부 웹 접속을 Cloudflare IP 대역으로 제한했다. 정상 도메인으로 접근할 때와 NAS 원본으로 직접 연결할 때의 결과를 따로 확인했다. 로그인 반복 시도 제한, SMTP 비밀번호 보호, 브라우저에 전달하는 사용자 정보의 범위, 배포 파일에 .env가 포함되지 않도록 하는 검사도 보완했다.
파일을 복사하고 이미지를 교체하는 것만으로 배포가 끝난 건 아니었다. DB 변경이 반영됐는지, 실제 요청이 올바른 경로로 흐르는지, 민감한 정보가 새어 나가지 않는지까지 확인해야 했다.
이메일 알림은 만들었는데, 직원들은 잘 보지 않았다
처음에는 업무 변경 사항을 이메일로 알렸다. 발송 기능은 동작했지만, 정작 직원들이 메일을 자주 확인하지 않았다. 알림을 보내는 것과 사람들이 확인하는 것은 전혀 다른 문제였다.
그래서 Electron 기반 Windows 데스크톱 앱을 만들었다. 인트라넷 알림을 PC에서 바로 확인하고, 웹과 앱의 읽음·삭제 상태가 동기화되도록 했다. 서버의 기록을 기준으로 두고 앱의 로컬 기록은 캐시로 다뤘다. 알림음과 표시 방식을 조정하고 오래된 알림이 한 번에 쏟아지지 않도록 제한했다.
이메일이 정상 발송됐다는 결과보다, 직원이 필요한 순간에 정보를 확인했는지가 더 중요했다.
내 PC 켜기·끄기와 작업폴더 열기
데스크톱 앱을 만들고 나니 인트라넷에서 자신의 PC 상태를 확인하고 전원을 켜거나 끄는 기능까지 연결하게 됐다. 켜기는 NAS에서 WOL 매직 패킷을 보내고, 종료는 해당 PC에 연결된 Agent가 명령을 처리하도록 나눴다. 서버는 로그인 사용자와 장치의 소유권을 확인하도록 했다.
WOL 구현에서는 뜻밖의 문제가 있었다. 코드에서는 패킷 전송에 성공했다고 나왔지만, 실제 PC는 켜지지 않았다. 확인해 보니 패킷이 Docker 네트워크에서는 보였지만 NAS의 물리 LAN 인터페이스까지 나가지 않았다. host network 시험에서 LAN 전송을 확인했고, 결국 WOL만 처리하는 NAS 내부 릴레이를 별도로 뒀다. 서명된 내부 요청만 받고 재사용과 반복 실행을 제한했으며, 릴레이 포트는 인터넷에 공개하지 않았다.
작업폴더 열기도 생각보다 간단하지 않았다. 같은 NAS 폴더라도 PC마다 드라이브 문자와 연결 기준 위치가 달랐다. 공통 작업폴더 아래 상대 경로를 사용하고, 각 PC에서 선택한 기준 폴더와 결합하도록 했다. 기준 경로는 PC에만 저장했다. NAS에 식별용 파일을 따로 만들지 않았으며, 허용된 작업폴더 밖으로 벗어나는 경로는 거부했다. 파일 자체를 실행하기보다 탐색기에서 위치를 보여 주도록 했다.
이 두 기능은 공통된 교훈을 줬다. 함수가 성공했다고 실제 환경에서도 원하는 결과가 발생한 것은 아니다. 네트워크와 각 PC의 설정까지 확인해야 했다.
직원 PC의 자동 업데이트와 GitHub 원격 배포
설치형 앱이 생기자 새 버전을 전달하는 방식도 필요해졌다. 업데이트를 자동으로 확인하고 다운로드하되, 설치는 직원이 재시작을 선택했을 때 진행하도록 했다. 업데이트 파일에 접근할 때 장치 자격증명을 사용하고, 설치 파일의 서명과 해시를 검증하도록 했다. 코드서명 개인키는 빌드 PC에만 보관하고, 배포가 취소된 업데이트는 설치 직전에도 다시 확인하도록 했다.
웹 서비스 배포도 처음에는 수동이었다. Docker 이미지를 NAS에 복사하고 SSH로 들어가 교체했다. 반복할수록 백업, 이미지 변경, migration, 재시작과 상태 확인을 같은 순서로 처리할 필요가 커졌다.
인트라넷 관리자의 릴리스 선택 화면, GitHub Actions 실행 화면, 배포 상태 표시
검증된 이미지와 배포 파일은 GitHub Release로 관리하고, 관리자가 인트라넷에서 버전을 선택해 GitHub Actions 배포를 요청하도록 만들었다. NAS의 전용 self-hosted Runner가 작업을 실행했다. 대용량 파일 다운로드 시간 초과, Runner 도구 누락, 컨테이너의 localhost를 NAS 호스트로 착각했던 상태 확인 오류 등도 이 과정에서 수정했다. Runner는 전용 Debian 컨테이너로 분리하고, 필요한 저장소와 권한을 제한했다. 배포가 동시에 여러 번 실행되지 않도록 막았다.
배포 중에도 데이터와 사용자를 보호해야 했다
자동 배포를 만들면서 가장 중요하게 생각한 것은 실패했을 때였다. 배포 전에 점검을 예고하고 신규 요청을 막은 다음, 웹 서비스를 멈추는 순서를 정리했다. 데이터베이스와 업로드 파일은 쓰기가 멈춘 상태에서 같은 시점의 세트로 백업하고, 이전 이미지는 롤백용으로 보관하도록 했다.
점검 화면은 웹 애플리케이션과 독립된 Nginx gateway에서 제공했다. 웹이나 DB가 잠시 중지돼도 직원에게는 점검 안내가 보이게 하기 위해서였다. migration, 내부 상태 확인, gateway를 통한 최종 확인이 모두 성공해야 점검을 해제하도록 했다. 실패하면 점검 상태를 유지하고 복구 자료를 남기도록 설계했다.
일간 데이터 백업과 주간 전체 백업, 환경 이전 및 장애 대응 절차도 문서화했다. 백업 파일이 만들어졌다는 것과 실제로 복원할 수 있다는 것은 다른 검증 항목으로 구분했다. 자동화 스크립트가 있다는 사실만으로 복구 준비까지 끝났다고 보지 않기 위해서였다.
운영하는 서비스에서 배포 성공보다 더 중요한 질문은, 실패했을 때 무엇을 보호할 수 있는가였다.
AI와 함께 만들면서 달라진 나의 기준
코드보다 결정 문서를 더 많이 작성하게된다.
돌아보면 이번 구축에서 내가 계속 결정해야 했던 것은 코드의 문법보다 회사의 업무 규칙, 직원들의 사용 습관, 접근을 허용할 범위, 문제가 생겼을 때의 처리 방식이었다.
이메일을 잘 읽지 않는다는 관찰은 Windows 앱으로 이어졌다. WOL 패킷이 LAN까지 나가지 않는다는 사실은 별도 릴레이를 만드는 이유가 됐다. PC마다 다른 경로는 로컬 기준 폴더 설정으로 해결했다. 운영 부담과 보안 점검은 배포 자동화, 접근 제어, 백업과 복구 절차로 확장됐다.
AI 덕분에 구현 속도는 빨라졌다. 그러나 실제로 사용하는 서비스를 만들려면 기능을 정의하고, 결과를 확인하고, 예외 상황과 운영 환경을 살펴보는 사람이 필요했다.
예전에는 AI가 작성한 코드를 일일이 검수하는 것이 내 역할이라고 생각했다. 지금은 모든 코드를 직접 읽기보다, 어떤 문제를 해결해야 하는지 정확하게 정의하고 무엇을 검증해야 하는지 판단하는 일에 더 많은 시간을 쓴다. 이것이 이번 작업을 통해 가장 크게 바뀐 점이다.
나는 앞으로도 AI와 함께 더 많은 것을 만들어 볼 생각이다. 다만 코드가 존재하는지, 자동 검사가 통과했는지, 실제 회사 NAS와 직원 PC에서 의도대로 동작하는지는 계속 구분해 확인하려 한다.