9과: 컴포넌트 생명주기: 멱등, 반복, 에포크, 비동기
한 줄 요약: 컴포넌트 = 「의존 선언 d(내가 필요한 것)」+「이펙트 함수 e(내가 기여하는 것)」. 목표 상태는 단 두 가지——Active(이펙트가 적용되었고 의존이 모두 충족됨)와 Inactive(하나라도 빠지면 비활성). 그리고 이 둘을 현실 세계에서 안전하게 오가게 하는 것이 네 가지 메커니즘——멱등(각 역함수는 최대 한 번만 효력을 가짐), 반복(한 번의 Reload를 여러 단계로 나누고 언제든 중단 가능), 에포크(목표 상태에 버전 번호를 붙여 낡은 상태를 간파), 비동기 관성(전이에는 실제 시간이 걸리므로, 일단 시작하면 끝까지 실행).
1. 컴포넌트와 목표 상태: 하나의 컴포넌트, 두 개의 얼굴
이전 두 과에서 우리는 두 개의 「절반」을 각각 알아보았습니다:
- 이펙트(6~7과): 컴포넌트가 활성화되었을 때 무엇을 하는가, 그리고 한 후에 어떻게 되돌리는가.
- 요구 이펙트(5과, 8과): 컴포넌트가 환경에 무엇을 제공받아야 하는가, 그리고 환경이 변할 때 어떻게 반응하는가.
이번 과에서는 양쪽을 하나의 런타임 엔티티——컴포넌트(component)——로 합칩니다. 논문의 정의 20은 아주 간결합니다:
컴포넌트 ℭ = 𝔇 × 𝔈, 즉 이원조 (d, e):
- d(의존 선언, 요구 이펙트 명세): 컴포넌트가 환경에 제공을 요구하는 의존——「내가 필요한 것」.
- e(이펙트 함수): 컴포넌트가 활성 상태일 때 컨텍스트에 기여하는 이펙트——「내가 기여하는 것」.
비유하자면: 플러그인을 설치하기 전에 먼저 「설치 설명서」를 봅니다——어떤 인터페이스에 연결하고 어떤 설정을 읽는지(d). 설치하고 활성화한 후에는 일을 시작합니다(e). 선언과 행동은 서로 다른 것입니다.
목표 상태: Active인가 Inactive인가?
컴포넌트의 두 측면이 공동으로 목표 상태를 결정합니다. 논문이 제시하는 판정 기준은 두 조건이 동시에 성립하는 것입니다:
| 측면 | 조건 | 쉽게 말하면 |
|---|---|---|
| 이펙트 측 | 컴포넌트가 「살아있음」: 이펙트가 컨텍스트에 적용되었고, 대응하는 역함수가 아직 호출되지 않음 | 내가 한 일이 아직 걸려 있고 되돌려지지 않았다 |
| 요구 이펙트 측 | 선언된 모든 의존이 충족됨(𝜎 ⊨ d) | 내가 원하는 것을 환경이 모두 주었다 |
- 둘 다 성립 → 목표 상태는 Active(활성)
- 그렇지 않으면 → 목표 상태는 Inactive(비활성)
「또는」이 아니라 「그리고」임에 주의하세요: 의존이 모두 갖춰져도 이펙트가 아직 적용되지 않았다면 컴포넌트는 Active가 아닙니다. 반대로, 이펙트가 걸려 있어도 의존이 빼앗기면 컴포넌트는 즉시 Inactive로 떨어집니다.
Reload와 Unload: 상태가 바뀌면 전이한다
목표 상태가 변할 때마다(어느 측면이 원인이든), 시스템은 전이를 시작합니다:
- Reload(로드): 컴포넌트의 이펙트 함수 e를 실행하여 부수 효과를 컨텍스트에 누적——「활성화하고 일을 시작」.
- Unload(언로드): 누적된 역함수를 재생하여 컨텍스트를 원래대로 복원——「일을 마치고 한 일을 모두 되돌림」.
가장 단순한 생명주기는 아래의 2-상태 머신입니다:
生命周期 = 可回退效应与反应式余效应「相遇」的地方
Reload와 Unload는 8과의 「반응형」이 구체화된 동작입니다: 환경이 바뀌면 컴포넌트가 자동으로 리로드되거나 언로드되며, 모든 것이 롤백 가능하고 흔적을 남기지 않습니다. 하지만 현실 세계는 2-상태 머신보다 훨씬 복잡합니다——논문은 이어지는 네 개의 절에서 네 가지 질문에 답합니다. 그것이 이번 과 제목의 네 단어입니다.
2. 멱등 복구: 역함수는 최대 한 번만 효력을 가진다
먼저 생각만 해 봐도 섬뜩한 질문을 봅시다: Unload는 「누적된 역함수를 재생」한다고 하는데, 같은 역함수가 두 번 재생되지 않는다는 것을 누가 보장할까요?
어떤 역함수가 두 번 호출되면, 그것은 한 적도 없는 일을 되돌린 것과 같습니다——컨텍스트가 두 번 파괴되는 셈으로, 롤백 가능한 시스템이 절대 받아들일 수 없는 일입니다. 논문이 지적하듯, 위험은 「전체 복구」 계층에 있지 않습니다(recover 계층은 구조상 안전합니다: 복구 후 누적 함수가 항등 함수로 리셋되므로 두 번째 복구는 자연스럽게 아무것도 하지 않습니다). 위험은 더 은밀한 곳——국소적 해제 함수——에 있습니다.
6과를 떠올려 보세요: effect는 하나의 이펙트의 해제 함수를 호출자에게 돌려주고(논문의 「𝜕 → 𝜕²」 성분), 이 이펙트를 언제 「놓아줄지」는 호출자 스스로 결정합니다. 여기서 문제가 생깁니다: 호출자가 이 해제 함수를 두 번 호출하면 어떻게 될까요? 그 역함수가 두 번 적용되어 컨텍스트가 망가집니다. 그래서 논문의 규정은 간단합니다:
반환되는 모든 해제 함수는 멱등이어야 한다——최대 한 번만 효력을 가지며, 두 번째 호출은 아무것도 하지 않는다.
핵심 설계: 생성적 핸들(idem)
이 「최대 한 번」은 전역 스위치가 아니라, 논문이 정의하는 멱등 가드 idem에 의해 실현되며, 그 핵심은 생성적(generative) 핸들입니다:
function idem(g) {
const h = freshHandle(); // 생성적: 래핑할 때마다 완전히 새로운 핸들 h를 얻음
return function (γ) {
if (used.has(h)) return γ; // h가 「사용됨」으로 표시됨 → 아무것도 하지 않고 그대로 반환
used.add(h); // 첫 번째 호출 → 먼저 h를 「사용됨」으로 표시
return g(γ); // 그 후에야 역함수를 실제로 실행
};
}
세 가지 요점으로 나눠 봅시다:
- 핸들은 새롭다: idem을 적용할 때마다 완전히 새로운 핸들 h가 생성되며, 그것은 이 해제 함수만의 것이고 다른 곳에서는 얻을 수 없습니다(h는 idem의 타입에 나타나지 않습니다). 따라서 임의의 두 해제 함수는 절대 핸들을 공유하지 않으며, 서로 간섭하지 않습니다.
- 상태는 「썼는지」만 기록: 「사용된 핸들」의 기록. h가 기록에 없으면 → 유효하고, 발화하면 h가 기록에 기록되어 → 무효가 됩니다. 두 번째로 들어왔을 때 h가 이미 기록에 있으면, 그대로 반환할 뿐입니다.
- 여전히 순수 함수: 해제 함수는 여전히 자신이 작용하는 상태 γ와 「전제 조건」(h가 미사용)에만 의존하고, 숨겨진 가변 상태에는 의존하지 않습니다——롤백 가능 시스템의 「모든 것이 설명 가능」이라는 성질이 조금도 훼손되지 않습니다.
일상의 말로 하면: 일회용 퓨즈나 1회용 입장권과 같습니다——쓰면 끊어지고, 두 번째로 발화하려 해 봐도 문은 이미 닫혀 있습니다. 구현상으로는, 각 dispose 클로저가 armed 변수를 새롭게 캡처하는 것이 바로 이 메커니즘입니다.
💡 논문에서 effect의 멱등 변형이 바꾸는 것은 한 곳뿐입니다: 반환되는 역함수를 idem으로 래핑하는 것. 순방향 누적 부분은 그대로입니다(원래 멱등인 recover가 뒷받침하므로). 따라서 이전에 배운 준동형사상, 누적 등의 결론은 전혀 영향을 받지 않습니다.
3. 반복: 한 번의 Reload를 여러 단계로 나눌 수 있다
현실의 Reload가 한 번에 완료되는 경우는 드뭅니다: 컴포넌트를 활성화하려면 연속적으로 여러 일을 해야 할 수 있습니다——데이터베이스 연결, 파일 열기, 리스너 등록, 백그라운드 태스크 시작. 이 모든 것을 한 번에 원자적으로 끝내야 한다면, 시스템은 너무 둔해질 것입니다.
논문의 답은 **이펙트 이터레이터(effect iterator)**입니다: 한 번의 Reload를 여러 단계로 나누고, 각 단계에서 조금씩 수행합니다. 각 단계의 실행이 끝나면 삼중조 (δ, g, o) 를 반환합니다:
| 성분 | 의미 |
|---|---|
| δ | 이 단계의 실행이 완료된 후의 새 컨텍스트 |
| g | 이 단계의 이펙트의 역함수(나중에 언로드할 때 되돌리는 데 사용) |
| o | 연속(continuation): Nothing = 여기까지. Just(다음 이터레이터) = 아직 다음 단계가 있음 |
전체 전이는 이 구조를 따라 재귀적으로 진행합니다: 한 단계 할 때마다, 그 단계의 역함수 g를 실행 순서대로 합성하여 누적 복구 함수에 넣습니다. 최종적으로 다음과 같이 누적됩니다:
φ ∘ g1 ∘ g2 ∘ … ∘ gk
이 합성 함수를 적용할 때는 가장 오른쪽부터 시작합니다——가장 나중에 실행된 이펙트가 가장 먼저 되돌려지는, 교과서적인 **후입선출(LIFO)**입니다: 나중에 연 것을 먼저 닫고, 먼저 연 것을 나중에 닫아 복구 순서에 빈틈이 없습니다.
단계와 단계 사이는 천연의 중단 지점
이터레이터의 가장 훌륭한 점은 두 단계 사이의 경계가 그대로 자연스러운 중단 지점이 된다는 것입니다.
- 두 단계 사이에서 목표 상태가 바뀌면(의존이 빼앗기거나, 사용자가 취소하거나, 컴포넌트가 교체되거나……), 시스템은 여기서 멈출 수 있습니다.
- 멈출 때는 현재까지 누적된 역함수를 재생하여, 이미 수행된 부분을 안전하게 모두 되돌립니다.
- 추가 메커니즘이 전혀 필요 없습니다——각 단계가 반환하는
Maybe연속 자체가 경계 표시이기 때문입니다.
논문에 따륩, 이펙트 이터레이터는 본질적으로 「구체화된 유계 연속(reified bounded continuation)」이며, 주류 언어의 yield가 바로 그것입니다: 제너레이터를 작성할 때, 각 yield 사이에서는 안전하게 일시 중단하거나 취소할 수 있습니다. 따라서 이 모델은 여러분이 이미 사용할 줄 아는 모든 것에 직접 매핑됩니다.
🎯 세분도 트레이드오프: 단일 단계 이펙트(한 단계로 완료하고 즉시 Nothing을 반환)는 퇴화된 경우입니다——전이가 원자적이고 중단 경계가 없습니다. 단계가 세밀할수록 시스템이 목표 상태 변화에 더 빨리 반응하지만, 매 단계마다 조건을 한 번씩 검사해야 하므로 오버헤드도 커집니다. 세밀함 = 민첩하지만 전력을 먹고, 거침 = 손이 덜 가지만 둔함——이것이 설계자가 저울질해야 할 트레이드오프입니다.
💡 여담: 논문은 양방향 이펙트 이터레이터도 언급합니다——역함수 성분도 이터레이터로 바꿔, Unload도 점진적으로 진행할 수 있고 중단되어 Reload로 되돌아갈 수 있게 하는 것입니다. 아쉽게도 양방향 반복을 네이티브로 지원하는 언어가 적어, 이것은 주로 이론적 확장에 머뭅니다.
4. 일관성과 에포크: 목표 상태에 버전 번호를 붙인다
이제 「반응형」과 「반복」을 함께 놓으면, 새로운 문제가 충돌하여 생깁니다.
컴포넌트의 의존이 런타임에 교체된다고 가정합시다(예를 들어, 그것이 읽는 설정 파일이 바뀐 경우). 목표 상태는 순식간에 세 번 점프할 수 있습니다:
Active → Inactive → Active
곤란한 것은, 첫 번째 Active가 시작한 Reload가 아직 진행 중일 수 있다는 것입니다(반복이 아직 끝나지 않음). 두 번째 Active가 도착했을 때, 시스템은 매우 교활한 상황에 직면합니다: 의존 값은 이미 바뀌었는데 상태는 여전히 Active——「Active → Active」입니다. 시스템이 순진하게 원래의 Reload를 계속하면, 컴포넌트는 낡은 의존 값에 바인딩되고 맙니다: 새 환경에 새 값이 설정되었는데, 컴포넌트는 옛 값으로 초기화를 마쳐버리는 것입니다. 이것이 **불일치(incoherence)**입니다.
에포크: 목표 상태의 「버전 번호」
논문의 해법은 에포크(epoch) 메커니즘——목표 상태에 버전을 부여하는 것입니다. 의존 명세 d에 대해, 에포크 함수는 d에 있는 모든 의존 키의 현재 값을 꺼내 버전 태그로 묶습니다:
εd(σ) = ⟨ σ(k) | k ∈ d ⟩
- 두 에포크가 같다 ⟺ 모든 의존 값이 같다. 값이 하나라도 바뀌면 에포크도 바뀝니다.
- Inactive의 에포크는 특수 상수 **⊥**이며, 어떤 활성 에포크와도 같지 않습니다——비활성은 「버전 없음」으로, 어떤 설정과도 무관합니다.
사용법은 두 단계뿐이지만 매우 중요합니다:
- 각 전이가 시작될 때, 현재 목표 상태의 에포크를 기록하여 εinert(관성 에포크)로 저장합니다.
- 각 이터레이터 단계 경계에서, 「지금」의 에포크 εtarget과 기록한 εinert를 비교합니다:
- 일치 → 세상이 변하지 않았다. 다음 단계로 진행.
- 불일치 → 세상이 이미 변했다. 즉시 전이를 중단하고, 누적된 역함수를 재생한 뒤, 작업을 마친다.
비유하자면: 작업을 시작하기 전에 「현재 설정」의 사진을 찍어두고, 한 단계 끝날 때마다 사진과 대조하는 것입니다. 사진과 맞지 않으면 즉시 작업을 멈추고, 이미 한 것을 모두 원래대로 되돌립니다.
「하나의 선」에서 「하나의 별」로
에포크 메커니즘은 생명주기를 1차원의 Inactive/Active에서 다차원의 별 모양 구조로 확장합니다:
| 상태 | 에포크(버전 태그) |
|---|---|
| Active · 설정 A | ⟨a:2, b:2⟩ |
| Active · 설정 B | ⟨a:2, b:3⟩ |
| Active · 설정 C | ⟨a:1, b:1⟩ |
| …… | …… |
| Inactive | ⊥(버전 없음) |
각 의존 설정이 독립된 Active 브랜치에 대응하고, 모든 브랜치가 Inactive로 연결됩니다. 의존 교체 = 브랜치 전환: 옛 브랜치에서의 전이는 그 자리에서 무효가 되고, 시스템은 Inactive로 돌아간 뒤 새 브랜치를 따라 다시 Reload합니다. 이로써 컴포넌트가 항상 「현재」의 의존과 정렬되고, 옛 값을 붙들고 살지 않는다는 것이 보장됩니다.
5. 비동기와 관성: 전이에는 실제 시간이 걸린다
지금까지 우리는 Reload/Unload가 순간적으로 완료된다고 기본 가정해 왔습니다——목표 상태가 바뀌면 전이가 즉시 완료된다. 하지만 현실 세계는 그렇지 않습니다: Reload는 데이터베이스에 연결하거나, 파일을 쓰거나, 네트워크를 요청해야 할 수 있으며, 실제 시간이 걸립니다.
논문의 추상화는 간결합니다: 한 번의 전이는 Future(A) 타입의 값을 생성하며, Future의 정의적 성질은——「전이 제출」과 「평가 완료」 사이에 외부 상태가 이미 변했을 수 있다는 것입니다. 다시 말해: 전이가 중간까지 진행되었을 때, 세상은 이미 변했을 수 있습니다.
두 가지 위협
전이가 실제 시간을 차지하면, 두 가지 리소스 안전 문제가 발생합니다:
- 진행 중인 Reload를 즉시 롤백: 앞 단계의 이펙트가 아직 실행을 마치지 않았는데 역함수를 한꺼번에 재생해 버린다——LIFO 순서가 그 자리에서 무너지고, 나중에 정말 되돌려야 할 이펙트에는 재생할 역함수가 없으며, 이미 해제된 것은 잘못된 타이밍에 해제됩니다.
- 빠른 진동(Reload → Unload → Reload → …): 목표 상태가 왔다 갔다 하면 같은 역함수가 두 번 호출될 수 있습니다——이것은 2절의 멱등 가드가 막아야 할 일이지만, 진동은 그것을 빠져나갑니다.
관성: 일단 달리기 시작하면, 끝까지 달리게 한다
안전을 회복하기 위해, 논문은 Reload와 Unload를 「순간적 전이」에서 **관성 상태(inertia)**로 격상시킵니다:
일단 어떤 전이에 들어가면, 그 전이는 완료까지 실행된다. 그 후에야 시스템이 목표 상태의 어떤 변화든 처리한다.
구체적인 시맨틱스는 세 가지입니다:
- 컴포넌트가 Reload 도중에 목표가 Inactive로 변함 → Reload가 먼저 완료되고, 그 후에 Unload가 시작된다. Reload 완료 전에 목표가 다시 Active로 돌아오면, 컴포넌트는 정상적으로 Active로 들어간다(즉 이 전이는 헛되지 않았다).
- 대칭적으로, 컴포넌트가 Unload 도중에 목표가 Active로 변함 → Unload가 먼저 완료되고, 그 후에 Reload가 시작된다. 다시 Inactive로 돌아가면, 컴포넌트는 Inactive로 들어간다.
- 보류 중인 역방향 전이는 지연되어 현재 관성 상태가 끝날 때까지 기다리고, 그 시점에 시스템은 그때의 목표 상태에 따라 실행할지 버릴지 결정한다——목표가 또 변했을 수 있으므로, 최신 상황에 따라 처리한다.
비유: 엘리베이터는 문이 닫히고 출발하면, 목표 층에 도착해야 문을 연다——도중에 다른 층을 눌러도 즉시 방향을 바꾸지 않고, 도착한 후에야 새로 누른 층에 응답합니다.
관성 × 에포크 = 완전한 세이프티넷
관성 상태와 이터레이터 경계는 조합하여 사용할 수 있으며, 이것이 3.3절 전체를 마무리하는 한 획입니다: 모든 단계 경계에서, 시스템은 에포크가 여전히 일치하는지 검사합니다——
- 일치 → 다음 단계로 진행.
- 불일치 → 전이 중단 → 누적된 역함수 재생 → 지연되었던 전이 시작.
이것이 바로 각 관성 전이의 내부에서 작동하는 동기적 중단 메커니즘입니다: 관성은 「현재 전이가 중단되지 않음」을 보장하고, 에포크는 「중단될 때는 반드시 안전한 중단 지점에서, 그리고 최신 상태에 근거한다」는 것을 보장합니다. 두 규칙이 각자의 구간을 맡아, 합쳐서 매끄럽고 안전한 생명주기를 만듭니다.
핵심 정리
이번 과는 정보량이 많지만, 다음 다섯 문장만 기억하면 충분합니다:
- 컴포넌트 = 의존 선언 d(내가 필요한 것) + 이펙트 함수 e(내가 기여하는 것). 목표 상태 Active ⟺ 「이펙트가 적용되었고 역함수가 미호출」 그리고 「모든 의존이 충족됨」. 그렇지 않으면 Inactive. 상태가 바뀌면 Reload(e를 실행해 부수 효과 누적) 또는 Unload(누적된 역함수 재생).
- 멱등: 각 역함수는 최대 한 번만 효력을 가진다——idem은 생성적 핸들 + 사용 기록으로 「쓰면 끊어짐」을 구현하여, 반복 언로드로 인한 컨텍스트 파괴를 막으면서도 순수 함수 성질을 유지한다.
- 반복: 한 번의 Reload는 여러 단계로 나눌 수 있다(이펙트 이터레이터). 각 단계는 δ, 역함수 g, 연속 o를 반환한다. 단계 사이는 천연의 중단 지점으로, 중간에 멈춰 안전하게 롤백할 수 있다. 역함수는 실행 순서대로 누적되고, 복구는 후입선출(LIFO).
- 에포크: 목표 상태는 버전 번호(의존 값의 스냅숏 εd(σ))를 가진다. 각 전이는 εinert를 기록하고, 단계 경계에서 εtarget과 비교하여, 불일치하면 전이를 중단——「의존이 교체된 후에도 옛 Reload를 계속」하는 낡은 바인딩 문제를 전문으로 치료한다.
- 비동기 관성: Reload/Unload에는 실제 시간이 걸린다. 일단 전이에 들어가면 완료까지 실행한 후 새로운 변화에 응답하고, 보류 중인 역방향 전이는 관성 상태가 끝날 때의 목표 상태에 따라 실행하거나 버린다——LIFO 파괴와 역함수 중복 호출을 피한다.
🚀 다음 과 예고: 컴포넌트와 생명주기를 갖춘 지금, 논문의 3.4절은 「이펙트 컨텍스트」와 「요구 이펙트 컨텍스트」를 하나의 구체적 구성으로 통일합니다——그것이 10과 「컨텍스트 패러다임: 통일 컨텍스트 타입」입니다. 이 메커니즘이 어떻게 실제로 돌아가는 런타임으로 결실을 맺는지 살펴립니다.
셀프 체크 · 컴포넌트 생명주기
답을 모두 선택한 후 「답안 제출」을 클릭하면 정답 여부와 해설을 확인할 수 있습니다.
