Next.js 앱의 Marp 슬라이드를 오프라인에 저장하기
기존 Marp 뷰어를 오프라인에서 실행하려던 접근부터 배포와 네트워크, 발표자 창에서 드러난 문제를 보완해 저장 기능을 완성하기까지의 기록
기존 Marp 뷰어를 오프라인에서 실행하려던 접근부터 배포와 네트워크, 발표자 창에서 드러난 문제를 보완해 저장 기능을 완성하기까지의 기록
10년쯤 전 카카오에서 Rails를 쓰던 주니어가 DHH의 Rails World 2026 키노트를 보고 정리한 생각. Rails의 무대와 DHH의 행보를 짚고, 손코딩 은퇴 선언이 왜 불편하게 남았는지, 아직 판단을 배우는 사람에게 무엇이 빠져 있었는지 키노트 원문을 근거로 따라간다.
Next.js 16의 cacheComponents는 떠난 페이지를 언마운트하지 않고 React Activity로 숨긴다. 하얗게 빈 three.js 캔버스를 따라 라우터, React, react-three-fiber의 소스를 읽고 브라우저에서 재현했다.
추석맞이 뻘짓 대작전 3탄: 사랑해요 토스증권
추석맞이 뻘짓 대작전 2탄: 여러차례 삽질에 막힌 V8 커버리지 분석기 제작기.
추석맞이 뻘짓 대작전 1탄: 내 블로그 부터 살펴보기
호출하지 않는 함수가 10MiB 들어 있으면 얼마나 손해일까. 바이트가 만든 비용과 코드 형태가 만든 비용을 갈라 855회 측정했다. 미호출 선언은 MiB당 약 21ms로 CPU를 4배 늦춰도 늘지 않았고, 파일을 읽자마자 실행되는 초기화 코드는 MiB당 201ms를 메인 스레드에 얹었다. 실제 라이브러리에서는 import만 해도 모듈 평가가 실행됐다.
블로그 마이그레이션 뒤 수식 글 LCP가 5.6초로 늘어난 원인을 추적했다. 일반 웹 글꼴을 실제 코드에서 제거하고 다시 빌드하자 수식 글 LCP는 5,650ms에서 2,352ms, FCP는 1,514ms에서 758ms로 줄었다. LCP 대상은 본문 문단에서 배너로 바뀌었다. 수식 전용 글꼴은 쓰는 글리프만 남긴 서브셋으로 바꿔 334KiB에서 26KiB가 됐다. 최초 비교 48회, 원인 대조 15회, 수정 전후 24회, 서브셋 전후 8회의 결과를 기록했다.
블로그의 remark/rehype 체인을 Rust로 옮기고 WASM으로 빌드해 Next.js 서버에 붙였다. 파싱과 HAST 생성에 이어 Oniguruma 하이라이트, MathML 수식, 이미지 크기와 MDX 속성 처리까지 한 호출로 묶었다. 메모리 전달과 해제, 기존 글의 호환성, WASI와 바이너리 배포를 구성하며 얻은 것과 감수한 비용을 기록했다.
블로그의 Tailwind 4를 StyleX로 옮겼다. 유틸리티만 옮긴 중간 상태에서는 CSS와 FCP가 줄었지만 전체 전송량은 늘었다. 전환 범위를 넓히자 홈 FCP가 59% 느려졌고, 작성 방식을 고친 뒤에도 격차가 남았다. 마지막 개선은 KaTeX 조건부 로딩과 본문 CSS 분리에서 나왔다. CSS 크기와 전체 전송량, 페인트 지표를 따로 비교한 기록이다. 블로그 성능 개선하기 시리즈의 첫 편이다.
『npm Deep Dive』가 2026년 세종도서 학술부문에 선정되었습니다. 함께 써주신 분, 만들어주신 분, 읽어주신 분들께 감사드립니다.
AI가 코드를 쓰는 시대의 개발자와 판단에 대한 에세이입니다. 초고를 읽고 의견을 주실 베타리더를 9월 30일까지 모집합니다. 인터뷰는 종료되었습니다.
자사 앱과 파트너 앱의 iOS, Android 웹뷰를 지원하며 흩어진 환경 분기를 어댑터로 모았다. 인셋과 브릿지, CSS를 정리한 과정과 SSR 시드, hydration에서 겪은 문제를 기록했다.
markdownlint-cli2를 Rust로 옮겨 블로그 저장소의 진단 264,114건을 바이트 단위로 맞췄다. 규칙 51개를 옮긴 커밋은 하루에 몰렸지만, 교체해 쓸 도구를 완성하는 일은 보름 동안 이어졌다. 37배 빠른 파서를 포기하고, 성능 측정의 오판을 고치고, 적대 리뷰에서 호환 차이 11건을 더 찾은 기록이다.
상수 하나를 import 했는데 번들의 97.7%가 따라왔다. 벤더는 고칠 일정이 미정이라기에, 배포된 소스맵을 뜯어 400개가 넘는 TypeScript 파일을 되찾고 빌드와 엔트리와 의존성을 내 맘대로 갈아엎었다. 로직만 빼고. 그래서 /send가 raw -77.5%. 그런데 어려운 건 그다음이었다. 테스트 1932개가 전부 통과했는데, 그중 몇 개는 아무것도 보고 있지 않았다.
2편 끝에 남긴 "워커 경유 비용 500ms"를 확정하려 했지만, 대조군이 되는 하드 리로드는 하루 한 건이 안 됐다. 그래서 Playwright와 셰이핑 프록시로 대조군을 직접 만들어 재 보니 내비게이션에서 워커 비용은 2ms였고, 비용은 지연이 아니라 글 하나를 클릭할 때마다 배경에서 더 받는 바이트 쪽에 있었다. 랩과 실사용자 데이터 사이에 남겨 둔 간극은 글을 다 쓰고 나서야 103 Early Hints가 만든 측정 정의 차이였다는 것을 알았다. 서비스 워커 캐싱 딥다이브 시리즈의 세 번째 편이다.
1편의 일반론을 들고 이 블로그(Next.js App Router)를 오프라인에서도 열리게 만들었다. 첫 배포에서는 방금 읽은 글이 오프라인에서 안 열렸고, 두 번째 배포에서는 글은 열리는데 이미지가 전부 깨졌다. 소프트 내비게이션과 프리페치, next/image가 만든 함정들을 하나씩 고쳐 배포한 연대기와, 그 결과를 GA4 실사용자 데이터로 정산한 기록이다. 재방문자 FCP는 평균 634ms 좋아졌고, TTFB는 평균 525ms 나빠졌다. 서비스 워커 캐싱 딥다이브 시리즈의 두 번째 편이다.
사용자가 준 URL을 서버가 대신 여는 기능은 SSRF의 교과서적 조건을 명세로 갖고 있다. 화이트리스트를 뚫는 우회 여섯 가지를 먼저 보고, 그것을 막는 방어 원리 다섯 개를 Node에서 실제로 돌려본 기록. 손으로 IPv4-mapped를 벗기면 16진 표기에서 뚫리고, undici의 lookup 훅은 호스트가 IP 리터럴이면 아예 호출되지 않으며, URL.hostname은 IPv6 리터럴의 대괄호를 남긴다. OG 스크래핑 서버 설계 노트 2부작의 마지막 편이다.
링크 미리보기 서버의 "에러율 10%"는 성질이 다른 다섯 종류의 실패가 뭉친 숫자다. 이 워크로드가 왜 I/O 바운드에 저 TPS인지, 런타임 선택이 실제로 갈리는 네 지점은 어디인지 따져본 뒤, User-Agent와 인코딩으로 에러율을 낮추는 일로 넘어간다. Node 내장 TextDecoder는 CP949 확장 문자를 에러 없이 다른 글자로 바꾸고, 스크랩해 온 og:title은 API 응답이 아니라 사용자 입력이다. 캐시 스탬피드와 negative caching, 그리고 "P95 1초 미만"을 캐시 히트율에서 역산해 검증하는 200만 건 시뮬레이션까지 담았다. OG 스크래핑 서버 설계 노트 2부작의 첫 편이다.
Next.js 16 turbopack 프로덕션 빌드에서 모듈 스코프 싱글톤이 런타임에 두 개가 됐다. 같은 동기 구간에서 조건 판정이 뒤집히고, 응답이 도착해도 타임아웃이 나는 증상을 번들 산출물로 추적한 기록. scope hoisting의 부분 병합, 순환 import, 이미 고쳐져 있던 upstream 버그, 그리고 뒤늦게 돌린 단일 변수 실험까지.