프론트엔드/풀스택 개발자 최재용(jaeyonging)의 개발 블로그입니다. React, TypeScript, Node.js 등 웹 개발 경험과 직접 만든 사이트, 프로젝트 기록을 공유합니다.
직접 만든 모니터링 서버 100일, 잘한 것과 고쳐야 할 것
27만 번 체크한 기록을 다시 뜯어봤다. 공개 도메인으로 체크한 건 잘한 선택이었고, 자기 자신을 감시하는 구조와 순차 체크, 끝없이 쌓이는 이력, 로그에 남는 토큰은 고쳐야 한다
헬스체크가 3달 동안 기록만 하고 아무한테도 안 알렸다
실패 31건이 쌓이는 동안 알림은 0건이었다. 실패마다 알리면 메일 58통, 연속 2회 기준이면 4통. 쌓인 기록으로 먼저 세보고 상태 전이 방식으로 붙인 과정
게스트 로그인을 지우고 status 페이지를 만들었다
대시보드를 보여주려고 게스트 권한 분기를 늘리다가, 보여줄 건 "살아있나" 하나였다는 걸 알고 공개 status 페이지를 따로 만든 이야기
PM2가 너무 잘 되살려서 장애를 못 보고 있었다
헬스체크는 계속 200이었는데 재시작이 96회였다. 누적이 아니라 구간 증가량으로 판정해야 하는 이유
Redis를 넣으려다 재보고 안 넣었다. 진짜 범인은 캐시 헤더였다
TTFB 6ms 서버에 캐시 레이어는 필요 없었다. FilesMatch가 프록시 응답에 안 먹는다는 걸 몰랐을 뿐
glass-style로 만들었더니 모바일이 20fps가 됐다
glass-style 포트폴리오가 모바일에서 20fps로 떨어졌다. CPU를 느리게 걸고 프레임 간격을 재서 배경 blur와 보이지 않는 배지, 카드 backdrop-filter를 찾아내 60fps로 돌린 과정이다.
번들 하나가 700KB였다. Vite manualChunks로 쪼개면서 정한 기준
무엇을 어느 청크에 넣을지 정하는 기준과, lazy를 언제 쓰고 언제 안 쓰는지
내 프로젝트 중 하나는 Zustand를 쓰고 하나는 Redux를 쓴다. 같은 사람이 왜 다르게 골랐나
Redux Toolkit, Zustand, React Query를 프로젝트 성격에 따라 다르게 고른 기준
거래소 WebSocket 두 개를 동시에 붙였더니 화면이 죽었다. dispatch를 requestAnimationFrame으로 묶은 이야기
초당 수백 건 들어오는 시세를 Map 큐와 rAF로 배치 처리한 과정, 그리고 안 골랐던 대안들
직선거리로 짠 여행 일정은 실제 도로랑 2시간 28분 어긋났다
직선거리에 평균 속도로 짠 여행 일정이 실제 도로 시간과 148분 어긋났다. 카카오 길찾기로 바꾸면서 붙인 좌표 반올림 캐시, 실패하면 추정치로 돌아가는 방식, 날짜를 동네별로 나누는 방법을 정리했다.
링크 하나 분석하는 데 17초가 걸려서 요청 응답 방식을 버렸다
링크 분석 API 하나가 17.5초씩 걸려서 요청 응답 방식을 작업 방식으로 바꿨다. 진행 단계 표시, 같은 링크 합류, 레이트리밋 함정, 병렬 처리로 기다리는 시간을 줄인 과정이다.
유튜브 영상에 나온 여행지를 뽑아내려고 자막까지 읽었다
유튜브 링크에서 여행지를 뽑으려고 설명란이 비면 yt-dlp로 자동 자막까지 읽었다. AI와 규칙 기반 추출을 합쳤다가 결과가 나빠진 이유와 둘의 역할을 나눈 방법을 실제 결과로 비교했다.
홈페이지 한 번 여는 데 19.7MB를 내려받고 있었다
개인 블로그 홈을 여는 데 19.7MB를 받고 있었고 그중 93%가 이미지였다. sharp 리사이즈, Lottie 분리, 라우트 분할, 폰트 서브셋으로 782KB까지 줄인 과정을 실측값으로 정리했다.
CSR, SSR, SSG, ISR, 스트리밍, RSC를 정리하고 내가 고른 건 반쪽짜리 SSR이었다
CSR, SSR, SSG, ISR, 스트리밍 SSR, RSC, 아일랜드를 코드와 장단점으로 비교했다. 블로그에 맞는 답을 알면서도 글 상세 경로에만 서버에서 메타를 넣는 반쪽짜리 방식을 고른 이유를 적었다.
react-query v3를 TanStack Query v5로 올리려고 보니 useErrorBoundary가 사라져 있었다
react-query v3에서 TanStack Query v5로 가면서 useErrorBoundary가 throwOnError로 바뀌었다. 인자 객체 통일, isPending, onSuccess 제거까지 무엇이 바뀌었고 왜 그렇게 바꿨는지 정리했다.
TypeScript 컴파일러가 Go로 다시 쓰여서 타입 체크가 10배 빨라졌다
tsc가 느렸던 건 컴파일러가 자바스크립트로 돌아서였다. TypeScript 7이 컴파일러를 Go로 다시 쓰면서 빨라진 이유와 Rust가 아니라 Go를 고른 배경, 작은 프로젝트에서의 체감을 솔직하게 적었다.
자바스크립트 Date 때문에 하루씩 어긋나던 걸 Node 26 Temporal은 어떻게 푸나
D-day가 하루씩 어긋나던 커플 앱 코드를 계기로 자바스크립트 Date의 설계 문제를 짚었다. Node 26에 들어온 Temporal의 PlainDate, ZonedDateTime, Duration으로 같은 계산을 어떻게 푸는지 정리했다.
React 19는 forwardRef랑 작별하고, 메타태그를 컴포넌트에서 박는다
React 19가 바꾼 것, ref와 Actions 그리고 네이티브 메타태그
Vite 8을 뜯어보니 빌드 번들러가 통째로 Rust로 바뀌어 있었다
Vite 8은 개발용 esbuild와 빌드용 Rollup을 Rust로 짠 Rolldown 하나로 합쳤다. 번들러가 둘이라 생기던 문제와 빌드가 몇 배씩 빨라지는 이유, 내 프로젝트들이 아직 올리지 못한 사정을 정리했다.
React로 구현했더니 카톡에 링크 미리보기가 다 똑같이 떴다
React SPA라 카톡 미리보기와 검색 결과가 전부 똑같이 떴다. SSR 없이 Express가 글마다 메타태그, JSON-LD, noscript 본문을 끼워 넣도록 만든 과정과 나중에 canonical에서 데인 이야기다.
새로고침하면 로그인이 풀리던 문제를 Zustand persist로 해결했다
새로고침하면 관리자 로그인이 풀리던 문제를 Zustand persist로 해결했다. 무엇을 저장하고 무엇은 저장하면 안 되는지, 토큰까지 localStorage에 들어간 지금 코드의 문제도 같이 적었다.
즐겨찾기를 Cookie로 저장했다가 localStorage로 옮긴 이유
며칠 만에 사라진 즐겨찾기를 계기로 Cookie와 localStorage의 차이를 정리했다. 탭 사이 동기화 문제와, 코드를 다시 열어보고 확인한 실제 저장 방식까지 적었다.
Socket.IO로 실시간 채팅이랑 시세, 접속자 수를 한 번에 처리했다
코인 사이트에서 Socket.IO 연결 하나로 채팅, 시세 알림, 접속자 수를 같이 처리했다. socket.emit과 io.emit을 나눠 쓰는 기준을 실제 서버 코드와 함께 정리했다.
실시간 모니터링 대시보드를 폴링 대신 SSE로 만들었다
2초마다 API를 부르던 서버 모니터링 화면을 SSE로 바꿨다. Express 스트림과 EventSource 코드, 그리고 Apache 프록시 버퍼링 때문에 이벤트가 몰아서 오던 문제를 flushpackets로 푼 과정이다.
예전에 SHA-256으로 저장한 비밀번호를 로그인할 때마다 bcrypt로 자동 변환했다
SHA-256으로 저장해둔 비밀번호를 서비스를 멈추지 않고 bcrypt로 옮겼다. 로그인에 성공하는 순간 다시 해싱하는 방식과, 레거시 검증 코드를 언제 지울 수 있는지에 대한 고민을 적었다.
mysql2로 바꿨더니 평점이 갑자기 깨졌다. DECIMAL이 문자열로 오는 함정
드라이버를 mysql2로 바꾸자 평균 별점 화면에서 toFixed is not a function 에러가 났다. DECIMAL을 문자열로 돌려주는 이유와 decimalNumbers 옵션으로 고친 과정을 정리했다.
요청마다 DB 연결을 새로 맺던 코드를 커넥션 풀로 갈아치웠다
요청마다 DB 연결을 새로 맺다가 Too many connections로 사이트가 멈췄다. mysql2 커넥션 풀로 바꾼 코드와, 다시 세어보니 아직 남아 있던 createConnection까지 적었다.
로컬에선 되던 삭제가 서버에선 405를 뱉었다
로컬에선 되던 DELETE와 PUT이 서버에서만 405로 막혔다. POST로 우회한 이유와, 나중에 찾은 진짜 원인인 ModSecurity 기본 규칙을 실제 요청 결과와 함께 정리했다.
git push 하나로 서버가 알아서 빌드하고 배포하게 만들었다
GitHub에 push하면 서버가 webhook을 받아 pull, 설치, 빌드, 복사까지 알아서 하게 만들었다. 서명 검증과 private 저장소 인증, 그리고 도메인을 옮긴 뒤 배포가 조용히 멈춘 이야기까지 담았다.
한 대의 서버에 8개 서비스를 PM2랑 Apache 리버스 프록시로 돌리고 있다
서버 한 대에서 서비스 여러 개를 PM2로 띄우고 Apache 리버스 프록시로 도메인마다 포트를 나눴다. VirtualHost 설정, 웹소켓 프록시, 와일드카드 인증서까지 실제 설정으로 정리했다.
fail2ban이랑 UFW를 직접 파싱해서 서버 보안 대시보드를 만들어봤다
SSH로 들어가 명령어를 치던 보안 점검을 웹 대시보드 하나로 모았다. 열린 포트, 로그인 실패 기록, fail2ban, UFW 상태를 Node에서 읽어 파싱한 코드와 sudo 없이 권한 문제를 피한 방법을 적었다.
React와 iframe 사이 postMessage 통신을 실제로 구현해본 이야기
게임 콘텐츠를 iframe으로 띄우고 React와 postMessage로 메시지를 주고받은 구현 기록이다. 양쪽 송수신 코드와 origin 검사, 그리고 이 방식이 임시방편인 이유를 정리했다.
PHP에서 React로, 그다음 Express로. 한 번에 못 바꾸는 현실 속 점진적 전환기 (2탄)
PHP 프로젝트 두 개가 얽혀 있어 한 번에 갈아엎을 수 없었다. iframe으로 React 화면을 먼저 붙이고, PHP를 API 역할로 줄여가며 필요한 것만 Express로 옮긴 점진적 전환 기록이다.
PHP + HTML에서 React + Express로 갈아탄 이유와 과정
PHP와 HTML로 만든 서비스가 커지면서 상태 관리, 컴포넌트 재사용, 비동기 UI에서 한계를 느꼈다. React와 Express로 옮기기로 한 이유와 Express를 고른 현실적인 사정을 적었다.
React 프로젝트에서 API 에러 처리, ApiErrorBoundary로 정리해봤다
react-query 에러 처리를 페이지마다 복사하던 코드를 ApiErrorBoundary 하나로 모았다. useErrorBoundary 옵션이 왜 필요한지, 네트워크 에러는 GlobalErrorBoundary로 넘긴 구조까지 코드로 정리했다.