


여러 소스에서 활동 이벤트를 모아 여러 서페이스에 보여 주는 앱 만들기
여러 도구에서 이벤트를 모아 에디터 확장과 독립 서버 양쪽에 보여 주는 앱을 만들 때 쓴다. 계층 의존을 한 방향으로 지키고, 모든 소스를 같은 이벤트 형태로 바꾸며, 다른 코드를 고치지 않고 새 소스를 붙일 수 있게 하고, 상태를 안전하게 저장한다.
이 AI 규칙을 사용하려면
## 의존 방향을 한쪽으로 지키고, 서페이스끼리 서로 임포트하지 않게 하기
이름이 붙은 레이어를 통해 의존성이 한 방향으로만 흐르게 유지한다. `core` 레이어는 런타임 부작용이 전혀 없는 프로토콜과 인터페이스 정의만 담는다. 런타임 레이어는 오직 `core`에만 의존한다. 에디터 확장 어댑터나 독립 실행형 커맨드라인 서버 같은 각 호스트 서페이스는 `core`와 런타임에 의존하지만, 서페이스끼리는 서로를 import하지 않는다. 허용된 의존 방향을 명시적으로 선언하고 강제해야 한다. 경계를 넘나드는 import 하나만으로도 독립적으로 교체 가능해야 할 두 서페이스가 조용히 결합되어 버리기 때문이다.
### 이 원칙이 성립하는 이유
의존성이 없는 계약(contract) 레이어 덕분에 모든 서페이스와 모든 통합은 호스트 전용 코드를 끌어들이지 않고도 동일한 타입을 기준으로 컴파일할 수 있다. 새로운 서페이스는 공유된 조각들을 조합할 뿐, 다른 서페이스의 내부로 옆으로 손을 뻗지 않는다. 기존 서페이스를 수정하지 않고는 새 서페이스를 추가할 수 없다면, 그 시점에 이미 계층 구조는 무너진 것이다.
## 프로토콜 파일 하나에서 타입 바인딩을 생성하고 CI에서 차이 확인하기
와이어 프로토콜의 단일 진실 공급원으로 기계가 읽을 수 있는 계약 하나를 두고, 타입이 지정된 바인딩은 손으로 작성하는 대신 이 계약에서 생성한다. 생성된 파일은 읽기 전용으로 취급한다. 자동 생성 배너를 붙이고 절대 손으로 편집하지 않는다. 바인딩을 다시 생성해 diff가 하나라도 남으면 빌드를 실패시키는 CI 단계를 추가해, 계약과 코드가 절대 어긋나지 않게 한다.
### 제너레이터 버전을 고정하라
계약 버전은 코드 제너레이터가 실제로 지원하는 버전으로 고정한다. 제너레이터가 처리하지 못하는 최신 스키마 버전을 사용하면, 명확한 에러 없이 생성된 타입이 무엇이든 허용하는 캐치올 타입으로 조용히 격하되어 타입 안전성이 통째로 사라질 수 있다. 왜 이 버전으로 고정했는지 기록해 두고, 툴링이 따라잡았을 때만 다시 검토한다.
## 모든 소스를 하나의 허브에서 같은 이벤트 형태로 바꾸기
모든 액티비티를 단 하나의 허브를 통해 집계한다. HTTP로 푸시되는 이벤트든, 로그나 트랜스크립트 파일에서 파싱한 줄이든, 소스가 무엇이든 런타임에 들어가기 전에 하나의 표준 이벤트 타입으로 정규화한다. 하위 코드는 소스별 액션 이름이나 도구 이름이 아니라 이벤트의 종류(kind)를 기준으로 분기해야, 소스를 추가해도 런타임이나 UI로 여파가 번지지 않는다.
### 이벤트 집합을 작고 닫힌 상태로 유지하라
세션 시작·종료, 작업 시작·종료, 서브태스크 생명주기, 진행 상황, 권한 요청처럼 작고 닫힌 이벤트 종류 집합을 정의한다. 각 수집 경로가 맡은 유일한 책임은 이 표준 이벤트를 만들어내는 것뿐이다. 허브를 지난 이후의 모든 것은 소스에 무관하며, 새 소스가 들어와도 안정적으로 유지된다.
## 통신 세부를 하나의 트랜스포트 인터페이스 뒤에 숨기기
메시지 전송, 수신 메시지 구독, ready 프로미스와 연결 상태 노출, dispose까지 아주 작은 표면을 가진 트랜스포트 인터페이스 하나 뒤에 와이어를 숨긴다. 환경마다 한 번씩만 구현한다 — 예를 들어 임베디드 서페이스에는 인프로세스 메시지 채널을, 독립 실행형에는 재연결되는 소켓을 구현하는 식이다. 구현체 간 분기는 딱 하나의 팩토리 함수에서만 한다. 하위 코드는 모두 이 인터페이스에만 의존하며, 어떤 트랜스포트가 실제로 쓰이고 있는지는 절대 알지 못한다.
### 복원력은 구현체 안에 있다
네트워크 기반 구현체에는 지수 백오프와, 재연결 시 비워지는 전송 큐를 둔다. 인프로세스 구현체는 생명주기 내내 연결됨 상태를 보고하면 된다. 호출하는 쪽에서는 어느 쪽이든 동일한 동작으로 보이므로, 기능 코드에 트랜스포트별 분기가 들어갈 일이 없다.
## 각 통합을 프로바이더로 추가해 다른 코드를 건드리지 않기
모든 외부 통합을 하나의 인터페이스를 구현하는 프로바이더로 모델링한다: 원시 이벤트를 표준 이벤트로 정규화하고, 훅을 설치·제거·조회하고, 상태를 포맷하고, 어떤 액션 이름이 특정 규칙에서 예외인지 선언한다. 폴링 폴백이나 확장된 멀티 액터 모드 같은 선택적 기능은 옵션 메서드로 제공해, 단순한 통합은 계속 작게 유지되도록 한다.
### 깨끗한 경계인지 판별하는 기준
새 통합은 인터페이스를 구현하는 새 서브디렉터리 하나로 끝나야 하며, 런타임이나 UI, 기존 프로바이더는 전혀 수정할 필요가 없어야 한다. 소스를 추가할 때 다른 곳까지 손대야 한다면 경계가 새고 있다는 뜻이며, 그 균열 위에 더 많은 통합이 쌓이기 전에 경계를 다잡아야 한다.
## 두 서페이스가 하나의 런타임 코어와 하나의 스토어를 함께 쓰기
두 호스트 서페이스가 동일한 런타임 코어를 조합하게 하고, 퍼시스턴스 네임스페이스와 트랜스포트, 호스트 전용 어댑터만 다르게 한다. 모든 상태 변경은 데이터를 소유하고 타입이 지정된 변경 이벤트를 발생시키는, 단일 진실 공급원인 하나의 스토어를 거치게 한다. 이 이벤트를 구독하는 브로드캐스트 레이어를 하나 두고 거기서만 아웃바운드 프로토콜 메시지로 변환한다 — 다른 어떤 모듈도 트랜스포트 메서드를 직접 호출하지 않는다.
### 호스트 이음매를 좁게 유지하라
작업 실행, 폴더 열기, 가져오기·내보내기처럼 정말로 호스트에 고유한 몇 가지를 위해, 런타임과 호스트 사이에 콜백 인터페이스를 딱 하나만 둔다. 이 이음매를 좁게 유지하는 것이야말로 두 서페이스가 코어를 손대지 않고 공유할 수 있게 하는 핵심이므로, 실제로 호스트 차이가 필요로 할 때만 넓힌다.
## 푸시 신호를 먼저 쓰고, 파일 폴링은 예비로 남겨 두기
빠르고 신뢰할 수 있는 신호가 있다면 그것을 우선하고, 신호가 없을 때를 위한 더 느린 폴백을 남겨둔다. HTTP를 통한 이벤트 콜백 푸시 채널은 즉각적이고 순서가 보장된 신호를 준다. 푸시가 설치되어 있지 않을 때는 파일 폴링이 폴백이 된다. 두 방식이 절대 중복 집계되지 않도록 게이트를 둔다 — 항목별 전달 완료 플래그와 전역 토글로 폴백 타이머가 돌아갈지를 결정한다.
### 콘텐츠 읽기와 상태 도출을 분리하라
세밀한 진행 상황이나 상태 텍스트 같은 일부 데이터는 폴백 경로에서만 얻을 수 있으므로, 푸시 모드에서도 그 부분의 폴링은 계속 돌린다. 다만 해당 항목에 대해 푸시가 확인되면 폴백이 파생시키는 타이머(권한 프롬프트, 유휴 감지)는 억제한다. "콘텐츠 읽기"와 "상태 도출"을 분리하는 것이 두 모드가 충돌 없이 공존하게 하는 핵심이다.
## 상태는 서페이스별로 나눠 저장하고, 사본을 확인한 뒤 옛것 지우기
영속 상태를 서페이스별로 네임스페이스를 나눠, 두 서페이스가 동시에 실행돼도 서로의 상태를 절대 덮어쓰지 않게 한다. 파일은 원자적으로 쓴다 — 임시 파일에 쓴 뒤 이름을 바꾼다 — 그리고 자기 자신이 쓴 내용임을 표시해, 프로세스 간 워처가 이를 외부 변경으로 오인해 다시 읽지 않게 한다. 일부 플랫폼에서는 네이티브 파일 워처가 신뢰할 수 없으므로, 파일 워칭에 폴링 백업을 짝지어 둔다.
### 지우기 전에 검증하라
레거시 상태를 마이그레이션할 때는 지우기 전에 검증한다: 새 위치에 쓰고, 다시 읽어서 확인한 다음에야 기존 키를 제거한다. 아직 마이그레이션되지 않은 부분이 남아 있는 동안에는 성공을 가정하지 말고 차단되지 않는 경고를 표시한다. 사본이 디스크에 저장됐다고 확인되기 전에는 절대 진실 공급원을 삭제하지 않는다.
## 상수를 한곳에 모으고 린트 규칙으로 지키게 하기
타이밍 상수, 식별자, 프로토콜 리터럴, 스타일 토큰 같은 모든 매직 넘버와 문자열은 레이어당 하나씩, 소수의 상수 모듈에 한데 모으고 절대 인라인으로 쓰지 않는다. 인라인 값은 시간이 지나면서 어긋나고 중복되며, 나중에 리팩터링할 때 발목을 잡는 결합을 숨긴다.
### 주의력이 아니라 린트로 강제하라
이 정책을 에러 레벨로 설정한 커스텀 린트 규칙으로 뒷받침해, 위반 시 머지를 막는다: 토큰 파일 밖에서 인라인 컬러·스타일 리터럴을 금지하고, 공유 섀도·스페이싱 토큰 사용을 강제하고, 공유 폰트 참조를 강제한다. 기계가 강제하는 규칙은 기여자가 바뀌어도 살아남지만, 문서에만 적힌 관례는 그렇지 않다.
## 테스트를 계층으로 나누고, 자주 바뀌는 코드는 엔드투엔드 테스트로 지키기
레이어마다 적합한 도구를 쓰는 계층형 테스트 전략을 사용한다. 빠른 단위 테스트와 통합 테스트는 상태 변경, 이벤트 라우팅, 파싱, 마이그레이션, 인스톨러의 원자성 같은 순수 로직을 다룬다. 엔드투엔드 테스트는 실제 서페이스를 구동해 내부 구현이 아니라 사용자에게 보이는 결과를 검증한다.
### 오픈 기여를 위해 안정적인 이음매를 우선하라
UI 내부처럼 기여자가 끊임없이 손대는 코드에는, 내부 구현에 묶인 단위 테스트보다 사용자에게 보이는 동작을 고정하는 엔드투엔드 테스트를 우선한다. 그래야 리팩터링을 할 때 기여자가 기능 작업 위에 내부 테스트까지 다시 써야 하는 일이 없다. 모든 테스트는 각자의 임시 홈·워크스페이스·데이터 디렉터리를 갖도록 격리해 테스트 간에 상태가 새어나가지 않게 하고, 자동 생성된 코드를 drift-check 하듯 자동 생성된 테스트 인벤토리도 CI에서 동일하게 drift-check 한다.
## 잘못된 입력은 건너뛰어 멈추지 않게 하고, 로그 줄마다 태그 붙이기
우아하게 저하시킨다: 에러는 잡아서 로그로 남기되, 절대 호스트를 크래시시키지 않는다. 형식이 잘못된 입력 줄은 전체 스트림을 중단시키는 대신 조용히 건너뛴다. 에셋이나 리소스가 없을 때는 전체 작업을 실패시키는 대신 경고를 로그로 남기고 null이나 폴백 값으로 계속 진행한다.
### 로그를 grep하기 쉽게 만들어라
모든 로그 줄 앞에 서브시스템 이름을 담은 대괄호 컨텍스트 태그를 붙여, 평면적인 콘솔 스트림에서도 검색이 가능하게 한다. 중앙화된 에러 리포팅이나 텔레메트리가 필요한지는 의도적으로 결정한다. 필요 없다고 판단했다면 그 결정을 명시하고 로컬 로깅을 일관되게 유지해, 나중에 코드를 읽는 사람이 헷갈리지 않게 한다.
## 계속 늘어나는 로그 파일을 안전하게 읽고, 비동기 테스트에 충분한 시간 주기
이 규칙들은 말로 하기는 쉬워도, 다시 배우려면 비싼 대가를 치른다.
### 추가 전용(append-only) 파일 읽기
네이티브 파일 워처는 반드시 짧은 간격의 주기적 폴링과 함께 사용하고, 끝나지 않은 부분 줄은 읽기 사이사이 버퍼에 이어서 보관한다. 리더가 파일을 쓰는 도중에 붙잡을 수 있기 때문이다. 세션을 지우거나 리셋하면 기존 파일은 그냥 멈추고 완전히 새로운 파일이 시작되는 경우가 많으므로, 같은 핸들이 계속 늘어난다고 가정하지 말고 내용을 기준으로 전환을 감지한다.
### 테스트의 타이밍 예산
비동기 파이프라인 끝에서 결과를 검증(assert)할 때는, 트리거 신호와 파생 신호 사이의 간격, 타이머 지연, 프로세스 간 및 렌더링 여유분까지 각 단계의 지연 시간을 모두 예산에 포함시킨다. 이 합산 예산보다 짧은 타임아웃은 구조적으로 불안정(flaky)할 수밖에 없다.
### 렌더링과 배칭
빈도가 높은 애니메이션 상태는 프레임워크 상태가 아니라 명령형(imperative) 스토어에 둔다. 그리고 "작업 완료" 전환은 살짝 지연시켜, UI 배칭 때문에 짧게 지나가는 활성 상태가 사용자에게 보이지 않고 사라지는 일이 없게 한다.
