


공개 API를 깨지 않고 TypeScript 모노레포의 양방향 프로토콜 SDK 유지보수하기
클라이언트와 서버 모두 요청을 보내는, 여러 패키지로 배포되는 타입 SDK를 고칠 때 씁니다. 공개 내보내기를 의도적으로 고르고, 새 작업을 양쪽에 기능 확인과 함께 추가하며, 호환성을 깨는 변경을 기록하고, 예제를 테스트로 실행합니다.
이 AI 규칙을 사용하려면
## 변경이 어느 계층에 속하고 공개 API에 닿는지 먼저 보기
이 가이드는 프로토콜 SDK를 게시하는 다중 패키지 TypeScript 워크스페이스 내부의 작업을 규율한다: 공유 내부 코어, 클라이언트 패키지, 서버 패키지, 독립된 스키마 패키지, 그리고 얇은 프레임워크 또는 런타임 어댑터 패키지들. 엄격한 타입 검사, ES 모듈, 그리고 하나의 패키지 매니저로 구동되는 워크스페이스 전역 스크립트를 전제로 한다.
모든 결정을 지배하는 두 가지 속성이 있다. 첫째, 게시된 표면은 계약(contract)이다: 패키지 진입점에서 도달 가능한 모든 것은 공개 API이며 함부로 바꿀 수 없다. 둘째, 프로토콜은 양방향이다 — 양쪽 피어(peer) 모두 요청을 시작할 수 있으므로, 한쪽에서의 변경은 대개 다른 쪽에서의 대칭적인 대응 의무를 수반한다.
편집하기 전에, 그 변경이 어느 계층에 속하는지, 그리고 게시된 표면을 건드리는지 파악해야 한다. 만약 건드린다면, 그것을 API 변경으로 취급하여 아래 스타일 규칙뿐 아니라 표면 규칙과 마이그레이션 규칙까지 적용해야 한다.
## 작업 중에는 테스트 하나만, 끝내기 전에는 모든 검사 돌리기
워크스페이스는 저장소 전체를 다루는 집계 스크립트들을 제공한다: 의존성 설치, 전체 패키지 빌드, 린팅·포맷 검사 실행, 린팅·포맷 자동 수정, 전체 패키지 타입 검사, 전체 테스트 스위트 실행, 그리고 타입 검사와 린팅을 묶은 통합 체크 스크립트.
반복 작업 중에는 루프를 좁혀야 한다. 패키지 매니저의 필터 플래그를 사용해 단일 패키지의 테스트 스크립트를 실행하고, 파일 경로를 전달해 테스트 파일 하나만 실행하며, 이름 필터를 전달해 테스트 케이스 하나만 실행한다. 촘촘한 반복 작업을 위한 워치 모드 스크립트도 존재한다.
전체 워크스페이스 실행은 작업 완료를 선언하기 직전 순간을 위해 남겨두고, 그 시점에 통합 체크와 테스트 스위트를 모두 실행한다. 타입 검사만으로는 런타임 동작을 증명하지 못한다 — 해당 변경을 검증하는 테스트를 실행하지 않고 "작동한다"고 주장해서는 안 된다.
## 저장소의 이름 짓기와 서식 규칙 따르기
- 전체에 걸쳐 엄격한 타입 검사를 적용하며, 내보내는(export) 함수에는 명시적 반환 타입을 지정한다.
- 클래스와 타입은 파스칼케이스(PascalCase), 함수와 변수는 카멜케이스(camelCase)를 사용한다.
- 파일명은 소문자에 하이픈을 사용하며, 테스트 파일은 확장자에 test 접미사를 붙인다.
- ES 모듈 import를 사용한다. 확장자 표기는 프로젝트의 모듈 해석(resolution) 설정을 따른다 — 번들러 방식 해석에서는 상대 경로 import에 파일 확장자를 붙이지 않는다. import는 논리적으로 그룹화한다.
- 들여쓰기는 스페이스 2칸, 세미콜론은 필수, 따옴표는 작은따옴표를 우선한다.
- 테스트는 각 패키지 전용 테스트 디렉터리 아래에 두어 테스트 러너의 include 패턴에 잡히도록 하며, 테스트 이름은 설명적으로 짓는다.
- 공개 API에는 문서화 주석을 달고, 인라인 주석은 정말로 자명하지 않은 로직에만 예약해 둔다.
규칙이 애매할 때는 주변 파일의 스타일을 따른다. 기능 변경 안에서 관련 없는 코드를 재포맷하거나 리팩터링하지 않는다.
## 문서 예제는 컴파일되는 예제 파일에서 가져오기
문서화 주석 안에 손으로 작성한 코드 조각은 아무것도 그것을 컴파일하지 않기 때문에 조용히 부패한다. 대신, 나머지 소스코드와 함께 컴파일되고 타입 검사되는 동반 예제 모듈에서 예제 코드를 가져와야 한다.
동반 파일은 대상 모듈 옆에, 일치하는 이름으로 위치한다. 동반 파일 안의 이름 붙은 영역(named region)은 문서 코드 펜스에서 파일과 영역을 가리키는 source 속성을 통해 참조된다. 영역 이름은 예측 가능한 패턴을 따른다: 내보낸 심볼 이름에 변형(variant)을 붙이거나, 클래스 이름에 메서드 이름과 변형을 붙인 형태다. 영역 참조를 생략하면 파일 전체가 포함되며, 이는 어떤 파일 유형에도 적용된다.
예제를 편집한 뒤에는 스니펫 동기화 스크립트를 실행하여 현재 내용을 문서화 주석과 마크다운 페이지에 복사하고, 동기화된 결과물을 해당 변경과 함께 커밋한다.
## 공개 항목은 이름으로 내보내고 패키지 루트를 런타임 중립으로 유지하기
모든 것을 내보내고 비공개(private)로 표시되며 워크스페이스 내부의 형제(sibling) 패키지들만 소비하는 내부 배럴(barrel)과, 타입·오류 클래스·상수·가드·공유 베이스 클래스를 노출하는 정선된(curated) 공개 export 모듈을 분리해야 한다. 패키지 진입점은 이 정선된 모듈을 재export하고, 그 패키지 자체의 명시적으로 이름 붙인 export를 추가로 내보낸다.
타협 불가능한 규칙들:
- 진입점과 정선된 모듈에서는 명시적 named export를 사용한다. 그곳에서 와일드카드 재export는 절대 사용하지 않는다.
- 패키지 진입점에 심볼을 추가하는 행위는 그것을 공개 API로 만든다. 의도를 가지고서만 그렇게 해야 한다.
- 내부 헬퍼는 내부 배럴에 머물러야 하며, 정선된 모듈이나 진입점에 절대 추가하지 않는다.
- 각 패키지의 루트 진입점은 브라우저와 엣지 런타임 번들러가 소비할 수 있도록 런타임 중립을 유지한다. 모듈 그래프가 프로세스 생성이나 raw 소켓 같은 플랫폼 전용 빌트인에 전이적으로 닿는 export는 반드시 이름 붙은 서브패스 export 뒤에 두어야 하며, 루트 배럴이 그것들로부터 자유롭다는 것을 검증하는 테스트로 커버되어야 한다.
- 특정 검증 라이브러리의 타입으로부터 자유롭다고 약속한 게시 표면에는 해당 타입을 넣지 않는다. 대신 전용 스키마 패키지에서 원시 런타임 스키마를 게시하고, 형제 패키지들은 스키마의 사설 사본을 들고 다니는 대신 그 패키지를 실제 외부 의존성으로서 해석(resolve)하게 한다.
## 스키마 계층을 기반으로 하고 전송 계층을 교체 가능하게 두기
SDK를 세 개의 계층으로 구성한다.
1. 타입과 스키마 계층 — 사양(specification)에 맞춘 프로토콜 메시지 타입, 검증 스키마, 상수들. 이것이 단일 진실 공급원(single source of truth)이며, 다른 모든 계층은 여기서 파생된다.
2. 프로토콜 계층 — 메시지 라우팅, 요청과 응답의 상관관계 매칭, 능력(capability) 협상, 전송 계층의 수명주기를 처리하는 추상 베이스 클래스. 양쪽 피어 구현체 모두 이를 확장하므로, 양방향 동작이 구조적으로 대칭이 된다.
3. 고수준 API — 타입이 지정된 메서드를 노출하는 클라이언트와 서버 클래스, 그리고 도구·리소스·프롬프트의 단순화된 등록을 제공하는 더 고수준의 서버 래퍼.
전송 계층은 하나의 인터페이스 뒤에 있는 별도의 플러그형(pluggable) 계층이다: 원격 피어에 권장되는 스트리밍 HTTP 전송, 하위 호환성을 위해 유지되는 레거시 이벤트 스트림 전송, 그리고 로컬에서 생성된(spawn) 프로세스 통합을 위한 표준 입출력(stdio) 전송. 검증 방식도 같은 정신으로 플러그형이며, 기본 검증기와 함께 제한된 런타임과 호환되는 대안들이 존재한다.
프레임워크와 런타임 어댑터 패키지는 얇은 통합 계층이다. 이들은 절대 프로토콜 기능을 추가해서는 안 되며, 프로토콜 동작을 바꾸는 것은 무엇이든 코어 계층에 속해야 한다.
## 양쪽 피어에서 요청이 나가고 들어오는 흐름 파악하기
아웃바운드(발신). 고수준 메서드는 베이스 요청 메서드에 위임하고, 이 베이스 메서드는 고유한 메시지 식별자를 부여하며, 원격 피어가 해당 메서드에 필요한 능력을 선언했는지 단언(assert)하고, 대기 중인 응답 핸들러를 등록하며, 직렬화된 요청을 전송 계층에 넘기고, 일치하는 응답이 도착하면 그 해결(resolution)을 기다린다.
인바운드(수신). 전송 계층이 수신 메시지를 표면화(surface)하면 베이스 라우팅이 이를 요청, 응답, 또는 알림(notification)으로 분류하여 처리한다. 요청 처리는 메서드 이름을 키로 하는 핸들러를 조회하고, 컨텍스트 객체를 구성하며, 하위 클래스가 그 컨텍스트를 전송 계층 특유의 세부 정보로 보강하도록 허용하고, 핸들러를 호출한 뒤, 핸들러 결과를 전송 계층을 통해 응답으로 반환한다.
컨텍스트의 형태. 세션 식별자; 메시지 식별자·메서드 이름·요청 메타데이터·취소를 위한 abort 시그널·피어에게 관련 요청이나 알림을 다시 보내기 위한 헬퍼들을 담은 요청 그룹; 그리고 전송 계층이 제공할 경우 검증된 인증 정보를 담는 선택적 전송 그룹. 서버 측 컨텍스트에는 로깅, 사용자 입력 유도(elicitation), 모델 샘플링 헬퍼가 추가된다.
능력(capability) 강제는 양방향으로, 그리고 핸들러 등록 시점에도 실행되므로, 피어는 자신이 선언한 적 없는 것에 대한 핸들러를 등록할 수 없다.
## 양쪽에 기능 확인을 넣어 새 프로토콜 작업 추가하기
1. 타입 계층에 요청 파라미터 스키마와 결과 스키마를 정의한다.
2. 클라이언트 또는 서버 능력(capability) 타입에 해당하는 능력 플래그를 추가한다.
3. 오퍼레이션을 개시하는 쪽 피어에 발신 메서드를 구현한다.
4. 해당 피어의 능력 검사 안에 능력 단언(assertion)을 추가한다.
5. 반대쪽 피어에 수신 핸들러를 등록한다.
6. 오퍼레이션이 그럴 가치가 있을 때는 단순화된 서버 API에 고수준 래퍼를 추가한다.
7. 양쪽에 테스트를 추가하고, 오퍼레이션이 사용자 대면(user-facing)일 경우 실행 가능한 예제도 추가한다.
2단계와 4단계가 가장 자주 건너뛰어지는 단계이면서 건너뛸 때 가장 비용이 큰 단계다: 능력 협상이 없는 오퍼레이션은, 초기화 시점에 깔끔하게 실패하는 대신 그것을 구현하지 않은 피어를 상대로 런타임에 실패한다.
## 호환성을 깨는 변경을 무엇·왜·어떻게 옮기는지로 기록하기
모든 호환성 파괴(breaking) 변경은 그것이 영향을 미치는 하위 시스템 섹션 안에, 마이그레이션 가이드에 기록한다. 관련 섹션을 찾아서 새 독립 섹션을 덧붙이는 대신 그 섹션을 확장한다 — 병렬적인 일회성 항목들로 이루어진 가이드는, 정작 누군가가 그것을 필요로 하는 바로 그 순간에 읽을 수 없게 되어버린다. 단일 릴리스 노트 파일 범위에 국한된 변경은 그쪽에 대신 기록한다.
모든 항목은 세 가지를 명시한다: 무엇이 바뀌었는지, 왜 바뀌었는지, 어떻게 마이그레이션하는지.
기계적인 이름 변경(rename)은 자동화된 마이그레이션 도구의 매핑 파일에 담는다. 이름 변경 표를 산문(prose) 안에 재현하는 대신 그 매핑으로 링크한다. 중복된 표는 실제로 이름 변경을 수행하는 도구와 동기화가 어긋나 버린다.
판단 기준: 추가적인(additive) 변경을 선호한다. 파괴적 변경이 불가피할 때는, 가능한 경우 한 주기 동안은 지원 중단(deprecation) 처리를 한 채로 기존 경로를 계속 작동시키고, 그 파괴가 조용한 동작 변화가 아니라 명확한 컴파일 타임 오류로 드러나도록 설계한다.
## 서버와 클라이언트 짝 예제를 CI에서 테스트로 실행하기
예제를 장식이 아니라 테스트로 취급한다. 각 예제는 짝을 이루는 서버와 클라이언트를 담은 그 자체의 워크스페이스 패키지다. 클라이언트는 접속하여 기대하는 결과를 단언(assert)하고, 불일치 시 실패 상태로 종료한다. 러너는 구성된 전송 계층과 프로토콜 버전(era) 조합 전체에 걸쳐 모든 예제를 실행하며, 그 작업은 변경 단위(per-change) 지속적 통합(CI) 게이트의 일부다.
공유되는 예제 배관(plumbing)은 루트 export를 최소한으로 유지하는 하나의 공유 패키지에 위치한다 — 인자 파싱, 단언 헬퍼, 형제 경로 해석 등. 시연용 인증 프로바이더나 인메모리 이벤트 스토어처럼 더 무거운 선택적 의존성은 서브패스 export 뒤에 두어, 관련 없는 예제들이 그것들을 무심코 로드하지 않도록 한다. 예제들은 이 배관만을 import하고 전송 계층은 인라인으로 직접 설정하므로, 각 예제는 숨겨진 설정 위의 래퍼가 아니라 실제 사용례처럼 읽힌다.
문서 가이드 페이지는 같은 스니펫 동기화 단계로 동기화되는 페이지별 스니펫 동반 파일을 갖고, 실행 가능한 것들은 지속적 통합에서도 함께 실행된다.
