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