사내 VM IP 관리 시스템 만들기 - React 대시보드 + Docker Compose 배포 (4편)

React 대시보드 대시보드는 탭 2개로 구성된다. VM 목록 — 전체 VM 현황, 상태별 필터, ONLINE/OFFLINE/UNKNOWN 배지 이벤트 로그 — IP 충돌, IP 변경, 오프라인 이벤트 타임라인 WebSocket이 아닌 30초 폴링으로 구현했다. VM 상태가 초 단위로 바뀌지 않고, 운영 대시보드 특성상 약간의 지연은 허용된다. 심플하게 가는 게 낫다고 판단했다. 30초 폴링 훅 // usePolling.js import { useEffect, useRef } from 'react'; export function usePolling(callback, intervalMs = 30_000) { const callbackRef = useRef(callback); useEffect(() => { callbackRef.current = callback; }); useEffect(() => { callbackRef.current(); // 마운트 시 즉시 1회 실행 const id = setInterval(() => callbackRef.current(), intervalMs); return () => clearInterval(id); }, [intervalMs]); } // App.jsx const [vms, setVms] = useState([]); const [loading, setLoading] = useState(true); usePolling(async () => { const data = await fetch('/api/vms').then(r => r.json()); setVms(data); setLoading(false); }, 30_000); callbackRef를 쓰는 이유는 setInterval 클로저가 최초 등록 시점의 callback을 계속 참조하는 문제를 피하기 위해서다. useRef로 항상 최신 콜백을 가리키도록 한다. ...

2026년 4월 15일 · 5 min · 958 words · Chanyeol

사내 VM IP 관리 시스템 만들기 - 기획 배경 + 전체 아키텍처 + DB 스키마 (1편)

왜 만들게 됐나 VM이 늘어나면서 IP 관리가 슬슬 문제가 됐다. 스프레드시트에 IP 할당 현황을 수동으로 관리하는데, 담당자마다 업데이트 타이밍이 달라서 실제 상태와 항상 일치하지 않음 VM을 삭제하면서 IP 해제를 빠뜨리면, 나중에 같은 IP를 다른 VM에 할당해서 충돌이 발생 VM이 죽어도 스프레드시트에 살아있는 것처럼 남아있어서 “이 서버 살아있어요?” 같은 질문이 자주 옴 특정 VM의 IP 변동 이력을 알고 싶어도 추적 방법이 없음 그래서 만들었다. VM에 경량 에이전트를 설치해서 주기적으로 heartbeat를 전송 서버에서 heartbeat를 분석해서 IP 충돌, IP 변경, 오프라인 등 이상 감지 이상 발생 시 Slack 알림 웹 UI에서 전체 VM 현황과 이벤트 로그 조회 기술 스택 영역 기술 선택 이유 에이전트 Go 1.22 단일 바이너리, 런타임 불필요, 크기 소형 백엔드 Spring Boot 3 + JPA 팀 표준 스택 DB PostgreSQL 16 이력 테이블 + 집계 쿼리 캐시/dedup Redis 7 Slack 알림 중복 방지 프론트엔드 React 18 + Vite 30초 폴링 대시보드 인프라 Docker Compose 4개 서비스 일괄 관리 에이전트는 처음에 Node.js로 만들었다가 Go로 전환했다. 이유는 2편에서 다룬다. ...

2026년 4월 12일 · 4 min · 759 words · Chanyeol

사내 인사정보 관리 시스템 만들기 - Docker Compose 멀티 서비스 배포 + 트러블슈팅 (3편)

Docker Compose 멀티 서비스 구성 세 서비스가 올바른 순서로 떠야 한다. db (PostgreSQL) → server (Express + Prisma) → client (React + nginx) server가 DB보다 먼저 뜨면 Prisma 연결이 실패하고, client가 server보다 먼저 뜨면 API 프록시가 동작하지 않는다. docker-compose.yml services: db: image: postgres:16-alpine container_name: insa-db environment: POSTGRES_DB: unipost_insa POSTGRES_USER: insa POSTGRES_PASSWORD: insa1234 volumes: - ./data/postgres:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U insa -d unipost_insa"] interval: 5s timeout: 5s retries: 10 server: build: ./server container_name: insa-server ports: - "4000:4000" env_file: - ./server/.env environment: DATABASE_URL: postgresql://insa:insa1234@db:5432/unipost_insa depends_on: db: condition: service_healthy healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/health', r => process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"] interval: 5s timeout: 5s retries: 10 restart: unless-stopped client: build: ./client container_name: insa-client ports: - "3002:80" depends_on: server: condition: service_healthy restart: unless-stopped depends_on에 condition: service_healthy를 쓰면 단순히 컨테이너가 “시작됐는지"가 아니라 “준비됐는지"를 확인하고 다음 서비스를 시작한다. ...

2026년 4월 11일 · 5 min · 862 words · Chanyeol

사내 인사정보 관리 시스템 만들기 - 아키텍처 + Prisma 스키마 설계 (1편)

왜 만들게 됐나 사내 그룹웨어에는 인사 정보가 있지만 조회 UI가 불편하고, 부서 이동이나 입퇴사 같은 변동 이력을 추적하는 기능이 없었다. 직접 관리하고 싶은 데이터가 생겼을 때 그때그때 SQL을 뽑아 쓸 수 있는 환경도 필요했다. 그래서 만들었다. 그룹웨어 API에서 인사 데이터를 매일 자동 수집 이전 상태와 비교해서 입사/퇴사/부서이동/직책변경 자동 감지 React 웹 UI에서 조회·수정·삭제, CSV 다운로드, INSERT SQL 생성 기술 스택 선택 영역 기술 선택 이유 백엔드 Node.js 20 + Express 기존 Slack 봇과 동일 스택 ORM Prisma 타입 안전한 쿼리, 마이그레이션 자동화 DB PostgreSQL 16 JSON 파일로는 한계, 이력 테이블 필요 스케줄러 node-cron 매일 1시 배치 실행 프론트엔드 React 18 + Vite 빠른 개발 환경 그리드 AG Grid Community 대용량 데이터 필터/정렬/페이지네이션 서버 상태 TanStack Query API 캐싱, 로딩/에러 상태 관리 인프라 Docker Compose 3개 서비스(DB, 서버, 클라이언트) 일괄 관리 기존 Slack 봇은 DB 없이 JSON 파일로 상태를 관리했는데, 인사 데이터는 수백 명 규모에 이력까지 쌓아야 하니 PostgreSQL이 필요했다. ...

2026년 4월 9일 · 4 min · 760 words · Chanyeol

사내 Slack 봇 만들기 - 관리자 페이지 + Docker 배포 + 트러블슈팅 (4편)

왜 관리자 페이지가 필요한가 처음엔 팀 Webhook이 하나라 teams.json을 직접 수정해도 됐다. 그런데 팀이 늘어나고, 알림 타입을 팀별로 다르게 설정하고 싶어지고, 출근 알림 대상 멤버도 자주 바뀌면서 파일을 직접 건드리는 게 너무 번거로워졌다. 그래서 관리자 페이지를 만들었다. React 같은 프레임워크 없이 Express + 바닐라 JS로 만들었다. 관리자 혼자 쓰는 내부 툴이라 빌드 파이프라인 없이 심플하게 가는 게 낫다고 판단했다. 관리자 페이지 구성 패널 4개로 구성된다. 패널 기능 팀·Webhook 팀 추가/수정/삭제, 활성 토글, 알림 타입 배지 토글, 테스트 발송 출근알림 멤버 멤버 추가/수정/삭제, Slack 이름 자동완성 출근현황 카드뷰 출근/미출근 현황, 수동 알림 발송 크론 관리 크론 활성화 토글, 스케줄 표현식 수정 설정 Slack Bot Token 관리 팀·Webhook 패널 팀별로 6가지 알림 타입을 배지 형태로 토글할 수 있다. ...

2026년 4월 8일 · 4 min · 821 words · Chanyeol

사내 Slack 봇 만들기 - 기획 배경 + 전체 아키텍처 (1편)

왜 만들게 됐나 조직 개편 전에는 팀원들이 한 채널에 모여 있어서 누가 휴가인지, 어떤 회의실이 예약됐는지 슬쩍 보면 알 수 있었다. 개편 이후 팀이 분리되면서 연계 채널이 새로 생겼다. 그런데 서로 다른 팀 채널에 있다 보니 상대 팀의 휴가·회의실 정보를 알기가 불편해졌다. 매번 그룹웨어에 들어가서 확인하는 게 번거로웠다. 그래서 만들었다. 매일 아침 Slack으로 당일 휴가자와 회의실 예약 현황을 자동으로 보내주는 봇. 만들다 보니 기능이 붙었다. 출근 미등록자 DM 알림, 슬래시 커맨드로 날짜별 조회, 관리자 웹 페이지까지. ...

2026년 4월 5일 · 3 min · 468 words · Chanyeol

노트북 한 대로 홈서버 구축하기 - 12편 완전 정복 총정리

시작은 단순했다 클라우드 비용이 아깝고, NAS도 필요하고, 사이드 프로젝트 서버도 있으면 좋겠고. 마침 집에 안 쓰는 ThinkPad가 있었다. 그렇게 시작된 홈서버 구축기가 어느새 12편이 됐다. 최종 구성 기기: ThinkPad E15 Gen3 (Ryzen 5 5600U, RAM 16GB) OS: Ubuntu Server 24.04 LTS 네트워크: Tailscale VPN + OCI Nginx 리버스 프록시 도메인: yourdomain.com (Cloudflare) 스토리지: 256GB SSD (OS/Docker) + 1TB SSD (/mnt/data, NTFS) 운영 중인 서비스 전체 목록 포트 서비스 접근 방식 2283 Immich (사진 관리) 도메인 (OCI 프록시) 9090 Filebrowser (파일 관리) 도메인 (OCI 프록시) 11000 Vaultwarden (비밀번호) 도메인 (OCI 프록시) 13000 Grafana (모니터링) Tailscale VPN 19000 Portainer (Docker GUI) Tailscale VPN 19090 Prometheus Tailscale VPN 19100 Node Exporter 내부 수집용 23000 컨테이너 대시보드 (React) Tailscale VPN 28080 컨테이너 대시보드 (Spring) Tailscale VPN 12편 한눈에 보기 1편 — 왜 홈서버인가? + 전체 아키텍처 → 보러가기 ...

2026년 4월 4일 · 3 min · 504 words · Chanyeol

노트북으로 홈서버 구축하기 - 직접 만든 컨테이너 대시보드 (Spring Boot + React + SSE) (12편)

왜 직접 만들었나 Portainer가 있는데 굳이 만든 이유는 단순하다. 그냥 만들어보고 싶었다. Portainer는 기능이 너무 많다. 나는 컨테이너 목록 확인, 시작/중지/재시작, 로그 보기 딱 세 가지만 필요했다. 이 정도면 직접 만들 수 있겠다 싶어서 Spring Boot 백엔드 + React 프론트엔드로 구성했다. 완성 화면 컨테이너 목록을 카드 UI로 표시 (상태별 색상 배지) Start / Stop / Restart 원클릭 제어 Logs 버튼으로 실시간 로그 스트리밍 (SSE) 5초 주기 자동 갱신 ...

2026년 4월 3일 · 5 min · 978 words · Chanyeol

노트북으로 홈서버 구축하기 - PostgreSQL 자동 백업 (pg_dump + cron) (10편)

왜 백업이 필요한가 Immich에 사진을 올리기 시작하면서 DB가 날아가면 복구할 방법이 없다는 게 갑자기 걱정됐다. 사진 원본은 /mnt/data에 있으니 파일은 살아있더라도 Immich DB가 날아가면 앨범, 태그, 얼굴 인식 데이터가 전부 사라진다. 백업 전략은 단순하게 잡았다. 대상 방법 위치 Immich DB pg_dumpall /backup/immich_db_YYYYMMDD.sql 사진 원본 추후 rsync 추가 예정 외장하드 구매 후 OS SSD에 204GB 여유가 있어서 DB 덤프는 우선 거기에 보관한다. 백업 디렉토리 생성 sudo mkdir -p /backup sudo chown your-username:your-username /backup chown으로 소유권을 넘겨줘야 한다. 처음에 sudo mkdir만 하고 스크립트를 실행했더니 아래 에러가 났다. ...

2026년 4월 1일 · 2 min · 355 words · Chanyeol

노트북으로 홈서버 구축하기 - Grafana + Prometheus로 서버 모니터링하기 (7편)

구성 개요 모니터링 스택은 세 가지로 구성된다. Prometheus — 메트릭 수집 및 저장 Grafana — 대시보드 시각화 Node Exporter — 서버 시스템 메트릭 노출 (CPU, RAM, 디스크, 네트워크 등) Prometheus와 Grafana는 홈서버에서 Docker로 실행하고, Node Exporter는 홈서버와 OCI 서버 양쪽에 systemd로 설치했다. 두 서버가 Tailscale VPN으로 연결돼 있으니 Prometheus가 VPN을 통해 OCI 메트릭도 수집할 수 있다. 서비스 포트 Prometheus 19090 Grafana 13000 Node Exporter 19100 1. 디렉토리 생성 mkdir ~/monitoring && cd ~/monitoring 2. docker-compose.yml 작성 services: prometheus: image: prom/prometheus:latest container_name: prometheus restart: unless-stopped ports: - "19090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus_data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--storage.tsdb.retention.time=30d' grafana: image: grafana/grafana:latest container_name: grafana restart: unless-stopped ports: - "13000:3000" volumes: - grafana_data:/var/lib/grafana environment: - GF_SECURITY_ADMIN_PASSWORD=your_password_here - GF_SERVER_ROOT_URL=http://100.109.108.36:13000 volumes: prometheus_data: grafana_data: version: "3.8" 은 Docker Compose v2부터 obsolete라 생략했다. 넣어도 동작하지만 경고가 뜬다. ...

2026년 3월 29일 · 3 min · 582 words · Chanyeol
1