금요일 정비: 홈랩과 허브 정리
출처: Dev.to
몇 주는 큰 기능을 배포하고, 다른 주는 큰 기능이 계속 동작하도록 바닥을 쓸어내린다. 이번 주는 바닥을 쓸어내는 주였다—서로 전혀 관계 없는 두 작업 흐름이 모두 주의를 필요로 했다.
트랙 1: 홈랩의 로컬 LLM 스택이 한 달째 손대지 않았다. 모델이 오래됐고, llama.cpp는 469 빌드 뒤처졌으며, 임베딩 모델은 한 세대 뒤였다.
트랙 2: 오픈소스로 공개한 여행 계획 사이트가 실제 그룹 여행에 쓸만해야 했다. 캘린더 동기화, 활동 투표, 비용 정산—브로셔를 도구로 바꾸는 기능들.
트랙 3: 이번 주 초에 언급한 Substack 연동 파이프라인? 한 번 실행하는 건 쉬웠지만, 매번 실행하려면 두 가지 미문서화된 문제를 발견했고 이를 가리기 위해 GitHub Action을 추가해야 했다.
각 이야기가 개별적으로는 화려하지 않다. 하지만 이들을 합치면 에이전트와 함께 구축할 때 유지보수 주가 어떤 모습인지 보여준다.
홈랩은 RTX 5090에 llama.cpp를 실행하고 6개의 전환 가능한 모델을 구동한다. 에이전트가 모든 것을 감사하고 다음과 같은 성적표를 반환했다:
| 구성 요소 | 이전 | 판단 |
|---|---|---|
llama.cpp | b8933 | 469 빌드 뒤처짐 |
| Qwen (일일 드라이버) | 3.5 35B‑A3B | 3.6 사용 가능 |
| 임베딩 | nomic‑embed v1.5 | v2‑moe 사용 가능 |
| Gemma 4, Devstral, DeepSeek | 최신 | 조치 필요 없음 |
| Codestral | v0.1 (2024) | 막다—Mistral이 Devstral로 전환 |
총 3개의 다운로드, 약 38 GB: Qwen 3.6, nomic‑embed v2‑moe, 그리고 새로 추가된 Qwen3‑Coder‑30B‑A3B(코딩 특화 MoE, 17 GB).
흥미로운 발견은 양자화(quant) 출처에 관한 것이었다. 우리 Qwen 모델은 UD‑Q4_K_XL 양자화를 사용한다—“XL” 양자화는 어텐션 레이어에서는 높은 정밀도를 유지하면서 MoE 전문가 레이어는 작게 만든다. 이는 unsloth 전용이며, 다른 주요 GGUF 배포자인 Bartowski는 제공하지 않는다. 에이전트가 처음에 Bartowski 버전을 찾았고, 우리는 같은 양자화 타입을 얻기 위해 unsloth로 방향을 바꿔야 했다.
양자화 포맷은 모델 이름만 보고는 알기 어려운 출력 품질에 영향을 미친다. Q4_K_M과 Q4_K_XL은 모두 “4‑bit”이지만 정밀도를 배분하는 방식이 다르다. 업그레이드 중 양자화 타입을 바꾸는 것은 통제되지 않은 변수다.
홈랩의 모델 전환 로직은 llm-switch.sh라는 셸 스크립트에 있다. 이 스크립트는 모델 이름을 파일 경로와 llama‑server 플래그에 매핑한다. 업데이트 내용: Qwen 경로를 3.5 → 3.6으로, 새로운 qwen‑coder 케이스에 128K 컨텍스트 추가, 임베딩 경로를 v1.5 → v2‑moe로, Codestral을 [legacy]로 표시.
함정: 여기서‑문서(here‑doc) 스크립트를 터미널에 붙여넣으면 백슬래시와 인용부호가 망가진다. 우리는 스크립트를 워크스페이스에 저장하고 GitHub에 푸시한 뒤 git pull && cp 한 줄 명령으로 배포하도록 바꿨다. 교훈: 채팅을 통해 셸 스크립트를 붙여넣지 말고 커밋하라.
| 구성 요소 | 이전 | 이후 |
|---|---|---|
llama.cpp | b8933 | b9402 |
| 생성 모델 | Qwen 3.5 | Qwen 3.6 |
| 임베딩 모델 | nomic v1.5 (262 MB) | nomic v2‑moe (914 MB) |
| 전환 가능한 모델 | 5 | 6 (추가: qwen‑coder) |
| VRAM | 26,262 MiB | 26,682 MiB (+420 MiB) |
감사 시작부터 완전 업데이트까지 약 20분이 걸렸으며, 다운타임은 전혀 없었다. 기존 모델은 새 바이너리로 서비스 재시작하기 전까지 계속 제공된다.
여행 허브는 포크 가능한 여행 계획 사이트다—Vercel에 배포하고 설정 마법사를 실행하면 그룹 전용 여행 노트, 일정, 숙소, 활동, 사진 페이지가 생긴다. 지난 주에 오픈소스로 공개했으며, 이번 주는 실제로 쓸만하게 만드는 작업이었다.
- 4개의 기능을 3일에 걸쳐 구현, 11개의 커밋, 3,484줄 추가. 하지만 흥미로운 부분은 기능이 아니라 버그다.
캘린더 연동
사람들은 여행 일정을 휴대폰 캘린더에 넣어야 한다. 두 가지 옵션이 있다: .ics 파일을 한 번 다운로드해 가져오기, 혹은 URL을 구독해 자동 동기화.
다운로드는 간단—버튼 클릭 → 파일 다운로드. 구독은 흥미로운 엔지니어링 문제다. Google Calendar, Apple Calendar, Outlook 모두 구독 URL을 서버에서 직접 가져온다. 브라우저도 쿠키도 없으므로 엔드포인트는 세션 없이 동작하는 인증 방식을 필요로 한다.
우리는 결정론적 HMAC 토큰을 사용했다: HMAC‑SHA‑256('calendar-subscribe', VACATION_HUB_SECRET). 내보내기 엔드포인트는 브라우저 다운로드용 쿠키와 캘린더 클라이언트용 ?token= 파라미터 중 하나를 받는다. 토큰은 만료되지 않는다—시간 제한 토큰은 만료 시 구독이 조용히 끊기고 재인증할 사용자가 없기 때문이다.
iCal 생성기는 202줄 코드이며 RFC 5545를 직접 구현했다. 미묘한 부분은 라인 폴딩이다—스펙은 문자가 아니라 옥텟 기준으로 최대 75옥텟을 요구한다. UTF‑8 멀티바이트 문자를 중간에 자르면 안 되므로 .slice(75)로는 안 된다. 폴드 함수는 잘라낸 지점에서 뒤로 돌아가 연속 바이트를 확인한다. 대부분의 iCal 라이브러리는 이 부분을 틀려 비ASCII 이벤트 이름을 손상시킨다.
활동 투표 시스템
Reddit 스타일의 찬성/반대 투표를 구현했다. 이름 기반 식별(localStorage, 계정 없음)과 업서트(upsert) 투표를 사용해 마음을 바꾸는 것이 멱등하도록 만들었다.
이 기능은 개발 환경에서는 완벽히 동작했지만, 프로덕션에서는 완전히 실패했다. 두 번, 서로 다른 이유로.
버그 1 — 트레일링 슬래시 대량 사망
next.config.ts에 trailingSlash: true가 설정돼 있어 Next.js가 /api/foo를 /api/foo/로 308 리다이렉트한다. 리다이렉트는 HTTP 메서드를 유지하지만 브라우저는 요청 본문을 버린다. 모든 POST, PUT, DELETE가 빈 본문으로 API에 도달