모든 글
뉴스 해석2026-09-2111 min read

훅 규칙을 없앤 방법은 문서가 아니라 컴파일 에러였다

Octane은 훅을 호출 순서가 아니라 호출 지점으로 추적해 규칙을 지웠고, 남은 한 경우는 빌드를 실패시킨다. 지켜야 할 규칙을 사람 주의력에 맡기지 않는 쪽.

React의 훅 규칙은 십 년 가까이 문서와 린트 규칙으로 유지돼 왔다. "조건문 안에서 훅을 부르지 마라, 순서가 어긋난다."

Octane은 그 규칙을 지우면서 이렇게 한다.

Hooks are tracked by call site, not call order, so a hook can live inside an if or after an early return.

그리고 남은 한 가지 제약 — 평범한 JS 루프 안의 훅 — 은 컴파일 에러로 막는다. 규칙을 없앤 방법이 "더 조심하기"가 아니라 "어길 수 없게 만들기"였다는 점이, 프런트엔드 프레임워크 소식인데도 이번 주 내내 나온 주제와 같은 자리에 있다.

출처: github.com/octanejs/octane (MIT)

확인된 것

저장소에서 직접 확인한 것만 적는다.

  • TypeScript로 작성된 UI 프레임워크. 저장소 설명은 React's programming model, compiled. The successor to Inferno. 2026-06-22 생성(약 3개월), 마지막 푸시 2026-09-21. 별 1,420, 포크 55, 열린 이슈 20. MIT.

  • 가상 DOM이 없다. 컴포넌트는 React처럼 다시 렌더링되지만, 컴파일된 렌더 경로와 LIS 기반 키드 리컨실러로 런타임 오버헤드를 줄인다.

  • 표준 .tsx가 그대로 돌아간다. 추가로 .tsrx 방언이 @if, @for, @switch, @try 같은 템플릿 지시자를 제공하고, 이것들이 키드 fast path로 컴파일된다. 두 방언을 한 앱에서 섞을 수 있다.

  • 의존성 배열을 생략하면 컴파일러가 유도한다. useEffect, useMemo, useCallback에서 배열을 빼면 클로저가 실제로 포획한 것에서 계산한다. 명시적 배열은 React와 똑같은 의미로 남는다.

  • 훅 규칙이 없다. 호출 순서가 아니라 호출 지점으로 추적하기 때문이다. 다만 평범한 JS 루프 안의 훅은 컴파일 에러다 — 모든 반복이 한 호출 지점 슬롯을 공유하게 되므로. 대신 키드 @for를 쓴다.

  • 합성 이벤트 계층이 없다. 실제 위임 DOM 이벤트를 쓰고, ref는 평범한 prop이다.

  • 빠진 기능을 결함이 아니라 선택으로 적어뒀다. 클래스 컴포넌트 없음, 서버 컴포넌트 없음, 합성 이벤트 없음.

    Those are choices, not gaps

  • 상호운용이 양방향이다. ReactCompat으로 진짜 React 컴포넌트를 Octane 앱 안에서(React·React DOM 19.2+), OctaneCompat으로 Octane 컴포넌트를 React 19 앱 안에서 돌린다.

  • 베타다. 런타임·컴파일러·SSR/하이드레이션 경로는 동작하지만 API는 아직 움직인다고 적혀 있다. 테스트는 3,900개 이상인데, 그 숫자에 단서를 스스로 붙였다.

    This is an Octane suite count, not a claim that every test was ported from React

판단

1. 규칙을 없애는 방법은 컴파일러를 만드는 것이었다

훅 규칙은 런타임 구현의 부산물이었다. React가 훅을 배열에 순서대로 쌓기 때문에 순서가 어긋나면 안 됐던 것이지, 조건부 훅이 개념적으로 나쁜 게 아니었다.

Octane은 호출 지점을 컴파일 시점에 알아내서 그 제약을 없앴다. 그리고 남은 한 경우는 문서에 "하지 마세요"라고 쓰는 대신 빌드를 실패시킨다.

→ 이번 주에 같은 형태를 여러 번 봤다. 접근성 검사를 스크립트로 박아둔 다이어그램 스킬, 승인된 값이 실제로 반영됐는지 되묻는 광고 에이전트. 공통점은 하나다 — 지켜야 할 규칙을 사람의 주의력에 맡기지 않는다.

내 프로젝트에 적용할 질문: 팀 규칙 문서에 적힌 항목 중 컴파일러나 CI가 대신 막을 수 있는 게 몇 개인가. 막을 수 있는데 문서에만 있는 항목은 언젠가 깨진다.

2. 의존성 배열 자동 유도는 편의가 아니라 버그 등급의 제거다

손으로 쓴 의존성 배열이 틀리면 어떤 버그가 되는지 생각해보면 값어치가 보인다. 앱은 돌아가고, 테스트도 통과하고, 가끔 옛날 값을 쓴다. 스테일 클로저는 재현이 어렵고 코드 리뷰에서 눈에 안 띈다. 배열이 한 줄 짧은 것뿐이니까.

컴파일러가 클로저가 실제로 잡은 것에서 유도하면 그 등급의 버그가 통째로 사라진다. 배열을 잘못 쓸 방법이 없어지기 때문이다.

다만 대가가 있다고 본다. 컴파일러가 틀렸을 때 들여다볼 표면이 줄어든다. 내가 쓴 배열은 내가 읽고 고치지만, 유도된 의존성은 컴파일 결과를 봐야 한다. 명시적 배열이 React와 같은 의미로 남아 있으니, 도입한다면 미묘한 곳은 명시적으로 쓰는 쪽이 안전하다. 이건 저장소의 권고가 아니라 내 판단이다.

3. 숫자 옆에 단서를 스스로 붙였다

테스트 3,900개는 자랑하기 좋은 숫자다. 그런데 그 옆에 "이건 Octane 스위트 개수이지 React에서 전부 이식했다는 주장이 아니다"라고 적었다. 커버리지도 스위트 크기로 추정하지 말고 생성된 리포트를 보라고 한다.

빠진 기능도 마찬가지다. 없는 걸 숨기지 않고 목록으로 적고 "선택이지 결함이 아니다"라고 쓴 뒤 문서로 링크한다.

→ 이번 주에 세운 기준이 또 나왔다. 좋은 숫자를 어디 뒀는지 말고, 그 숫자의 한계를 같이 적었는지 봐라. 저장소를 평가할 때 이건 코드 품질보다 먼저 읽히는 신호다. 아직 3개월 된 프로젝트에서 이렇게 쓰는 건 드물다.

4. 진짜 리스크는 런타임이 아니라 생태계다

성능 주장이나 API 설계는 검증 가능하다. 검증하기 어려운 건 내가 쓰는 React 라이브러리들이 여기서 도는가이다. Preact가 오래 겪은 문제이고, 토론에서도 같은 지적이 나왔다.

상호운용 계층이 양방향으로 있긴 하다. 그런데 그 말은 곧 경계로 감싸야 한다는 뜻이다. 라우터, 폼, 데이터 페칭, 차트, 애니메이션, 드래그앤드롭 — 이 중 몇 개를 감싸야 하는지가 실제 도입 비용이고, 감싼 경계마다 상태·컨텍스트·이벤트가 어떻게 건너가는지 따로 알아야 한다.

→ 그래서 지금 단계의 판단은 이렇다. 새 프로젝트의 프런트엔드 전체를 여기 걸 때가 아니다. 저자가 직접 베타라고 쓰고 API가 움직인다고 밝혔다. 대신 성능이 실제로 문제인 화면 하나를 OctaneCompat으로 끼워보는 것이 배움과 위험이 맞는 크기다. 그 한 화면에서 라이브러리 경계 비용이 얼마나 드는지가, 나중에 전체를 옮길지 말지의 실측 데이터가 된다.

모르는 것

  • 직접 써보지 않았다. 저장소 README와 API 메타데이터를 읽은 결과다.
  • 성능 수치를 확인하지 못했다. 가상 DOM을 없앴다는 설계 설명은 있지만, README에서 벤치마크 숫자를 찾지 못했다. "빠르다"는 내가 검증한 게 아니다.
  • 의존성 유도가 어떤 경우에 실패하거나 과하게 잡는지 모른다. 사례가 필요한데 없다.
  • .tsrx 방언을 쓰지 않고 표준 .tsx만 쓸 때 무엇을 잃는지 정량적으로 모른다. 템플릿 지시자가 키드 fast path로 컴파일된다면, 안 쓰면 그 경로를 못 타는 것으로 읽히는데 확인하지 못했다.
  • 한국어 요약은 Inferno를 만든 개발자의 후속 프로젝트라고 적는다. 저장소 설명도 Inferno의 후속이라고 쓰지만, 개발자 신원은 내가 저장소에서 확인하지 않았다.
  • 별 1,420에 3개월이면 초기 단계다. 프로덕션 사용 사례를 찾지 못했다.

댓글

불러오는 중...