formatCurrency 한 줄을 고친 PR이 있다고 해 봅시다. diff는 작지만 이 함수는 결제 API, 장바구니 화면, 야간 리포트 작업에서 모두 호출됩니다. 리뷰어가 diff만 보면 이 사실은 보이지 않습니다. Farol은 바로 이 지점, 즉 "바뀐 코드가 어디까지 도달하는가"를 AI 코드 리뷰 뒤에 보여 주겠다는 도구입니다.
공식 저장소의 README를 읽고 정리했습니다. 저장소는 2026년 10월 21일에 만들어졌고(GitHub API 기준), README에는 릴리스가 비어 있습니다. 10월 25일 Hacker News 화제는 발견 경로일 뿐이며, 아래 내용은 README가 주장하는 동작입니다. Farol을 직접 실행하지는 않았고, 개념 이해를 위해 직접 만든 축소 모델만 실행했습니다.
무엇을 계산한다고 하는가
README에 따르면 Farol은 CLI와 GitHub Action으로, 저장소 코드를 실행하지 않고 정적 분석만 합니다. PR 코멘트와 단일 HTML 리포트를 만들고 머지를 막지는 않습니다. 보고서의 핵심 항목은 다음과 같습니다.
| 항목 | README의 설명 | 리뷰어에게 주는 의미 |
|---|---|---|
| Must check | 호환성을 깨는 API 계약 변경, 보안 발견, lint 오류, 취약한 의존성 | 읽기 전에 해결 여부부터 확인할 목록 |
| Can affect | 변경된 코드를 실행하는 기존 페이지·작업과 호출 체인 | diff 밖의 영향 범위 |
| Diff coverage | 변경된 실행 가능 줄 중 테스트가 실행한 비율 | 전체 커버리지가 높아도 이번 변경은 비어 있을 수 있음 |
| Files by risk | High/Medium/Skim/테스트 파일 단계와 테스트가 지나가지 않은 줄 | 파일 단위 읽기 순서 |
지원 범위는 README 기준으로 TypeScript(React Router, Next.js, TanStack Router 파일 기반, GraphQL, REST, CLI), Go(chi, gin, echo, net/http, gRPC), Python(FastAPI, Flask, Django URL, MCP, Celery, Temporal, Prefect)입니다. Node.js 22 이상과 git이 필요하고, Go와 Python 프로젝트는 각 런타임이 필요합니다. 설치 없이 npx @winninjs/farol analyze --base origin/main --head HEAD --out farol.json으로 분석합니다.
도달 범위는 어떻게 "더 넓게" 계산되는가
README의 한계 절에서 가장 중요한 문장은 "호출을 해석할 수 없으면 Farol은 더 적게가 아니라 더 많이 닿는다고 가정한다"는 부분입니다. 정적 분석이 놓칠 수 있는 getattr, importlib, DI 컨테이너, 런타임에 만들어지는 라우트는 보이지 않는다고도 명시합니다.
이 설계 선택은 리뷰 도구에서 합리적입니다. 놓친 영향보다 과한 경고가 대체로 값이 쌉니다. 다만 해석할 수 없는 호출이 많은 코드베이스에서는 경고가 넓게 퍼져 신호가 약해집니다. 아래는 Farol의 코드가 아니라, 이 원리를 보여 주는 직접 작성한 교육용 모델입니다. Node.js 18 이상이면 설치 없이 실행됩니다.
const calls = {
"route:POST /checkout": ["checkout"],
"route:GET /cart": ["cartTotal"],
"job:nightly-report": ["cartTotal", "formatCurrency"],
checkout: ["cartTotal", "chargeCard"],
cartTotal: ["applyDiscount"],
applyDiscount: ["formatCurrency"],
chargeCard: [], formatCurrency: [],
"route:GET /plugin/:name": ["loadPlugin"],
loadPlugin: ["UNRESOLVED"],
};
// 변경된 노드에서 호출자 방향으로 거슬러 올라가 route/job 진입점을 모읍니다.
전체 스크립트(reach_demo.mjs)를 실행한 결과입니다.
changed formatCurrency -> [ 'route:POST /checkout', 'route:GET /cart', 'job:nightly-report' ]
changed chargeCard -> [ 'route:POST /checkout' ]
changed applyDiscount, unresolved treated as 'reaches more' -> 4개 (플러그인 라우트가 추가됨)
diff coverage: 75.0%
formatCurrency의 변경은 세 진입점에 닿고, chargeCard는 하나에만 닿습니다. 같은 한 줄 수정이라도 읽는 깊이가 달라야 합니다. 마지막 줄은 12개 변경 줄 중 9개를 테스트가 실행한 가정입니다. 저장소 전체 커버리지와 이 지표는 별개입니다.
0~100 점수를 읽는 법
README는 점수를 도달 범위, diff coverage, 보안 발견, 계약 변경, 의존성 변경으로 구성한 "변경 중 위험한 부분 하나 이상이 문제를 일으킬 가능성"으로 설명합니다. 동시에 "점수와 판정 임계값은 몇 개 저장소에서 확인한 휴리스틱이므로 직접 보정하라"고 적었습니다. 즉 60점이 장애 확률 60%를 뜻하지 않습니다. 팀에서 쓰려면 지난 PR의 점수와 실제 회귀 여부를 대조해 임계값을 정하는 작업이 선행되어야 합니다.
점수는 리뷰어가 항목을 확인할수록 내려간다고 합니다. 이는 "읽었다"는 행위가 점수를 낮춘다는 뜻이므로, 점수 하락을 코드 품질 개선으로 읽으면 안 됩니다.
도입 전에 확인할 것
- 범위 밖 프레임워크: README는 Django REST Framework 라우터, Remix, Vue, Svelte, 코드로 구성한 TanStack 트리를 읽지 않는다고 적었습니다. 사용 중인 스택이 여기에 있다면 도달 범위가 비어 보일 수 있습니다.
- JS 파일과 TypeScript 백엔드: 모듈 단위 도달 범위를 쓴다고 합니다. 함수 단위보다 거칠다는 뜻입니다.
- Action 분리: README의 예시 워크플로는 테스트 잡(비밀 없음, 읽기 전용 토큰)과 PR에 코멘트를 다는 Farol 잡을 분리하고, PR 코드는 실행하지 않습니다. 포크 PR을 받는 저장소라면 이 분리를 그대로 따르는 편이 안전합니다.
- 선택 기능: Datadog 트래픽·오류는 참고용이며 점수를 바꾸지 않는다고 합니다.
이 글에서 확인하지 못한 것도 분명히 해 둡니다. 실제 Farol 출력, 정확도(오탐·미탐), 실행 시간, 대형 저장소 동작은 측정하지 않았습니다.
개발자 관점의 결론
AI가 코드를 쓰고 AI가 먼저 리뷰한 PR에서 사람은 어디를 읽어야 할까요? Farol의 가치는 점수 하나가 아니라 "이 변경이 닿는 진입점과 테스트가 지나가지 않은 줄"이라는 읽기 순서를 준다는 데 있습니다. 반대로 동적 디스패치가 많거나 지원하지 않는 프레임워크를 쓰는 팀, 점수를 머지 조건으로 삼으려는 팀에는 맞지 않습니다. 시작은 지난 PR 20~30개에 돌려 보고, 실제로 문제가 된 변경이 상위 단계에 들어갔는지 확인하는 정도가 적당합니다. PR 협업 흐름 자체는 Git 실무 형상관리 해설에서 정리했습니다.
아래 Git 실무 강의는 유료 강의 홍보 링크입니다. PR과 브랜치 협업 기초를 보완하는 선택지이며, Farol 전용 과정이라는 뜻은 아닙니다.