블로그 배너
thumbnail
thumbnail
thumbnail
thumbnail
thumbnail
thumbnail
thumbnail
thumbnail
728x90
반응형

헤드룸이 뭐예요?

한 줄로 말하면  AI 에이전트가 LLM에 보내는 것들 — 도구 출력, 로그, RAG 조각, 대화 기록 — 을 로컬에서 압축하고, 필요할 때 원문을 다시 꺼낼 수 있게 하는 계층이에요.

 

이름부터 좋아요. "헤드룸(headroom)"은 원래 오디오·엔지니어링에서 "여유 공간"을 뜻해요. 소리가 클리핑되지 않도록 남겨두는 헤드 위 공간처럼, LLM에도 컨텍스트를 밀어넣을 여유가 필요하다는 뜻이에요.

 

GitHub 저장소는 chopratejas/headroom입니다. Apache-2.0 라이선스이고 Python·Rust·TypeScript가 섞인 하이브리드 프로젝트예요. 인기가 어마무시한 레포인데요. 스타수 및 트랜딩 랭킹만 보아도 현재 전세계 개발자들이 엄청나게 관심있어하는 오픈소스이구나~ 를 느낄 수 있습니다.

https://github.com/headroomlabs-ai/headroom

 

왜 이런 게 필요한가

 

AI 에이전트를 조금이라도 오래 굴려본 사람은 다 알아요. 컨텍스트가 금방 차요. 특히 요즘 유행하는 "에이전트가 도구를 여러 번 호출하는" 흐름에서요.

 

간단한 코드 리팩터링 작업 하나만 시켜도 이런 게 컨텍스트에 다 들어가요.

 

  • ls 결과 (수백 줄)
  • grep 검색 결과 (수천 줄)
  • 파일 전체 읽기 (수만 자)
  • 이전 대화 기록
  • RAG로 가져온 관련 문서 조각
  • 시스템 프롬프트

 

한 번쯤 다들 경험이 있을 거예요. 에이전트한테 "이 리포 분석해줘" 하고 시작했는데 3~4번의 도구 호출 후에 컨텍스트가 40만 토큰을 넘어가면서 실행이 느려지고 비용이 폭증하는 상황이요.

 

컨텍스트 윈도우(context window): LLM이 한 번의 요청에서 볼 수 있는 최대 텍스트 양. GPT-5는 400K, Claude Opus 4는 200K 토큰이 기본. 여기를 넘기면 요청이 실패하거나 앞부분이 잘려요. 대략 200K 토큰 = A4 400페이지 분량이다.

 

여기서 두 가지 문제가 생겨요.

 

첫째, 비용이에요. 요청 하나당 입력 토큰이 30만 개라면 그것만으로도 몇 백원씩 나가요. 하루에 수십 번 요청하는 에이전트라면 하루 몇 만원이 순식간이에요. 구독제라면 단순 주간 리밋이 빠르게 찬다정도지만, API 과금 형태를 사용한다면 순식간에 몇백달러가 차기도 하는 큰 문제에요ㅠ

 

둘째, 품질 저하입니다. 최근 연구들이 반복해서 보여준 게, 컨텍스트가 길어질수록 모델이 중간 정보를 잊어버리는 경향이 있어요("lost in the middle" 현상이에요). 결국 넣어봤자 안 읽어요.

 

여기서 나온 발상이에요. "정말 필요한 것만 압축해서 보내고, 원문은 나중에 필요할 때만 다시 꺼내자."

 

어떻게 동작하는가

 

헤드룸의 핵심 아이디어는 네 가지예요. 하나씩 뜯어볼게요.

 

1) ContentRouter — "이건 어떻게 압축할까"를 결정해요

 

에이전트가 보내는 컨텍스트는 종류가 다양해요. JSON 응답도 있고 코드 스니펫도 있고 자연어 대화도 있고 이미지 URL도 있어요. 이걸 다 똑같이 압축하면 안 돼요.

 

JSON은 키 이름 반복이 많으니까 구조화된 압축이 유리해요. 코드는 공백·주석 처리를 다르게 해야 하고 자연어는 요약이 잘 먹혀요. ContentRouter는 입력을 보고 어떤 압축기를 쓸지 라우팅해요.

 

라우터(router) 패턴: 여러 종류의 입력을 받아서 각각에 맞는 처리기로 보내는 디자인 패턴. HTTP 라우터, 이벤트 라우터 같은 게 같은 원리예요. headroom에서는 "콘텐츠 유형 → 압축기"의 라우팅이에요.

 

2) CCR (Compressed Context with Retrieval) — 원문으로 되돌아갈 수 있어요

 

여기가 헤드룸의 핵심이에요. 압축하면서 원문에 접근할 수 있는 도구 인터페이스도 같이 심어놔요.

 

비유하면 이래요. 도서관에서 두꺼운 백과사전을 통째로 들고 다닐 수 없으니까 요약본만 가지고 다녀요. 그런데 요약본에는 "이 항목의 원본은 3층 서가 B-24에 있어요" 같은 위치 정보가 붙어 있어요. 에이전트가 "이 부분 더 자세히 봐야겠어" 하면 서가에서 원본을 꺼내와요.

 

그래서 이름이 Compressed Context with Retrieval이에요. 압축된 컨텍스트 + 필요할 때 원문 조회. 이 조회가 빠지면 압축은 그냥 정보 손실이에요.

 

3) CacheAligner — 프로바이더 캐시 히트를 노려요

 

OpenAI나 Anthropic 같은 프로바이더는 프롬프트의 앞부분(prefix)이 같으면 캐시를 태워서 요금을 할인해줘요. 최대 90% 할인까지 가요. 여기에 함정이 있어요.

 

앞부분에 매번 바뀌는 값(현재 시각, 랜덤 ID, 세션 정보)이 하나라도 있으면 캐시가 안 먹어요. 개발자가 무심코 넣은 datetime.now() 한 줄 때문에 모든 요청이 캐시 미스가 되는 흔한 실수예요.

 

CacheAligner는 프롬프트의 불안정한 prefix 부분을 감지해서 캐시 히트가 잘 되도록 위치를 조정하거나 안정화해요. 이 부분은 프로바이더별로 캐시 규칙이 조금씩 달라서 어댑터가 여럿 필요해요.

 

용어: 프롬프트 캐싱: 같은 프롬프트 앞부분을 프로바이더가 서버 측에 캐시해두고, 다음 요청에서 재사용하는 기능이에요. Anthropic은 cache_control 마커로, OpenAI는 자동으로 처리해요. 잘 쓰면 비용이 70~90% 줄어요.

 

4) Cross-agent memory — 여러 에이전트가 기억을 공유해요

 

한 에이전트가 학습한 컨텍스트를 다른 에이전트도 쓸 수 있게 공유 저장소를 만들어요. 예를 들어 Claude로 코드베이스를 파악한 결과를 저장해두면, 나중에 Codex로 이어서 작업할 때 그 요약본을 재사용할 수 있어요.

 

팀 단위에서 특히 유용해요. "지난주에 누가 이 리포 분석했었나" 하는 반복을 줄여줘요.

 

네 가지 사용 모드

 

헤드룸이 재밌는 게, 기존 에이전트 흐름에 끼워 넣는 방법을 네 가지나 제공한다는 점이에요.

 

모드 어떤 경우에 쓰나 침습성
라이브러리 (SDK) 내가 만드는 에이전트 코드에 직접 통합 코드 수정 필요
프록시 기존 앱을 건드리지 않고 HTTP 레이어에서 가로채기 무침습 (URL만 변경)
래퍼 Claude Code, Codex 같은 CLI 에이전트를 감싸기 명령어만 변경
MCP 서버 Claude Desktop, MCP 지원 에이전트에 도구로 붙이기 MCP 설정만 추가

 

이 네 가지가 왜 다 필요한지 알겠어요? 압축은 어디에서 끼어들 수 있는지에 따라 다른 방식이 유리하기 때문이에요.

 

새 프로젝트를 만들 때는 SDK로 처음부터 통합하는 게 깔끔해요. 이미 만들어진 서비스라면 코드를 안 건드리고 프록시로 앞에 세우는 게 편해요. Claude Code 같은 완제품 CLI는 래퍼로 감싸는 게 자연스럽고 Cursor나 Windsurf처럼 MCP를 지원하는 앱은 MCP 서버 붙이는 게 정석이에요.

 

실제로 어떻게 쓰나

 

README에 제시된 명령들을 정리하면 이래요.
이 명령들은 정적 분석 기준이라 실제 실행 결과는 각자 확인해봐야 합니다.

 

설치

설치 (Python 또는 Node)

# Python
pip install "headroom-ai[all]"

# Node
npm install headroom-ai

 

에이전트 래핑 (가장 쉬운 시작)

Claude Code나 Codex를 래핑

# Claude Code 감싸기
headroom wrap claude

# Codex CLI 감싸기
headroom wrap codex

 

Claude Code를 그냥 실행하는 대신 headroom wrap claude로 실행하면 그 세션의 모든 요청이 헤드룸을 거쳐요. 컨텍스트 압축·CCR·캐시 정렬이 자동으로 적용돼요. 코드 수정이 전혀 필요 없어요.

 

프록시 모드

프록시 서버 실행

headroom proxy --port 8787

 

내 앱의 OPENAI_BASE_URL이나 ANTHROPIC_BASE_URLhttp://localhost:8787로 바꾸면 끝이에요. 헤드룸이 요청을 받아서 압축·전달·복원까지 처리해요. 이미 만들어진 앱에 붙이기 좋아요.

 

효과 측정

압축 효과 확인

headroom perf

 

실제로 얼마나 절약됐는지 리포트를 뽑아줘요. 도입 초기에 ROI 근거로 쓰기 좋아요.

 

아키텍처: Python + Rust 하이브리드

 

코드 구조를 잠깐 뜯어보면 아래와 같습니다.

Python 패키지 (headroom/) — 사용자 대면 부분이에요.

 

Python 쪽 주요 모듈

headroom/
├── cli/main.py              # CLI 진입점
├── compress.py              # 라이브러리 방식 진입점
├── compression/universal.py # 콘텐츠 유형별 압축 정책
├── ccr/                     # 압축 + retrieval 도구 주입
├── memory/                  # cross-agent 공유 저장소
├── cache/                   # provider 캐시 최적화
└── providers/               # OpenAI/Anthropic/Bedrock/Vertex 어댑터

 

Rust 워크스페이스 (crates/) — 성능이 중요한 부분이에요.

Rust 쪽 크레이트 구성

crates/
├── headroom-core     # 핵심 압축·복원 로직
├── headroom-proxy    # HTTP 프록시 서버
├── headroom-py       # Python 바인딩
└── headroom-parity   # Python-Rust 결과 대조 (동등성 검증)

 

왜 두 언어를 섞었을까요?

Python 쪽은 SDK로 쓸 때 접근성이 좋아요. AI 커뮤니티는 Python이 표준이니까요. 그런데 프록시 서버처럼 요청을 계속 받는 부분은 Python으로 짜면 처리량이 아쉬워요. GIL(Global Interpreter Lock) 때문에 병렬 처리가 제대로 안 되고 메모리도 많이 먹어요.

 

성능이 중요한 코어와 프록시는 그래서 Rust로 옮겼어요. headroom-parity라는 크레이트도 있는데, Python 구현과 Rust 구현이 같은 결과를 내는지 대조하는 테스트 하네스예요. 두 언어로 같은 로직을 유지하는 데 필수적인 안전장치죠.

 

Parity 테스트: 두 개 이상의 구현체가 같은 입력에 같은 출력을 내는지 검증하는 방식. C와 Rust로 다시 짠 Linux 유틸리티(coreutils)도 이런 걸 해요. 언어를 마이그레이션할 때 회귀를 잡는 데 유용해요.

 

도입 전에 확인할 것들

 

정적 분석 기준이라 실제로 도입할 때는 몇 가지 체크가 필요해요.

 

1) 압축 손실이 우리 워크로드에 치명적인가

압축은 원래 정보 손실이에요. CCR로 원문을 다시 꺼낼 수 있다지만, 에이전트가 "지금 원문이 필요하다"는 판단을 잘못하면 놓치는 정보가 생겨요. 코드 리뷰나 요약처럼 정보 손실이 크게 문제 안 되는 태스크에는 잘 맞아요. 반대로 법률 문서나 의료 정보처럼 원문 정확성이 중요한 곳은 조심해야 해요.

 

2) 프로바이더별 실패 모드가 넓어요

OpenAI, Anthropic, Bedrock, Vertex, LiteLLM 등 여러 프로바이더 어댑터가 있어요. 각각 인증 방식, 요청 스키마, 캐시 규칙이 달라요. 도입 전에 우리가 쓰는 프로바이더 어댑터가 안정적인지 확인해야 해요.

 

3) 캐시·생성 파일이 저장되는 위치

압축 대상에는 대화 기록도 들어갈 수 있어요. 이게 로컬 어디에 저장되는지, 회사 정책상 허용되는 위치인지 먼저 봐야 해요. 사내 코드나 개인정보가 들어간 요청을 압축·저장할 때는 특히요.

 

4) benchmarks 결과 재현

저장소에 benchmarksdocs/content/docs/benchmarks.mdx가 있어요. 정확도·비용·지연시간·캐시 히트율 같은 지표가 정리돼 있는데, 우리 워크로드에 맞는지 자체 벤치마크로 재확인하는 게 좋아요.

 

마무리

컨텍스트 압축은 요즘 AI 인프라의 뜨거운 주제예요. 모델은 계속 커지는데, 그만큼 프롬프트도 무거워지고 있거든요. 헤드룸은 이 문제를 로컬 계층 하나 추가하는 방식으로 풀어보려는 시도예요.

 

두 가지가 눈에 띄는데요.

 

첫째, 도입 마찰을 줄인 설계.
네 가지 사용 모드를 다 제공한다는 건, 사용자가 어떤 상태에서든 자기 상황에 맞게 끼워 넣을 수 있다는 거예요. 새 프로젝트든, 기존 앱이든, 완제품 CLI든 상관없어요.

 

둘째, Python + Rust 하이브리드.
SDK 접근성과 프록시 성능을 둘 다 잡으려는 실용적인 선택이에요. Parity 테스트로 두 언어 구현의 일관성을 유지하려는 것도 좋은 엔지니어링이에요.

 

물론 조심할 부분도 있어요. 압축은 원래 정보 손실이라 CCR retrieval이 얼마나 잘 동작하느냐가 실사용성을 좌우해요. 프로바이더 어댑터가 여러 개라 실패 모드도 넓어요. 도입 전에 우리 워크로드에서 실제로 통하는지 자체 벤치마크로 확인하는 게 안전해요.

 

궁금하면 저장소 한 번 훑어보세요. README.md의 How it works와 When to use부터 읽고, headroom/compression, headroom/ccr, headroom/cache 순서로 보면 개념이 잘 잡혀요.

 


 

참고:

 

728x90
반응형
728x90
반응형

it 정보들을 이곳저곳에서 수집하며 트랜드를 따라가보고 있는 바쁜 요즘인데요. 

수집중에 RSS 까지 가게 되었어요. RSS를 다시 보게 된 계기는 별거 없어요. 트위터에서 좋은 링크를 자꾸 놓치는 게 지겨워서였어요..
타임라인이 넘어가는 속도는 점점 빨라지는데, 정작 저장해두고 싶은 글은 스크롤 몇 번 만에 사라져 있더라고요.

큐레이션이 사람 손을 타는 순간, 우리는 그 사람의 관심사를 따라가게 돼요. 그게 늘 나쁜 건 아니지만, 매일 마시는 정보가 알고리즘 편식에 물드는 건 좀 문제였어요. 트위터에서 프론트엔드 얘기만 보다 보니, 백엔드 인프라 트렌드나 경제 뉴스 같은 건 몇 달째 감을 못 잡고 있었거든요.

 

RSS(Really Simple Syndication): 웹사이트가 새 글을 발행할 때마다 XML 형식으로 목록을 내보내는 표준. 리더 앱이 이 XML을 주기적으로 읽어서 새 글을 모아 보여준다. 트위터·인스타 같은 소셜 없이도 여러 사이트를 한꺼번에 구독할 수 있음

 

그래서 이노리더(Inoreader)로 RSS 구독을 다시 시작했어요. 그리고 6개월 만에 구독 소스가 40개를 넘겼어요. 프론트엔드 공식 블로그, 백엔드 회사 기술 블로그, AI 뉴스레터, 경제 뉴스, 심지어 좋아하는 애니메이션 리뷰 사이트까지요.

 

여기서 첫 번째 문제가 생겼어요. 이노리더의 폴더 UX가 관리에 최적화돼 있지 않아요. 소스 하나를 다른 폴더로 옮기려면 클릭이 세 번은 필요하고, 이름 오타라도 나면 처음부터 다시 만들어야 해요. 소스가 10개일 때는 괜찮은데, 40개 넘어가면 이게 은근히 부담이에요.

 

그러다 우연히 발견한 게 커스텀 RSS URL을 하나 만들어서 이노리더에 꽂는 방식이에요. 여러 RSS를 서버(또는 정적 파일)에서 하나로 합쳐서 서빙하면, 이노리더 입장에서는 그냥 한 개의 소스로 보여요. 관리는 내 리포지토리에서 하고, 리더에서는 읽기만 하는 거죠.

 

그렇게 만든 게 나만의 rss 피드 tera-rss예요. https://github.com/terajh/tera-rss

 

어떤 도구로 만들까 고민한 것들

 

처음에는 어떤 스택으로 갈지 좀 헤맸어요. 후보를 정리하면 이래요.

 

방식 장점 단점
Node.js + Express 익숙함, 라이브러리 풍부 서버 띄워야 함, 배포 귀찮음
Cloudflare Workers 서버리스, 빠름 파일 시스템 다루기 애매함
Deno + GitHub Pages TS 그대로 실행, 정적 배포, 서버 필요 없음 런타임 파편화는 감수
Python 스크립트 RSS 라이브러리 많음 의존성 관리 피곤

 

결국 Deno + GitHub Pages 조합으로 갔어요. 이유는 3개였습니다.

 

1) TypeScript를 별도 빌드 없이 그대로 실행해요

 

사이드 프로젝트에서 제일 아까운 시간이 tsconfig.json이나 웹팩 설정 만지는 거예요. Deno는 .ts 파일을 그대로 실행해줘요. tsc도 없고 ts-node도 없어요. 그냥 deno run script.ts예요.

 

Deno: Node.js를 만든 라이언 달(Ryan Dahl)이 "Node를 처음부터 다시 만든다면 이렇게 하겠다"며 만든 자바스크립트 런타임. TypeScript를 기본 지원하고, 웹 표준 API(fetch, URL 등)를 그대로 써요. 보안 모델도 다른데, 기본은 파일 시스템·네트워크 접근이 차단돼 있다고 하네요.

 

2) 표준 라이브러리가 웹 표준에 붙어 있어요

 

fetch, TextDecoder, URL 같은 걸 임포트 없이 그냥 써요. Node에서 axios나 node-fetch 붙일 때 느끼는 그 미묘한 어색함이 없어요. 브라우저에서 쓰던 그대로 서버 스크립트에 써요.

 

3) 결과물이 정적 파일이라 서버가 필요 없어요

 

이 프로젝트의 결과물은 RSS XML 파일이에요. 서버가 요청을 받아서 실시간으로 만드는 게 아니라, 미리 만들어둔 XML을 그냥 서빙만 하면 돼요. 이러면 서버 유지비가 0원이에요. 로컬(또는 CI)에서 파일을 뱉어놓고, GitHub Pages가 정적으로 서빙하면 끝이거든요.

 

deno task collect로 XML을 뽑아놓기만 하면, GitHub Actions가 매일 cron으로 굴려주고 Pages에 배포해요. AWS도, Vercel도, DB도 필요 없어요.

 

큰 그림: 2단계 파이프라인

 

tera-rss는 두 단계로 나뉘어요. 모으는 단계(collect)합치는 단계(merge)예요.

 

파이프라인 개요

feed_specs.csv ─┐
                ├─▶ collect ─▶ jsons/*.json ─▶ merge ─▶ all.xml + publishers/*.xml + categories/*.xml
publishers.csv ─┘

 

왜 두 단계로 나눴냐면, 네트워크가 필요한 부분과 필요 없는 부분을 분리하고 싶었어요. collect는 외부 사이트에서 데이터를 가져오니까 실패할 수 있고 시간도 오래 걸려요. merge는 이미 받아둔 파일을 정리만 하니까 빠르고 안정적이에요. 이 두 개를 섞어놓으면 하나가 실패했을 때 재실행 범위가 애매해져요.

 

1단계: collect

 

feeds/{group}/feed_specs.csv를 읽어서, 각 피드 URL을 fetch하고 결과를 JSON으로 저장해요.

 

src/collect.ts (핵심 로직 요약)

const specs = await loadAllFeedSpecs(feedsDir)

for (let i = 0; i < specs.length; i++) {
  const spec = specs[i]
  try {
    const xml = await fetchWithTimeout(spec.url)
    const items = parseFeed(xml, spec)
    await Deno.writeTextFile(
      `rss/${group}/jsons/${spec.publisher.id}-${spec.categories.join('_')}.json`,
      JSON.stringify(items, null, 2)
    )
  } catch (err) {
    console.warn(`skip ${spec.url}:`, err.message)
  }

  await sleep((1 + (i % 5)) * 1000)
}

 

여기서 신경 쓴 게 두 개예요.

 

Fail-soft 전략. 40개 피드 중에 하나가 500 에러를 뱉는다고 전체 파이프라인이 멈추면 안 돼요. 그래서 try/catch로 감싸고 실패한 피드는 로그만 남긴 채 다음으로 넘어가요. 다음 실행 때 부활하면 자연스럽게 다시 잡히니까 큰 문제가 없어요.

 

용어: Fail-soft: 일부가 실패해도 전체가 죽지 않고 계속 돌아가게 만드는 설계예요. 반대말이 fail-fast(하나라도 문제 있으면 즉시 중단)인데, 사이드 프로젝트 파이프라인에서는 fail-soft가 훨씬 편해요. 새벽 3시에 알림이 안 와야 잠을 자거든요.

 

Throttle(요청 간격 조절). sleep((1 + (i % 5)) * 1000)으로 1~5초 사이 랜덤한 간격을 둬요. 순차로 요청을 보내긴 하지만 같은 호스트에 몰리는 걸 조금 완화하려는 목적이에요. RSS 피드를 자주 두들기면 IP 차단당하는 경우가 있어서 예방책이에요.

 

2단계: merge

 

수집된 JSON 파일들을 다 읽어서 하나로 합치고 정렬한 다음 XML로 다시 뱉어요.

 

src/merge.ts (핵심 로직 요약)

const items = await loadAllJsons(`rss/${group}/jsons`)

const sorted = items.toSorted(
  (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
)

// 1) 전체 병합본
await Deno.writeTextFile(`rss/${group}/all.xml`, buildRssXml(sorted))

// 2) 발행사별 XML
const byPublisher = groupBy(sorted, (item) => item.spec.publisher.id)
for (const [id, items] of Object.entries(byPublisher)) {
  await Deno.writeTextFile(`rss/${group}/publishers/${id}.xml`, buildRssXml(items))
}

// 3) 카테고리별 XML
const byCategory = groupByCategory(sorted)
for (const [cat, items] of Object.entries(byCategory)) {
  await Deno.writeTextFile(`rss/${group}/categories/${cat}.xml`, buildRssXml(items))
}

 

세 가지 종류의 XML이 나와요. all.xml(전체), publishers/*.xml(발행사별), categories/*.xml(카테고리별)이에요. 이노리더에서는 이 중에 필요한 것만 골라서 구독하면 돼요.

 

예를 들어 프론트엔드 카테고리만 보고 싶으면 categories/frontend.xml만 구독하면 돼요. 특정 회사의 글만 모아 보고 싶으면 publishers/vercel.xml을 구독하는 식이에요.

 

_all_이라는 특수 카테고리에는 트릭이 하나 있어요. 이 카테고리가 붙은 피드는 카테고리별 XML에는 안 들어가고 all.xml에만 포함돼요. 발행사가 하나뿐이라 굳이 카테고리로 나눌 필요가 없는 소스에 유용해요.

 

데이터 모델은 딱 세 개예요

 

src/parser.ts 맨 위에 정의된 타입 세 개가 이 프로젝트의 뼈대예요. 이거만 이해하면 전체 흐름이 다 보여요.

 

src/parser.ts

type Publisher = {
  readonly id: string       // 파일명·URL에 쓰이는 슬러그 (예: "nextjs")
  readonly name: string     // 사람이 읽는 이름 (예: "Next.js Blog")
  readonly url: string      // 홈페이지 URL (피드 URL 아님)
}

type FeedSpec = {
  readonly publisher: Publisher
  readonly title: string
  readonly categories: readonly string[]
  readonly url: string      // RSS/Atom 피드 URL
}

type FeedItem = {
  readonly spec: FeedSpec
  readonly title: string
  readonly partialText: string
  readonly date: string
  readonly url: string
}

 

전부 readonly예요. 파이프라인 중간에 어떤 코드가 값을 바꾸려고 하면 타입 검사에서 걸려요. 사이드 프로젝트일수록 이런 제약을 걸어둬야 나중에 밤 12시에 이상한 버그로 디버깅 안 해요. "여기서 왜 값이 바뀌지?" 하는 순간이 제일 무서워요.

 

용어: readonly / 불변성(immutability): 한번 만들어진 객체를 절대 수정하지 않는 원칙이에요. 값을 바꿔야 할 때는 새 객체를 만들어요. 이렇게 하면 "이 객체가 어디서 언제 바뀐 거야?"를 추적할 필요가 없어져요. 함수형 프로그래밍에서 자주 강조되는 개념이에요.

 

CSV 스키마: 심플한 게 최고예요

 

피드 목록을 어떻게 저장할지도 고민이었어요. JSON? YAML? DB? 결국 CSV로 갔어요. 이유는 하나예요. VS Code나 스프레드시트로 편집하기 제일 편하거든요.

 

feeds/dev/publishers.csv

id,name,url
nextjs,Next.js Blog,https://nextjs.org
vercel,Vercel Blog,https://vercel.com/blog

 

feeds/dev/feed_specs.csv

publisher,title,categories,url
Next.js Blog,Next.js 공식 블로그,frontend,https://nextjs.org/feed.xml
Vercel Blog,Vercel 엔지니어링,frontend|devops,https://vercel.com/blog/rss.xml

 

두 파일을 나눈 이유는 관심사의 분리 때문이에요. publishers.csv는 "누가 발행하는가", feed_specs.csv는 "어떤 피드를 어떤 카테고리로 볼 것인가"예요. 한 발행사가 여러 피드를 낼 수 있으니까요. 예를 들어 Vercel은 엔지니어링 블로그, 프로덕트 업데이트, 채용 뉴스가 각각 다른 RSS를 낼 수도 있어요.

 

여기서 조인 조건이 조금 특이해요. feed_specs.csvpublisher 컬럼이 publishers.csvname과 정확히 일치해야 해요. id로 조인하는 게 아니라 name으로요.

 

왜 이렇게 했냐면요. CSV를 직접 편집할 때, id(예: nextjs)보다 name(예: Next.js Blog)이 사람 눈에 훨씬 잘 들어와요. 오타 나면 스킵되는 게 위험할 수도 있는데, fail-soft라 파이프라인이 죽진 않아요. 대신 로그에 skip ...이라고 뜨니까 눈에 띄어요.

 

categories 컬럼은 |(파이프)로 여러 개를 구분해요. frontend|devops처럼 쓰면 두 카테고리에 다 포함돼요.

 

3단계 청크 관리: active / slow / archive

 

피드를 40개 넘게 구독하고 있으면 슬슬 문제가 생기기 시작해요. 몇몇은 3개월째 새 글이 없고, 몇몇은 아예 사이트가 죽어서 404가 나와요. 이런 걸 매번 collect할 때마다 fetch하니까 시간이 아깝고 로그도 지저분해져요.

 

그래서 청크를 세 개로 나눴어요.

 

feeds/dev/ 디렉터리 구조

feeds/dev/
├── publishers.csv
├── feed_specs.csv          # active — 90일 이내 새 글이 있는 피드
├── feed_specs_slow.csv     # slow — 90~365일 사이에 새 글이 있는 피드
├── feed_specs_archive.csv  # archive — 1년 넘게 조용한 피드 (수집 제외)
└── classification.json     # 자동 분류 스냅샷 (gitignore)

 

파일명이 feed_specs로 시작하면 자동으로 로드되고 archive만 예외예요. 즉 feed_specs.csv(active)와 feed_specs_slow.csv(slow)만 매일 수집돼요.

 

왜 3단계로 나눴냐면요. 처음에는 그냥 활성/비활성 2단계로 했어요. 근데 그렇게 하니까 애매한 피드들이 생겼어요. "3개월째 조용하긴 한데, 예전엔 좋은 글을 냈고 앞으로 다시 쓸 수도 있을 것 같은" 피드들이요. 완전히 버리기엔 아쉽고 매일 fetch하기엔 시간이 아까웠어요.

 

그래서 중간에 slow를 넣었어요. Slow는 여전히 수집은 하지만 "이 피드는 언제 새 글이 올지 모른다"는 표시예요. Archive로 완전히 밀어내기 전의 유예 기간 같은 거예요.

 

분류는 자동으로 해요.

 

분류 자동화 커맨드

deno task classify:dev  # 각 피드 fetch → 최신 pubDate로 active/slow/archive 판정
deno task split dev     # 분류 결과대로 CSV 파일을 3개로 재구성

 

classify.ts는 10개씩 병렬로 fetch해요. Collect는 throttle 때문에 5분 걸리지만 classify는 1분이면 끝나요. 목적이 다르니까 동시성도 다르게 잡았어요. Collect는 결과 파일을 만드는 게 목적이니까 안정성이 중요하고 classify는 그냥 상태만 확인하는 거니까 속도가 우선이에요.

 

한 가지 안전 장치를 넣어뒀어요. 매치 안 되는 행은 무조건 active로 유지해요. 새로 추가한 피드가 실수로 archive로 밀려나는 걸 방지하려고요. 신규 피드는 아직 데이터가 부족하니까 판정할 수 없거든요. 그래서 "일단 active로 두고, 다음 classify 때 다시 판정한다"는 규칙이에요.

 

split 실행 후에는 반드시 git diff feeds/dev/를 확인해요. Archive로 옮겨진 피드가 진짜 죽은 건지, classifier가 잠깐 타임아웃 낸 건지 눈으로 확인해요. 자동화도 좋지만 마지막 커밋은 사람이 봐야 안심이에요.

 

그룹은 폴더만 추가하면 끝이에요

 

feeds/{new-group}/ 폴더에 CSV 두 개만 놓으면 코드 수정 없이 동작해요. 이게 이 프로젝트에서 가장 마음에 드는 부분이에요.

 

mkdir -p feeds/tech-news

 

feeds/tech-news/publishers.csv

id,name,url
techcrunch,TechCrunch,https://techcrunch.com
theverge,The Verge,https://www.theverge.com

 

feeds/tech-news/feed_specs.csv

publisher,title,categories,url
TechCrunch,전체,_all_,https://techcrunch.com/feed/
The Verge,전체,_all_,https://www.theverge.com/rss/index.xml

 

이제 deno task update를 돌리면 discoverGroups 함수가 feeds/ 아래를 순회하며 새 그룹을 자동으로 잡아요. 기존 코드는 한 줄도 안 건드렸어요. Convention over Configuration 사고방식이에요.

 

용어: Convention over Configuration: "설정보다 관례"라는 뜻이에요. 규칙을 정해두면 설정 파일 없이도 프레임워크가 알아서 동작해요. Next.js의 pages/ 폴더가 자동으로 라우팅되는 것도 같은 원리예요. tera-rss에서는 feeds/ 아래 폴더 이름이 곧 그룹 이름이에요.

 

GitHub Actions로 매일 자동 배포

 

로컬에서 매일 커맨드를 치는 건 금방 지쳐요. 그래서 GitHub Actions에 맡겨요.

 

.github/workflows/update-feeds.yaml

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 0 * * *'   # 매일 00:00 UTC (한국 시간 오전 9시)
  workflow_dispatch:      # 웹 UI에서 수동 실행도 가능

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: denoland/setup-deno@v1
      - run: deno task collect
      - run: deno task merge
      - uses: actions/upload-pages-artifact@v3
        with:
          path: ./rss
      - uses: actions/deploy-pages@v4

 

매일 00:00 UTC에 cron이 돌아요. 한국 시간으로는 오전 9시쯤이에요. rss/ 디렉터리를 통째로 GitHub Pages에 올려요.

 

용어: Cron: 유닉스 계열 시스템에서 "언제 실행할 것인가"를 표현하는 문법이에요. 0 0 * * *은 "매일 자정 0분 0시"라는 뜻이에요. 다섯 자리가 각각 분/시/일/월/요일을 의미해요.

 

배포된 URL은 이런 식이에요.

 

  • https://terajh.github.io/tera-rss/dev/all.xml
  • https://terajh.github.io/tera-rss/dev/categories/frontend.xml
  • https://terajh.github.io/tera-rss/anime/publishers/ign.xml

 

이노리더에는 저 URL을 그냥 구독으로 등록하면 돼요. 카테고리 단위로도, 발행사 단위로도, 전체로도 원하는 대로 구독할 수 있어요.

 

한 가지 주의할 게 있어요. rss/ 폴더는 gitignore에 넣어놨어요. 로컬에서 실수로 커밋하면 안 돼요. 소스 오브 트루스(원본)는 항상 CSV고 rss/ 아래 파일들은 매번 새로 생성되는 파생물이에요. 이걸 리포에 넣기 시작하면 diff가 지저분해지고 어떤 게 사람이 편집한 건지 CI가 만든 건지 헷갈려요.

 

GitHub Actions 실행 결과 화면

 

파서 짜면서 조심한 것들

 

RSS 표준이 하나가 아니에요. RSS 2.0, Atom 1.0, 심지어 커스텀 XML까지 섞여 있어요. 옛날 블로그일수록 규격을 반쯤 자기 마음대로 해석한 게 많아요. 그래서 파서 짤 때 두 가지를 특히 신경 썼어요.

 

1) 날짜 파싱은 절대 예외를 던지지 않아요

 

RSS에서 날짜는 RFC 822(예: Wed, 02 Oct 2002 08:00:00 EST)를 쓰기도 하고, ISO 8601(예: 2026-07-26T09:00:00Z)을 쓰기도 하고, 그냥 2026년 7월 26일 같은 한글을 쓰는 곳도 있어요. 이걸 다 파싱하려면 fuzzy parser가 필요해요.

 

parseFuzzyDate 함수는 여러 형식을 시도해보고 다 실패하면 현재 시각을 반환해요. 예외를 던지지 않아요. 하나의 이상한 피드 때문에 전체 파이프라인이 죽는 게 훨씬 나쁘거든요. 대신 로그에 "날짜 파싱 실패, 현재 시각으로 대체" 하고 남겨요.

 

2) XML 인코딩은 preamble에서 자동 탐지해요

 

이게 은근히 까다로운 부분이에요. 한국 사이트 중에 아직 EUC-KR로 서빙하는 곳이 있어요. XML의 첫 줄(preamble)에 <?xml version="1.0" encoding="euc-kr"?>가 있는데, 만약 그냥 UTF-8로 디코드하면 한글이 다 깨져요.

 

용어: EUC-KR: 유니코드 이전 시대에 한국어를 표현하려고 만든 문자 인코딩이에요. UTF-8이 국제 표준이 된 지금도, 오래된 국내 사이트는 여전히 EUC-KR로 서빙하는 경우가 있어요. 브라우저는 자동으로 감지해주지만, 스크립트에서 직접 디코드할 때는 명시적으로 지정해야 해요.

 

그래서 이런 코드가 필요해요.

 

src/parser.ts (인코딩 자동 탐지)

function decodeXml(bytes: Uint8Array): string {
  // 앞 200바이트만 ascii로 훑어서 encoding 선언을 찾아요
  const head = new TextDecoder('ascii').decode(bytes.slice(0, 200))
  const match = head.match(/encoding=["']([^"']+)["']/i)
  const encoding = match?.[1]?.toLowerCase() ?? 'utf-8'
  return new TextDecoder(encoding).decode(bytes)
}

 

두 단계로 디코드해요. 먼저 앞부분만 ASCII로 슬쩍 봐서 encoding 선언을 찾아요. 그 다음에 진짜 인코딩으로 전체를 다시 디코드해요. UTF-8을 무작정 가정하면 한 달에 한 번씩 "어떤 피드가 깨져 있어요" 이슈가 올라와요. 자동 탐지가 훨씬 편해요.

 

반년 써보고 느낀 점

 

만든 지 반년쯤 됐어요. 지금은 dev, economy, anime, AI 네 개 그룹, 총 60개 넘는 피드가 매일 자동으로 굴러가요.

 

좋은 점. 소스 관리가 git이에요. 새 피드를 추가하고 싶으면 CSV에 한 줄을 넣고 커밋하면 끝이에요. 오래된 피드를 정리하고 싶으면 classify:dev 한 번 돌리면 자동으로 분류돼요. 이노리더에서는 어떻게 관리하든 결국 클릭 노가다인데, 여기서는 텍스트 편집이에요.

 

아쉬운 점. Deno 생태계가 아직 얇아요. XML 파서 라이브러리를 몇 개 찾아봤는데 마땅한 게 없어서 결국 정규식으로 반쯤 직접 짰어요. 라이브러리 하나 추가하려고 URL 임포트에서 버전 고민하는 시간이 npm보다 오래 걸릴 때도 있었어요.

 

의외로 잘한 점. 처음부터 파일 시스템을 결과물로 두고 서버를 안 만든 거예요. 만약 서버를 만들었다면 지금쯤 monit(프로세스 모니터링)이나 Sentry 붙이고 있었을 거예요. 사이드 프로젝트에서 그런 관리를 하기 시작하면 재미가 급격히 떨어져요.

 

놀랐던 점. 이노리더에서는 정말 "그냥 하나의 소스"로 보여요. 이노리더 안에서는 40개 소스가 아니라 4개(그룹 개수) 또는 카테고리 개수만큼만 보이니까요. 정신 위생에 도움이 돼요.

 

마무리

 

RSS는 죽었다는 말이 자주 나와요. 소셜 미디어가 대체했다고요. 근데 개인용으로는 아직 최고의 도구라고 생각합니다.
알고리즘이 아닌 내가 고른 소스만 볼 수 있기도 하고, 광고도 없고, 로그인이 없구요.

그리고 이런 도구를 직접 만드는 게 결국 내가 제일 원하는 피드만 뽑아볼 수 있는 것 같아요.

 

tera-rss는 앞으로 이런 걸 더 붙여볼 생각이에요.

 

  • 요약 파이프라인 — fetch → LLM 요약 → 요약본 XML로 배포. 아침에 30초 스크롤로 하루치 뉴스를 훑을 수 있게요.
  • 카테고리 자동 분류 — title/description을 임베딩으로 벡터화해서 카테고리를 예측. 지금은 사람이 CSV에 손으로 적어요.
  • Slack DM 봇 — 특정 키워드가 걸리면 Slack DM으로 알림. 좋아하는 프레임워크의 새 릴리즈 같은 것만 골라 받고 싶어요.

 

이런 자잘한 도구를 만들 때는 결과물이 파일이면 서버가 필요 없어요. GitHub Actions + Pages 조합은 개인용 툴로는 반칙 수준이에요. 서버 비용도, 배포 파이프라인도, 도메인도 없어요. 그냥 리포지토리 하나면 끝이거든요.

 

혹시 비슷한 필요를 느끼셨다면, 며칠 잡고 자기만의 RSS 허브를 만들어보시는 걸 권해요. 생각보다 훨씬 빨리 완성돼요.

 


 

참고:

 

728x90
반응형
728x90
반응형
AI 개발 Claude Code 에이전트 개발 방법론 하네스 엔지니어링

AI 코딩 에이전트가 실수를 하지 않게 만드는 핵심은 모델 자체의 지능만이 아니다.
오히려 같은 실수가 반복되지 않도록 구조적으로 막아주는 시스템 설계가 더 중요할 수 있다.

이런 관점에서 등장한 개념이 하네스 엔지니어링(Harness Engineering)이다.

하네스 엔지니어링은 AI에게 "잘해봐"라고 부탁하는 것이 아니라,
실수를 구조적으로 반복할 수 없게 만드는 환경을 설계하는 일이다.
Stop fighting the chatbot. Start engineering the harness.

모델과 싸우며 프롬프트를 끝없이 다듬는 대신, 에이전트가 잘 일할 수밖에 없는 환경 자체를 설계하라는 뜻이다.

 


1. 하네스 엔지니어링이란?

하네스 엔지니어링은 AI 코딩 에이전트의 반복 실수를 시스템적으로 방지하는 설계 방법론이다.

핵심은 단순하다.

  • 모델이 실수했다
  • 같은 실수가 또 나오지 않게 구조를 바꾼다
  • 그 구조가 다음 작업부터 자동으로 실수를 막아준다

즉, 프롬프트를

계속 잘 쓰는 것이 아니라,
AI가 올바르게 동작할 수밖에 없는 환경을 만드는 것이 하네스 엔지니어링이다.

결국 개발자의 역할도 바뀐다.

  • 예전: 코드를 직접 잘 짜는 사람
  • 지금: AI가 코드를 잘 짜도록 시스템을 설계하는 사람

2. 하네스(Harness)라는 말의 의미

하네스는 원래 말에 씌우는 마구를 뜻한다.

야생말은 힘이 세지만, 방향 없이 뛰면 위험하다. 반대로 마구를 채운 말은 인간의 의도에 따라 정확한 트랙 안에서 더 빠르고 안정적으로 달릴 수 있다.

AI 모델도 비슷하다. Claude, GPT, Gemini 같은 모델은 강력하지만 방향과 제약이 없으면 엉뚱한 행동을 할 수 있다. 하네스는 이 힘을 억누르는 장치가 아니라, 올바른 방향으로 집중시키는 구조다.

그래서 다음과 같은 것들이 전부 하네스가 될 수 있다.

  • 프로젝트 규칙 문서
  • Claude.md, AGENTS.md 같은 컨텍스트 파일
  • MCP, 스킬, 훅(hook)
  • 자동 테스트, 린트, 프리커밋 검사
  • 권한 제한

모델이 아닌 것, 그리고 모델을 둘러싸고 동작을 제어하는 거의 모든 것이 하네스다.


3. 우리는 이미 하네스를 쓰고 있다

AI 모델은 원래 텍스트를 입력받아 텍스트를 출력하는 함수에 가깝다. 혼자서는 대화를 기억하지도 못하고, 인터넷에 접속하지도 못하며, 파일을 직접 읽거나 저장하지도 못한다.

우리가 채팅창에서 "이 AI는 맥락을 기억한다"고 느끼는 이유는, 하네스가 이전 대화를 계속 모아 매번 다시 모델에게 전달해주기 때문이다.

  • 대화 내역을 이어붙이는 루프
  • 현재 프로젝트 파일을 읽게 해주는 구조
  • 외부 도구를 붙여주는 인터페이스

이 모든 것이 이미 하네스다.


4. 왜 하네스 엔지니어링이 중요해졌을까?

AI 활용 방식은 대략 아래 순서로 발전해왔다.

4.1. 프롬프트 엔지니어링

처음에는 "AI에게 명령을 잘 내리는 법"이 중요했다. 하지만 프롬프트만으로는 한계가 있었다. AI는 프로젝트 상황이나 코드 스타일을 모르기 때문이다.

4.2. 컨텍스트 엔지니어링

그래서 프롬프트에 더해 프로젝트 구조, 기존 코드, 팀 규칙 같은 배경 정보를 넣기 시작했다. "말을 잘 거는 것"과 "상황을 잘 알려주는 것", 이 둘이 함께 중요해졌다.

4.3. MCP와 스킬

그 다음에는 MCP와 스킬이 붙었다. 이 단계에서 AI는 꽤 유능해졌지만, 도구가 너무 많아지고 규칙이 과도해지면서 오히려 헷갈리는 문제가 생겼다.

"도구를 계속 얹는 것보다, 정확하게 동작할 수 있는 환경 자체를 설계하자"
→ 이게 하네스 엔지니어링이다.

4.4. 프롬프트, 컨텍스트, 하네스는 동심원 관계다

이 셋은 서로 대체 관계가 아니라 동심원 관계에 가깝다.

  • 가장 안쪽: 프롬프트
  • 그 바깥: 컨텍스트
  • 가장 바깥: 하네스

하네스는 프롬프트와 컨텍스트가 더 잘 작동하도록 무엇을 허용하고, 무엇을 막고, 무엇을 강제할지를 정하는 바깥 구조다.


5. 하네스가 필요한 이유

5.1. 컨텍스트 부패

작업이 길어질수록 컨텍스트가 가득 차고, 앞에서 했던 중요한 내용을 조금씩 잊기 시작한다. 대표적인 실패 패턴은 이렇다.

  • 모든 걸 한 번에 하려다가 절반만 구현하고 끝남
  • 아직 할 일이 남았는데 "다 끝났다"고 착각함
  • 다음 세션에서 이전 작업을 기억하지 못해 다시 파악함

5.2. 규칙과 울타리 부재

AI는 정보를 다 알고 있어도 엉뚱한 행동을 할 수 있다. 결제 시스템 수정하라고 했는데 테이블을 지운다거나, 테스트를 안 돌리고 완료 선언을 한다거나. 이건 지식 부족 문제가 아니라 하지 말아야 할 행동을 강제하는 구조가 없기 때문이다.

5.3. 통제되지 않은 에이전트가 만드는 사고 유형

  • AI slop: 그럴듯하지만 품질이 낮고 일관성 없는 코드/문서가 쌓인다
  • 치명적 명령 실행: 잘못된 파일 삭제, DB 초기화 등
  • doom loop: 같은 작업을 반복 호출하며 빠져나오지 못한다
  • shadow agent: 누가 어떤 권한으로 에이전트를 돌리는지 조직이 알지 못하는 상태

6. 하네스는 이 문제를 어떻게 해결하는가?

6.1. 컨텍스트 파일로 기억 회복

Claude.md, AGENTS.md, 프로젝트 온보딩 문서 같은 파일은 새 세션이 시작될 때마다 다시 읽히는 기준 문서 역할을 한다. 새로 출근한 AI가 반드시 읽어야 하는 온보딩 문서라고 보면 된다.

6.2. 훅과 자동 검사로 규칙 강제

훅(hook)은 AI가 작업을 마무리하려는 순간 자동으로 실행되는 검증 장치다.

  • 저장 직전에 타입체크
  • 커밋 전에 린트
  • PR 생성 전에 테스트 실행
  • 위험한 명령어 차단
프롬프트는 부탁, 하네스는 강제.
이 차이가 핵심이다. 틀린 코드는 통과 자체가 안 되게 만드는 것이 목표다.

7. 하네스 엔지니어링의 핵심 철학

에이전트가 실수할 때마다, 그 실수가 다시는 반복되지 않도록 구조를 고친다.

좋은 하네스는 "AI가 잘하길 기대하는 시스템"이 아니라, AI가 틀리기 어려운 시스템이다.

공장 안전장치와 비슷하다. 안전모를 안 쓰면 출입문이 열리지 않는다. 사람의 판단에 맡기는 게 아니라, 시스템 안에 제약이 내장되어 있어 자동으로 강제된다.


8. 하네스의 구성 요소

8.1. 컨텍스트 파일

AI가 작업 전에 읽는 기준 문서다. 중요한 점은 모든 걸 설명하는 백과사전이 아니라 지도여야 한다는 것이다.

  • 항상 적용되는 규칙만 적는다
  • 세부 내용은 다른 문서로 분리한다
  • 실패가 생길 때마다 점진적으로 보강한다

8.2. 자동 강제 시스템

린트, 타입체크, 테스트, 프리커밋 훅, 자동 수정 루프. 여기서 중요한 원칙은 다음과 같다.

성공은 조용히, 실패만 시끄럽게.

8.3. 가비지 컬렉션

기존 코드베이스에 나쁜 패턴이 있으면 AI는 그걸 그대로 학습해 반복한다. 실제 코드와 문서의 불일치, 더 이상 쓰지 않는 코드, 규칙 위반 패턴을 주기적으로 청소해야 한다. 하네스는 한 번 만들어두고 끝나는 게 아니라, 실패를 먹고 계속 진화하는 구조다.

8.4. 상태 관리

에이전트가 작업을 이어갈 수 있도록 상태를 어디에 둘지도 설계해야 한다. 진행 로그 파일, 작업 체크리스트, 세션 간 공유 메모, Git 히스토리 등이 에이전트의 외부 기억 장치가 된다.

좋은 하네스는 상태를 모델 안이 아니라 모델 바깥의 영속 계층에 둔다.

8.5. 관측 가능성

에이전트가 왜 틀렸는지 모르면 하네스를 개선할 수 없다. 어떤 도구를 호출했는지, 어떤 규칙에 걸렸는지, 어느 테스트에서 실패했는지를 추적할 수 있어야 한다. 관측 가능성은 디버깅 편의가 아니라 하네스를 계속 진화시키기 위한 센서에 가깝다.


9. 하네스 엔지니어링은 모델보다 중요한가?

강한 모델은 여전히 중요하다. 하지만 실무에서 결과를 좌우하는 건 모델만이 아니다.

모델이 조금 덜 좋아도 컨텍스트가 잘 정리되어 있고, 도구 경계가 명확하고, 실패 시 자동 피드백 루프가 돌고, 위험한 행동이 구조적으로 막혀 있으면 훨씬 안정적으로 일한다.

반대로 모델이 아주 좋아도 규칙이 없고, 컨텍스트가 더럽고, 검증 장치가 없으면 엉뚱한 실수를 반복할 수 있다.

9.1. 도구를 줄이면 오히려 정확도가 오를 수 있다

도구가 너무 많아지면 어떤 도구를 써야 할지 망설이게 되고, 도구 설명 자체가 컨텍스트를 잡아먹는다. "무엇을 더 줄까"보다 무엇을 빼서 선택지를 줄일까가 더 중요할 수 있다.

9.2. 기억은 컨텍스트 창 밖에 두는 편이 낫다

기억을 모델 안에 넣지 말고, 바깥으로 빼라.

todo.md, progress.md, 설계 결정 로그, 체크포인트 파일 같은 것들이 에이전트의 외부 기억 장치가 된다.

9.3. 결정론과 확률론을 분리해야 한다

결정론적 (기계에게) 확률론적 (AI에게)
린터, 타입체커, 빌드, 테스트 리뷰, 설계 판단, 대안 탐색

기계 검증은 빠르게 자동 수행하고, AI는 그 결과를 해석하고 수정 전략을 세운다. 이렇게 해야 품질을 부탁하는 것이 아니라 품질을 구조적으로 강제하는 시스템이 된다.

9.4. 좋은 팀들은 비슷한 구조로 수렴한다

서로 다른 회사와 팀이 결국 비슷한 구조로 수렴한다. 격리된 실행 환경 → 제한된 도구 세트 → 기계적 린트/테스트 게이트 → AI 생성 및 수정 루프 → E2E 검증 → 인간 검토. 이건 유행이라기보다, 프로덕션 환경이 요구하는 필연에 가깝다.


10. 지금 당장 어떻게 활용할 수 있을까?

당장 거창하게 시작할 필요는 없다.

레벨 1
  • 프로젝트 규칙 .md 파일 만들기
  • 저장/커밋 전 자동 검사 훅 붙이기
레벨 2
  • 에이전트 실수를 기록하기
  • 같은 실패는 규칙/훅으로 막기
레벨 3
  • 도구 접근 범위 제한하기
  • 작업 단위 스킬 문서 만들기
  • 문서↔코드 불일치 주기 점검

중요한 건 처음부터 완벽하게 설계하려 하지 않는 것이다. 실제로 일하게 해보고, 실패가 생길 때마다 그 실패를 구조의 개선 포인트로 삼는 방식이 현실적이다.


11. 앞으로 개발자의 역할은 어떻게 바뀔까?

11.1. 오래된 원칙은 사라지지 않는다

SOLID 원칙 하네스 엔지니어링 관점
SRP 한 에이전트에게 한 가지 책임만 맡긴다
OCP 규칙과 도구는 플러그인처럼 확장 가능하게
ISP 필요 없는 도구를 과하게 노출하지 않는다
DIP 계약과 인터페이스 위에서 에이전트가 일하게

11.2. 코더에서 지휘자로

AI 시대의 개발자는 코드를 직접 쓰는 사람에서, AI가 올바르게 코드를 쓰게 만드는 환경을 설계하는 사람으로 이동하고 있다.

  • 예전: 직접 공을 차는 선수
  • 지금: 전술을 설계하고 팀을 운영하는 감독

더 덜 기술적이 되는 것이 아니라 오히려 더 높은 차원의 기술적 판단이 요구된다.

11.3. 하네스는 영구 구조물이 아닐 수도 있다

모델이 충분히 좋아지면 어떤 규칙은 모델 내부로 흡수되고, 어떤 제약은 덜 필요해지는 시점이 온다. 하네스는 때로 언젠가 제거하기 위해 잠시 세우는 비계(scaffolding)에 가깝다.


정리

하네스 엔지니어링은 AI를 더 똑똑하게 만드는 기술이 아니다. AI가 덜 틀리게, 그리고 같은 실수를 반복하지 않게 만드는 기술이다.

핵심 세 가지
  • 컨텍스트 파일로 기억을 회복시킨다
  • 훅과 자동 검증으로 규칙을 강제한다
  • 실패가 생길 때마다 구조를 고친다

결국 AI 코딩 에이전트가 기대만큼 동작하지 않을 때, 모델만 탓할 것이 아니라 먼저 이걸 봐야 한다.

  • 기준 문서가 있는가?
  • 도구 경계가 명확한가?
  • 피드백 루프가 자동화되어 있는가?
  • 같은 실수를 막는 구조가 있는가?

좋은 하네스는 AI를 통제하는 장치가 아니라, AI의 힘을 올바른 방향으로 최대한 활용하게 해주는 시스템이다.

728x90
반응형
728x90
반응형

CSS :near() — 마우스가 '가까이' 오면 반응하는 새로운 의사 클래스

:hover는 요소 위에 마우스가 정확히 올라가야 반응한다. 1px이라도 벗어나면 끝이다. 하지만 실제 사용자 경험을 생각해보면, "근처에 왔을 때" 미리 반응하는 게 더 자연스러운 경우가 많다. 툴팁이 마우스가 다가오면 슬쩍 나타나거나, 숨겨진 버튼이 포인터가 근처에 오면 드러나는 식이다. CSS Working Group에 제안된 :near()는 이 "근접성"을 CSS에서 직접 다룰 수 있게 하는 의사 클래스다.

 

:near()가 뭔가

 

:near()는 포인터가 요소로부터 지정한 거리 이내에 있을 때 스타일을 적용하는 CSS 의사 클래스다. 핵심은 유클리드 거리(직선 거리) 계산이다. 비유하면 :hover가 "문을 열어야 불이 켜지는 센서"라면, :near()는 "복도를 걸어오는 것만으로 불이 켜지는 모션 센서"다.

 

near-basic.css

button:near(3rem) {
  opacity: 1;
  transform: scale(1);
}

button:not(:near(3rem)) {
  opacity: 0.3;
  transform: scale(0.9);
}

 

소괄호 안의 <length> 값이 활성화 반경이다. 3rem이면 요소 경계에서 3rem 이내에 포인터가 들어왔을 때 매칭된다.

 

[💡 잠깐! 이 용어는?] 유클리드 거리(Euclidean Distance): 두 점 사이의 직선 거리를 뜻한다. :near()는 포인터 좌표와 요소의 가장 가까운 경계 사이의 직선 거리를 계산해서 지정한 반경과 비교한다.

 


 

기존 선택자와 뭐가 다른가

 

선택자 트리거 조건 반경 설정 용도
:hover 포인터가 요소 위에 있을 때 불가능 (요소 경계 고정) 기본 상호작용
:focus 키보드/탭으로 포커스 해당 없음 접근성
:has() 자식/형제 상태 기반 해당 없음 구조적 선택
:near() 포인터가 근처에 있을 때 사용자 지정 가능 근접 기반 UI

 

중요한 설계 원칙이 하나 있다. :near():hover:focus-visible암시하지 않는다. "가까이 왔다"와 "위에 올렸다"는 다른 상태이고, 이 둘을 혼동하면 사용자가 클릭 가능한 상태와 단순 시각 피드백을 구분할 수 없게 된다.

 


 

어디에 쓸 수 있나

 

조건부 가시성

 

이미지 위에 공유 버튼이 있는데, 평소에는 숨겨두고 마우스가 근처에 오면 드러나는 패턴이다. 기존에는 부모 요소에 :hover를 걸어야 했는데, 부모 영역이 넓으면 의도치 않게 버튼이 나타나는 문제가 있었다. :near()는 버튼 자체에 반경을 설정하므로 더 정밀하게 제어할 수 있다.

 

conditional-visibility.css

.share-button {
  content-visibility: hidden;
  contain-intrinsic-size: auto none;
}

.share-button:where(:near(3rem), :hover, :focus-visible) {
  content-visibility: visible;
  background: black;
  color: white;
}

 

contain-intrinsic-size: auto none이 핵심이다. 버튼이 숨겨져 있어도 레이아웃 공간은 유지되므로 포인터가 그 영역에 접근할 수 있다. 비유하면 투명 망토를 쓴 사람이 여전히 자리를 차지하고 있는 것과 같다.

 

[💡 잠깐! 이 용어는?] content-visibility: 요소의 렌더링을 지연하거나 숨기는 CSS 속성이다. hidden으로 설정하면 display: none과 달리 레이아웃 공간은 유지하면서 렌더링만 건너뛴다.

 

프리페치 트리거

 

링크에 마우스가 가까이 오면 해당 페이지를 미리 로드하는 용도다. Speculation Rules API와 결합하면 :near()가 프리페치/프리렌더의 트리거 역할을 할 수 있다. 사용자가 실제로 클릭하기 전에 페이지를 준비해두는 셈이다.

 

툴팁·힌트 조기 표시

 

:hover로 툴팁을 띄우면 정확히 요소 위에 올려야 나타난다. :near()를 쓰면 포인터가 접근하는 순간 툴팁이 먼저 페이드인되어 체감 반응 시간이 줄어든다.

 


 

접근성 고려 사항

 

:near()의 가장 큰 우려는 거짓 어포던스(false affordance) 문제다. 시각적으로 활성화된 것처럼 보이지만 실제로는 클릭 가능한 상태가 아닐 수 있다. WCAG 2.4.7(Focus Visible)과의 관계도 명확하지 않다.

 

우려 사항 설명
거짓 어포던스 근접 시 시각 변화가 "클릭 가능"으로 오인될 수 있음
타겟 크기 모호성 :near() 반경이 WCAG 2.5.8(Target Size) 계산에 포함되는지 불명확
남용 가능성 히트맵 수집, 광고 트래킹에 악용될 소지
열악한 UX 은폐 좋지 않은 레이아웃을 :near()로 가리려는 시도 가능

 

CSS-Tricks 원문에서도 강조하는 원칙은 이것이다. :near()선제적(preemptive) 힌트여야 하지, 추정적(presumptive) 상태 표시여서는 안 된다.

 


 

지금 쓸 수 있나

 

결론부터 말하면, 아직 쓸 수 없다. :near()는 W3C CSS Working Group에 제안(GitHub issue #13271)된 단계이며, 어떤 브라우저에서도 구현되지 않았다. CSS-Tricks 원문의 데모도 패딩과 :hover를 이용한 시뮬레이션이다.

 

하지만 제안 자체가 의미 있는 이유가 있다. 현재 이 효과를 구현하려면 pointermove 이벤트 리스너로 매 프레임 좌표를 계산해야 하는데, 성능 비용이 크고 구현도 복잡하다. 브라우저 네이티브로 이 계산을 처리하면 메인 스레드 부담 없이 더 효율적으로 동작할 수 있다.

 

current-workaround.js

document.addEventListener('pointermove', (e) => {
  const button = document.querySelector('.target');
  const rect = button.getBoundingClientRect();
  const distance = Math.hypot(
    Math.max(rect.left - e.clientX, e.clientX - rect.right, 0),
    Math.max(rect.top - e.clientY, e.clientY - rect.bottom, 0)
  );
  button.classList.toggle('near', distance < 48);
});

 

이런 JavaScript 코드가 CSS 한 줄로 대체될 수 있다는 게 :near()의 핵심 가치다.

 

[💡 잠깐! 이 용어는?] CSS Working Group(CSSWG): W3C 산하에서 CSS 명세를 작성하고 관리하는 워킹 그룹이다. 새로운 CSS 기능은 보통 GitHub issue로 제안되고, 논의를 거쳐 명세로 발전한다.

 


 

정리

 

  • :near()는 포인터가 요소 근처에 왔을 때 스타일을 적용하는 새 CSS 의사 클래스 제안이다
  • 유클리드 거리 기반으로 활성화 반경을 <length> 값으로 지정할 수 있다
  • 조건부 가시성, 프리페치 트리거, 툴팁 조기 표시 등에 활용할 수 있다
  • 아직 어떤 브라우저에서도 구현되지 않았지만, 기존 JavaScript 구현을 대체할 가능성이 있다
  • 거짓 어포던스와 접근성 문제에 대한 신중한 설계가 필요한 제안이다

 


 

참고:

  • CSS-Tricks: https://css-tricks.com/potentially-coming-to-a-browser-near-you/
  • CSSWG Proposal: https://github.com/w3c/csswg-drafts/issues/13271

 

728x90
반응형
728x90
반응형

GenAI 플랫폼 협업을 상징하는 기술팀 업무 이미지

조직에서 AI를 도입할 때 가장 흔한 패턴이 있다. 팀 A는 OpenAI API 키를 발급받고, 팀 B는 Anthropic 계정을 만들고, 팀 C는 Google 모델을 쓴다. 계정과 키가 사방에 흩어지고, 누가 얼마나 쓰는지 파악도 안 되고, 프롬프트는 각 팀의 코드 저장소 안에 묻혀 있다. 마치 각 부서가 독립적으로 전화 회선을 개통해서 쓰는 것과 같다. 당근은 이 혼란을 중앙 교환기 하나로 정리했다.
 
당근의 Tech Core ML Applications팀과 LLM Infra TF가 구축한 GenAI 플랫폼은 LLM Router, Prompt Studio, KarrotChat이라는 세 축으로 구성된다. 각각 "어떤 모델을 쓸지", "프롬프트를 어떻게 관리할지", "만든 AI를 어떻게 쓸지"에 대한 답을 제공한다.
 


 

LLM Router — 모든 AI 호출의 관문

 
LLM Router의 핵심 아이디어는 단순하다. 모든 AI API 호출을 하나의 관문으로 통과시키는 것이다. 각 팀이 직접 OpenAI, Anthropic, Google의 API를 호출하는 대신, LLM Router를 거치도록 한다.
 

LLM Router 흐름

[제품 팀 A] ──┐
[제품 팀 B] ──┼── LLM Router ──┬── OpenAI
[제품 팀 C] ──┘                ├── Anthropic
                               └── Google

 
이 구조가 주는 이점은 명확하다.
 

계정 관리 팀마다 별도 계정/키 중앙 집중 관리
비용 추적 파악 불가 팀/기능별 사용량 모니터링
모델 전환 각 팀이 코드 수정 Router 설정만 변경
장애 대응 각자 해결 Router에서 폴백 처리

 
비유하면 LLM Router는 회사의 내선 전화 교환기다. 외부 통신사(LLM 제공사)가 바뀌어도 내선 번호(API 인터페이스)는 그대로 유지된다. 제품 팀은 "어떤 모델 제공사를 쓰는지" 신경 쓸 필요 없이, Router에 요청만 보내면 된다.
 
[💡 잠깐! 이 용어는?] LLM Gateway(Router): 여러 LLM 제공사(OpenAI, Anthropic, Google 등)의 API를 통합 인터페이스 뒤에 추상화하는 프록시 서버다. 라우팅, 인증, 사용량 추적, 폴백 등을 중앙에서 관리한다.
 


 

Prompt Studio — 프롬프트를 코드처럼 관리하다

 
Prompt Studio는 AI 기능과 Agent를 만드는 곳이다. 핵심 가치는 프롬프트를 코드 저장소에 묻어두지 않고, 독립적인 라이프사이클로 관리하는 데 있다.
 
하나의 프로젝트 안에서 여러 버전을 함께 관리할 수 있어서, 프롬프트나 모델을 바꿀 때 각 변경 사항을 서로 비교하며 쉽게 오갈 수 있다. Prompt Studio API 호출 시 어떤 배포 버전을 사용할지 UI에서 바로 지정할 수 있어서, 실제 반영도 클릭 한 번이면 가능하다.
 

Prompt Studio 버전 관리 개념

project: "상품 설명 생성"
versions:
  - v1:
      model: "gpt-4o"
      prompt: "상품명과 카테고리를 기반으로..."
      status: "archived"
  - v2:
      model: "claude-3.5-sonnet"
      prompt: "중고거래 맥락에서 상품명과..."
      status: "production"
  - v3:
      model: "claude-3.5-sonnet"
      prompt: "구매자 관점에서 궁금해할 정보를..."
      status: "staging"

 
이 구조 덕분에 엔지니어의 도움 없이도 누구나 오너십을 가지고 AI 기능을 지속적으로 테스트하고 개선할 수 있게 되었다. 프롬프트 변경이 코드 배포 사이클에 묶이지 않는다는 것은, 마케팅 담당자나 기획자도 직접 프롬프트를 실험할 수 있다는 뜻이다.
 
Agent가 실행되는 전체 과정은 Trace로 기록되어서, 문제가 생겼을 때 단계별로 확인하며 디버깅할 수 있다. 이건 마치 택배 송장 번호로 배송 과정을 추적하는 것처럼, AI 응답의 생성 과정을 처음부터 끝까지 들여다볼 수 있다는 의미다.
 
[💡 잠깐! 이 용어는?] Trace(트레이스): AI Agent의 실행 과정을 단계별로 기록한 로그다. 어떤 프롬프트가 전달되었고, 어떤 도구가 호출되었고, 최종 응답이 어떻게 생성되었는지를 시간 순서대로 추적할 수 있다.
 


 

KarrotChat — 만든 Agent를 쓰는 곳

 
KarrotChat은 Prompt Studio에서 구성한 Agent를 직접 사용하는 채팅 플랫폼이다. 사내용 ChatGPT라고 생각하면 된다. Prompt Studio에서 만들어진 Agent들을 탐색하고 바로 사용할 수 있다.
 

LLM Router 모든 AI API 호출의 관문 엔지니어 (인프라 수준)
Prompt Studio AI 기능/Agent를 만들고 관리 엔지니어 + PM + 기획자
KarrotChat 만들어진 Agent를 사용 모든 구성원

 
세 플랫폼이 만드는 흐름은 이렇다. LLM Router가 인프라 레이어에서 모델 접근을 통합하고, Prompt Studio가 그 위에서 AI 기능을 구성하고, KarrotChat이 최종 사용자에게 전달한다. 인프라 → 개발 → 사용이라는 계층적 구조가 깔끔하게 분리되어 있다.
 


 

GenAI Labs — 실험과 평가의 순환

 
AI 기능의 품질을 높이려면 실험과 평가가 반복되어야 한다. 당근의 GenAI Labs는 프롬프트 관리 → 실험 실행 → 성능 평가 → 결과 분석까지 LLM 실험의 전 과정을 하나의 워크플로우로 제공한다.
 

GenAI Labs 실험 루프

[프롬프트 버전 작성]
       ↓
[테스트 데이터셋 실행]
       ↓
[자동 평가 (LLM-as-Judge)]
       ↓
[결과 비교 대시보드]
       ↓
[승인 → Production 배포]  or  [수정 → 재실험]

 
이 과정에서 LLM-as-Judge(LLM이 다른 LLM의 출력을 평가하는 방식)를 활용하면, 수백 개의 테스트 케이스에 대한 평가를 사람이 일일이 하지 않아도 된다.
 
[💡 잠깐! 이 용어는?] LLM-as-Judge: LLM의 출력 품질을 평가할 때 사람 대신 다른 LLM을 평가자로 사용하는 방법이다. 대규모 평가를 자동화할 수 있지만, 평가 기준을 명확하게 정의하는 것이 중요하다.
 


 

직군의 경계를 넘는 AI 활용

 
당근의 GenAI 플랫폼이 만든 가장 큰 변화는 기술적인 것이 아니라 조직적인 것이다. AI 실험이 더 이상 엔지니어만의 영역이 아니게 되었다. 아이디어만 있다면 누구든 직접 실험을 실행할 수 있는 구조가 만들어졌다.
 

PM이 AI 기능 요청 → 엔지니어가 구현 PM이 직접 Prompt Studio에서 프로토타입
프롬프트 변경 = 코드 배포 프롬프트 변경 = UI에서 버전 전환
모델 변경 = API 코드 수정 모델 변경 = LLM Router 설정 변경
AI 실험 결과 = 엔지니어가 수동 확인 AI 실험 결과 = GenAI Labs 대시보드

 
당근에서는 비개발자 구성원들도 AI Show & Tell 해커톤에 참여해 직접 AI 도구를 만들어보는 문화가 정착되었다. 직군이라는 경계 없이, 각자의 문제를 정의하고 스스로 해결책을 실험하며 실제 변화를 만들어내고 있다.
 


 

실무 적용 체크리스트

 
글의 핵심 메시지를 실제 운영 환경에 옮길 때는 아래 3가지를 먼저 확인하면 시행착오를 크게 줄일 수 있다.
 

  • 지표 기준선(Baseline) 확보: 현재 상태를 수치로 먼저 잡아야 개선 효과를 검증할 수 있다.
  • 점진 적용(Gradual Rollout): 한 번에 전면 적용하지 말고 구간별로 확장해야 리스크를 제어할 수 있다.
  • 실패 시 되돌림 경로(Rollback) 준비: 문제가 생겼을 때 즉시 원복할 수 있어야 운영 사고를 줄일 수 있다.

 

리스크 시나리오와 대응

 

기대만 반영되고 실행이 지연됨 일정 지연, 의사결정 정체 범위를 축소해 우선순위 높은 항목부터 실행
초기 성과 이후 품질 저하 오류율 재상승, 사용자 불만 증가 모니터링 지표 재정의 + 주기적 점검 루틴 강화
외부 변수로 계획이 틀어짐 트래픽/비용 급변, 의존 서비스 이슈 대안 경로 준비 + 임계치 기반 자동 알림 구성

 

마무리

 
당근의 GenAI 플랫폼은 "AI를 잘 쓰는 몇 명의 엔지니어"가 아니라 "모든 구성원이 AI를 활용할 수 있는 인프라"를 만드는 데 초점을 맞추고 있다. LLM Router로 인프라를 정리하고, Prompt Studio로 개발을 민주화하고, KarrotChat으로 사용을 일상화하는 3단계 전략이 핵심이다. AI 기능을 도입하려는 조직이라면, 개별 기능 구현 전에 이런 플랫폼 레이어를 먼저 고려해 볼 만하다.
 

728x90
반응형
728x90
반응형

대규모 데이터 처리와 JSON 직렬화를 표현한 이미지

JSON.stringify는 JavaScript에서 가장 자주 호출되는 내장 함수 중 하나다. API 응답 직렬화, 로깅, 캐시 키 생성, 심지어 깊은 복사 트릭(JSON.parse(JSON.stringify(obj)))까지. 이렇게 많이 쓰이는 함수가 2배 빨라졌다면? V8 엔진 13.8(Chrome 138)에 포함된 이 최적화는 별도의 코드 변경 없이도 모든 JavaScript 애플리케이션에 적용된다.

 


 

얼마나 빨라졌나

 

JetStream2의 json-stringify-inspector 벤치마크 기준으로 2배 이상 빨라졌다. 비유하면 같은 도로, 같은 자동차인데 엔진 튜닝만으로 최고 속도가 두 배로 뛴 것이다. 운전자(개발자)가 할 일은 아무것도 없다.

 

항목 변경 전 변경 후
벤치마크 성능 기준 2배+
V8 버전 13.7 이하 13.8
Chrome 버전 137 이하 138
Node.js 반영 미정 V8 13.8 포함 버전부터

 

개발자가 코드를 한 줄도 바꾸지 않아도 Chrome 138로 업데이트하면 자동으로 적용된다.

 

[💡 잠깐! 이 용어는?] JetStream2: Apple이 만든 JavaScript/WebAssembly 벤치마크 스위트. 다양한 워크로드(정렬, 파싱, 직렬화 등)로 엔진의 종합 성능을 측정한다.

 


 

6가지 핵심 최적화

 

1. 부작용 없는 패스트 패스(Side-Effect-Free Fast Path)

 

기존 JSON.stringify는 직렬화 중간에 사용자 코드가 실행될 가능성(getter, toJSON, Proxy 등)을 항상 고려해야 했다. 비유하면 "혹시 폭탄이 있을지 모르니" 매 단계마다 방폭 검사를 하는 것과 같다.

 

새 구현은 객체에 부작용을 일으킬 요소가 없는지 먼저 확인하고, 없으면 검사 과정을 전부 건너뛰는 전용 경로로 진입한다. 게다가 기존의 재귀 호출 방식 대신 반복(iterative) 방식을 사용해 깊은 중첩에서도 스택 오버플로 위험이 줄었다.

 

2. 템플릿화된 문자열 처리(Templatized String Handling)

 

JavaScript 문자열은 1바이트(ASCII) 또는 2바이트(유니코드)로 저장된다. 기존에는 직렬화할 때 매 문자마다 "이게 1바이트인가 2바이트인가"를 체크했다.

 

새 구현은 컴파일 타임에 1바이트 전용2바이트 전용, 두 가지 버전의 직렬화 코드를 생성한다. 비유하면 국내 택배와 국제 택배 라인을 분리 운영하는 물류 센터와 같다. 섞어서 처리하는 것보다 분류해서 전용 라인에 태우는 게 빠르다.

 

3. SIMD 기반 문자 이스케이핑

 

JSON에서는 ", \, 제어 문자 등을 이스케이프해야 한다. 문자열이 길면 이 탐색 비용이 누적된다.

 

문자열 길이 기법 설명
긴 문자열 하드웨어 SIMD ARM64 Neon 명령어로 한 번에 16바이트씩 처리
짧은 문자열 SWAR 일반 레지스터에서 비트 연산으로 여러 바이트를 동시에 처리

 

[💡 잠깐! 이 용어는?] SIMD(Single Instruction, Multiple Data): 하나의 명령어로 여러 데이터를 동시에 처리하는 CPU 기법. 이미지 처리, 벡터 연산, 문자열 탐색 등에서 성능을 크게 끌어올린다.

 

4. Hidden Class의 fast-json-iterable 플래그

 

V8은 객체의 구조를 Hidden Class로 관리한다. 새 최적화는 객체를 처음 직렬화할 때 아래 조건을 만족하면 Hidden Class에 플래그를 찍는다:

 

  • Symbol 키가 없음
  • 모든 프로퍼티가 enumerable
  • 키에 이스케이프가 필요한 문자가 없음

 

같은 Hidden Class를 가진 다음 객체들은 이 검증 과정을 통째로 건너뛴다. 같은 구조의 객체를 반복 직렬화하는 API 응답 같은 시나리오에서 위력을 발휘한다.

 

5. Dragonbox 숫자 변환

 

숫자를 문자열로 바꾸는 알고리즘이 Grisu3에서 Dragonbox로 교체됐다. 이 변경은 JSON.stringify뿐 아니라 V8 전체의 Number.prototype.toString()에 영향을 준다.

 

알고리즘 특징
Grisu3 (기존) 빠르지만 일부 케이스에서 정확도를 위해 폴백 필요
Dragonbox (신규) 항상 정확하면서도 Grisu3보다 빠름

 

6. 세그먼트 버퍼 관리

 

기존에는 직렬화 결과를 하나의 연속 메모리 버퍼에 쌓았다. 객체가 크면 버퍼가 꽉 찰 때마다 더 큰 메모리를 할당하고 전체를 복사하는 비용이 발생했다.

 

새 구현은 세그먼트 단위로 메모리를 관리한다. 비유하면 이사할 때 짐이 많으면 더 큰 트럭을 빌리는 대신, 트럭 여러 대를 쓰는 방식이다. 재할당과 복사 비용이 사라진다.

 


 

패스트 패스가 작동하지 않는 경우

 

모든 경우에 2배가 되는 건 아니다. 아래 조건에 해당하면 기존 일반 직렬화 경로로 폴백한다:

 

  • replacer 또는 space 인자를 사용한 경우
  • 객체에 커스텀 .toJSON() 메서드가 있는 경우
  • 배열 형태의 인덱스 프로퍼티를 가진 객체
  • ConsString 같은 복합 문자열 타입

 

fast-path-fallback-cases.ts

// 패스트 패스 O — 단순 객체 직렬화
JSON.stringify({ name: 'Kim', age: 30, active: true })

// 패스트 패스 X — replacer 사용
JSON.stringify(data, ['name', 'age'])

// 패스트 패스 X — space(들여쓰기) 사용
JSON.stringify(data, null, 2)

// 패스트 패스 X — toJSON 커스텀 메서드
const obj = { toJSON() { return 'custom' } }
JSON.stringify(obj)

 

로깅이나 디버깅 목적의 JSON.stringify(data, null, 2)는 패스트 패스를 타지 않는다. 성능이 중요한 경로에서는 replacerspace 없이 호출하는 것이 유리하다.

 


 

정리

 

  • V8 13.8(Chrome 138)에서 JSON.stringify 성능이 2배 이상 개선됐다
  • 핵심은 부작용 없는 패스트 패스, SIMD 문자 탐색, Hidden Class 플래그, Dragonbox 숫자 변환, 세그먼트 버퍼의 조합이다
  • replacer, space, toJSON 사용 시 패스트 패스가 작동하지 않는다
  • 코드 변경 없이 런타임 업데이트만으로 적용된다
  • Node.js는 V8 13.8을 포함하는 버전부터 동일한 혜택을 받는다

 

API 서버에서 매 요청마다 JSON.stringify를 호출한다면, Chrome 138 이후의 V8을 사용하는 것만으로 직렬화 구간의 CPU 시간이 절반으로 줄어든다. 인프라 비용 절감이 코드 한 줄 없이 가능한 드문 케이스다.

 


 

참고:

  • V8 Blog — Faster JSON.stringify: https://v8.dev/blog/json-stringify
  • Dragonbox Algorithm: https://github.com/jk-jeon/dragonbox

 

728x90
반응형
728x90
반응형

AI 에이전트와 웹 상호작용을 표현한 미래형 인터페이스 이미지

AI 에이전트가 웹사이트를 이용하는 방식은 지금까지 "사람이 보는 화면을 캡처해서 해석"하는 수준이었다. 버튼을 클릭하려면 스크린샷을 찍고, 좌표를 추론하고, 클릭 이벤트를 흉내 낸다. 비유하면 외국인이 한글 메뉴판 없이 음식 사진만 보고 주문하는 것과 같다. Google이 Chrome 146에 탑재한 WebMCP는 이 상황을 근본적으로 바꾼다. 웹사이트가 AI 에이전트에게 "여기 우리가 할 수 있는 일 목록이야"라고 구조화된 메뉴판을 직접 건네는 프로토콜이다.

 

[💡 잠깐! 이 용어는?] MCP(Model Context Protocol): AI 에이전트가 외부 도구(파일 시스템, API, 데이터베이스 등)와 표준화된 방식으로 통신하는 오픈 프로토콜. Anthropic이 주도하고 있다. WebMCP는 이 개념을 웹 브라우저 환경으로 확장한 것이다.

 


 

WebMCP가 뭔가

 

WebMCP는 웹사이트가 AI 에이전트에게 사용 가능한 기능(Tool)을 선언적으로 공개하는 브라우저 API다. 핵심은 navigator.modelContext라는 새로운 브라우저 API를 통해 사이트가 "Tool Contract"를 발행하는 것이다.

 

포인트: 기존에는 에이전트가 DOM을 크롤링해서 "이게 버튼인 것 같다"고 추론했다면, WebMCP에서는 사이트가 "이건 buyTicket(destination, date) 함수야"라고 명시적으로 알려준다.

 

예를 들어 항공권 예약 사이트가 WebMCP를 지원하면, 에이전트는 스크린샷 해석 없이 searchFlights({ from: 'ICN', to: 'NRT', date: '2026-04-01' }) 같은 구조화된 호출을 할 수 있다. 비유하면 음식점이 외국인 손님에게 QR 코드를 주고, QR을 찍으면 다국어 메뉴와 주문 API가 뜨는 것과 같다.

 


 

두 가지 API 모드

 

WebMCP는 두 가지 API를 제공한다. 하나는 HTML 폼 기반의 단순한 액션용이고, 다른 하나는 JavaScript가 필요한 복잡한 인터랙션용이다.

 

구분 Declarative API Imperative API
대상 HTML 폼으로 처리 가능한 액션 JavaScript 실행이 필요한 동적 액션
복잡도 낮음 높음
예시 검색 폼 제출, 로그인 장바구니 관리, 결제 플로우, 실시간 필터
구현 방식 HTML 속성으로 선언 navigator.modelContext API 호출

 

Declarative API

 

기존 HTML 폼에 메타데이터를 추가하는 방식이다. 폼의 action, method, input 필드를 에이전트가 이해할 수 있는 구조로 노출한다. 이미 서버사이드 렌더링을 하고 있는 사이트라면 최소한의 변경으로 WebMCP를 지원할 수 있다.

 

Imperative API

 

navigator.modelContext를 통해 Tool 목록을 프로그래밍 방식으로 등록한다. SPA처럼 페이지 전환 없이 동적으로 상태가 바뀌는 애플리케이션에 적합하다.

 

navigator.modelContext — Imperative API 예시

navigator.modelContext.addTool({
  name: 'searchFlights',
  description: '출발지, 목적지, 날짜로 항공편을 검색한다',
  parameters: {
    type: 'object',
    properties: {
      from: { type: 'string', description: '출발 공항 코드 (예: ICN)' },
      to: { type: 'string', description: '도착 공항 코드 (예: NRT)' },
      date: { type: 'string', description: '출발 날짜 (YYYY-MM-DD)' }
    },
    required: ['from', 'to', 'date']
  },
  handler: async ({ from, to, date }) => {
    const results = await flightAPI.search({ from, to, date })
    return { flights: results }
  }
})

 

에이전트가 이 Tool을 호출하면 handler 함수가 실행되고, 결과가 구조화된 데이터로 반환된다. DOM 탐색, 클릭 시뮬레이션, 스크린샷 해석 같은 불안정한 과정이 전부 사라진다.

 

[💡 잠깐! 이 용어는?] Tool Contract: 에이전트가 호출할 수 있는 함수의 이름, 설명, 파라미터 스키마, 반환값을 정의한 명세. OpenAPI 스펙과 유사하지만, 브라우저 내에서 실시간으로 등록·해제할 수 있다는 점이 다르다.

 


 

기존 방식과 뭐가 다른가

 

항목 스크린샷 기반 (기존) DOM 크롤링 WebMCP
정확도 낮음 (좌표 추론) 중간 (셀렉터 깨짐 가능) 높음 (명시적 스키마)
속도 느림 (이미지 처리) 중간 빠름 (직접 호출)
유지보수 사이트 변경 시 깨짐 사이트 변경 시 깨짐 사이트가 스키마를 관리
보안 통제 불가 통제 어려움 사이트가 허용 범위 결정
표준화 없음 없음 W3C 표준화 예정

 

가장 큰 차이는 주도권의 이동이다. 기존에는 에이전트가 사이트를 "역공학"해서 이해해야 했다. WebMCP에서는 사이트가 직접 "이건 이렇게 쓰는 거야"라고 알려준다. 비유하면 박물관 가이드 없이 전시물을 보는 것과, 오디오 가이드가 "이 작품은 ~이고, 다음 작품으로 가려면 오른쪽으로 가세요"라고 안내하는 것의 차이다.

 


 

실제 활용 시나리오

 

여행 예약

 

사용자가 에이전트에게 "4월 첫째 주 도쿄 왕복 항공편 찾아줘"라고 말하면, 에이전트는 항공사 사이트의 searchFlights Tool을 호출하고, 결과를 구조화된 JSON으로 받아 비교표를 만들어 준다. 가격 정렬, 경유 필터, 좌석 선택까지 모두 Tool 호출로 처리된다.

 

고객 지원

 

고객이 "배송 상태 확인해줘"라고 하면, 에이전트가 쇼핑몰의 trackOrder Tool을 호출해서 주문번호, 현재 위치, 예상 도착일을 즉시 가져온다. 기존에는 마이페이지 → 주문 내역 → 배송 조회 버튼 클릭이라는 3단계 UI 탐색이 필요했다.

 

이커머스

 

"검은색 러닝화, 270mm, 5만원 이하"라는 요청에 에이전트가 searchProducts Tool로 필터링된 결과를 받고, addToCart Tool로 장바구니에 담는다. 상품 목록 페이지의 레이아웃이 바뀌어도 Tool Contract만 유지되면 에이전트는 동일하게 동작한다.

 


 

브라우저 지원과 도입 시점

 

환경 현황
Chrome 146 Canary chrome://flags에서 "WebMCP for testing" 플래그로 활성화 가능
Chrome Stable 미정 (2026년 하반기 예상)
Firefox / Safari 미지원 (논의 중)
Early Preview Program Chrome 개발자 사이트에서 신청 가능

 

아직 Canary 단계이므로 프로덕션에 바로 적용할 수는 없다. 하지만 Google이 Chrome에 직접 탑재하고, MCP 생태계와 연동하는 방향으로 움직이고 있다는 점에서 웹 개발의 패러다임 변화가 시작됐다고 볼 수 있다.

 

[💡 잠깐! 이 용어는?] Chrome Canary: Chrome의 최신 실험 기능을 먼저 써볼 수 있는 개발자용 빌드. 안정성이 보장되지 않으며, 매일 업데이트된다. Stable 버전에 반영되기까지 보통 3~6개월이 걸린다.

 


 

웹 개발자가 준비할 것

 

WebMCP가 표준화되면 웹 개발에 새로운 레이어가 추가된다. 기존에는 사람을 위한 UI만 만들면 됐지만, 이제는 에이전트를 위한 Tool Contract도 함께 설계해야 한다.

 

  • API 설계 사고방식: REST API를 설계하듯, 사이트의 핵심 기능을 Tool 단위로 추출하는 작업이 필요하다
  • 보안 경계: 어떤 Tool을 공개하고, 어떤 Tool은 인증 후에만 허용할지 결정해야 한다
  • 프로그레시브 인핸스먼트: WebMCP를 지원하지 않는 브라우저에서도 기존 UI가 정상 동작해야 한다

 


 

정리

 

  • Google이 Chrome 146에 WebMCP를 Early Preview로 탑재했다
  • navigator.modelContext API를 통해 웹사이트가 AI 에이전트에게 구조화된 Tool을 노출한다
  • Declarative API(HTML 폼 기반)와 Imperative API(JavaScript 기반) 두 가지 모드를 제공한다
  • 스크린샷 해석이나 DOM 크롤링 없이 직접 함수 호출 방식으로 동작한다
  • 아직 Canary 단계이지만, 웹이 "사람을 위한 인터페이스"에서 "사람 + 에이전트를 위한 인터페이스"로 확장되는 시작점이다

 

웹사이트를 운영 중이라면 당장 WebMCP를 도입할 필요는 없다. 하지만 "우리 사이트의 핵심 기능을 Tool 단위로 정리하면 어떤 목록이 나올까?"라는 질문은 지금부터 던져볼 만하다. 그 목록이 곧 WebMCP Tool Contract의 초안이 된다.

 


 

참고:

  • WebMCP Early Preview — Chrome Developers: https://developer.chrome.com/blog/webmcp-epp
  • Google AI Introduces WebMCP — MarkTechPost: https://www.marktechpost.com/2026/02/14/google-ai-introduces-the-webmcp-to-enable-direct-and-structured-website-interactions-for-new-ai-agents/
  • MCP (Model Context Protocol): https://modelcontextprotocol.io/

 

728x90
반응형
728x90
반응형

JavaScript 성능 최적화를 상징하는 코드 실행 이미지

브라우저가 JavaScript 파일을 받으면 즉시 모든 코드를 컴파일하지 않는다. "이 함수는 나중에 호출될 수도 있고, 안 될 수도 있으니까 일단 넘어가자"는 전략을 쓴다. 이를 지연 컴파일(Lazy Compilation)이라고 한다. 대부분의 경우 합리적인 전략이지만, 페이지 로드 직후 반드시 실행되는 함수까지 뒤로 미루면 오히려 병목이 된다. V8 13.5(Chrome 136)에 도입된 Explicit Compile Hints는 개발자가 "이 파일의 함수들은 전부 바로 실행된다"고 V8에 알려주는 기능이다. 주석 한 줄이면 된다.

 


 

지연 컴파일이 문제가 되는 순간

 

V8의 기본 전략은 이렇다.

 

단계 동작 시점
1. 파싱 함수 경계만 파악 (가벼운 파싱) 스크립트 로드 시
2. 대기 함수 본문 컴파일 보류
3. 호출 함수가 실제 호출될 때 컴파일 메인 스레드 블로킹

 

비유하면 식당에서 주문을 받은 뒤 "손님이 젓가락을 들면 그때 요리를 시작"하는 것과 같다. 주문이 들어왔으면 미리 조리를 시작하는 게 당연히 빠르다.

 

문제는 3단계다. 함수가 호출되는 시점에 메인 스레드에서 컴파일이 일어나면, 그 시간 동안 UI가 멈춘다. 페이지 로드 직후 실행되는 초기화 코드, 라우팅 로직, 이벤트 핸들러 등이 전부 이 패턴에 해당한다.

 

[💡 잠깐! 이 용어는?] 지연 컴파일(Lazy Compilation): JavaScript 엔진이 함수 정의를 만나도 즉시 컴파일하지 않고, 함수가 처음 호출될 때 컴파일하는 전략. 메모리를 절약하지만, 호출 시점에 지연이 발생할 수 있다.

 


 

Explicit Compile Hints의 동작 원리

 

사용법은 놀라울 정도로 단순하다. JavaScript 파일 최상단에 매직 코멘트를 추가하면 된다.

 

entry.js — Explicit Compile Hint 적용

//# allFunctionsCalledOnLoad

function initializeApp() {
  const root = document.getElementById('root')
  const config = loadConfig()
  renderMainView(root, config)
}

function loadConfig() {
  return JSON.parse(localStorage.getItem('app-config') || '{}')
}

function renderMainView(root, config) {
  root.innerHTML = buildTemplate(config)
  attachEventListeners(root)
}

function attachEventListeners(root) {
  root.addEventListener('click', handleClick)
  root.addEventListener('scroll', handleScroll)
}

initializeApp()

 

//# allFunctionsCalledOnLoad 한 줄이면 V8은 이 파일의 모든 함수를 즉시 컴파일한다. 지연 없이 백그라운드 스레드에서 미리 컴파일을 끝내기 때문에, 함수가 호출되는 시점에는 이미 실행 준비가 완료되어 있다.

 

즉시 컴파일이 빠른 두 가지 이유

 

이유 설명
중복 작업 제거 지연 컴파일에서는 가벼운 파싱(함수 경계 파악) + 본격 파싱(컴파일)을 두 번 한다. 즉시 컴파일은 한 번에 끝낸다
백그라운드 병렬화 스크립트가 네트워크에서 로드되는 동안 백그라운드 스레드에서 컴파일을 진행한다. 메인 스레드가 블로킹되지 않는다

 

비유하면 택배가 오는 동안 포장을 뜯을 가위를 미리 준비해놓는 것과 같다. 택배가 도착하면 바로 개봉할 수 있다.

 


 

성능 벤치마크

 

V8 팀이 상위 20개 인기 웹사이트를 대상으로 테스트한 결과:

 

측정 항목 결과
개선된 사이트 20개 중 17개
평균 파싱+컴파일 시간 감소 630ms
적용 방식 코드 변경 없이 주석 한 줄 추가

 

630ms는 사용자가 체감할 수 있는 수준의 차이다. Core Web Vitals의 FCP(First Contentful Paint)가 1.8초 이내여야 "Good" 등급인 점을 감안하면, 파싱+컴파일에서만 630ms를 절약하는 건 상당한 개선이다.

 

[💡 잠깐! 이 용어는?] FCP(First Contentful Paint): 브라우저가 페이지의 첫 번째 콘텐츠(텍스트, 이미지 등)를 화면에 렌더링하는 시점. LCP(Largest Contentful Paint)와 함께 Core Web Vitals의 핵심 지표다.

 


 

언제 써야 하고, 언제 피해야 하나

 

모든 파일에 //# allFunctionsCalledOnLoad를 붙이는 건 좋은 전략이 아니다. 즉시 컴파일은 메모리와 CPU를 더 사용하기 때문이다.

 

시나리오 Explicit Compile Hint 이유
앱 진입점 (entry.js) O 페이지 로드 직후 모든 함수가 실행됨
라우터 초기화 코드 O 첫 렌더링에 필수적인 코드
Critical Path의 유틸리티 O 초기 렌더링에 사용되는 헬퍼 함수들
서드파티 라이브러리 X 라이브러리 전체를 즉시 컴파일하면 메모리 낭비
코드 스플리팅된 청크 X 지연 로드 목적으로 분리한 코드를 즉시 컴파일하면 취지에 반함
관리자 페이지 전용 코드 X 대부분의 사용자가 접근하지 않는 코드

 

비유하면 레스토랑 오픈 전에 모든 메뉴를 미리 조리해두는 건 비효율적이다. 오늘의 추천 메뉴만 미리 준비하고, 나머지는 주문이 들어오면 조리하는 게 맞다.

 


 

적용 가이드

 

1단계: 크리티컬 패스 파일 식별

 

Chrome DevTools의 Performance 탭에서 "Compile Script" 항목을 확인한다. 파싱+컴파일에 시간이 많이 소요되면서 페이지 로드 직후 실행되는 파일이 후보다.

 

2단계: 매직 코멘트 추가

 

webpack.config.js — 번들러에서 자동 삽입

const webpack = require('webpack')

module.exports = {
  plugins: [
    new webpack.BannerPlugin({
      banner: '//# allFunctionsCalledOnLoad',
      raw: true,
      entryOnly: true
    })
  ]
}

 

Webpack의 BannerPlugin을 사용하면 엔트리 파일에만 자동으로 매직 코멘트를 삽입할 수 있다. 수동으로 파일마다 추가할 필요가 없다.

 

3단계: 효과 측정

 

Chrome DevTools에서 성능 측정

# Chrome을 --enable-benchmarking 플래그로 실행
# Performance 탭 → Record → 페이지 로드
# "Parse Script" + "Compile Script" 항목의 시간 변화를 비교

 


 

향후 발전 방향

 

현재는 파일 단위(allFunctionsCalledOnLoad)로만 힌트를 줄 수 있다. V8 팀은 향후 함수 단위의 세분화된 컴파일 힌트를 계획하고 있다. 특정 함수만 골라서 "이건 바로 실행된다"고 지정할 수 있게 되면, 파일 전체를 즉시 컴파일하는 것보다 더 정밀한 최적화가 가능해진다.

 


 

정리

 

  • V8 13.5(Chrome 136)에 Explicit Compile Hints 기능이 도입됐다
  • //# allFunctionsCalledOnLoad 매직 코멘트 한 줄로 파일 내 모든 함수를 즉시 컴파일한다
  • 상위 20개 웹사이트 중 17개에서 평균 630ms 파싱+컴파일 시간이 감소했다
  • 앱 진입점, 라우터, Critical Path 코드에 적용하면 효과적이다
  • 서드파티 라이브러리나 코드 스플리팅된 청크에는 적용하지 않는 것이 좋다
  • 향후 함수 단위 힌트로 확장될 예정이다

 

코드 한 줄 바꾸지 않고 주석 한 줄 추가만으로 수백 밀리초를 절약할 수 있다. 비용 대비 효과가 이렇게 높은 최적화는 드물다. 페이지 로드 성능이 중요한 프로젝트라면 엔트리 파일에 먼저 적용해보는 것을 권한다.

 


 

참고:

  • V8 Blog — Explicit Compile Hints: https://v8.dev/blog/explicit-compile-hints
  • Chrome 136 Release Notes: https://developer.chrome.com/blog/chrome-136-beta

 

728x90
반응형