arxiblog: arXiv 논문을 읽고 싶은 블로그 글로
논문을 읽어야지, 하고 arXiv 링크를 열어두는 습관이 있다. 문제는 그 탭이 30개쯤 쌓일 때까지 실제로 읽는 건 두세 개라는 거다. PDF를 열면 초록부터 관련 연구까지 빽빽한 두 컬럼이 나오고, 정작 궁금한 “이게 뭘 풀려는 건데?”에 도달하기까지 스크롤을 한참 해야 한다. 그러다 보면 그냥 탭을 닫는다.
themoonlight.io 같은 논문 리뷰 사이트를 보면서, 논문을 한 편의 글처럼 따라 읽게 해주면 얼마나 좋을까 싶었다. 그래서 만든 게 arxiblog다. arXiv ID 하나를 던지면 LLM이 그 논문을 “사람이 쓴 듯한 논문 읽기 블로그 글”로 바꿔준다.
라이브로 돌고 있는 건 여기다: arxiblog.jiun.dev. 이름은 “Paper Notes”로 붙여뒀다.

한 줄이면 논문이 글이 된다
쓰는 건 정말 한 줄이다.
arxiblog add 1706.03762arxiblog add https://arxiv.org/abs/2106.09685arxiblog add arXiv:2010.11929v1ID든 abs URL이든 pdf URL이든 다 받는다. 위에 있는 건 순서대로 Transformer, LoRA, ViT 논문이다. 데모 사이트엔 Transformer, ViT, BERT, GAN 같은 유명한 논문들을 넣어두고 결과를 확인했다. 다들 한 번쯤 읽어봤을 논문이라, 변환이 원문을 얼마나 잘 살렸는지 비교하기 좋았다.
내부적으로는 대충 이런 파이프라인을 탄다.
arXiv ID/URL ↓ Ingest export.arxiv.org로 메타데이터, arxiv.org에서 PDF 본문 추출 ↓ Transform LLM이 "논문 읽기 블로그" 글 + 용어 주석 + mermaid 도식으로 재구성 ↓ Store SQLite (papers / posts / annotations / usage_logs) ↓ Build 정적 HTML (KaTeX 수식 + mermaid + 주석 팝오버 + 용어집) ↓ Serve 정적 파일 + /api/chat + /admin가장 신경 쓴 건 Transform 단계다. 그냥 “요약해줘”라고 하면 LLM은 불릿 목록을 뱉는다. 배경 셋, 방법 셋, 결과 셋. 이건 읽기 싫은 글이다. 그래서 프롬프트를 배경 → 풀려는 문제 → 핵심 아이디어 → 방법 → 결과 → 의의와 한계 순으로 흐르는 문단이 되도록 잡았다. 목록 나열이 아니라 이야기처럼. LLM의 출력은 구조화된 JSON으로 받아서 본문, 주석, 도식을 분리해 저장한다.
여기가 제일 오래 걸렸다. 첫 결과물을 보고 남긴 피드백이 세션 로그에 그대로 남아 있는데, “블로그 말투 같지 않습니다. human writable한 말투로 작성되어서 읽고 싶은 느낌이 들어야 합니다”라는 말을 두 번 연속으로 했다. 처음 나온 글은 문장만 이어 붙인 요약이었지 글이 아니었다. 프롬프트를 고치고, 디자인을 갈고, mermaid 도식을 붙이라고 다시 시키고, 그걸 몇 바퀴 돌고 나서야 “논문 읽기 블로그”라고 부를 만한 게 나왔다.
의외로 발목을 잡은 건 화려한 LLM 쪽이 아니라 맨 앞의 Ingest였다. arXiv 메타데이터는 export.arxiv.org API로 가져오는데, 조금만 자주 때리면 429가 날아온다. 그래서 백오프를 넣어야 했다. 진짜 골칫거리는 PDF였다. 논문 PDF를 파싱하는 건 언제 터질지 모르는 작업이라 — 이상하게 인코딩된 PDF 하나가 프로세스를 통째로 멈춰 세울 수 있다 — 격리된 워커에서 제한 시간을 두고 돌린다. 워커가 시간 안에 못 끝내면 그냥 죽이고 넘어간다. 여기에 SSRF 가드도 붙였다. 사용자가 arXiv ID인 척 내부 네트워크 주소를 넣어서 서버가 엉뚱한 곳에 요청을 보내게 만드는 걸 막아야 하니까. “논문 하나 가져오기”가 이렇게 방어적인 코드가 될 줄은 몰랐다.

빌드는 정적 HTML로 뽑는다. 본문 마크다운은 marked로 렌더링하고, 수식은 KaTeX로, 도식은 mermaid로 그린다. LLM 출력을 그대로 HTML에 꽂으면 위험하니 sanitize-html로 한 번 걸러낸다. 읽는 화면 자체도 논문 PDF보다는 나아야 한다고 생각해서, 다크/라이트 테마, 상단 읽기 진행률 바, 우측 목차(TOC), 본문 글꼴은 Pretendard로 맞췄다. 별것 아닌 것 같아도, 두 컬럼 PDF를 스크롤하는 것과 한 컬럼으로 흐르는 글에 진행률 바가 채워지는 걸 보는 건 체감이 꽤 다르다.
모르는 용어엔 밑줄, 마우스 올리면 설명
논문 글의 진짜 장벽은 전문용어다. “attention”이 뭔지 모르면 그 다음 문장부터 다 막힌다. 그래서 변환할 때 LLM이 전문용어에 [[용어]] 형태로 주석을 달게 했다. 빌드하면 그 단어에 밑줄이 생기고, 마우스를 올리면 설명 팝오버가 뜬다. 글 맨 아래엔 용어집도 따로 모인다.

이게 은근히 품질을 탄다. 큰 모델을 쓰면 주석이 풍부하게 붙고, 빠르고 저렴한 모델을 쓰면 주석 수가 확 줄어든다. Gemini flash-lite로 돌리면 빠르긴 한데 주석이 좀 인색하다. 그리고 본문은 토큰을 아끼려고 약 48,000자까지만 LLM에 넣는다. 논문이 아무리 길어도 그 안쪽만 본다는 뜻이라, 뒷부분 실험 디테일이 통째로 빠지기도 한다. 이건 트레이드오프라 아직 정답을 못 찾았다.
도식도 LLM이 만든다. 구조나 파이프라인, 흐름을 mermaid 다이어그램으로 뽑아내는데, 잘 나올 때는 정말 그럴듯하고 못 나올 때는 화살표가 엉킨 스파게티가 된다. 그럴 땐 그냥 다시 돌린다.
챗 때문에 결국 내 맥을 켜두게 됐다
각 글 우측 하단엔 “물어보기” 버튼이 있다. 누르면 사이드 패널이 열리고, 질문을 보내면 그 글과 용어집, 그리고 원문 논문 발췌(글 8,000자 + 원문 14,000자 정도로 잘라서)를 컨텍스트에 넣어 LLM에 물어본다. 읽다가 막히면 바로 그 자리에서 물어보라는 거다. 본문에서 문장을 드래그해서 “이거 무슨 뜻이야”라고 물을 수도 있고, 추천 질문도 몇 개 띄워준다.
여기서 문제가 하나 생겼다. 챗은 서버가 살아있어야 동작한다. /api/chat 엔드포인트가 필요하니까. GitHub Pages 같은 정적 호스팅에 올리면 글이랑 주석, 용어집은 멀쩡히 보이지만 챗 버튼은 “serve 모드에서만 동작한다”는 안내만 뜬다.
라이브 데모에서 챗까지 살려두고 싶었다. 그래서 택한 게, 그냥 내 맥에서 서버를 계속 켜두고 cloudflared 터널로 arxiblog.jiun.dev에 붙이는 거였다. 이 터널 연결부터 순탄치 않았다. 스크립트를 만들어 터널을 띄웠는데 정작 브라우저에선 ERR_NAME_NOT_RESOLVED만 떴다. “저는 접속이 안 되는데 왜일까요”를 반복하면서 DNS와 터널 설정을 몇 바퀴 다시 검토하고 나서야 도메인이 잡혔다.
붙고 나서도 문제였다. 처음엔 터미널에서 백그라운드로 돌렸는데, 맥을 재부팅하면 죽고, 어쩌다 프로세스가 뻗으면 아무도 모르게 사이트가 내려가 있었다. 몇 번 당하고 나서 LaunchAgent로 바꿨다. 로그인하면 자동으로 뜨고, 비정상 종료되면 알아서 재시작한다. 설치 과정은 scripts/macos-launch-agent.sh 스크립트로 만들어뒀다.
한 가지 삽질 포인트. LaunchAgent는 --no-build로 서버만 띄운다. 재시작 도중에 빌드가 반쯤 된 불완전한 사이트가 게시되면 안 되니까. 그래서 글을 새로 추가한 뒤엔 arxiblog build를 먼저 성공시키고 나서 서비스를 restart 해야 한다. 이걸 까먹고 “왜 새 글이 안 보이지” 하면서 한참 헤맨 적이 있다. 빌드와 서빙을 분리해놓고 그걸 스스로 잊어버린 거다.
아무나 논문을 넣게 두면 안 되니까
라이브 사이트를 열어두다 보니 신경 쓸 게 늘었다. arxiblog serve를 하면 관리 페이지(/admin)가 같이 뜨는데, 여기서 논문을 추가하고 설정을 바꾸고 글을 지울 수 있다. LAN이든 터널이든 외부에 열리는 순간, 아무나 내 API 키로 논문을 마구 변환하게 둘 순 없다.
그래서 상태를 바꾸는 API(/api/add, /api/settings, /api/delete)엔 토큰 인증을 걸었다. 서버를 켤 때마다 토큰이 새로 발급되고, 콘솔에 /admin#token=… 형태로 관리 주소가 찍힌다. 토큰을 URL fragment에 둔 건 의도적이다. fragment는 브라우저가 서버로 안 보내니까, 프록시 로그에도 안 남고 브라우저 세션에만 남는다. 읽기랑 블로그 자체는 공개, 쓰기는 토큰 없으면 차단이다.
공개 챗도 마찬가지다. 누가 재미로 질문을 수천 번 날리면 LLM 요금이 폭탄이 된다. 그래서 IP당 시간 제한과 전체 하루 제한을 걸어뒀고, Gemini는 여러 키를 돌려쓰다가 무료 한도가 떨어지면 유료로 폴백하게 해뒀다. 이 방어 로직 대부분은 초기 버전엔 없었고, “이거 인터넷에 열 건데 괜찮나”를 뒤늦게 점검하면서 하나씩 붙인 것들이다.
페르소나로 말투를 고른다
같은 논문이어도 읽는 사람이 다르다. 그래서 말투를 personas/*.json에 정의해두고 고를 수 있게 했다. 기본은 비유를 많이 쓰는 쉬운 해설의 friendly이고, “그래서 이걸 어떻게 써먹나”부터 궁금한 개발자용 engineer, 이야기처럼 읽히는 걸 원하면 storyteller가 있다. 난이도는 --level beginner|intermediate로 조절하고, JSON 파일 하나 추가하면 나만의 페르소나도 만들 수 있다.
arxiblog add 2305.12345 --persona engineer --level intermediateLLM은 Gemini, Anthropic, OpenAI, Azure를 지원한다. 나는 주로 Gemini로 돌린다. 무료 키로도 꽤 굴러가고, 품질이 아쉬우면 큰 모델로 바꾸면 되니까.
CLI 명령은 몇 개 안 된다. 논문을 넣는 add, 정적 사이트를 만드는 build, 서버를 띄우는 serve, 목록을 보는 list, 지우는 remove, 토큰 사용량까지 보여주는 status, 그리고 GitHub Pages나 Vercel로 올리는 deploy. 데모 사이트는 앞서 말한 이유로 deploy 대신 내 맥에서 serve를 상시로 돌리는 쪽을 택했지만, 챗이 필요 없다면 그냥 build 결과물을 정적 호스팅에 올리는 게 제일 간단하다.
이 프로젝트의 커밋 히스토리를 보면 좀 웃긴다. 6월 1일에 빈 baseline만 하나 찍혀 있고, 실제 변환 기능이 들어온 첫 커밋은 6월 4일 16시 3분이다. 그런데 바로 1분 뒤 커밋이 “Cloudflare Tunnel 셋업 스크립트”이고, 16시 10분에 “공개 챗 레이트 리밋”, 16시 20분에 “Gemini 멀티키 로테이션”이 붙어 있다. 기능을 만든 지 한 시간도 안 돼서 “이거 인터넷에 내놓으면 뭐가 터지지”를 걱정하기 시작했다는 뜻이다. 6월 27일엔 아예 배포 가능성 전반을 리뷰 라운드로 훑으면서 챗 쿼터, 키 가드, IP 신뢰 처리 같은 걸 손봤다. 재밌는 장난감을 만드는 것과 그걸 남이 접속할 수 있는 곳에 걸어두는 건 완전히 다른 일이더라.
아직은 뻘짓과 실용 사이
솔직히 이게 얼마나 쓸모 있는지는 아직 잘 모르겠다. LLM이 재구성한 글이라, 원문을 잘못 읽거나 빼먹는 경우가 분명히 있다. 그래서 각 글엔 원문 arXiv 링크를 항상 같이 걸어두고, “정확한 건 원문 확인”이라는 면책도 박아뒀다. 이건 논문을 대체하는 게 아니라, 읽을지 말지 정하는 관문에 가깝다. 글을 훑고 “오 이거 재밌겠는데” 싶으면 그때 원문을 여는 거다.
Bun과 TypeScript로 짰고 MIT 라이선스다. 설계는 예전에 만든 kiwimu를 많이 참고했다. 사실 프로젝트를 시작할 때 Claude에게 던진 첫 메시지부터가 “kiwimu 같은 서비스가 있으니 이를 참고해서 arxiv + blog를 만드세요”였다. 소스는 GitHub에 공개했다. 다만 npm 배포는 아직이고, PDF 워커 격리나 LaunchAgent 스크립트 같은 최근 작업들도 커밋 안 된 채 로컬에 쌓여 있어서, 공개된 건 조금 이전 시점의 스냅샷이다.
그래도, 내 맥 한 대가 지금 이 순간에도 arXiv 논문을 블로그 글로 바꾸면서 arxiblog.jiun.dev를 서빙하고 있다는 게 나름 마음에 든다. 30개씩 쌓이던 탭이 조금은 줄었으면 좋겠다.
Comments
Loading comments...