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

11과: Cordis 코어 라이브러리: 이펙트 추적과 코이펙트 해석

한 줄 요약: 이번 과에서는 논문을 「수식에서 코드로」 옮깁니다——Cordis 코어 라이브러리는 모든 컨텍스트 변경을 단 하나의 프리미티브 ctx.effect(자동 추적, 언제든 취소 가능)로 수렴시키고, 그 위에 코이펙트의 읽기와 쓰기, 컴포넌트의 로드와 언로드를 쌓아 올린 뒤, 마지막으로 Proxy로 「선언하지 않은 것은 아예 건드릴 수 없다」를 보장합니다.

1단계: 먼저 대응표 맞춰보기——이론 기호가 코드에서는 어떻게 생겼는가

10과에서 우리는 「컨텍스트 패러다임」이라는 이론을 알아봤습니다. 이번 과부터는 논문 제4장으로 들어갑니다: Cordis가 이 이론을 실제로 어떻게 구현하는가입니다.

논문 제3절의 기호들(Γ∞, 𝔈Γ, ℭΓ……)은 무서워 보이지만, 사실 각각에 대응하는 「프로그래머의 오랜 친구」가 있습니다. 코어 라이브러리가 가장 먼저 하는 일은 이 대응 관계를 하나의 표로 못 박는 것입니다——논문의 표 1을 아래처럼 단순화했습니다.

이론(제3절)구현(제4절)한 줄 설명
Γ∞(컨텍스트 타워)ctx, 일급 컨텍스트컴포넌트가 공유하는 「공용 칠판」
𝔈Γ / 𝔈Γiter(이펙트)역변환을 반환하거나 차례로 산출하는 이펙트 콜백「취소 설명서가 딸린」 코드
effectiter Γ(𝑒)ctx.effect(callback)칠판을 수정하는 「유일한 입구」
Σ / Σiso / Σinterctx[@@store] / ctx[@@isolate] / ctx[@@intercept]칠판 위의 세 서랍
get(𝑘) / set(𝑘, 𝑣)ctx.get(key) / ctx.set(key, value)값 읽기 / 값 쓰기
isolate(𝑘, 𝑟)ctx.isolate(key, realm)같은 키에 「서랍을 하나 더 여는」 것
intercept(𝑘, 𝜈)ctx.intercept(key, metadata)값 조회에 「필터를 씌우는」 것
ℭΓ(컴포넌트 인스턴스)fiber(파이버)컴포넌트의 「런타임 신분증」
𝑑 ∶ 𝔇Γfiber.inject컴포넌트가 「내게 필요한 것」을 선언
𝑒 ∶ 𝔈Γfiber.apply컴포넌트가 「내가 할 일」을 말함
𝜀𝑑(𝜎)fiber.epoch목표 상태의 「버전 번호」
recoverfiber.dispose(누적된 역변환)실행 대기 중인 「취소 목록」

이 표를 보고 나면 세 가지 번역어부터 기억하세요. 이후에는 계속 이 이름을 씁니다.

  • ctx = 일급 컨텍스트: 바로 그 「공용 칠판」으로, 모든 컴포넌트가 그 위에서 읽고 씁니다.
  • 이펙트 콜백 = 이펙트: 「칠판을 변경하면서 동시에 취소 방법도 제공하는」 코드입니다.
  • fiber(파이버) = 컴포넌트의 런타임 인스턴스: 컴포넌트가 인스턴스화된 뒤 메모리에 살아 있는 상태를 가진 객체입니다.

표의 두 표기에 대해 논문이 특별히 주의를 환기합니다.

  1. @@name심벌 키를 나타냅니다: ctx[@@store]의 대괄호는 「심벌 키로 컨텍스트의 불투명 슬롯에 접근한다」는 뜻이지, 문자열을 키로 하는 맵에 인덱싱하는 것이 아닙니다.
  2. fiber는 하나의 객체에 두 종류의 것을 담습니다: 정적 명세(fiber.inject가 의존성을 선언, fiber.apply가 이펙트 함수, 그리고 fiber.parent가 부모 컨텍스트, fiber.ctx가 부모에서 파생된 자식 컨텍스트)와 실시간 전이 상태(fiber.dispose가 누적된 역변환, fiber.epoch이 목표 버전 번호, fiber.inertia가 진행 중인 마이그레이션 핸들)입니다.

코어 라이브러리는 바닥부터 쌓아 올린 4층 건축입니다.

① 되돌릴 수 있는 이펙트(ctx.effect)   ← 기초: 컨텍스트를 수정하는 유일한 프리미티브
② 리액티브 코이펙트(get / set)        ← 1층: 이펙트 위에 「읽기/쓰기」를 만든다
③ 컴포넌트 라이프사이클(use)          ← 2층: 앞의 둘을 조합해 컴포넌트의 일생으로 만든다
④ 컨텍스트 접근(Proxy)                ← 최상층: 호스트 언어에 더 가까운 사용법

이제 기초부터 시작해 한 층씩 올라가 봅시다.

2단계: ctx.effect——모든 컨텍스트 변경의 「유일한 입구」

먼저 논문 4.1.1의 핵심 주장을 기억하세요.

Cordis에서 모든 컨텍스트 변경은 동일한 프리미티브 ctx.effect를 거칩니다. 코이펙트 공급(set), 컴포넌트 인스턴스화(use)…… 컨텍스트를 수정하는 모든 작업은 최종적으로 한 번의 ctx.effect 호출로 귀결됩니다.

이것이 의미하는 바는 무엇일까요? 컨텍스트를 통해 실행되는 모든 작업이 자동으로 추적되며, 컴포넌트가 언로드될 때 전부 자동으로 복원됩니다. 「이번 변경을 어떻게 취소해야 하는지」를 기억할 필요가 없습니다——ctx.effect가 대신 기억해 줍니다.

콜백 = 이펙트 이터레이터: 매 단계마다 「역변환」을 산출

ctx.effect는 콜백 하나를 받아 이펙트 이터레이터로 구동합니다. 콜백이 한 단계 yield할 때마다 「역변환」——즉 「이 단계를 어떻게 취소하는가」의 설명서——를 하나 건넵니다. 일반 이펙트 함수는 「역변환을 딱 하나만 산출하는 퇴화된 이터레이터」일 뿐이므로, 같은 입구가 일반 함수와 이터레이터를 구분 없이 모두 받습니다.

알고리즘 1의 구조를 단순화하면 다음과 같습니다.

// 실행 엔진: 콜백을 이터레이터로 구동하고, 각 단계의 역변환을 하나의 복합 역변환으로 접는다
async function execute(callback, guard) {
  const iter = callback();                 // 콜백이 이터레이터가 된다
  let inverse = id;                        // 역변환은 「빈 연산」부터 누적을 시작
  while (guard()) {                        // 한 걸음 나아가기 전에 먼저 가드에게 묻는다: 계속핏도 될까?
    const { value, done } = await iter.next();
    if (value) inverse = compose(value, inverse); // 새 역변환을 「앞에 붙여」 누적
    if (done) break;
  }
  return inverse;                          // 접힌 「복합 역변환」을 반환
}

// ctx.effect: execute 위의 가벼운 래퍼 한 겹
function effect(ctx, callback) {
  let armed = true;                        // ① 무장 플래그: 아직 한 번 복원할 수 있다
  const task = execute(callback, () => armed); // 가드는 바로 armed 그 자체

  async function dispose() {
    if (!armed) return;                    // 이미 복원했다면? 바로 종료(멱등)
    armed = false;                         // 먼저 무장 해제: 아직 실행 중인 이터레이션을 중단
    const recover = await task;            // ② 누적된 역변환을 꺼낸다
    recover();                             // ③ 호출 = 이펙트 전체를 한 번에 복원
  }

  ctx.dispose = compose(dispose, ctx.dispose); // ④ 부모 컨텍스트에 앞에 붙인다(LIFO)
  return dispose;                          // dispose를 호출자에게 넘긴다
}

이 코드에는 세 가지 핵심 설계가 있습니다. 하나씩 뜯어봅시다.

설계코드에서의 위치어떤 문제를 해결하는가
콜백이 역변환을 산출yield의 value「변경」에 「취소 설명서」를 붙여, 무엇을 바꿨든 되돌릴 수 있게 한다
dispose 클로저 반환return dispose호출하면 곧 복원. dispose를 누구에게 넘기느냐에 따라 취소권을 누가 쥐는지가 정해진다
멱등한 자기 해제armed 플래그 + 가드복원은 최대 한 번만 발화하고, 반복 호출은 안전한 빈 연산

멱등성: armed 플래그 하나가 두 가지 일을 한다

armed는 처음에 true이며, 가드스위치를 동시에 겸합니다.

  • armedtrue인 동안은 execute 안의 이터레이션이 계속 진행될 수 있습니다.
  • dispose가 호출되는 순간 먼저 armedfalse로 설정합니다——한편으로는 아직 진행 중인 이터레이션을 모두 중단하고, 다른 한편으로는 복원이 최대 한 번만 발화하도록 보장합니다.

이것이 논문 정의 21의 「멱등」(idem)입니다: dispose를 몇 번을 호출하든 한 번 호출한 것과 동등합니다.

부모 컨텍스트 합성: dispose를 앞에 붙여 LIFO와 캐스케이드 형성

먼저 표기를 하나 세우겠습니다(논문 원문도 이렇게 정의합니다): a ∘ b는 「b를 먼저 실행하고, 그다음 a를 실행하는」 합성 함수를 나타냅니다. 그러면 inverse = compose(value, inverse)는 새로운 역변환을 매번 누적된 역변환의 앞에 붙이는 것이 되어, 후입선출(LIFO) 복원 순서——가장 나중에 세워진 이펙트가 가장 먼저 복원되는——를 얻습니다.

다시 ctx.dispose = compose(dispose, ctx.dispose)를 봅시다: 새로 생긴 dispose가 바깥 컨텍스트의 누적 역변환에 앞에 붙습니다. 다시 말해——자식 이펙트의 역변환 자체가 부모 컨텍스트 위의 하나의 이펙트인 것입니다(논문의 ∂²Γ 재귀 구조). 중첩된 이펙트는 층층이 장부에 기록되므로:

  • 부모 컴포넌트 언로드 → 부모 컨텍스트 위의 이펙트 복원 → 모든 자식 이펙트를 캐스케이드 복원합니다.
  • 이 「캐스케이드」는 손으로 작성된 것이 아니라 합성 구조에서 자연스럽게 따라 나옵니다.

💡 논문에는 복선이 하나 더 묻혀 있습니다: 컴포넌트 층(4단계에서 다룰 reload / unload)은 같은 execute를 재사용하고, 가드만 armed에서 「에포크가 안정적인가」로 바뀝니다. 같은 엔진에 가드를 바꾸면 다른 의미론이 된다——이 실마리를 기억해 두세요.

3단계: 코이펙트 연산——ctx의 세 서랍

2단계가 「기초」였다면, 이번 단계에서는 그 위에 1층을 지읍니다: 리액티브 코이펙트——ctx.set(key, value)로 쓰고, ctx.get(key)로 읽습니다.

모든 코이펙트 연산은 각 컨텍스트가 지닌 심벌을 키로 하는 세 개의 슬롯에 작용합니다.

슬롯(심벌 키)학명무엇을 저장하는가한 줄 설명
ctx[@@store]값 저장소 σ도메인 심벌 → 타입 있는 값진짜로 「값을 넣는」 서랍
ctx[@@isolate]도메인 테이블 ρ코이펙트 키 → 도메인 심벌키의 「리다이렉션 테이블」
ctx[@@intercept]인터셉트 테이블 ι키 → 메타데이터값 조회 시의 「필터」
ctx(上下文)@@store 值存储 σ@@isolate 域表 ρ@@intercept 拦截表 ιget(key)两层解析set(key)一个效应

ctx.set / ctx.get 都是效应:自动被跟踪,卸载即回退

이중 해석: get은 두 단계를 거친다

ctx.get(key)는 저장소를 바로 뒤지지 않고 두 겹의 해석을 합니다.

key → ρ(key) → σ(ρ(key))
① 먼저 @@isolate에 묻는다: 이 키는 어느 도메인에 속하는가?
② 다음으로 @@store에 들어간다: 그 도메인에 바인딩된 값은 얼마인가?

중간의 ρ(도메인 테이블)는 의도적으로 추가된 「간접층」입니다——격리 연산은 바로 이 층을 바꾸는 방식으로, 키를 독립적인 바인딩으로 리다이렉트합니다. 그리고 @@intercept는 바인딩에 접근할 때만 조회되며, 조정하는 것은 「바인딩을 어떻게 사용하는가」이지 「바인딩이 무엇으로 해석되는가」가 아닙니다.

이 두 슬롯의 역할 분담은 코이펙트 연산 구현의 두 부분과 정확히 대응합니다: (1) 공급과 통지——바인딩을 세우거나 철거하고, 변화를 의존 측에 전파합니다. (2) 격리와 인터셉트——키가 해석되는 방식을 재구성합니다.

공급과 통지: set은 본질적으로 「한 번의 ctx.effect」

set(k, v)의 타입이 𝔈Σ이므로, 코이펙트를 공급하는 것은 곧 한 번의 ctx.effect 호출입니다——2단계의 추적 및 복원 메커니즘을 자동으로 물려받습니다. 알고리즘 2를 단순화하면 다음과 같습니다.

// 알고리즘 2 단순화: ctx.set —— 값을 바인딩하고, 반환된 dispose가 취소를 담당
function set(ctx, key, value) {
  function callback() {
    const realm = ctx[@@isolate][key];   // ① 먼저 해석: 이 키는 어느 도메인에 속하는가?
    ctx[@@store][realm] = value;         // ② 값을 해당 서랍에 넣는다
    notify(ctx, [key]);                  // ③ 의존 측에 통지: 값이 바뀌었다!
    return function () {                 // ④ 역변환 = 이번 바인딩 취소
      delete ctx[@@store][realm];        //    값을 꺼낸다
      notify(ctx, [key]);                //    다시 통지: 값이 없어졌다
    };
  }
  return ctx.effect(callback);           // ⑤ 모든 것을 ctx.effect의 추적에 맡긴다
}

주의: 바인딩을 세울 때도 제거할 때도 notify를 호출합니다——변화를 「이 키에 관심 있는」 컴포넌트에 전파하기 위해서입니다. 알고리즘 3을 단순화하면 다음과 같습니다.

// 알고리즘 3 단순화: notify —— 바인딩이 바뀔 때마다 관심 있는 파이버에게 브로드캐스트
function notify(ctx, keys) {
  for (const fiber of all_fibers) {
    for (const key of keys) {
      if (key가 fiber.inject에 있고 같은 도메인으로 해석된다면) {
        refresh(fiber);   // 파이버가 새 상태로 다시 평가하게 한다
        break;
      }
    }
  }
}

이것은 논문 정의 15의 리액티브 분류에 대응합니다: 어떤 변화가 「이 파이버의 명세가 충족된다」의 진릿값을 바꾸면, 그 파이버는 활성화되거나 비활성화됩니다. 그리고 refresh는 멱등입니다——중립적인 변화는 아묵런 영향도 만들지 않습니다. refresh가 구체적으로 무엇을 하는지는 4단계에서 밝히겠습니다.

격리와 인터셉트: 바뀌는 것은 「해석 방식」, 복원은 암묵적

ctx.isolate(key, realm)ctx.intercept(key, metadata)는 구조적으로 같은 종류의 동작입니다.

  • 각각 자식 컨텍스트를 하나 파생시켜, 지정한 key에 대해 상속받은 테이블 하나를 조정하고, 부모 컨텍스트는 그대로 유지합니다.
  • 따라서 복원은 암묵적입니다: 자식 컨텍스트를 버리기만 하면 되고, 명시적 역변환을 실행할 필요가 없습니다(반면 set은 값을 명시적으로 삭제해야 합니다).
연산어느 테이블을 바꾸는가효과
ctx.isolate(key, realm)도메인 매핑 ρ를 realm으로 덮어쓴다(지정하지 않으면 새 심벌 생성)서로 다른 심벌 아래의 두 컨텍스트에서 같은 키가 서로 독립적인 바인딩으로 해석된다
ctx.intercept(key, metadata)메타데이터를 인터셉트 테이블 ι에 병합한다새 메타데이터가 기존 메타데이터와 병합되며, 오래된 것보다 우선한다

4단계: 컴포넌트의 일생——라이프사이클과 컨텍스트 접근

2, 3단계가 「부품」이었다면, 이번 단계에서는 그것들을 컴포넌트로 조립하고, 컴포넌트가 ctx와 어떻게 상호작용하는지 살펴웅니다.

ctx.use: 컴포넌트를 파이버로 「인스턴스화」

컴포넌트는 ctx.use에 의해 파이버로 인스턴스화됩니다. 컴포넌트는 코이펙트 명세(component.inject, 무엇이 필요한지 선언)와 이펙트 함수(component.apply, 무엇을 하는지 정의)를 한 쌍으로 묶은 것입니다. 알고리즘 4를 단순화하면 다음과 같습니다.

// 알고리즘 4 단순화: ctx.use —— 컴포넌트를 살아 있는 fiber로 만든다
function use(ctx, component, config) {
  function callback() {
    refresh(fiber);                       // 실행 시: 자식 파이버의 라이프사이클을 시작
    return function () {                  // 역변환: 복원 = 자식 컴포넌트 언로드
      fiber.epoch = null;                 // 목표 상태를 Inactive(빈 값)로 설정
      unload(fiber);                      // 실제로 언로드를 수행
    };
  }
  const fiber = new Fiber({ parent: ctx, inject: component.inject });
  fiber.ctx = deriveChildCtx(ctx);        // 부모 컨텍스트에서 새 자식 컨텍스트를 파생
  fiber.apply = () => component.apply(fiber.ctx, config); // 설정을 바인딩
  ctx.effect(callback);                   // 부모 컨텍스트 위에서 추적되는 이펙트로 등록
  return fiber;
}

코드 마지막의 ctx.effect(callback)에 주목하세요: 이 콜백은 부모 파이버 안에서 추적되도록 등록됩니다. 그래서 부모 컴포넌트를 언로드하면 모든 자식 컴포넌트가 자동으로 캐스케이드 언로드됩니다——부모의 이펙트를 복원할 때 「자식 파이버의 에포크를 비우고 unload하는」 역변이 실행되기 때문입니다. 이것이 바로 지난 과에서 말한 「부모의 이펙트 컨텍스트 위의 ⋄ 합성」입니다.

refresh와 에포크: 버전 번호가 바뀌어야 움직인다

파이버는 언제 리로드해야 할까요? 답은 에포크를 보는 것입니다: 컴포넌트가 선언한 각 키를 현재 코이펙트 저장소에서 값으로 해석한 뒤, 그것들을 하나의 튜플로 묶은 것——이것이 목표 상태의 「버전 번호」입니다(𝜀𝑑(𝜎), 여기서 「빈 값」은 Inactive를 나타냅니다). notify가 코이펙트가 바뀔 때마다 에포크를 다시 계산하므로, 파이버는 정확히 그 해석 값이 변할 때 리로드됩니다.

알고리즘 5의 전반부를 단순화하면 다음과 같습니다.

// 알고리즘 5 단순화: refresh —— 「에포크」로 움직일지 결정한다
function refresh(fiber) {
  const epoch = computeEpoch(fiber);      // 에포크 = 선언된 키를 모두 값 튜플로 해석
  if (epoch === fiber.epoch) return;      // 버전 번호가 그대로? 중립적 변화이므로 아무것도 하지 않는다
  fiber.epoch = epoch;                    // 새 목표 버전을 기록
  if (fiber.inertia) return;              // 마이그레이션 중? 실행 중인 것을 끝까지 달리게 한다(관성!)
  fiber.inertia = epoch !== null
    ? createTask(reload(fiber))           // 값 있음 → 로드 / 리로드
    : createTask(unload(fiber));          // 값 없음 → 언로드
}

관성: 일단 시작된 마이그레이션은 끝까지 달린다

reloadunload는 상호 재귀 쌍으로, 이것이 바로 9과의 「관성 상태 기계」의 구현입니다(알고리즘 5의 후반부).

// reload: 컴포넌트의 이펙트 함수를 실행하고, 끝나면 버전 번호가 아직 맞는지 확인한다
async function reload(fiber) {
  const epoch0 = fiber.epoch;                    // 시작 시점의 목표 버전을 기록
  const recover = await execute(fiber.apply, () => fiber.epoch === epoch0);
  fiber.dispose = compose(recover, fiber.dispose); // 누적된 역변환을 장부에 기입
  if (fiber.epoch === epoch0) {
    fiber.inertia = null;                        // 버전 불변 → Active로 안정
  } else {
    fiber.inertia = createTask(unload(fiber));   // 버전이 바뀜 → 이어서 언로드!
  }
}

// unload: LIFO로 추적된 모든 이펙트를 복원
async function unload(fiber) {
  await fiber.dispose();                         // 누적된 「취소 목록」을 실행
  fiber.dispose = id;                            // 장부를 비운다
  if (fiber.epoch === null) {
    fiber.inertia = null;                        // Inactive로 안정
  } else {
    fiber.inertia = createTask(reload(fiber));   // 새 버전이 생김 → 이어서 로드!
  }
}

두 함수 모두 마이그레이션 완료 시점에 에포크를 확인한 뒤, 「안정」할지 「다음 마이그레이션으로 직렬 연결」할지를 결정합니다. 이 상호 재귀가 논문의 관성 속성을 구현합니다.

마이그레이션은 일단 시작되면 먼저 완료까지 실행되고, 그 후에야 새로운 마이그레이션의 시작이 허용된다.

그리고 2단계에 남겨 둔 실마리도 여기서 마무리됩니다: reload는 같은 execute를 재사용하고, 가드는 armed에서 「에포크가 시작 시점의 버전과 여전히 같은가」로 바뀝니다——에포크가 바뀌는 순간 이터레이션은 즉시 멈추고, 그 시점까지 누적된 역변환만 유지합니다. 이렇게 전체 메커니즘이 두 수준에서 작동합니다.

수준어디서 에포크를 확인하는가무엇을 지키는가
마이그레이션 수준reload / unload 완료 시마이그레이션을 가로지르는 관성 직렬 연결(먼저 끝까지 달리고, 그다음 기어를 바꾼다)
단계 수준execute의 각 이터레이터 단계 경계단일 마이그레이션 내 부분 롤백(절반쯤 달리다 버전이 바뀐 것을 발견하면 멈춘다)

Proxy 문지기: 선언한 것만 쓸 수 있다

마지막으로 최상층을 봅시다. 3단계의 ctx.get / ctx.set리플렉티브 API(이름을 키로 하는 읽기/쓰기)입니다. 그 위에 Cordis는 호스트 언어에 더 가까운 두 번째 사용법을 제공합니다: 프로퍼티 접근——컴포넌트는 ctx[key]처럼 직접 쓸 수 있고, 네이티브 구조체에 접근하듯 메서드 호출 없이 사용합니다.

TypeScript에서 Cordis는 Proxy로 이 메커니즘을 구현하며, 그 get 트랩이 모든 프로퍼티 접근을 중개합니다. 알고리즘 6을 단순화하면 다음과 같습니다.

// 알고리즘 6 단순화: resolve —— 사용 지점에서 위로 올라가며 「누가 이 키를 선언했는가」를 찾는다
function resolve(ctx, key) {
  let fiber = ctx.fiber;                    // 접근을 시작한 컨텍스트에서 출발
  while (true) {
    if (key가 fiber.inject에 있다면) return get(ctx, key); // 선언 발견 → 조회 허가
    if (fiber가 루트 노드라면) throw UNDECLARED_ACCESS;     // 꼭대기까지 선언이 없다 → 거부!
    fiber = fiber.parent.fiber;             // 아니면 파이버 체인을 따라 위로
  }
}

파이버 체인을 따라 위로 올라가면서, inject에 그 key를 선언한 첫 번째 파이버를 찾으면 get(알고리즘 2)을 통해 값을 얻습니다. 루트 노드까지 순회핬도 찾지 못하면 UNDECLARED_ACCESS(미선언 접근)를 던집니다.

이것이 바로 「프록시」와 「ctx.get을 직접 호출」의 근본적인 차이입니다.

ctx.get(key)프로퍼티 접근 ctx[key](Proxy 중개)
조회 방식전역적 조회파이버 체인을 따라 「선언」을 찾는다
찾지 못했을 때빈 값을 반환, 결코 실패하지 않음UNDECLARED_ACCESS를 던진다
명세 강제강제하지 않음사용 지점에서 코이펙트 명세 d를 강제

보장이 하나 더 있습니다: 「선언됐지만 존재하지 않는」 값은 발생하지 않습니다——파이버는 선언된 모든 코이펙트가 충족된 후에야 Active에 들어가기 때문입니다(제4.1.3절). 선언돼 있고 Active 상태라면, 값은 반드시 그 자리에 있습니다.

이러한 거부는 접근 지점에서 수행되는 런타임 검사입니다. 그리고 코이펙트 명세 d는 정적으로 선언되므로, 같은 종류의 위반은 원칙적으로 컴파일 타임에도 탐지할 수 있습니다(논문 제5.3절에서 호스트 언어가 타입 계층을 빌려 같은 중개를 구현하는 방법을 다룹니다).

핵심 정리

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

  1. 표 1은 번역 사전이다: ctx는 컨텍스트 타워, 이펙트 콜백은 이펙트, fiber는 컴포넌트 인스턴스에 대응합니다. @@name은 심벌 키이지 문자열 인덱스가 아닙니다.
  2. 모든 변경은 한 번의 ctx.effect로 귀결된다: 콜백이 역변환을 산출하고, 반환된 dispose를 호출하면 곧 복원됩니다. armed가 복원을 최대 한 번으로 보장하고, dispose는 부모 컨텍스트에 앞에 붙어 LIFO와 캐스케이드 언로드를 형성합니다.
  3. ctx에는 세 개의 서랍이 있다: @@store(값), @@isolate(키에서 도메인으로의 「리다이렉션 테이블」), @@intercept(메타데이터 필터). get은 「먼저 ρ를 조회하고, 다음으로 σ를 조회하는」 이중 해석입니다.
  4. fiber의 일생은 에포크에 의해 구동된다: ctx.use가 파이버를 만들고, refresh가 에포크에 따라 reload / unload를 결정합니다. 일단 시작된 마이그레이션은 끝까지 달립니다——이것이 「관성」입니다.
  5. Proxy는 사용 지점에서 명세를 강제한다: 접근 지점에서 파이버 체인을 따라 위로 올라가며 선언을 찾고, 찾지 못하면 UNDECLARED_ACCESS를 던집니다. ctx.get은 결코 실패하지 않지만, 프록시는 모든 미선언 접근을 거부합니다——「선언하지 않은 것은 아예 건드릴 수 없습니다」.

🚀 다음 과(12과)에서는 코어 라이브러리 위의 2층으로 올라갑니다: 컴포넌트 로더——Cordis가 설정 조율과 핫 모듈 리플레이스먼트(HMR: 재시작 없이 코드 변경)를 어떻게 제공하는지, 그리고 Koishi의 4000+ 플러그인으로 검증된 과정을 살펴웅니다.

자가 테스트 · Cordis 코어 라이브러리

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

1. Cordis 코어 라이브러리에서 ctx.effect는 어떤 역할을 합니까?
2. ctx.effect가 반환하는 dispose 클로저의 역할은?
3. ctx의 세 심벌 슬롯에 대해 올바른 설명은?
4. Proxy 프로퍼티 접근으로 「미선언」 코이펙트(ctx.someKey)에 접근하면 어떻게 됩니까?