스폰서LobeHubLobeHub자세히 알아보기
dshfind

12과: 컴포넌트 로더와 Koishi 사례

한 문장 요약: 이번 과에서는 「컴포넌트 로더」를 다룹니다. 로더는 오케스트레이터가 적어 내린 「어떤 컴포넌트가 필요한가」라는 선언적 설정을, 실행 중인 파이버에 대한 최소한의 변경으로 번역합니다. 엔트리는 필드 단위로 증분 리컨실되고, 코드 변경은 HMR을 통해 재시작 없이 핫스왑됩니다. 그리고 4000+ 개의 커뮤니티 플러그인을 보유한 Koishi가 실제 프로덕션 환경에서 이 설계의 표현력과 범용성을 검증했습니다.


1. 왜 「선언적 설정 레이어」가 필요한가?

먼저 역할 분담을 복습해 봅시다. 앞의 과에서 우리는 Cordis 코어 라이브러리의 명령형 프리미티브를 알아봤습니다: ctx.effect(이펙트 설치), ctx.use(컴포넌트 로드), ctx.set(서비스 제공)——이것들은 컴포넌트 개발자가 컴포넌트 낶에서 코드를 작성할 때 사용하는 도구입니다.

그러나 「애플리케이션 오케스트레이터」(기성 컴포넌트들을 조립해 하나의 실행 시스템으로 만드는 사람)가 직면하는 것은 다른 종류의 문제입니다.

「컴포넌트를 어떻게 작성하는가」가 아니라, 「시스템에 어떤 컴포넌트가 필요하고, 그 조합이 시스템 수명 주기 동안 어떻게 변하는가」입니다.

Cordis의 답은 선언적 설정 레이어를 도입하는 것입니다.

  • 오케스트레이터는 영속화된 데이터 구조로 「내가 원하는 조합」을 기술합니다——원하는 것을 선언하는 일만 담당합니다.
  • 로더는 이 명세에 대한 모든 변경을 대응하는 명령형 파이버 연산으로 번역합니다——실행을 담당합니다.

비유하자면, 오케스트레이터는 「발주자」로 설계도면의 요구 사항만 변경하고, 로더는 「시공팀」으로 도면의 수정 사항 하나하나를 이미 지어진 건물에 정확히 반영하는 역할입니다——걱물을 허물고 다시 짓는 것이 아니라.

1.1 설정 트리: 엔트리는 「작업 지시서」

정의 26에 따륩, 엔트리(entry)는 하나의 파이버를 선언하고 다음 필드들을 기록합니다.

필드의미쉬운 설명
id안정적인 식별자. 소속 그룹의 하위 엔트리 목록이 변할 때 리컨실 키로 사용됨사람을 구분하는 데 쓰는 ID 번호
url인스턴스화할 컴포넌트 모듈의 URL이 컴포넌트 코드를 어디서 가져올지
isolate이 엔트리의 컨텍스트에 적용되는 격리 어노테이션컴포넌트에 구획을 배정함
intercept이 엔트리의 컨텍스트에 적용되는 인터셉트 어노테이션컴포넌트 입구에 감시 장치를 설치함
config컴포넌트에 바인딩되는 설정으로, 컴포넌트의 이펙트 함수 apply를 구성함컴포넌트의 「사용 설명서」
disabled이 엔트리가 관리적으로 비활성화되었는지 여부정지 스위치

한 문장으로 기억하세요. 엔트리 = 파이버가 어떤 모습이어야 하는지를 선언하는 한 장의 「작업 지시서」. 런타임에는 이 엔트리가 선언한 파이버를 관리하고 필드 변화에 응답합니다.

이러한 엔트리들이 조직화되어 하나의 설정 트리를 구성합니다. 이것은 「시스템이 현재 무엇을 로드했는가」에 대한 권위 있는 기록입니다.

  • 리프 노드 엔트리: 단일 파이버에 대응합니다.
  • 브랜치 노드 엔트리: 해당 컴포넌트가 더 많은 컴포넌트를 계속 로드하므로 서브트리가 자라납니다.

Cordis는 또한 두 개의 전용 컴포넌트를 제공합니다. @cordisjs/group은 하위 엔트리 목록을 설정으로 받아 서브그룹으로 로드하고, @cordisjs/include는 외부 설정 파일(YAML 또는 JSON)을 로드하여 파일 안의 엔트리를 중첩된 서브트리로 접목합니다.

1.2 증분 리컨실리에이션: 변화한 필드만 처리하기

설정이 바뀌면 로더는 어떻게 실행할까요? 트리 전체를 허물고 재구축하는 일은 결코 없고, 대신 증분 리컨실리에이션을 수행합니다. 변화한 필드를 검사하고, 필드마다 가장 지장이 적은 연산을 실행합니다.

변화한 필드로더의 동작
id, url해당 엔트리를 재구축——아이덴티티 또는 컴포넌트가 바뀌었기 때문
isolate마이그레이션: 해당 엔트리의 도메인 매핑 테이블을 다시 쓰고, 제공하는 모든 코이펙트를 이전하며, 해석 결과가 변한 의존 측에 통지
intercept제자리에서 갱신——인터셉트 메타데이터는 읽을 때만 조회되므로 재로드가 불필요
config컴포넌트에 처리를 맡김. 일반적으로 이전 페이로드와 비교해 diff를 하고, 실질적인 변화가 있을 때만 재로드
disabled참으로 설정 → 파이버 폐기. 해제 → 파이버 재로드

🎁 놓치기 쉬운 묘미가 하나 있습니다. @cordisjs/groupconfig가 바로 하위 엔트리 목록이므로, 로더는 하위 엔트리의 id를 키로 diff를 하여 각 하위 엔트리를 생성·제거·갱신합니다. 그리고 「아직 존재하는 하위 엔트리의 갱신」은 다시 같은 필드 단위 디스패치 과정으로 들어갑니다——즉 그룹 리컨실리에이션과 엔트리 갱신은 트리를 따라 재귀적으로 납려갑니다. 하나의 규칙 세트가 어디에나 적용됩니다.

또한 Cordis는 컴포넌트가 런타임에 스스로 config를 갱신하거나 스스로 비활성화하는 것도 허용합니다. 어느 경우든 로더는 변경을 설정 레이어에 다시 기록합니다——「영속화된 명세」가 항상 「실행 중인 시스템」을 충실히 반영하여 양쪽이 결코 어긋나지 않도록 하기 위해서입니다.


2. HMR: 재시작 없이 코드를 바꾸는 세 단계

HMR(핫 모듈 리플레이스먼트, Hot Module Replacement) 은 「롤백 가능한 이펙트」 패턴을 모듈 계층에 적용한 것입니다.

소스 파일이 바뀌면(보통 개발 중), 시스템은 영향받는 모듈을 제자리에서 교체하고 프로세스를 재시작하지 않습니다.

Cordis는 왜 이것이 가능할까요? 파이버가 이미 컴포넌트의 모든 이펙트와 코이펙트에 경계를 그어 놓았기 때문입니다. 오래된 파이버를 폐기하는 것 = 컴포넌트가 설치한 모든 것을 자동으로 되돌리는 것. 재로드된 모듈에서 새 파이버를 인스턴스화하는 것 = 모든 것을 다시 설치하는 것. 모듈 자체가 곧 컴포넌트이므로, 교체에는 두 개의 파이버 연산만 필요합니다.

Webpack / Vite와의 비교: 이들의 HMR은 개발자가 accept 같은 수용 경계를 직접 작성해야 합니다(빌드 도구에 「이 모듈은 핫 리플레이스를 허용한다」고 알려주는 것). 빠뜨리면 전체 페이지 새로고침으로 퇴화합니다. Cordis의 HMR은 개발자 어노테이션이 전혀 필요 없습니다.

@cordisjs/hmr 컴포넌트가 HMR 엔진을 제공하며, 세 단계로 실행됩니다.

단계 1: 모듈 분류(알고리즘 7)

엔진은 두 개의 입력을 받습니다.

  • stash 집합(stashed): 지난 재로드 이후 내용이 변경된 파일의 URL.
  • 외부 모듈 집합(externals): 핫 리플레이스가 불가능하여 완전한 재시작을 유발하는 모듈.

그런 다음 변경과 관련된 의존 서브그래프에 대해 고정점 계산을 수행하여 각 모듈에 「수용됨(accepted)」 또는 「거부됨(declined)」 표시를 합니다.

규칙결론
어떤 모듈의 임포트 중 하나라도 수용됨→ 그것을 수용
어떤 모듈의 모든 임포트가 거부됨→ 그것을 거부
계속 미정 상태로 임포트 사이클에 갇힌 모듈기본적으로 거부로 분류

(「고정점」이란, stash된 파일의 임포트를 시드로 삼아 반복적으로 퍼뜨리다가 더 이상 새 모듈에 표시할 수 없는 라운드에 도달하는 것을 말합니다.)

단계 2: 무효 엔트리 검출(알고리즘 8)

분류가 끝나면 엔진은 「수용됨 / 거부됨」을 이용해 컴포넌트 엔트리를 선별하고, 의존 트리가 변경된 모듈에 도달할 수 있는 무효 엔트리만 남깁니다.

  • get_dependencies는 의존 트리를 따라 모듈의 전이 임포트를 수집하되, declined 모듈을 만나면 멈춥니다(그것이 순회 경계입니다).
  • 엔트리는 그 의존 트리가 accepted와 교차할 때 정확히 무효화됩니다. 이후 그 트리는 accepted에 병합됩니다——따라서 경로상의 모든 무효 모듈이 다음 단계에서 폐기됩니다.

단계 3: 트랜잭셔널 재로드(알고리즘 9)

마지막으로 엔진은 먼저 백업하고, 그다음 손을 대고, 실패하면 롤백하는 절차를 따릅니다.

function reload(ctx, accepted, staleEntries) {
  const backup = invalidateCaches(accepted);                       // ① accepted 모듈의 캐시를 무효화하고 백업
  try {
    for (const entry of staleEntries) {
      entry.fiber.dispose();                                        // ② 오래된 파이버 폐기: 설치한 모든 것을 자동으로 되돌림
      entry.fiber = ctx.use(import(entry.url), entry.config);      // ③ 새 모듈을 임포트하고 새 파이버로 교체
    }
  } catch (error) {
    restoreCaches(backup);                                          // ④ 실패 시: 먼저 캐시를 복원
    for (const entry of staleEntries) {
      entry.fiber.dispose();
      entry.fiber = ctx.use(backup[entry.url], entry.config);      // ⑤ 백업 안의 옛 컴포넌트로 각 무효 엔트리를 재구축
    }
    throw error;                                                    // ⑥ 에러를 그대로 다시 던짐
  }
}

트랜잭셔널 보장: 시스템이 「절반만 완료된 재로드」 상태에서 멈추는 일은 결코 없습니다. 어느 모듈이든 임포트에 실패하면(예: 구문 오류), 캐시가 복원되고 backup[entry.url](막 캐시가 복원된, 재로드 이전의 컴포넌트)로 모든 무효 엔트리가 재구축됩니다——완료된 교체는 전부 일괄 취소되어, 아무 일도 없었던 것처럼 됩니다.

💡 잡학 지식: Node.js에서 「캐시를 무효화한다」는 것은 ES 모듈과 CommonJS 두 모듈 시스템의 캐시를 동시에 비우는 것을 의미합니다——ES 로더를 통해 임포트된 모듈이 양쪽 모두에 나타날 수 있기 때문입니다.

세 단계를 이으면 아래와 같은 파이프라인이 됩니다.

① 模块变化改代码保存② 分类接受 / 拒绝(依赖子图)③ 失效条目依赖树可达已变更模块④ 事务性重载(失败)自动回滚)全程不重启:旧纤程释放(恢复效应),新纤程装上

分类 → 失效检测 → 事务性重载:与 Webpack/Vite 不同,无需手写接受边界


3. Koishi: 4000+ 플러그인으로 검증한 프로덕션

앞의 두 절은 「설계」 이야기였고, 이번 절에서는 「실전」을 봅니다. Koishi는 Cordis 위에 구축된 오픈소스 챗봇 애플리케이션 프레임워크입니다. 4년 넘는 개발을 거쳐 4000+ 개의 커뮤니티 기여 플러그인을 축적했으며, 인스턴트 메시징(IM) 어댑터, 데이터베이스 드라이버, 관리 콘솔, 최종 사용자용 기능을 아우릅니다. 이 규모와 다양성이 프로덕션 환경에서 Cordis의 동적 컴포저빌리티를 검증하는 대표 사례로 만들어 줍니다.

📌 용어 팁 두 가지: Koishi는 현재 Cordis v3를 사용하고, 논문이 소개하는 것은 이펙트/코이펙트 의미론을 정교화하고 로더를 재설계한 v4입니다. 둘은 핵심 컴포지션 모델을 공유합니다. 또한 Koishi의 「플러그인(plugin)」은 논문에서 형식화한 「컴포넌트(component)」 그 자체입니다.

Koishi는 Cordis 모델의 두 묶음의 속성을 뒷받침합니다.

3.1 표현력과 범용성: 하나의 모델, 두 개의 세계

  • 표현력: Koishi는 서버 사이드 봇으로 실행되며, 그 모든 능력은 컨텍스트 프리미티브 위에 플러그인 형태로 구현됩니다. Koishi 자체는 챗봇 도메인의 어휘만 제공합니다——프리미티브가 완전한 프로덕션 시스템을 감당하기에 충분하다는 것을 보여줍니다.
  • 범용성: 같은 모델이 전혀 다른 런타임에도 나타납니다——Koishi의 웹 콘솔은 또 다른 독립적인 Cordis 애플리케이션으로, 그 플러그인들은 서버 프리미티브가 아니라 브라우저와 사용자 인터페이스의 프리미티브를 조합합니다.

같은 컴포지션 모델이 「서버 사이드 봇」과 「브라우저 UI」라는 두 세계에서 동시에 돌아가고 있다——이것이 바로 범용성의 직접적인 증거입니다. 모델은 이펙트와 코이펙트가 어떻게 조합되는가만을 규정하고, 그 의미는 각 애플리케이션이 결정하도록 남겨 둡니다. 따라서 도메인도 런타임도 전제하지 않습니다.

3.2 시간적 컴포저빌리티: 플러그인 켜고 끄기, 인지 오버헤드 제로

논문의 1.2.1절을 기억하시나요? 전통적인 플러그인 시스템은 호스트를 재시작하지 않고는 단일 확장의 이펙트를 언로드할 수 없습니다. Koishi는 이것을 일상적으로 합니다.

  • 오케스트레이터가 콘솔에서 어떤 플러그인을 비활성화 → 그 이펙트는 즉시 제자리에서 취소됨.
  • 개발 중에는 HMR 엔진이 저장 시 편집된 플러그인을 재적용하면서, 시스템 나머지 부분의 캐시 상태와 활성 연결을 그대로 유지.

핵심은 플러그인 작성자가 이를 위해 추가 작업을 거의 하지 않아도 된다는 점입니다. 컨텍스트를 통해 설치된 이펙트는 자동으로 추적되고, 그 역함수도 자동으로 합성됩니다——경험이 부족한 작성자가 언인스톨 경로를 작성하지 않았더라도, 플러그인에서 컨텍스트를 통해 설치된 이펙트는 순서대로 정리됩니다. 「관심의 국소성」이라는, 원래라면 작성자 한 사람 한 사람의 성실함에 의존해야 했던 정확성 요구 사항이, 이 추상화에 의해 한꺼번에 통일적으로 이행됩니다.

3.3 공간적 컴포저빌리티: 서로 다른 작성자의 플러그인이 조합된다

전통적인 플러그인 시스템은 대부분 플러그인 간 의존성이 부족합니다. 반면 Koishi 생태계는 진정한 의존 토폴로지를 보입니다.

  • IM 어댑터가 각종 메시징 플랫폼에 대한 접근을 제공합니다.
  • 데이터베이스 드라이버가 영속화 스토리지를 제공합니다.
  • 기능 플러그인이 이 능력들을 코이펙트로 선언하고 접근합니다.

런타임에 프로바이더를 재설정핟(스토리지 백엔드 전환, 어댑터 재접속 등) 재활성화되는 것은 해석 결과가 변한 의존 측뿐입니다. 의존 항목이 일시적으로 사용 불가능한 플러그인은 해당 의존 항목이 나타날 때까지 비활성 상태를 유지합니다——에러가 발생하지 않습니다.

그리고 플러그인과 그 의존 항목은 보통 서로 다른 작성자가 작성하며, 작성자들은 둘을 잇는 코이펙트 외에는 아무것도 조율할 필요가 없습니다. 즉, 리액티브 코이펙트가 이 복합체를 독립적인 기여자들로 이루어진 오픈 생태계 속에서 일관되게 유지하는 것입니다.


4. 타당성에 대한 위협: 이 증거는 얼마나 견고한가?

논문은 끝부분에서 정직한 방법론적 자기 점검을 수행하며, 이 사례 연구의 두 가지 한계를 지적합니다.

  1. 단일 호스트 언어: 증거는 단일 호스트 언어(TypeScript)의 단일 생태계에서 나왔으므로, 「패러다임 자체의 장점」을 「TypeScript 구현이나 Koishi 특유의 도메인의 장점」과 구분할 수 없습니다.
  2. 관찰적 증거: 증거는 관찰에서 나온 것이지, 대안 아키텍처와의 통제된 비교 실험이 아닙니다.

따라서 결론은 정확하게 읽어야 합니다. 이 사례 연구가 증명하는 것은 해당 패러다임이 존재하며 채택되었다는 것이지, 정량적 결과가 아닙니다——어떤 기준선을 기준으로 이 추상화의 오버헤드와 개발자 생산성에 대한 영향을 측정하는 것은 미래의 과제로 남아 있습니다.

⚠️ 쉽게 말하면: 「유명 셰프의 요리가 맛있다」는 것은 레시피가 통한다는 증명이 되지만, 「다른 레시피보다 30% 더 맛있다」고 말하려면 더 엄밀한 대조 실험이 필요합니다.


핵심 포인트 복습

이번 과는 정보량이 적지 않지만, 다음 다섯 문장만 기억하면 충분합니다.

  1. 선언적 설정 레이어: 오케스트레이터는 「어떤 컴포넌트가 필요한가」를 설정 트리로 적어 둡니다. 엔트리(id / url / isolate / intercept / config / disabled)는 하나의 파이버를 선언하고, 로더가 설정 변경을 파이버 연산으로 번역합니다.
  2. 증분 리컨실리에이션: 변화한 필드만 처리합니다——id/url은 재구축, isolate는 마이그레이션, intercept는 제자리 갱신, config는 diff, disabled는 언로드/로드. 트리 전체 재구축은 결코 하지 않습니다.
  3. HMR의 세 단계: 모듈 분류(고정점: 임포트 중 하나라도 수용되면 수용, 전부 거부되면 거부, 사이클 내는 기본 거부) → 무효 엔트리 검출(의존 트리가 변경 모듈에 도달하는 엔트리가 무효화) → 트랜잭셔널 재로드(먼저 백업, 새 파이버로 교체, 실패 시 롤백). 전 과정에서 재시작이 없고, 손으로 쓴 accept 경계도 필요 없습니다.
  4. Koishi 검증: 4000+ 플러그인의 프로덕션 시스템에서, 서버 사이드 봇과 웹 콘솔이라는 두 개의 서로 다른 런타임이 모두 Cordis로 돌아갑니다——표현력, 범용성, 인지 오버헤드 없는 시간적 컴포저빌리티, 오픈 생태계를 가로지르는 공간적 컴포저빌리티를 뒷받침합니다.
  5. 타당성에 대한 위협: 증거는 단일 호스트 언어 + 관찰에서 나왔으며, 「패러다임이 존재하고 채택되었다」는 것을 증명할 뿐 정량적 결론은 아닙니다.

🚀 다음 과(13과)에서는 논문의 마무리——논의, 관련 연구, 결론——로 들어가, 「시공간 컴포저빌리티」 패러다임을 더 넓은 좌표계 안에서 살펴올립니다.

셀프 퀴즈 · 로더와 Koishi

답을 모두 선택한 후 「답안 제출」을 클릭하면 정답 여부와 해설을 확인할 수 있습니다.

1. 로더가 「증분 리컨실리에이션」을 수행하다가 어떤 엔트리의 url 필드가 변한 것을 발견했습니다. 어떻게 해야 할까요?
2. Cordis의 HMR 엔진은 세 단계로 실행됩니다. 올바른 순서는 무엇일까요?
3. 트랜잭셔널 재로드 단계에서 새 모듈 임포트가 실패하면(예: 구문 오류) 무슨 일이 일어날까요?
4. Koishi 사례에서 Cordis 모델의 「범용성」을 가장 잘 보여주는 증거는 무엇일까요?