Dock 에서 앱을 열고, 창을 끌어 옮기고, 메모 앱에서 블로그를 읽는 macOS 모양의 포트폴리오입니다. 휴대폰으로 열면 iOS 홈 화면이 됩니다. 관리자는 GitHub 로 로그인해 메모 앱에서 바로 글을 쓰고, API 는 무료 Oracle VM 에서 SSH 외에는 포트를 열지 않고 운영합니다.
macOS 모양 뒤에서 글쓰기 · 로그인 · 데이터 저장이 실제로 동작합니다. API 경로 85개의 권한을 표 하나에 정리해 테스트로 검사하고, SSH 외에는 포트를 열지 않은 무료 서버에서 운영하며, 개발 과정은 사이트 안의 메모 앱에 200편이 넘는 글로 남겼습니다.
개요
MacFolio 는 macOS 데스크톱을 웹으로 옮긴 포트폴리오입니다. Dock 에서 앱을 열고, 제목 막대를 끌어 창을 옮기고, 메모 앱에서 블로그를 읽습니다. 터미널에 about, skills, projects 를 입력하면 자기소개가 나오고, Safari 와 프로젝트 앱에서는 배포한 다른 작업물을 창 안에서 바로 실행합니다. 휴대폰으로 열면 iOS 홈 화면과 위젯, 스와이프로 여는 제어 센터가 나타납니다.
2024년에 Create React App 과 Firebase 로 만든 원형은 보기만 하는 모형이었습니다. 관리자 비밀번호를 Firebase 에 두었는데 보안 규칙이 열려 있어 누구나 평문으로 읽을 수 있었고, CRA 는 2025년 2월에 지원이 종료되었습니다. 2026년 9월, Vite + NestJS + PostgreSQL 모노레포로 다시 만들면서 macOS 모양은 유지하고, 글쓰기 · 로그인 · 데이터 저장이 실제로 동작하는 서비스로 바꾸는 것을 목표로 삼았습니다.
개발 연표
시점
내용
2024.08.27
첫 커밋. 빌드 도구 없는 HTML · CSS · JS 파일 3개
2024.10
Create React App · TypeScript 로 전환, Express 서버와 MySQL 을 연결한 뒤 Google Cloud SQL 로 이전
2024.12
서버를 걷어 내고 Firebase Hosting · Realtime DB 로 이전, React 19
데스크톱과 휴대폰 창 끌기 · 크기 조절 · 최소화 · 전체 화면, 메뉴 막대, Launchpad, 바탕화면 아이콘. 휴대폰에서는 iOS 홈 화면과 제어 센터, 터미널 대신 단축어. 레이아웃은 화면 폭이 아니라 창 폭을 기준으로 하는 컨테이너 쿼리라, 데스크톱에서 창을 좁히면 그 창 안이 휴대폰 화면으로 바뀝니다.
하나의 앱 목록 앱마다 폴더 하나를 두고, manifest.ts 한 곳에서 Dock · Launchpad · 터미널 open · 휴대폰 홈 화면을 함께 관리합니다. 앱을 하나 추가하면 네 곳에 함께 나타납니다.
메모 = 블로그 폴더 · 갤러리 보기, 태그, 검색과 찾기 · 바꾸기. 관리자로 로그인하면 읽는 화면이 그대로 편집기가 되고, 임시 저장 · 게시 · 예약 게시 · 버전 기록을 지원합니다.
방문자 기능 메시지 앱의 피드백, 블로그 댓글과 좋아요를 로그인 없이 남깁니다. 이름은 방문자 쿠키를 기준으로 자동으로 붙고(예: 부지런한 판다), 같은 브라우저에서만 지울 수 있습니다.
주소가 있는 앱/memo/<글>, /safari/<프로젝트> 로 들어오면 그 앱이 그 화면으로 열리고, 주소 막대도 맨 앞 창에 맞춰 바뀝니다.
운영 도구 메뉴 막대의 서버 상태, 쿠키 없는 방문자 분석(활동 상태 보기), Finder 의 이력서 · 문서 미리보기, 관리자의 배경화면 업로드.
주요 앱
MacFolio 의 앱은 모양만 흉내 내지 않고 실제로 동작합니다. 내장 앱 18개와 배포한 프로젝트를 띄우는 프로젝트 앱 5개가 있습니다.
Finder 문서 · 프로젝트 · 블로그 · 응용 프로그램 네 묶음을 파일처럼 둘러봅니다. 이력서 PDF 와 그림은 미리보기 앱으로 열고, 새 폴더 · 이름 변경 · 끌어서 옮기기를 지원합니다. 파일은 Cloudflare R2 에 저장하고 Worker 가 /files/ 경로로 제공합니다.
메일 서버가 Resend 로 실제 메일을 보내고, 답장 주소에는 방문자 주소를 넣습니다. Turnstile 로 사람인지 확인하고, IP 하나당 하루 5통 · 사이트 전체 하루 50통으로 제한합니다. 보낸 편지함에는 서버가 방문자 쿠키의 HMAC 으로 걸러 낸 본인 메일만 보여 줍니다.
사진 프로젝트 정보를 읽어 앨범을 자동으로 만듭니다. 일곱 프로젝트에서 사진 65장과 영상 5개를 모았습니다.
날씨 키가 필요 없는 Open-Meteo 를 씁니다. 도시 검색에 1~10초가 걸려서 내장 도시를 39곳에서 211곳으로 늘렸습니다.
시스템 설정 코드를 고치지 않고 프로필과 프로젝트를 편집합니다. 메일 · 댓글 · 메시지마다 Turnstile 을 켜고 끌 수 있습니다.
API 문서 API 코드에서 생성한 OpenAPI 명세를 Scalar 로 보여 주고, 서버가 꺼져 있어도 열립니다. 명세가 코드와 어긋나면 CI 가 실패합니다.
프로젝트 앱 · 새싹 농장 HYEONIVERSE · NewPick · QRU · WTD · 새싹 농장을 한 컴포넌트로 창 안에 띄우고, Unity WebGL 게임도 iframe 으로 실행합니다.
방문자 분석
외부 분석 도구 없이 방문자 분석을 직접 만들었습니다.
쿠키 없는 순방문자 하루 순방문자는 HMAC(날짜 + IP + User-Agent, 비밀 키) 로 집계합니다. 날짜가 계산에 들어가므로 날이 바뀌면 같은 사람을 이어서 추적할 수 없습니다. 주간 순방문자는 일별 순방문자의 합입니다.
보관 범위 원본 IP 는 저장하지 않습니다. 일부를 가린 IP 는 7일, 방문 이벤트는 90일 보관하고, 일별 집계만 계속 보관합니다.
수집 이벤트 5종(visit · app · item · link · leave)을 5초마다 묶어 sendBeacon 으로 보냅니다. 로컬 주소, GPC 를 켠 브라우저, 관리자 방문은 집계하지 않습니다.
활동 상태 보기 개요 · 유입 · 앱과 글 · 지역과 기기 · 실시간 다섯 탭으로 나누고, 기간은 오늘 · 7일 · 30일 · 90일 가운데 고릅니다. 그래프는 라이브러리 없이 SVG 로 그렸습니다.
공개 범위 합계와 유입 묶음은 방문자에게도 공개하고, 상세 주소 · UTM 상세 · 실시간은 관리자에게만 보입니다. 공개 범위는 서버에서 구분합니다.
UTM 조합 표 · 추이 · 링크 생성기를 두고, 읽은 UTM 값은 주소창에서 지웁니다.
기술 스택
영역
기술
선택 이유
화면
React 19 · TypeScript · Vite
지원이 끝난 Create React App 대신 Vite 로 빌드해 빌드 시간을 6.33초에서 1.88초로 줄였습니다
편집기
Milkdown (ProseMirror)
Markdown 을 그대로 저장해, 저장소의 글 파일과 서버의 글이 같은 형식을 씁니다
공유 규칙
zod (packages/contracts)
요청과 응답 형식을 화면과 서버가 같은 스키마로 검사합니다
서버
NestJS
Pipe · Guard 로 모든 경로에 검증 · 권한 확인 · 요청 제한을 같은 방식으로 적용합니다
데이터
Prisma 7 · PostgreSQL 17
스키마에서 타입을 만들고, 필수 규칙은 CHECK 제약으로 한 번 더 지킵니다. 이미지도 DB 에 두어 pg_dump 한 번으로 백업합니다
저장소
pnpm · Turborepo
앱 두 개와 패키지 두 개를 한 저장소에서 함께 빌드하고 테스트합니다
테스트
Vitest · Playwright
단위 테스트, 실제 PostgreSQL 에 연결한 API 테스트, 세 화면의 브라우저 E2E
프론트엔드 React 19 SPA. 창 관리 · 메모 · 음악 로직은 React 와 DOM 없이 테스트할 수 있도록 packages/desktop-core 로 분리했습니다.
백엔드 NestJS 기능별 모듈, Prisma 7 + PostgreSQL 17. 모든 입력은 DTO 화이트리스트로 검사하고, 응답 형식은 @macfolio/contracts 의 zod 스키마 하나로 정의해 화면과 서버가 함께 씁니다.
인프라 프론트엔드는 Cloudflare Workers 정적 자산, API 는 Oracle Always Free VM 의 Docker Compose. cloudflared 가 서버 안에서 바깥으로 먼저 연결하므로 80 · 443 포트를 열지 않고, 같은 사이트의 하위 도메인이라 SameSite=Lax 세션 쿠키를 함께 씁니다.
프로젝트 구조
MacFolio/├─ apps/│ ├─ react/ 웹 화면 · Vite · React 19│ │ └─ src/│ │ ├─ apps/ 앱 18개 — 앱 하나에 폴더 하나│ │ ├─ desktop/ 창 · Dock · 메뉴 막대 · 휴대폰 홈│ │ └─ shared/ API 클라이언트 · 인증 · 공통 UI│ └─ api/ 서버 · NestJS · 기능별 모듈 24개├─ packages/│ ├─ contracts/ zod 스키마 — 화면 · 서버 공유│ └─ desktop-core/ 창 · 메모 · 음악 규칙 (React · DOM 없음)└─ ops/ audit · backup · deploy · restore 스크립트
의존 방향은 apps → packages 한쪽으로만 허용합니다. 순환 의존이 생기면 CI 검사에서 막습니다.
desktop-core 에 React 나 window 가 들어오면 타입 검사와 린트가 실패합니다. 화면 쪽은 useSyncExternalStore 로 이 규칙을 구독합니다.
앱 목록은 manifest.ts 한 곳에 있고, Dock · Launchpad · 터미널 · 휴대폰 홈이 모두 여기서 읽습니다.
요청 흐름
브라우저의 API 요청은 Cloudflare Tunnel 을 지나, 서버 안에서 먼저 연결을 맺어 둔 cloudflared 를 통해 NestJS 에 도착합니다. NestJS 에서는 모든 요청이 같은 처리 단계를 차례로 거칩니다.
단계
역할
차단 시
trust proxy
터널 뒤에서 실제 방문자 IP 를 읽습니다
—
helmet · CORS
보안 헤더를 붙이고, 사이트 주소에서 온 요청만 쿠키와 함께 받습니다
차단
ValidationPipe
DTO · zod 로 입력을 검사하고, 정의하지 않은 필드가 있으면 거부합니다
400
AdminGuard
관리자 경로에서 요청마다 DB 의 세션을 확인합니다
401
@RateLimit
쓰기 경로 33개에 IP 마다 요청 제한을 겁니다
429
전역 예외 필터
오류 응답 형식을 통일하고, 예상하지 못한 오류는 내용을 감춥니다
500
따라서 처리 함수에 도달한 요청은 앞의 검사를 모두 통과한 상태입니다.
설계 결정
주제
선택한 방식
막는 위험
관리자 로그인
GitHub OAuth 직접 구현, state 쿠키를 상수 시간 비교
비밀번호 저장, 로그인 CSRF, 타이밍 공격
세션
쿠키에는 무작위 토큰, DB 에는 SHA-256 해시만
DB 가 유출돼도 세션을 가로챌 수 없고, 로그아웃하면 바로 끊깁니다
관리자 확인
계정 이름이 아닌 GitHub 숫자 ID
이름이 바뀌거나 다른 사람이 그 이름을 가져가도 권한이 넘어가지 않습니다
방문자
쿠키 토큰의 HMAC 만 저장
가입 없이 자신이 쓴 글만 지울 수 있고, 토큰 원문은 서버에 없습니다
필수 항목
화면 · API 검증에 더해 DB CHECK 제약
API 를 거치지 않은 쓰기(스크립트, 버그)
업로드
DB 에 저장, 10MB 제한, 파일 앞부분으로 형식 확인, 무작위 ID
백업 누락, 확장자만 바꾼 파일, 주소 추측
GitHub 연동
서버 캐시, 동시 요청 하나로 묶기, 실패하면 마지막으로 받은 값 사용
요청 제한(시간당 60회), GitHub 장애 때 빈 화면
배포
check 를 통과한 커밋만
테스트를 건너뛴 배포
테스트와 배포
브랜치 · 커밋 · PR 규칙을 문서로만 두지 않고 CI 의 conventions 검사로 지켰습니다. PR 과 main push 마다 포맷 → 린트 → 타입 체크 → 단위 테스트 → 빌드 → 실제 PostgreSQL 에 연결한 API E2E → 브라우저 E2E 를 차례로 돌리고, check 를 통과한 커밋만 Cloudflare Workers 에 배포됩니다. PR 마다 미리보기 주소가 생기고, 미리보기 주소에는 CORS 로 읽기만 허용하고 관리자 쿠키는 보내지 않습니다.
Playwright 는 데스크톱 Chrome, iPhone, Android 세 화면에서 가짜 API(page.route)로 로그인 · 글쓰기 · 게시까지 확인하고, README 와 블로그의 스크린샷도 같은 코드로 찍습니다. 2026년 10월 19일 기준 브라우저 E2E 486개, 단위 테스트 659개, API E2E 131개입니다.
머지에서 배포까지 15분이 걸렸고, 그중 E2E 가 8~9분이었습니다. E2E 를 6개로 나눠 동시에 돌리고, PR 에서 통과한 같은 코드(HEAD^{tree} 가 같은 커밋)는 main 에서 다시 돌리지 않도록 했습니다.
보안 점검은 Semgrep(규칙 266개), pnpm audit, osv-scanner, gitleaks 로 합니다. Semgrep 이 찾은 7건 가운데 실제 취약점은 없었고, gitleaks 로 커밋 791개를 검사해 비밀 값이 섞이지 않았는지 확인했습니다. 사이트에는 CSP 등 보안 헤더 5종을 적용하고 위반이 생기면 E2E 가 실패하게 했으며, 업로드한 사진의 EXIF 는 방향 정보만 남기고 지웁니다.
만드는 과정은 모두 사이트 안의 메모 앱(개발기 폴더)에 200편이 넘는 글로 남겼습니다.
기술적 도전
정적 사이트에서 관리자 권한 지키기
증상 원형은 관리자 비밀번호를 Firebase 에 두었고, 보안 규칙이 열려 있어 누구나 평문으로 읽을 수 있었습니다. 관리자가 아니면 편집 버튼을 숨겼지만, 버튼을 숨기는 것만으로는 수정을 막을 수 없었습니다.
원인 정적 사이트에는 요청을 검증할 곳이 없습니다. 브라우저가 가진 값은 모두 공개된 값입니다.
해결 검증을 서버로 옮겼습니다. 관리자 로그인은 GitHub OAuth 로 하고, 바뀔 수 있는 계정 이름 대신 숫자 ID 로 관리자를 판별합니다. state 쿠키로 로그인 CSRF 를 막고, 세션은 httpOnly · Secure · SameSite=Lax 쿠키에 12시간, DB 에는 토큰의 SHA-256 해시만 둡니다. AdminGuard 가 요청마다 DB 의 세션을 확인하고, API 경로 85개와 각 경로의 권한 검사를 표 하나에 정리해, 검사가 빠진 경로가 생기면 테스트가 실패합니다.
결론 편집 버튼은 화면에서 감출 뿐이고, 실제로 수정을 막는 것은 요청마다 이루어지는 서버의 권한 검사입니다. 비밀번호를 쓰지 않으므로 해시 저장과 재설정 기능도 필요 없어졌습니다.
바로 고치는 블로그에 임시 저장과 버전 기록 더하기
증상 메모 앱처럼 읽는 화면에서 바로 고치면 편하지만, 저장하는 순간 방문자에게 보이고 되돌릴 수 없었습니다.
원인 글 한 행에 내용을 하나만 저장해서 "고치는 중" 과 "게시한 것" 을 나눌 수 없었습니다.
해결 게시본과 임시 저장본을 한 행에 함께 두고, 게시할 때마다 PostRevision 을 남깁니다. "게시한 글에는 제목과 본문이 있어야 한다" 같은 규칙은 Prisma 스키마로 적을 수 없어 마이그레이션 SQL 의 CHECK 제약으로 DB 에서 한 번 더 확인합니다. 저장소의 Markdown 글은 그대로 두고, 서버에서 고친 글만 같은 주소로 대신 보이게 했습니다.
결론 고치는 동안에는 임시 저장만 되고, 게시해야 방문자에게 보입니다. 미래 날짜로 게시하면 예약되고, 예전 버전은 되살릴 수 있습니다.
로그인 없이 쓰고, 원문은 저장하지 않기
증상 메시지와 댓글을 쓸 때마다 이름과 비밀번호를 받았고, 서버에 IP 를 원문으로 남겼습니다.
원인 글쓴이를 확인할 방법이 비밀번호뿐이었습니다.
해결 방문자 쿠키 토큰으로 글쓴이를 확인하고, 이름도 이 쿠키를 기준으로 붙입니다. 서버에는 토큰과 IP 의 HMAC, 세션의 SHA-256 해시만 저장합니다. 쓰기 경로 33개에는 같은 방식(@RateLimit)의 IP별 요청 제한을 걸고, 필요하면 시스템 설정에서 Cloudflare Turnstile 사람 확인을 켭니다.
결론 방문자는 아무것도 입력하지 않고 글을 남기고, 같은 브라우저에서 지울 수 있습니다. 서버에는 원래 값을 알아낼 수 있는 개인 정보가 남지 않습니다.
외부 포트를 열지 않는 무료 서버
증상 Render 같은 무료 호스팅은 15분 동안 요청이 없으면 잠들어 깨어나는 데 1분쯤 걸렸고, VM 을 직접 쓰면 80 · 443 포트를 열고 인증서를 갱신해야 했습니다.
원인 무료 플랫폼은 운영 부담을 줄이는 대신 응답 속도를 포기해야 하고, 직접 운영하면 공격받을 수 있는 지점이 늘어납니다.
해결 Oracle Always Free VM 에 PostgreSQL, API, cloudflared 를 Docker Compose 로 함께 띄웠습니다. compose 에 ports 가 하나도 없고, 서버에서 열린 포트는 SSH 하나뿐입니다. API 이미지는 GitHub Actions 에서 amd64 · arm64 로 빌드해 GHCR 에 올리고, 서버에서는 이미지만 교체합니다. 헬스 체크가 실패하면 자동으로 되돌리며, 일부러 깨진 이미지를 올려 되돌리기가 동작하는지 확인했습니다.
결론 잠들지 않는 서버를 비용 없이 운영하면서 서버 IP 도 드러내지 않습니다. 하드닝은 문서가 아니라 ops/audit.sh 스크립트로 남겨 언제든 다시 점검할 수 있게 했습니다.
열흘 동안 쌓인 중복을 일곱 단계로 정리
증상 기능을 빠르게 추가하는 동안 화면마다 응답 타입을 직접 다시 정의했고, 12개 파일이 각자 fetch 를 호출했으며, z-index 가 23가지, Memo.css 가 4,166줄, ESLint 경고가 185개가 되었습니다.
원인 기능을 하나 더할 때마다 고쳐야 할 곳이 여러 군데였고, 보안 설정도 기능마다 따로였습니다.
해결 타입 → 구조 → 공통 기반 → 컴포넌트 → 스타일 → 서버 → 보안의 일곱 단계로 나눴습니다. 1~6단계는 동작을 바꾸지 않는 것이 규칙이라, 빌드한 CSS 를 바이트 단위로, 화면을 계산된 스타일로 비교해 결과가 같은지 확인했습니다. 동작이 바뀌는 보안 PR 은 리팩터링과 섞지 않았습니다. 디자인 토큰은 색 288곳, 글자 크기 645곳(이름 8개), 모서리 239곳(이름 6개)에 적용했고, !important 72개를 정리했습니다.
결론 타입은 contracts 의 zod 스키마 하나, 서버 요청은 client.ts 하나, 순환 의존 0(CI 가 막음), 디자인 토큰 6종, ESLint 경고 0개(--max-warnings 0)가 되었습니다. 사이트에는 CSP 와 iframe sandbox 를 적용하고 의존성 취약점 6건을 고쳤습니다.
공통 버튼의 여백이 사라진 문제
증상 공통 Button 의 padding 이 0px 로 그려졌습니다.
원인 CSS 레이어(@layer) 순서는 문서에서 처음 나온 선언이 정합니다. 컴포넌트 CSS 가 먼저 불러와져 레이어 순서가 어긋났고, 초기화 레이어의 * { padding: 0 } 이 우선 적용되었습니다.
해결 레이어 순서 선언을 index.html 의 인라인 <style> 맨 앞으로 옮겼습니다.
결론 padding 이 5px 14px 로 돌아왔고, 이후 어떤 CSS 가 먼저 불러와져도 레이어 순서가 바뀌지 않습니다.
배포 직후 메모 앱이 열리지 않던 문제
증상 배포 직후 열려 있던 탭에서 "memo 앱에서 문제가 발생했습니다" 가 표시되었습니다.
원인 열려 있던 탭이 배포 과정에서 삭제된 이전 코드 파일을 요청했습니다. 존재하지 않는 파일을 요청해도 index.html 이 200 응답과 1년 캐시 설정으로 돌아와, 브라우저가 HTML 을 코드로 읽으려다 실패했습니다.
해결 존재하지 않는 주소만 처리하는 Worker 를 두어 앱 주소는 200, 코드 파일 주소는 404 로 나누고, 코드 파일을 불러오지 못하면 새로고침을 안내하는 경고창을 띄웠습니다.
결론 배포 직후에도 잘못된 응답이 캐시되지 않고, 사용자는 새로고침 한 번으로 새 버전을 받습니다.
서버는 켜져 있는데 "연결할 수 없음" 이 표시되던 문제
증상 PR 미리보기 주소에서 서버가 정상인데도 메뉴 막대에 "서버에 연결할 수 없음" 이 표시되었습니다.
원인 CORS 설정이 미리보기 주소를 허용하지 않았고, 브라우저의 fetch 는 CORS 로 막힌 경우와 서버가 꺼진 경우를 같은 TypeError 로 돌려줍니다.
해결 미리보기 주소를 CORS 에 허용하되 쿠키는 주지 않고, no-cors 요청을 한 번 더 보내 막힘과 꺼짐을 구분했습니다.
결론 서버 상태 표시가 실제 상태와 일치하고, 미리보기에서는 관리자 API 를 쓸 수 없습니다.
한국어 뒤에서 굵은 글씨가 깨지던 문제
증상**취소(cancelled)**가 처럼 굵은 글씨 바로 뒤에 조사가 붙으면 별표가 그대로 보였습니다.
원인 CommonMark 의 강조 닫힘 규칙이 조사를 붙여 쓰는 한국어와 맞지 않습니다.
해결remark-cjk-friendly 를 읽기 화면 3곳과 편집기(읽기 · 쓰기)에 적용했습니다.
결론 한국어 문장 안의 굵은 글씨가 의도대로 표시됩니다.
CRA → Vite 마이그레이션
2025년 3월에 빌드 도구를 Vite 로 바꿨고, 2026년 9월 재구축 첫날 남아 있던 CRA 흔적을 정리한 뒤 같은 조건에서 다시 측정했습니다.
항목
CRA
Vite
dev 서버 시작
5.05초
0.58초
프로덕션 빌드
6.33초
1.88초
JS 번들 (gzip)
138.2 KB
127.7 KB
설치 패키지 수
1,438개
171개
정리 과정에서 그동안 드러나지 않던 오류 4건도 고쳤습니다. 배포 경로가 CRA 의 build/ 를 가리키고 있었고, REACT_APP_ 환경 변수 이름이 남아 있었고, @vitejs/plugin-react 설정이 없었고, react-dom · typescript 가 의존성에서 빠져 있었습니다.
운영과 감시
무료 서버 회수 감시 Oracle Always Free VM(1GB)은 7일 동안 CPU p95 · 네트워크 · 메모리 사용률이 모두 20% 미만이면 회수 대상입니다. 1분마다 /proc 을 읽어 20% 미만은 위험, 30% 미만은 주의로 판단하고, 위험하면 메일을 보냅니다(같은 알림은 3일에 한 번).
가동 확인 GitHub Actions 가 10분마다 /health 와 첫 화면을 확인하고, 실패하면 30초 간격으로 세 번 다시 확인합니다.
로그 상한 로그는 컨테이너마다 10MB 파일 3개(30MB)까지만 남겨 디스크가 차지 않게 했습니다.
백업 매일 pg_dump 로 백업해 서버에 7일, R2 에 30일 보관하고, healthchecks.io 로 실패를 알립니다. 백업을 별도 DB 에 되살려 글 수가 실제 DB 와 같은지 확인했습니다.
메뉴 막대 아이콘 Wi-Fi 자리는 30초마다 확인한 서버 응답 상태, 배터리 자리는 관리자에게는 회수 위험, 방문자에게는 지난주 대비 방문 추이를 보여 줍니다.
운영 결과
방문 분석을 켠 2026년 10월 7일부터 19일까지 2주 동안의 수치입니다. 사이트에 공개된 방문 통계와 같은 값이며, 관리자 방문 · 로컬 주소 · GPC 를 켠 브라우저는 집계하지 않습니다.
항목
값
방문
104회
순방문자 (일별 합계)
52명
앱 실행
64회
블로그 글 조회
99회
유입
직접 65% · 링크 27% · 검색 8%
기기
데스크톱 91% · 모바일 9%
나라
한국 67% · 미국 33%
가장 많이 연 앱
메모(블로그) 38% · 시스템 설정 · 메일
돌아보며
모형을 실제 서비스로 바꾸면서 화면 뒤에 확인 장치를 겹겹이 두었습니다. 권한은 서버가 요청마다 확인하고, 데이터 규칙은 DB 제약과 테스트가 한 번 더 확인하며, 검사를 통과한 커밋만 배포됩니다. 혼자 운영하면서도 부담 없이 코드를 고칠 수 있었던 것은 이 장치 덕분입니다.
작업하면서 세 가지 원칙을 지켰습니다. 첫째, 고치기 전에 먼저 측정했습니다. 공통 부품으로 묶을 후보가 9개 있었지만 정말 필요한 것은 3개였고, 쓰지 않는 것처럼 보이던 CSS 150개 가운데 실제로 지운 것은 9개뿐이었습니다. 둘째, 리팩터링한 뒤에는 결과가 그대로인지 빌드 결과와 계산된 스타일로 확인했습니다. 셋째, 동작이 달라지는 보안 수정은 리팩터링과 섞지 않고 따로 검토해 배포했습니다.
1 / 24
팀원
김정현1인 개발 · 기획 · 디자인 · 프론트엔드 · 백엔드 · 인프라 · CI/CD
김정현
🧑💻 프론트엔드 집중 풀스택 개발자, React와 Next.js를 좋아합니다.
백엔드
NestJS 11개 모듈, Prisma 7 + PostgreSQL 데이터 모델
GitHub OAuth 관리자 로그인과 해시만 저장하는 세션
임시 저장·게시·버전·예약 게시, CHECK 제약
방문자 쿠키 기반 댓글·메시지, 쿠키 없는 트래픽 분석
프론트엔드
창 관리·Dock·메뉴 막대·Launchpad 를 갖춘 macOS 데스크톱 셸과 iOS 모바일 셸
manifest 한 곳에서 앱 목록을 읽는 앱 구조, 앱 23개(내장 18 · 프로젝트 5)
Milkdown 기반 메모 편집기(서식 메뉴, 표 손잡이, 찾기·대치, 토글·각주)
창 폭 기준 컨테이너 쿼리, 앱마다 주소(/memo, /safari)
품질 · 보안
API 경로 85개 권한 표 시험, Playwright 3기기 E2E
일곱 단계 리팩터링, ESLint 경고 185 → 0
CSP·iframe sandbox·요청 제한, 의존성 취약점 6건 수정
인프라 · CI/CD
Oracle VM + Docker Compose + Cloudflare Tunnel 배포, 바깥 포트 0